closure like syntax v1

This commit is contained in:
Jonas Rabenstein 2026-02-27 06:24:31 +01:00
commit 9ab32a7c85

View file

@ -1,57 +1,167 @@
// macros/src/lib.rs
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::{quote, ToTokens};
use syn::{parse_macro_input, ItemFn, FnArg, Pat, Type, Ident, Attribute}; use syn::{
parse_macro_input, AttributeArgs, FnArg, Ident, ItemFn, Pat, PatIdent, PatType, ReturnType,
/// Simple argument representation Type, Expr, ExprClosure, token::Comma, spanned::Spanned,
struct Arg { };
attrs: Vec<Attribute>,
ident: Ident,
ty: Type,
}
/// The procedural macro
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn builder(_attr: TokenStream, item: TokenStream) -> TokenStream { pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the input function // 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; // Parse the closure-like attribute: |x: u128, y: String| POST "/some/{x}/{y}"
let name = &input.sig.ident; let closure_expr: ExprClosure = match syn::parse(attr.clone()) {
let generics = &input.sig.generics; Ok(c) => c,
Err(e) => return e.to_compile_error().into(),
};
// Collect arguments // Extract path parameters
let mut body_args: Vec<Arg> = Vec::new(); let mut path_idents = Vec::new();
let mut path_types = Vec::new();
for arg in &input.sig.inputs { for fnarg in closure_expr.inputs.iter() {
if let FnArg::Typed(pat_type) = arg { match fnarg {
if let Pat::Ident(pat_ident) = &*pat_type.pat { FnArg::Typed(PatType { pat, ty, .. }) => {
body_args.push(Arg { if let Pat::Ident(PatIdent { ident, .. }) = **pat {
attrs: pat_type.attrs.clone(), path_idents.push(ident.clone());
ident: pat_ident.ident.clone(), path_types.push(*ty.clone());
ty: (*pat_type.ty).clone(), } else {
}); return syn::Error::new(pat.span(), "Unsupported pattern in path parameters")
} else { .to_compile_error()
panic!("Only simple identifier patterns are supported"); .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 // Extract method and path literal from closure body
let arg_idents: Vec<_> = body_args.iter().map(|a| &a.ident).collect(); let (method, path_lit) = match *closure_expr.body {
let arg_tys: Vec<_> = body_args.iter().map(|a| &a.ty).collect(); Expr::Assign(ref assign) => {
let arg_attrs: Vec<_> = body_args.iter().map(|a| &a.attrs).collect(); 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! { let expanded = quote! {
#[bon::builder] #[bon::builder]
#vis fn #name #generics ( #vis async fn #orig_name #orig_generics (
#( #( #[builder(finish_fn)] #path_idents: #path_types, )*
#( #arg_attrs )* #arg_idents : #arg_tys client: restson::RestClient,
),* #( #body_idents: #body_types ),*
) -> impl std::future::Future<Output = Result<(), restson::Error>> { ) -> Result<#output, restson::Error> {
todo!() // 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<String, restson::Error> {
Ok(format!(#path_lit #(, #path_idents )* ))
}
}
// Response placeholder
#[derive(serde::de::DeserializeOwned)]
struct Response(Vec<serde_json::Value>);
// Make the REST call
let response: restson::Response<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 ),* )
} }
}; };