use quote::quote;
use syn::ItemFn;
pub fn has_input_named(func: &ItemFn, name: &str) -> bool {
func.sig.inputs.iter().any(|arg| match arg {
syn::FnArg::Typed(pt) => pat_binds_name(&pt.pat, name),
syn::FnArg::Receiver(_) => false,
})
}
fn pat_binds_name(pat: &syn::Pat, name: &str) -> bool {
match pat {
syn::Pat::Ident(i) => i.ident == name,
syn::Pat::TupleStruct(ts) => ts.elems.iter().any(|p| pat_binds_name(p, name)),
syn::Pat::Tuple(t) => t.elems.iter().any(|p| pat_binds_name(p, name)),
syn::Pat::Struct(s) => s.fields.iter().any(|fp| pat_binds_name(&fp.pat, name)),
syn::Pat::Reference(r) => pat_binds_name(&r.pat, name),
_ => false,
}
}
const GUARD_GATE_TYPE_PREFIXES: &[&str] = &[
"__AutumnSecuredGate_",
"__AutumnStepUpGate_",
"__AutumnThrottleGate_",
"__AutumnFlagGate_",
];
pub fn has_any_guard_gate_param(func: &ItemFn) -> bool {
GUARD_GATE_TYPE_PREFIXES
.iter()
.any(|prefix| has_guard_gate_param_with_prefix(func, prefix))
}
pub fn has_guard_gate_param_with_prefix(func: &ItemFn, prefix: &str) -> bool {
func.sig.inputs.iter().any(|arg| {
let syn::FnArg::Typed(pat_type) = arg else {
return false;
};
type_name_starts_with(&pat_type.ty, prefix)
})
}
fn type_name_starts_with(ty: &syn::Type, prefix: &str) -> bool {
let syn::Type::Path(type_path) = ty else {
return false;
};
let Some(segment) = type_path.path.segments.last() else {
return false;
};
segment.ident.to_string().starts_with(prefix)
}
pub fn attr_or_cfg_attr_matches_any(attr: &syn::Attribute, names: &[&str]) -> bool {
let path_matches = |path: &syn::Path| {
path.segments
.last()
.is_some_and(|segment| names.contains(&segment.ident.to_string().as_str()))
};
if attr.path().is_ident("cfg_attr") {
let Ok(nested) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
) else {
return false;
};
return nested.iter().skip(1).any(|meta| path_matches(meta.path()));
}
path_matches(attr.path())
}
pub const STATIC_ROUTE_HANDLER_MARKER: &str = "__AUTUMN_STATIC_ROUTE_HANDLER_MARKER";
pub const WS_HANDLER_MARKER: &str = "__AUTUMN_WS_HANDLER_MARKER";
pub fn has_body_const_marker(func: &ItemFn, marker_name: &str) -> bool {
crate::edge::stmts_have_marker(&func.block.stmts, marker_name)
}
pub fn prepend_body_const_marker(func: &mut ItemFn, marker_name: &str) {
let marker_ident = syn::Ident::new(marker_name, proc_macro2::Span::call_site());
func.block.stmts.insert(
0,
syn::parse_quote! {
#[allow(dead_code)]
const #marker_ident: () = ();
},
);
}
pub fn reject_if_incompatible_route_marker(func: &ItemFn) -> Option<proc_macro2::TokenStream> {
if has_body_const_marker(func, STATIC_ROUTE_HANDLER_MARKER) {
return Some(
syn::Error::new_spanned(&func.sig, crate::static_route::INCOMPATIBLE_GUARD_MSG)
.to_compile_error(),
);
}
if has_body_const_marker(func, WS_HANDLER_MARKER) {
return Some(
syn::Error::new_spanned(&func.sig, crate::ws::INCOMPATIBLE_GUARD_MSG)
.to_compile_error(),
);
}
None
}
pub fn split_leading_items_and_reject_incompatible(
item: &proc_macro2::TokenStream,
attr_name: &str,
) -> Result<(proc_macro2::TokenStream, ItemFn), proc_macro2::TokenStream> {
let (leading_items, input_fn) = crate::parse::split_leading_items_and_fn(item)?;
if input_fn.sig.asyncness.is_none() {
return Err(syn::Error::new_spanned(
input_fn.sig.fn_token,
format!("#[{attr_name}] can only be applied to async functions"),
)
.to_compile_error());
}
if let Some(err) = reject_if_incompatible_route_marker(&input_fn) {
return Err(err);
}
Ok((leading_items, input_fn))
}
pub fn build_original_response(
original_body: &syn::Block,
output: &syn::ReturnType,
) -> proc_macro2::TokenStream {
match output {
syn::ReturnType::Default => quote! {
let __autumn_inner: () = (async move #original_body).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
},
syn::ReturnType::Type(_, ty) if type_contains_impl_trait(ty) => {
quote! {
::autumn_web::reexports::axum::response::IntoResponse::into_response(
(async move #original_body).await
)
}
}
syn::ReturnType::Type(_, ty) => quote! {
let __autumn_inner: #ty = (async move #original_body).await;
::autumn_web::reexports::axum::response::IntoResponse::into_response(__autumn_inner)
},
}
}
#[cfg(test)]
pub fn extract_fn_item(tokens: proc_macro2::TokenStream, name: &str) -> ItemFn {
let file: syn::File = syn::parse2(tokens).expect("generated tokens must parse as a file");
file.items
.into_iter()
.find_map(|item| match item {
syn::Item::Fn(f) if f.sig.ident == name => Some(f),
_ => None,
})
.unwrap_or_else(|| panic!("fn `{name}` not found among the generated items"))
}
pub fn type_contains_impl_trait(ty: &syn::Type) -> bool {
match ty {
syn::Type::ImplTrait(_) => true,
syn::Type::Path(tp) => tp.path.segments.iter().any(|seg| match &seg.arguments {
syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| match arg {
syn::GenericArgument::Type(t) => type_contains_impl_trait(t),
_ => false,
}),
syn::PathArguments::Parenthesized(args) => {
args.inputs
.iter()
.any(|arg| type_contains_impl_trait(&arg.ty))
|| matches!(&args.output,
syn::ReturnType::Type(_, t) if type_contains_impl_trait(t))
}
syn::PathArguments::None => false,
}),
syn::Type::Reference(r) => type_contains_impl_trait(&r.elem),
syn::Type::Tuple(t) => t.elems.iter().any(type_contains_impl_trait),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn detects_simple_ident_binding() {
let f: ItemFn = parse_quote! {
async fn h(__autumn_session: Session) {}
};
assert!(has_input_named(&f, "__autumn_session"));
assert!(!has_input_named(&f, "__autumn_state"));
}
#[test]
fn detects_tuple_struct_binding_for_state() {
let f: ItemFn = parse_quote! {
async fn h(State(__autumn_state): State<AppState>) {}
};
assert!(has_input_named(&f, "__autumn_state"));
assert!(!has_input_named(&f, "__autumn_session"));
}
#[test]
fn no_inputs_returns_false() {
let f: ItemFn = parse_quote! { async fn h() {} };
assert!(!has_input_named(&f, "__autumn_session"));
}
#[test]
fn finds_in_later_position() {
let f: ItemFn = parse_quote! {
async fn h(other: i32, __autumn_session: Session) {}
};
assert!(has_input_named(&f, "__autumn_session"));
}
#[test]
fn has_any_guard_gate_param_detects_each_kind() {
let secured: ItemFn = parse_quote! {
async fn h(_g: __AutumnSecuredGate_h) {}
};
let step_up: ItemFn = parse_quote! {
async fn h(_g: __AutumnStepUpGate_h) {}
};
let throttle: ItemFn = parse_quote! {
async fn h(_g: __AutumnThrottleGate_h) {}
};
assert!(has_any_guard_gate_param(&secured));
assert!(has_any_guard_gate_param(&step_up));
assert!(has_any_guard_gate_param(&throttle));
}
#[test]
fn has_any_guard_gate_param_false_without_one() {
let f: ItemFn = parse_quote! {
async fn h(Json(body): Json<T>) {}
};
assert!(!has_any_guard_gate_param(&f));
}
#[test]
fn has_any_guard_gate_param_detects_feature_flag_too() {
let flagged: ItemFn = parse_quote! {
async fn h(_g: __AutumnFlagGate_h) {}
};
assert!(has_any_guard_gate_param(&flagged));
}
}