closure like syntax v2
This commit is contained in:
parent
9ab32a7c85
commit
f99777bf77
1 changed files with 42 additions and 122 deletions
|
|
@ -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<String, restson::Error> {
|
||||
Ok(format!(#path_lit #(, #path_idents )* ))
|
||||
}
|
||||
}
|
||||
|
||||
// Response placeholder
|
||||
// Response
|
||||
#[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"),
|
||||
};
|
||||
let response: restson::Response<Response> =
|
||||
client.post_capture_with(body, query_vec).await?;
|
||||
|
||||
// Wrap the original todo!(response -> OutputType)
|
||||
| #( #path_idents ),* | {
|
||||
todo!(response -> #output)
|
||||
todo!(response -> _)
|
||||
} ( #( #path_idents ),* )
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue