closure like syntax v3

This commit is contained in:
Jonas Rabenstein 2026-02-27 07:13:29 +01:00
commit 9a5a5fb9d2

View file

@ -1,89 +1,120 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{parse_macro_input, Expr, ExprClosure, FnArg, ItemFn, Pat, PatIdent, PatType}; use syn::{
parse_macro_input, Attribute, FnArg, Ident, ItemFn, Pat, PatIdent, PatType, Type,
};
#[proc_macro_attribute] /// Represents a function argument we care about
pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { #[derive(Clone)]
let input_fn = parse_macro_input!(item as ItemFn); struct Arg {
let closure_expr = parse_macro_input!(attr as Expr); ident: Ident,
ty: Type,
attrs: Vec<Attribute>,
}
let fn_name = input_fn.sig.ident.clone(); /// Extract arguments marked as `#[path]` or the rest
let vis = input_fn.vis.clone(); fn extract_args<'a, I>(inputs: I, path_only: bool) -> Vec<Arg>
let generics = input_fn.sig.generics.clone(); where
I: IntoIterator<Item = &'a FnArg>,
// Collect query and body args {
let mut query_idents = Vec::new(); let mut args = Vec::new();
let mut query_types = Vec::new(); for fnarg in inputs {
let mut body_idents = Vec::new(); if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = fnarg {
let mut body_types = Vec::new();
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 { let ident = if let Pat::Ident(PatIdent { ident, .. }) = &**pat {
ident.clone() ident.clone()
} else { continue; };
if attrs.iter().any(|a| a.path().is_ident("query")) {
query_idents.push(ident);
query_types.push(*ty.clone());
} else { } else {
body_idents.push(ident); panic!("Only simple identifiers are supported in endpoint arguments");
body_types.push(*ty.clone()); };
}
} if path_only && !attrs.iter().any(|a| a.path().is_ident("path")) {
continue;
} }
// Extract path args from closure args.push(Arg {
let mut path_idents = Vec::new(); ident,
let mut path_types = Vec::new(); ty: *ty.clone(),
attrs: attrs.clone(),
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());
}
} }
} }
args
} }
// Generate the final function /// Implements #[get("…")] and #[post("…")]
macro_rules! endpoint_macro {
($method:ident) => {
#[proc_macro_attribute]
pub fn $method(attr: TokenStream, item: TokenStream) -> TokenStream {
let path_lit = parse_macro_input!(attr as syn::LitStr);
let input_fn = parse_macro_input!(item as ItemFn);
let vis = &input_fn.vis;
let fn_name = &input_fn.sig.ident;
let generics = &input_fn.sig.generics;
let inputs = &input_fn.sig.inputs;
let output = &input_fn.sig.output;
let path_args = extract_args(inputs, true);
let other_args: Vec<_> = extract_args(inputs, false)
.into_iter()
.filter(|a| !path_args.iter().any(|p| p.ident == a.ident))
.collect();
// for builder: path args first
let path_idents: Vec<_> = path_args.iter().map(|a| &a.ident).collect();
let path_types: Vec<_> = path_args.iter().map(|a| &a.ty).collect();
let body_idents: Vec<_> = other_args.iter().map(|a| &a.ident).collect();
let body_types: Vec<_> = other_args.iter().map(|a| &a.ty).collect();
let method_upper = stringify!($method).to_uppercase();
let expanded = quote! { let expanded = quote! {
#[bon::builder] #[bon::builder]
#vis async fn #fn_name #generics ( #vis async fn #fn_name #generics (
#[ builder(finish_fn) ] client: restson::RestClient,
#( #[builder(finish_fn)] #path_idents: #path_types, )* #( #[builder(finish_fn)] #path_idents: #path_types, )*
#( #query_idents: #query_types, )*
#( #body_idents: #body_types, )* #( #body_idents: #body_types, )*
) -> Result<_, restson::Error> { ) -> Result<#output, restson::Error> {
// build path
let path = format!(#path_lit, #( #path_idents = #path_idents ),*);
// Query struct
#[derive(serde::Serialize)]
struct Query {
#( #query_idents: #query_types, )*
}
let query = Query { #( #query_idents ),* };
let query_vec = query.to_vec::<(&str, &str)>();
// Body struct
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
struct Body { struct Body {
#( #body_idents: #body_types, )* #( #body_idents: #body_idents, )*
} }
let body = Body { #( #body_idents ),* };
// Response let body = Body {
#[derive(serde::de::DeserializeOwned)] #( #body_idents, )*
struct Response(Vec<serde_json::Value>); };
let response: restson::Response<Response> = // for query arguments, if any
client.post_capture_with(body, query_vec).await?; #[derive(serde::Serialize)]
struct Query {
#( #body_idents: #body_idents, )*
}
| #( #path_idents ),* | { let query = Query {
todo!(response -> _) #( #body_idents, )*
} ( #( #path_idents ),* ) };
// placeholder: convert query to vec of pairs
let query_pairs: Vec<(&str, &str)> = Vec::new();
let response = match #method_upper {
"GET" => client.get_with(path, query_pairs).await?,
"POST" => client.post_capture_with(body, query_pairs).await?,
_ => unreachable!(),
};
todo!(response)
} }
}; };
TokenStream::from(expanded) expanded.into()
} }
};
}
endpoint_macro!(get);
endpoint_macro!(post);