From a03c1891c86c4ccc7960e2456f6f73e51adfd796 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Fri, 27 Feb 2026 05:46:12 +0100 Subject: [PATCH] v1 --- macros/Cargo.toml | 12 +++++ macros/src/lib.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 macros/Cargo.toml create mode 100644 macros/src/lib.rs diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..38091e7 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "spond-macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.106" +quote = "1.0.44" +syn = { version = "2.0.117", features = ["extra-traits", "full"] } diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..4b489f0 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,109 @@ +use proc_macro::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + parse_macro_input, Attribute, FnArg, ItemFn, Lit, Meta, Pat, PatType, Type, spanned::Spanned, +}; + +/// Endpoint macro +#[proc_macro_attribute] +pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { + // parse the endpoint attribute + let attr = proc_macro2::TokenStream::from(attr); + let func = parse_macro_input!(item as ItemFn); + + let vis = &func.vis; + let name = &func.sig.ident; + let inputs = &func.sig.inputs; + let output = &func.sig.output; + + // must be async + if func.sig.asyncness.is_none() { + return syn::Error::new(name.span(), "endpoint function must be async") + .to_compile_error() + .into(); + } + + // defaults + let mut method = quote! { GET }; + let mut path = None; + + // parse #[endpoint(...)] + if !attr.is_empty() { + let attr_str = attr.to_string(); + // simple heuristic: if contains "POST", switch + if attr_str.contains("POST") { + method = quote! { POST }; + } + // simple heuristic: extract path in quotes + if let Some(start) = attr_str.find('"') { + if let Some(end) = attr_str[start+1..].find('"') { + path = Some(attr_str[start+1..start+1+end].to_string()); + } + } + } + + let path = match path { + Some(p) => p, + None => return syn::Error::new(name.span(), "endpoint path must be provided") + .to_compile_error() + .into(), + }; + + // process arguments + let mut client_arg = None; + let mut other_args = Vec::new(); + + for input in inputs { + match input { + FnArg::Receiver(_) => continue, // skip self + FnArg::Typed(PatType { pat, ty, .. }) => { + if let Pat::Ident(ident) = &**pat { + if ident.ident == "client" { + client_arg = Some((ident.ident.clone(), ty)); + } else { + other_args.push((ident.ident.clone(), ty)); + } + } + } + } + } + + // generate tokens for function with builder + let arg_defs: Vec = other_args.iter().map(|(id, ty)| { + // wrap Option with #[builder(default)] + if let Type::Path(tp) = ty.as_ref() { + let is_option = tp.path.segments.last().map(|seg| seg.ident == "Option").unwrap_or(false); + if is_option { + quote! { #[builder(default)] #id: #ty } + } else { + quote! { #id: #ty } + } + } else { + quote! { #id: #ty } + } + }).collect(); + + let client_def = match client_arg { + Some((id, ty)) => quote! { #[builder(finish_fn)] #id: #ty }, + None => quote! { #[builder(finish_fn)] client: &restson::RestClient }, + }; + + let call_args: Vec = other_args.iter().map(|(id, _)| { + quote! { #id } + }).collect(); + + let expanded = quote! { + #[bon::builder] + #vis fn #name( + #client_def, + #(#arg_defs),* + ) -> impl std::future::Future> + '_ { + async move { + let result = client.get::<_, serde_json::Value>(#path).await?; + Ok(result) + } + } + }; + + expanded.into() +}