use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
use crate::params::BodyParamType;
use crate::utils::{
extract_body_param_from_attrs, extract_param_types_from_attrs,
extract_path_parameters, extract_request_param_name, has_body_attribute, has_request_attribute,
validate_route_path,
};
use std::collections::HashMap;
pub fn http_method_macro_impl(method: &str, args: TokenStream, input: TokenStream) -> TokenStream {
let route_path = if args.is_empty() {
"".to_string()
} else {
let path_lit = match syn::parse::<syn::LitStr>(args) {
Ok(lit) => lit,
Err(_) => {
return syn::Error::new(
proc_macro2::Span::call_site(),
format!("Invalid path argument for {} macro. Hint: Use a string literal like #[{}(\"/users/{{id}}\")]", method, method.to_lowercase())
)
.to_compile_error()
.into();
}
};
let path = path_lit.value();
if let Err(msg) = validate_route_path(&path) {
return syn::Error::new_spanned(
&path_lit,
format!("Invalid route path '{}': {}. Hint: Use format like '/users/{{id}}' with proper parameter syntax.", path, msg)
)
.to_compile_error()
.into();
}
path
};
let input_fn = parse_macro_input!(input as ItemFn);
let path_params = extract_path_parameters(&route_path);
let param_types = extract_param_types_from_attrs(&input_fn.attrs);
let has_self = input_fn
.sig
.inputs
.iter()
.any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
let has_param_annotations = !param_types.is_empty();
let has_request_attr = has_request_attribute(&input_fn.attrs);
let has_body_attr = has_body_attribute(&input_fn.attrs);
let needs_validation = has_self && (
(!path_params.is_empty() && has_param_annotations) ||
has_body_attr ||
has_request_attr
);
if needs_validation {
let body_param = extract_body_param_from_attrs(&input_fn.attrs);
if let Err(validation_error) = validate_method_consistency(
&route_path,
&path_params,
¶m_types,
&input_fn.sig,
&body_param,
has_request_attr,
) {
return syn::Error::new_spanned(&input_fn.sig, validation_error)
.to_compile_error()
.into();
}
}
let body_param = extract_body_param_from_attrs(&input_fn.attrs);
generate_injected_method(
&input_fn,
&path_params,
¶m_types,
has_request_attr,
body_param,
)
}
pub fn get_impl(args: TokenStream, input: TokenStream) -> TokenStream {
http_method_macro_impl("GET", args, input)
}
pub fn post_impl(args: TokenStream, input: TokenStream) -> TokenStream {
http_method_macro_impl("POST", args, input)
}
pub fn put_impl(args: TokenStream, input: TokenStream) -> TokenStream {
http_method_macro_impl("PUT", args, input)
}
pub fn delete_impl(args: TokenStream, input: TokenStream) -> TokenStream {
http_method_macro_impl("DELETE", args, input)
}
pub fn patch_impl(args: TokenStream, input: TokenStream) -> TokenStream {
http_method_macro_impl("PATCH", args, input)
}
pub fn head_impl(args: TokenStream, input: TokenStream) -> TokenStream {
http_method_macro_impl("HEAD", args, input)
}
pub fn options_impl(args: TokenStream, input: TokenStream) -> TokenStream {
http_method_macro_impl("OPTIONS", args, input)
}
fn generate_injected_method(
input_fn: &ItemFn,
path_params: &[String],
param_types: &HashMap<String, String>,
has_request_attr: bool,
body_param: Option<(String, BodyParamType)>,
) -> TokenStream {
let original_name = &input_fn.sig.ident;
let original_fn_name = quote::format_ident!("{}_original", original_name);
let mut param_extractions = Vec::new();
let mut call_args = Vec::new();
let mut modified_inputs = Vec::new();
let request_param_name = if has_request_attr {
extract_request_param_name(&input_fn.attrs)
} else {
"req".to_string()
};
let mut has_existing_request_param = false;
for input in &input_fn.sig.inputs {
match input {
syn::FnArg::Receiver(_) => {
modified_inputs.push(input.clone());
}
syn::FnArg::Typed(pat_type) => {
let param_type = &pat_type.ty;
let param_type_str = quote! { #param_type }.to_string();
match pat_type.pat.as_ref() {
syn::Pat::Ident(pat_ident) => {
let param_name = pat_ident.ident.to_string();
if param_type_str.contains("ElifRequest") {
call_args.push(quote! { request });
modified_inputs.push(input.clone());
has_existing_request_param = true;
} else if path_params.contains(¶m_name) {
let param_ident = &pat_ident.ident;
let extraction_method =
get_extraction_method(¶m_name, param_types, ¶m_type_str);
param_extractions.push(quote! {
let #param_ident = request.#extraction_method(#param_name)
.map_err(|e| ::elif_http::HttpError::bad_request(format!("Invalid parameter '{}': {:?}", #param_name, e)))?;
});
call_args.push(quote! { #param_ident });
modified_inputs.push(input.clone());
} else if let Some((body_param_name, _)) = &body_param {
if param_name == *body_param_name {
let param_ident = &pat_ident.ident;
call_args.push(quote! { #param_ident });
modified_inputs.push(input.clone());
} else {
return syn::Error::new_spanned(
pat_type,
format!(
"Unsupported parameter '{}' of type '{}'. Only path parameters (specified in route), body parameters (annotated with #[body]), and ElifRequest are supported. \
Hint: Remove this parameter, add it to the route path like '/users/{{{}}}' and annotate with #[param({}: type)], annotate with #[body({}: Type)], or use #[request] to enable automatic request injection.",
param_name, param_type_str, param_name, param_name, param_name
)
)
.to_compile_error()
.into();
}
} else {
return syn::Error::new_spanned(
pat_type,
format!(
"Unsupported parameter '{}' of type '{}'. Only path parameters (specified in route), body parameters (annotated with #[body]), and ElifRequest are supported. \
Hint: Remove this parameter, add it to the route path like '/users/{{{}}}' and annotate with #[param({}: type)], annotate with #[body({}: Type)], or use #[request] to enable automatic request injection.",
param_name, param_type_str, param_name, param_name, param_name
)
)
.to_compile_error()
.into();
}
}
syn::Pat::Wild(_) => {
modified_inputs.push(input.clone());
}
_ => {
return syn::Error::new_spanned(
pat_type,
format!(
"Unsupported parameter pattern: '{}'. \
Only simple identifiers (e.g., 'param: Type') and wildcards (e.g., '_: Type') are supported in controller methods. \
Hint: Use simple parameter names without destructuring or complex patterns.",
quote! { #pat_type.pat }
)
)
.to_compile_error()
.into();
}
}
}
}
}
if has_request_attr && !has_existing_request_param {
let req_ident = quote::format_ident!("{}", request_param_name);
let request_param = syn::parse_quote! {
#req_ident: ::elif_http::ElifRequest
};
modified_inputs.push(request_param);
call_args.push(quote! { request });
}
if let Some((body_param_name, _)) = &body_param {
if has_existing_request_param {
return syn::Error::new_spanned(
&input_fn.sig,
format!(
"Conflicting parameter usage: #[body({})] cannot be used with ElifRequest parameter. \
The body extraction wrapper handles the request automatically. \
Hint: Remove the ElifRequest parameter from the function signature when using #[body].",
body_param_name
)
)
.to_compile_error()
.into();
}
}
if let Some((body_param_name, body_param_type)) = &body_param {
let body_param_ident = quote::format_ident!("{}", body_param_name);
let body_extraction = match body_param_type {
BodyParamType::Custom(_) => {
quote! {
let #body_param_ident = request.json()
.map_err(|e| ::elif_http::HttpError::bad_request(format!("Invalid JSON body: {:?}", e)))?;
}
}
BodyParamType::Form => {
quote! {
let #body_param_ident = request.form()
.map_err(|e| ::elif_http::HttpError::bad_request(format!("Invalid form data: {:?}", e)))?;
}
}
BodyParamType::Bytes => {
quote! {
let #body_param_ident = request.body_bytes()
.ok_or_else(|| ::elif_http::HttpError::bad_request("No request body".to_string()))?
.clone();
}
}
};
param_extractions.push(body_extraction);
}
let original_attrs = &input_fn
.attrs
.iter()
.filter(|attr| {
!attr.path().is_ident("get")
&& !attr.path().is_ident("post")
&& !attr.path().is_ident("put")
&& !attr.path().is_ident("delete")
&& !attr.path().is_ident("patch")
&& !attr.path().is_ident("head")
&& !attr.path().is_ident("options")
&& !attr.path().is_ident("param")
&& !attr.path().is_ident("request")
&& !attr.path().is_ident("body")
})
.collect::<Vec<_>>();
let original_vis = &input_fn.vis;
let original_block = &input_fn.block;
let original_return = &input_fn.sig.output;
let original_asyncness = &input_fn.sig.asyncness;
let wrapper_asyncness = quote! { async };
let method_call = if original_asyncness.is_some() {
quote! {
self.#original_fn_name(#(#call_args),*).await
}
} else {
quote! {
self.#original_fn_name(#(#call_args),*)
}
};
let return_category = analyze_return_type(original_return);
let response_handling = match return_category {
ReturnTypeCategory::HttpResultElifResponse => {
quote! { #method_call }
}
ReturnTypeCategory::ElifResponse => {
quote! { Ok(#method_call) }
}
ReturnTypeCategory::ResultType => {
quote! {
match #method_call {
Ok(result) => Ok(ElifResponse::ok().json(&result)?),
Err(e) => Err(::elif_http::HttpError::internal(format!("Handler error: {:?}", e)).into()),
}
}
}
ReturnTypeCategory::Unit => {
quote! {
#method_call;
Ok(ElifResponse::ok())
}
}
ReturnTypeCategory::SerializableType => {
quote! {
let result = #method_call;
Ok(ElifResponse::ok().json(&result)?)
}
}
};
let expanded = quote! {
#(#original_attrs)*
#original_vis #original_asyncness fn #original_fn_name(#(#modified_inputs),*) #original_return #original_block
#original_vis #wrapper_asyncness fn #original_name(&self, request: ElifRequest) -> HttpResult<ElifResponse> {
#(#param_extractions)*
#response_handling
}
};
TokenStream::from(expanded)
}
fn analyze_return_type(return_type: &syn::ReturnType) -> ReturnTypeCategory {
match return_type {
syn::ReturnType::Default => ReturnTypeCategory::Unit,
syn::ReturnType::Type(_, ty) => {
let type_str = quote! { #ty }.to_string();
if type_str.contains("HttpResult") && type_str.contains("ElifResponse") {
ReturnTypeCategory::HttpResultElifResponse
}
else if type_str.contains("ElifResponse") {
ReturnTypeCategory::ElifResponse
}
else if type_str.contains("Result") || type_str.contains("HttpResult") {
ReturnTypeCategory::ResultType
}
else {
ReturnTypeCategory::SerializableType
}
}
}
}
#[derive(Debug, PartialEq)]
enum ReturnTypeCategory {
Unit, ElifResponse, HttpResultElifResponse, ResultType, SerializableType, }
fn get_extraction_method(
param_name: &str,
param_types: &HashMap<String, String>,
rust_type: &str,
) -> proc_macro2::Ident {
if let Some(param_type) = param_types.get(param_name) {
return match param_type.as_str() {
"Integer" => {
if rust_type.contains("u32") {
quote::format_ident!("path_param_u32")
} else if rust_type.contains("u64") {
quote::format_ident!("path_param_u64")
} else if rust_type.contains("i64") {
quote::format_ident!("path_param_i64")
} else {
quote::format_ident!("path_param_int") }
},
"String" => quote::format_ident!("path_param_string"),
"Uuid" => quote::format_ident!("path_param_uuid"),
_ => quote::format_ident!("path_param_string"), };
}
if rust_type.contains("i32") {
quote::format_ident!("path_param_int")
} else if rust_type.contains("u32") {
quote::format_ident!("path_param_u32")
} else if rust_type.contains("i64") {
quote::format_ident!("path_param_i64")
} else if rust_type.contains("u64") {
quote::format_ident!("path_param_u64")
} else {
quote::format_ident!("path_param_string")
}
}
fn validate_method_consistency(
route_path: &str,
path_params: &[String],
param_types: &HashMap<String, String>,
sig: &syn::Signature,
body_param: &Option<(String, BodyParamType)>,
has_request_attr: bool,
) -> Result<(), String> {
use syn::{FnArg, Pat, PatIdent};
let mut fn_params = HashMap::new();
let mut has_request_param = false;
for input in &sig.inputs {
match input {
FnArg::Receiver(_) => {
}
FnArg::Typed(pat_type) => {
let param_type_str = quote! { #pat_type.ty }.to_string().replace(" ", "");
match pat_type.pat.as_ref() {
Pat::Ident(PatIdent { ident, .. }) => {
let param_name = ident.to_string();
if param_type_str.contains("ElifRequest") {
has_request_param = true;
} else if param_name.starts_with('_') {
} else {
fn_params.insert(param_name, param_type_str);
}
}
Pat::Wild(_) => {
}
Pat::Tuple(_) => {
return Err("Unsupported tuple destructuring pattern in parameter. \
Hint: Use individual parameters instead of tuple destructuring. \
Example: Change '(a, b): (Type1, Type2)' to 'a: Type1, b: Type2'."
.to_string());
}
Pat::Struct(_) => {
return Err(format!(
"Unsupported struct destructuring pattern in parameter: '{}'. \
Hint: Use the complete struct type as parameter. \
Example: Change 'User {{ name, .. }}: User' to 'user: User' and access 'user.name'.",
quote! { #pat_type.pat }
));
}
Pat::Reference(_) => {
return Err(format!(
"Unsupported reference pattern in parameter: '{}'. \
Hint: Remove the reference pattern and use the type directly. \
Example: Change '&name: &Type' to 'name: &Type'.",
quote! { #pat_type.pat }
));
}
_ => {
return Err(format!(
"Unsupported parameter pattern: '{}'. \
Only simple identifiers (e.g., 'param: Type') and wildcards (e.g., '_: Type') are supported in controller methods. \
Hint: Use simple parameter names without destructuring or complex patterns.",
quote! { #pat_type.pat }
));
}
}
}
}
}
for path_param in path_params {
if !param_types.contains_key(path_param) {
return Err(format!(
"Route parameter '{}' in path '{}' is missing #[param] declaration. \
Hint: Add #[param({}: type)] above the function.",
path_param, route_path, path_param
));
}
}
for param_name in param_types.keys() {
if !path_params.contains(param_name) {
return Err(format!(
"Parameter '{}' has #[param] declaration but is not present in route path '{}'. \
Hint: Add '{{{}}}' to the route path or remove the #[param({})] declaration.",
param_name, route_path, param_name, param_name
));
}
}
for path_param in path_params {
if !fn_params.contains_key(path_param) {
return Err(format!(
"Route parameter '{}' is declared but missing from function signature. \
Hint: Add '{}: SomeType' to the function parameters.",
path_param, path_param
));
}
}
if has_request_attr && has_request_param {
return Err("Redundant #[request] attribute: function already has ElifRequest parameter. \
Hint: Remove either the #[request] attribute or the ElifRequest parameter from the function signature.".to_string());
}
if let Some((body_param_name, _)) = body_param {
if !fn_params.contains_key(body_param_name) {
return Err(format!(
"Body parameter '{}' specified in #[body] but missing from function signature. \
Hint: Add '{}: SomeType' to the function parameters.",
body_param_name, body_param_name
));
}
if has_request_param {
return Err(format!(
"Conflicting parameter usage: #[body({})] cannot be used with ElifRequest parameter. \
The body extraction wrapper handles the request automatically. \
Hint: Remove the ElifRequest parameter from the function signature when using #[body].",
body_param_name
));
}
}
for fn_param_name in fn_params.keys() {
let is_path_param = path_params.contains(fn_param_name);
let is_body_param = body_param
.as_ref()
.is_some_and(|(name, _)| name == fn_param_name);
if !is_path_param && !is_body_param {
return Err(format!(
"Function parameter '{}' is not handled by any route parameter, #[body], or ElifRequest. \
Hint: Add '{{{}}}' to the route path and #[param({}: type)], or annotate with #[body({}: Type)].",
fn_param_name, fn_param_name, fn_param_name, fn_param_name
));
}
}
Ok(())
}