From 9ab32a7c85b0dc5cd28811b92c48d3a437141fc9 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Fri, 27 Feb 2026 06:24:31 +0100 Subject: [PATCH] closure like syntax v1 --- macros/src/lib.rs | 190 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 150 insertions(+), 40 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 841eba8..5811fbf 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,57 +1,167 @@ -// macros/src/lib.rs use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, ItemFn, FnArg, Pat, Type, Ident, Attribute}; - -/// Simple argument representation -struct Arg { - attrs: Vec, - ident: Ident, - ty: Type, -} +use quote::{quote, ToTokens}; +use syn::{ + parse_macro_input, AttributeArgs, FnArg, Ident, ItemFn, Pat, PatIdent, PatType, ReturnType, + Type, Expr, ExprClosure, token::Comma, spanned::Spanned, +}; +/// The procedural macro #[proc_macro_attribute] -pub fn builder(_attr: TokenStream, item: TokenStream) -> TokenStream { +pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse the input function - let input = parse_macro_input!(item as ItemFn); + let input_fn = parse_macro_input!(item as ItemFn); - let vis = &input.vis; - let name = &input.sig.ident; - let generics = &input.sig.generics; + // Parse the closure-like attribute: |x: u128, y: String| POST "/some/{x}/{y}" + let closure_expr: ExprClosure = match syn::parse(attr.clone()) { + Ok(c) => c, + Err(e) => return e.to_compile_error().into(), + }; - // Collect arguments - let mut body_args: Vec = Vec::new(); + // Extract path parameters + let mut path_idents = Vec::new(); + let mut path_types = Vec::new(); - for arg in &input.sig.inputs { - if let FnArg::Typed(pat_type) = arg { - if let Pat::Ident(pat_ident) = &*pat_type.pat { - body_args.push(Arg { - attrs: pat_type.attrs.clone(), - ident: pat_ident.ident.clone(), - ty: (*pat_type.ty).clone(), - }); - } else { - panic!("Only simple identifier patterns are supported"); + for fnarg in closure_expr.inputs.iter() { + match fnarg { + FnArg::Typed(PatType { pat, ty, .. }) => { + if let Pat::Ident(PatIdent { ident, .. }) = **pat { + path_idents.push(ident.clone()); + path_types.push(*ty.clone()); + } else { + return syn::Error::new(pat.span(), "Unsupported pattern in path parameters") + .to_compile_error() + .into(); + } + } + _ => { + return syn::Error::new(fnarg.span(), "Expected typed parameter") + .to_compile_error() + .into(); } - } else { - panic!("&self / &mut self not supported"); } } - // Destructure the arguments for quote repetitions - let arg_idents: Vec<_> = body_args.iter().map(|a| &a.ident).collect(); - let arg_tys: Vec<_> = body_args.iter().map(|a| &a.ty).collect(); - let arg_attrs: Vec<_> = body_args.iter().map(|a| &a.attrs).collect(); + // Extract method and path literal from closure body + let (method, path_lit) = match *closure_expr.body { + Expr::Assign(ref assign) => { + return syn::Error::new(assign.span(), "Unexpected assignment in endpoint") + .to_compile_error() + .into(); + } + Expr::Path(_) | Expr::Call(_) | Expr::Lit(_) => { + return syn::Error::new(closure_expr.body.span(), "Expected method + path string") + .to_compile_error() + .into(); + } + Expr::Tuple(ref tup) => { + return syn::Error::new(closure_expr.body.span(), "Unexpected tuple in endpoint") + .to_compile_error() + .into(); + } + Expr::Binary(ref _bin) => { + return syn::Error::new(closure_expr.body.span(), "Unexpected binary in endpoint") + .to_compile_error() + .into(); + } + _ => { + // We will parse method & path using a simple hack: the closure body is `POST "/some/{x}"` etc + let ts = closure_expr.body.to_token_stream().to_string(); + let ts = ts.trim(); + let parts: Vec<&str> = ts.splitn(2, ' ').collect(); + if parts.len() != 2 { + return syn::Error::new(closure_expr.body.span(), "Expected `METHOD \"/path\"`") + .to_compile_error() + .into(); + } + let method = parts[0].trim().to_uppercase(); + let path_lit = syn::LitStr::new(parts[1].trim_matches('"'), closure_expr.body.span()); + (method, path_lit) + } + }; - // Generate the builder function + // Extract original function signature details + let vis = &input_fn.vis; + let orig_name = &input_fn.sig.ident; + let orig_generics = &input_fn.sig.generics; + let orig_inputs = &input_fn.sig.inputs; + let output = match &input_fn.sig.output { + ReturnType::Type(_, ty) => ty, + ReturnType::Default => { + return syn::Error::new(input_fn.sig.output.span(), "Function must have a return type") + .to_compile_error() + .into(); + } + }; + + // Split query vs body params + let mut query_idents = Vec::new(); + let mut query_types = Vec::new(); + let mut body_idents = Vec::new(); + let mut body_types = Vec::new(); + + for arg in orig_inputs.iter() { + match arg { + FnArg::Typed(PatType { pat, ty, attrs, .. }) => { + let is_query = attrs.iter().any(|a| a.path().is_ident("query")); + if let Pat::Ident(PatIdent { ident, .. }) = &**pat { + if is_query { + query_idents.push(ident.clone()); + query_types.push(*ty.clone()); + } else { + body_idents.push(ident.clone()); + body_types.push(*ty.clone()); + } + } + } + _ => {} + } + } + + // Build the transformed function let expanded = quote! { #[bon::builder] - #vis fn #name #generics ( - #( - #( #arg_attrs )* #arg_idents : #arg_tys - ),* - ) -> impl std::future::Future> { - todo!() + #vis async fn #orig_name #orig_generics ( + #( #[builder(finish_fn)] #path_idents: #path_types, )* + client: restson::RestClient, + #( #body_idents: #body_types ),* + ) -> Result<#output, restson::Error> { + // Query struct + #[derive(serde::Serialize)] + struct Query { + #( #query_idents: #query_types, )* + } + let query = Query { #( #query_idents ),* }; + let query = query.to_vec::<(&str, &str)>(); + + // Body struct + #[derive(serde::Serialize)] + struct Body { + #( #body_idents: #body_types, )* + } + let body = Body { #( #body_idents ),* }; + + // RestPath impl for path parameters + impl restson::RestPath<( #( #path_types ),* )> for Body { + fn get_url(&self, #( #path_idents: #path_types ),* ) -> Result { + Ok(format!(#path_lit #(, #path_idents )* )) + } + } + + // Response placeholder + #[derive(serde::de::DeserializeOwned)] + struct Response(Vec); + + // Make the REST call + let response: restson::Response = match #method { + ref m if m == "GET" => client.get_with(body, query).await?, + ref m if m == "POST" => client.post_capture_with(body, query).await?, + _ => panic!("Unsupported method"), + }; + + // Wrap the original todo!(response -> OutputType) + | #( #path_idents ),* | { + todo!(response -> #output) + } ( #( #path_idents ),* ) } };