use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{FnArg, LitStr, ReturnType, Type};
use crate::api_doc;
use crate::parse;
pub fn route_macro(
http_method: &str,
axum_fn: &str,
attr: TokenStream,
item: TokenStream,
) -> TokenStream {
let route_args = match parse::parse_route_attr(attr) {
Ok(a) => a,
Err(err) => return err,
};
let path = route_args.path.clone();
let mut input_fn = match parse::parse_async_handler(item) {
Ok(f) => f,
Err(err) => return err,
};
let interceptors = parse::extract_interceptors(&mut input_fn.attrs);
let api_doc_attr = match api_doc::extract(&mut input_fn.attrs) {
Ok(v) => v,
Err(err) => return err,
};
let fn_name = &input_fn.sig.ident;
let route_info_name = format_ident!("__autumn_route_info_{}", fn_name);
let vis = &input_fn.vis;
let helper_ident = route_args.helper_ident(fn_name);
let path_helper_name = format_ident!("__autumn_path_{}", helper_ident);
let fn_name_alias = emit_fn_name_alias(
route_args.name_override.as_ref(),
fn_name,
&path_helper_name,
);
let method_const = format_ident!("{}", http_method); let routing_fn = format_ident!("{}", axum_fn);
let primitive_wrapper = if should_stringify_primitive_output(&input_fn.sig.output) {
let wrapper_name = format_ident!("__autumn_primitive_handler_{}", fn_name);
let mut wrapper_inputs = Vec::new();
let mut call_args = Vec::new();
for (idx, arg) in input_fn.sig.inputs.iter().enumerate() {
match arg {
FnArg::Typed(pat_type) => {
let arg_name = format_ident!("__autumn_arg_{idx}");
let ty = &pat_type.ty;
wrapper_inputs.push(quote! { #arg_name: #ty });
call_args.push(quote! { #arg_name });
}
FnArg::Receiver(receiver) => {
return syn::Error::new_spanned(
receiver,
"Autumn route handlers cannot take a self receiver",
)
.to_compile_error();
}
}
}
Some(quote! {
#[doc(hidden)]
async fn #wrapper_name(#(#wrapper_inputs),*) -> ::std::string::String {
#fn_name(#(#call_args),*).await.to_string()
}
})
} else {
None
};
let handler_name = primitive_wrapper.as_ref().map_or_else(
|| fn_name.clone(),
|_| format_ident!("__autumn_primitive_handler_{}", fn_name),
);
let handler_expr = build_handler_expr(&routing_fn, &handler_name, &interceptors);
let path_params = api_doc::extract_path_params(&path.value());
let path_params_tokens = api_doc::emit_path_param_slice(&path_params);
let request_body = api_doc::schema_option(api_doc::infer_request_body(&input_fn));
let response_body = api_doc::schema_option(api_doc::infer_response_body(&input_fn));
let query_schema = api_doc::schema_option(api_doc::infer_query_params(&input_fn));
let (secured, required_roles) = api_doc::extract_secured_info(&input_fn);
let api_doc_fields = api_doc_attr.emit_ident_fields(fn_name);
let http_method_lit = LitStr::new(http_method, Span::call_site());
let path_helper = emit_path_helper(&path_helper_name, &path, &path_params);
quote! {
#input_fn
#primitive_wrapper
#[doc(hidden)]
#vis fn #route_info_name() -> ::autumn_web::Route {
::autumn_web::Route {
method: ::autumn_web::reexports::http::Method::#method_const,
path: #path,
handler: #handler_expr,
name: ::core::stringify!(#fn_name),
api_doc: ::autumn_web::openapi::ApiDoc {
method: #http_method_lit,
path: #path,
path_params: #path_params_tokens,
request_body: #request_body,
response: #response_body,
query_schema: #query_schema,
secured: #secured,
required_roles: #required_roles,
register_schemas: ::core::option::Option::None,
#api_doc_fields
},
repository: ::core::option::Option::None,
}
}
#path_helper
#fn_name_alias
}
}
fn build_handler_expr(
routing_fn: &proc_macro2::Ident,
handler_name: &proc_macro2::Ident,
interceptors: &[syn::Path],
) -> TokenStream {
let mut expr = quote! { ::autumn_web::reexports::axum::routing::#routing_fn(#handler_name) };
for interceptor in interceptors.iter().rev() {
expr = quote! {
::autumn_web::reexports::axum::routing::MethodRouter::<
::autumn_web::AppState, ::core::convert::Infallible
>::layer(#expr, #interceptor)
};
}
expr
}
fn emit_fn_name_alias(
name_override: Option<&syn::LitStr>,
fn_name: &proc_macro2::Ident,
path_helper_name: &proc_macro2::Ident,
) -> TokenStream {
let fn_path_helper_name = format_ident!("__autumn_path_{}", fn_name);
if name_override.is_some() && fn_path_helper_name != *path_helper_name {
quote! {
#[doc(hidden)]
pub use self::#path_helper_name as #fn_path_helper_name;
}
} else {
quote! {}
}
}
fn emit_path_helper(
helper_name: &proc_macro2::Ident,
path: &LitStr,
params: &[String],
) -> TokenStream {
let param_idents: Vec<proc_macro2::Ident> = params
.iter()
.map(|p| {
let sanitized = p.trim_start_matches('*').replace('-', "_");
proc_macro2::Ident::new_raw(&sanitized, proc_macro2::Span::call_site())
})
.collect();
let format_str = positional_format_string(&path.value());
let format_lit = LitStr::new(&format_str, path.span());
let encoded_params: Vec<TokenStream> = param_idents
.iter()
.map(|ident| quote! { ::autumn_web::paths::encode_path_segment(#ident) })
.collect();
quote! {
#[doc(hidden)]
pub fn #helper_name(#(#param_idents: impl ::std::fmt::Display),*) -> ::std::string::String {
format!(#format_lit, #(#encoded_params),*)
}
}
}
fn positional_format_string(path: &str) -> String {
let mut result = String::with_capacity(path.len());
let mut chars = path.chars().peekable();
while let Some(c) = chars.next() {
match c {
'{' if chars.peek() == Some(&'{') => {
chars.next();
result.push_str("{{");
}
'{' => {
result.push_str("{}");
let mut depth: u32 = 1;
for inner in chars.by_ref() {
match inner {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
}
}
'}' if chars.peek() == Some(&'}') => {
chars.next();
result.push_str("}}");
}
_ => result.push(c),
}
}
result
}
fn should_stringify_primitive_output(output: &ReturnType) -> bool {
let ReturnType::Type(_, ty) = output else {
return false;
};
let Type::Path(path) = ty.as_ref() else {
return false;
};
if path.qself.is_some() || path.path.segments.len() != 1 {
return false;
}
let ident = path.path.segments[0].ident.to_string();
matches!(
ident.as_str(),
"bool"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "f32"
| "f64"
)
}
#[cfg(test)]
mod tests {
use super::positional_format_string;
#[test]
fn positional_plain_params() {
assert_eq!(positional_format_string("/posts/{id}"), "/posts/{}");
}
#[test]
fn positional_regex_constrained_params() {
assert_eq!(positional_format_string("/users/{id:[0-9]+}"), "/users/{}");
}
#[test]
fn positional_multiple_params() {
assert_eq!(
positional_format_string("/posts/{year}/{slug}"),
"/posts/{}/{}"
);
}
#[test]
fn positional_static_path() {
assert_eq!(positional_format_string("/hello"), "/hello");
}
#[test]
fn positional_catch_all_param() {
assert_eq!(positional_format_string("/files/{*path}"), "/files/{}");
}
#[test]
fn positional_hyphenated_param() {
assert_eq!(positional_format_string("/items/{item-id}"), "/items/{}");
}
#[test]
fn positional_regex_with_quantifier_braces() {
assert_eq!(
positional_format_string("/users/{id:[0-9]{1,3}}"),
"/users/{}"
);
}
#[test]
fn positional_keyword_param() {
assert_eq!(positional_format_string("/items/{type}"), "/items/{}");
}
#[test]
fn positional_escaped_braces_pass_through() {
assert_eq!(positional_format_string("/{{hello}}"), "/{{hello}}");
assert_eq!(
positional_format_string("/{{literal}}/{id}"),
"/{{literal}}/{}"
);
}
}