diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 5811fbf..a09a442 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,137 +1,69 @@ use proc_macro::TokenStream; -use quote::{quote, ToTokens}; -use syn::{ - parse_macro_input, AttributeArgs, FnArg, Ident, ItemFn, Pat, PatIdent, PatType, ReturnType, - Type, Expr, ExprClosure, token::Comma, spanned::Spanned, -}; +use quote::quote; +use syn::{parse_macro_input, Expr, ExprClosure, FnArg, ItemFn, Pat, PatIdent, PatType}; -/// The procedural macro #[proc_macro_attribute] pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { - // Parse the input function let input_fn = parse_macro_input!(item as ItemFn); + let closure_expr = parse_macro_input!(attr as Expr); - // 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(), - }; + let fn_name = input_fn.sig.ident.clone(); + let vis = input_fn.vis.clone(); + let generics = input_fn.sig.generics.clone(); - // Extract path parameters - let mut path_idents = Vec::new(); - let mut path_types = Vec::new(); - - 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(); - } - } - } - - // 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) - } - }; - - // 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 + // Collect query and body args 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()); - } - } + for input in &input_fn.sig.inputs { + if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = input { + let ident = if let Pat::Ident(PatIdent { ident, .. }) = &**pat { + ident.clone() + } else { continue; }; + + if attrs.iter().any(|a| a.path().is_ident("query")) { + query_idents.push(ident); + query_types.push(*ty.clone()); + } else { + body_idents.push(ident); + body_types.push(*ty.clone()); } - _ => {} } } - // Build the transformed function + // Extract path args from closure + let mut path_idents = Vec::new(); + let mut path_types = Vec::new(); + + if let Expr::Closure(ExprClosure { inputs, .. }) = closure_expr { + for input in inputs.iter() { + if let Pat::Type(PatType { pat, ty, .. }) = input { + if let Pat::Ident(PatIdent { ident, .. }) = &**pat { + path_idents.push(ident.clone()); + path_types.push(*ty.clone()); + } + } + } + } + + // Generate the final function let expanded = quote! { #[bon::builder] - #vis async fn #orig_name #orig_generics ( + #vis async fn #fn_name #generics ( #( #[builder(finish_fn)] #path_idents: #path_types, )* - client: restson::RestClient, - #( #body_idents: #body_types ),* - ) -> Result<#output, restson::Error> { + #( #query_idents: #query_types, )* + #( #body_idents: #body_types, )* + ) -> Result<_, 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)>(); + let query_vec = query.to_vec::<(&str, &str)>(); // Body struct #[derive(serde::Serialize)] @@ -140,27 +72,15 @@ pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { } 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 + // Response #[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"), - }; + let response: restson::Response = + client.post_capture_with(body, query_vec).await?; - // Wrap the original todo!(response -> OutputType) | #( #path_idents ),* | { - todo!(response -> #output) + todo!(response -> _) } ( #( #path_idents ),* ) } };