use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, FnArg, ItemFn, Pat, Type};
fn ferro() -> TokenStream2 {
quote!(::ferro)
}
enum ParamKind {
Request,
Primitive,
Model,
FormRequest,
}
pub fn handler_impl(_attr: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
let ferro = ferro();
let fn_vis = &input_fn.vis;
let fn_name = &input_fn.sig.ident;
let fn_generics = &input_fn.sig.generics;
let fn_output = &input_fn.sig.output;
let fn_block = &input_fn.block;
let fn_attrs = &input_fn.attrs;
let is_async = input_fn.sig.asyncness.is_some();
let async_token = if is_async {
quote! { async }
} else {
quote! {}
};
let params: Vec<_> = input_fn.sig.inputs.iter().collect();
if params.is_empty() {
let output = quote! {
#(#fn_attrs)*
#fn_vis #async_token fn #fn_name #fn_generics(_: #ferro::Request) #fn_output {
#fn_block
}
};
return output.into();
}
let mut extractions = Vec::new();
let mut has_request_consumer = false;
let mut has_request_param = false;
for param in ¶ms {
match param {
FnArg::Typed(pat_type) => {
let param_pat = &pat_type.pat;
let param_type = &pat_type.ty;
let param_name = extract_param_name(param_pat);
let kind = classify_param_type(param_type);
let extraction = generate_extraction(
&ferro,
param_pat,
param_type,
¶m_name,
&kind,
&mut has_request_consumer,
&mut has_request_param,
);
extractions.push(extraction);
}
FnArg::Receiver(_) => {
return syn::Error::new_spanned(
param,
"#[handler] does not support methods with self receiver",
)
.to_compile_error()
.into();
}
}
}
let output = if has_request_param {
quote! {
#(#fn_attrs)*
#fn_vis #async_token fn #fn_name #fn_generics(__ferro_req: #ferro::Request) #fn_output {
let __ferro_params = __ferro_req.params().clone();
#(#extractions)*
#fn_block
}
}
} else {
quote! {
#(#fn_attrs)*
#fn_vis #async_token fn #fn_name #fn_generics(__ferro_req: #ferro::Request) #fn_output {
let __ferro_params = __ferro_req.params().clone();
#(#extractions)*
#fn_block
}
}
};
output.into()
}
fn extract_param_name(pat: &Pat) -> String {
match pat {
Pat::Ident(pat_ident) => pat_ident.ident.to_string(),
Pat::Wild(_) => "_".to_string(),
_ => "param".to_string(),
}
}
fn classify_param_type(ty: &Type) -> ParamKind {
match ty {
Type::Path(type_path) => {
let segments = &type_path.path.segments;
if segments.len() == 1 && segments[0].ident == "Request" {
return ParamKind::Request;
}
if segments.len() == 2 && segments[0].ident == "ferro" && segments[1].ident == "Request"
{
return ParamKind::Request;
}
if segments.len() == 1 {
let ident = segments[0].ident.to_string();
if is_primitive_type_name(&ident) {
return ParamKind::Primitive;
}
}
if let Some(last_segment) = segments.last() {
if last_segment.ident == "Model" && segments.len() >= 2 {
return ParamKind::Model;
}
}
ParamKind::FormRequest
}
_ => ParamKind::FormRequest,
}
}
fn is_primitive_type_name(name: &str) -> bool {
matches!(
name,
"i8" | "i16"
| "i32"
| "i64"
| "i128"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "isize"
| "String"
)
}
fn generate_extraction(
ferro: &TokenStream2,
pat: &Pat,
ty: &Type,
param_name: &str,
kind: &ParamKind,
has_consumer: &mut bool,
has_request: &mut bool,
) -> TokenStream2 {
match kind {
ParamKind::Request => {
*has_request = true;
*has_consumer = true;
quote! {
let #pat: #ty = __ferro_req;
}
}
ParamKind::Primitive => {
quote! {
let #pat: #ty = {
let __value = __ferro_params.get(#param_name)
.ok_or_else(|| #ferro::FrameworkError::param(#param_name))?;
<#ty as #ferro::FromParam>::from_param(__value)?
};
}
}
ParamKind::Model => {
quote! {
let #pat: #ty = {
let __value = __ferro_params.get(#param_name)
.ok_or_else(|| #ferro::FrameworkError::param(#param_name))?;
<#ty as #ferro::AutoRouteBinding>::from_route_param(__value).await?
};
}
}
ParamKind::FormRequest => {
*has_consumer = true;
quote! {
let #pat: #ty = <#ty as #ferro::FromRequest>::from_request(__ferro_req).await?;
}
}
}
}