use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{parse_macro_input, Expr, FnArg, ItemFn, Lit, Meta};
use crate::utils::{classify_param_type, extract_param_name, ferro, ParamKind};
struct ActionAttrs {
redirect_to: String,
#[allow(dead_code)]
method: String,
}
fn parse_action_attrs(attr: TokenStream) -> Result<ActionAttrs, syn::Error> {
let mut redirect_to: Option<String> = None;
let mut method: String = "POST".to_string();
let parser = syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated;
let metas = syn::parse::Parser::parse(parser, attr).map_err(|e| {
syn::Error::new(
e.span(),
format!("#[action]: invalid attribute syntax: {e}"),
)
})?;
for meta in metas {
match meta {
Meta::NameValue(nv) => {
let key = nv
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
match key.as_str() {
"redirect_to" => {
if let Expr::Lit(expr_lit) = &nv.value {
if let Lit::Str(lit_str) = &expr_lit.lit {
redirect_to = Some(lit_str.value());
continue;
}
}
return Err(syn::Error::new_spanned(
&nv.value,
"#[action]: `redirect_to` must be a string literal",
));
}
"method" => {
if let Expr::Lit(expr_lit) = &nv.value {
if let Lit::Str(lit_str) = &expr_lit.lit {
method = lit_str.value();
continue;
}
}
return Err(syn::Error::new_spanned(
&nv.value,
"#[action]: `method` must be a string literal (default \"POST\")",
));
}
other => {
return Err(syn::Error::new_spanned(
nv.path,
format!(
"#[action]: unknown attribute `{other}` — supported keys: `redirect_to`, `method`"
),
));
}
}
}
other => {
return Err(syn::Error::new_spanned(
other,
"#[action]: only `key = \"value\"` attributes are supported",
));
}
}
}
let redirect_to = redirect_to.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"#[action]: `redirect_to` is required, e.g. #[action(redirect_to = \"/dashboard/foo\")]",
)
})?;
Ok(ActionAttrs {
redirect_to,
method,
})
}
fn generate_action_extraction(
ferro: &proc_macro2::TokenStream,
pat: &syn::Pat,
ty: &syn::Type,
param_name: &str,
kind: &ParamKind,
) -> proc_macro2::TokenStream {
match kind {
ParamKind::Request => {
quote! {
let #pat: &mut #ferro::Request = &mut __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 => {
quote! {
compile_error!("#[action] does not yet support FormRequest parameters. Extract the form from `req` inside the body, e.g. `let form: MyForm = req.form().await?;`");
}
}
}
}
pub fn action_impl(attr: TokenStream, input: TokenStream) -> TokenStream {
let attrs = match parse_action_attrs(attr) {
Ok(a) => a,
Err(e) => return e.to_compile_error().into(),
};
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_block = &input_fn.block;
let fn_attrs = &input_fn.attrs;
let params: Vec<_> = input_fn.sig.inputs.iter().collect();
let mut extractions = Vec::new();
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_action_extraction(&ferro, param_pat, param_type, ¶m_name, &kind);
extractions.push(extraction);
}
FnArg::Receiver(_) => {
return syn::Error::new_spanned(
param,
"#[action] does not support methods with `self` receiver",
)
.to_compile_error()
.into();
}
}
}
let redirect_to_lit = &attrs.redirect_to;
let output = quote! {
#(#fn_attrs)*
#fn_vis async fn #fn_name #fn_generics(__ferro_req: #ferro::Request) -> #ferro::Response {
let mut __ferro_req = __ferro_req;
let __ferro_params = __ferro_req.params().clone();
#(#extractions)*
let __action_result: #ferro::ActionResult = async move { #fn_block }.await;
#ferro::http::action::handle_action_result(
__action_result,
#redirect_to_lit,
concat!(module_path!(), "::", stringify!(#fn_name)),
&mut __ferro_req,
)
}
};
output.into()
}