closure like syntax v2

This commit is contained in:
Jonas Rabenstein 2026-02-27 06:38:25 +01:00
commit f99777bf77

View file

@ -1,137 +1,69 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{quote, ToTokens}; use quote::quote;
use syn::{ use syn::{parse_macro_input, Expr, ExprClosure, FnArg, ItemFn, Pat, PatIdent, PatType};
parse_macro_input, AttributeArgs, FnArg, Ident, ItemFn, Pat, PatIdent, PatType, ReturnType,
Type, Expr, ExprClosure, token::Comma, spanned::Spanned,
};
/// The procedural macro
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the input function
let input_fn = parse_macro_input!(item as ItemFn); 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 fn_name = input_fn.sig.ident.clone();
let closure_expr: ExprClosure = match syn::parse(attr.clone()) { let vis = input_fn.vis.clone();
Ok(c) => c, let generics = input_fn.sig.generics.clone();
Err(e) => return e.to_compile_error().into(),
};
// Extract path parameters // Collect query and body args
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
let mut query_idents = Vec::new(); let mut query_idents = Vec::new();
let mut query_types = Vec::new(); let mut query_types = Vec::new();
let mut body_idents = Vec::new(); let mut body_idents = Vec::new();
let mut body_types = Vec::new(); let mut body_types = Vec::new();
for arg in orig_inputs.iter() { for input in &input_fn.sig.inputs {
match arg { if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = input {
FnArg::Typed(PatType { pat, ty, attrs, .. }) => { let ident = if let Pat::Ident(PatIdent { ident, .. }) = &**pat {
let is_query = attrs.iter().any(|a| a.path().is_ident("query")); ident.clone()
if let Pat::Ident(PatIdent { ident, .. }) = &**pat { } else { continue; };
if is_query {
query_idents.push(ident.clone()); if attrs.iter().any(|a| a.path().is_ident("query")) {
query_types.push(*ty.clone()); query_idents.push(ident);
} else { query_types.push(*ty.clone());
body_idents.push(ident.clone()); } else {
body_types.push(*ty.clone()); 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! { let expanded = quote! {
#[bon::builder] #[bon::builder]
#vis async fn #orig_name #orig_generics ( #vis async fn #fn_name #generics (
#( #[builder(finish_fn)] #path_idents: #path_types, )* #( #[builder(finish_fn)] #path_idents: #path_types, )*
client: restson::RestClient, #( #query_idents: #query_types, )*
#( #body_idents: #body_types ),* #( #body_idents: #body_types, )*
) -> Result<#output, restson::Error> { ) -> Result<_, restson::Error> {
// Query struct // Query struct
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
struct Query { struct Query {
#( #query_idents: #query_types, )* #( #query_idents: #query_types, )*
} }
let query = Query { #( #query_idents ),* }; let query = Query { #( #query_idents ),* };
let query = query.to_vec::<(&str, &str)>(); let query_vec = query.to_vec::<(&str, &str)>();
// Body struct // Body struct
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
@ -140,27 +72,15 @@ pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
} }
let body = Body { #( #body_idents ),* }; let body = Body { #( #body_idents ),* };
// RestPath impl for path parameters // Response
impl restson::RestPath<( #( #path_types ),* )> for Body {
fn get_url(&self, #( #path_idents: #path_types ),* ) -> Result<String, restson::Error> {
Ok(format!(#path_lit #(, #path_idents )* ))
}
}
// Response placeholder
#[derive(serde::de::DeserializeOwned)] #[derive(serde::de::DeserializeOwned)]
struct Response(Vec<serde_json::Value>); struct Response(Vec<serde_json::Value>);
// Make the REST call let response: restson::Response<Response> =
let response: restson::Response<Response> = match #method { client.post_capture_with(body, query_vec).await?;
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 ),* | { | #( #path_idents ),* | {
todo!(response -> #output) todo!(response -> _)
} ( #( #path_idents ),* ) } ( #( #path_idents ),* )
} }
}; };