use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse2, FnArg, ImplItem, ItemImpl, Meta, Pat, PatIdent, PatType, Result};
fn is_action_method(method: &syn::ImplItemFn) -> bool {
if method.sig.asyncness.is_none() {
return false;
}
method.sig.inputs.iter().any(|arg| {
if let FnArg::Typed(PatType { pat, .. }) = arg {
if let Pat::Ident(PatIdent { ident, .. }) = pat.as_ref() {
return ident == "ctx";
}
}
false
})
}
struct FilterAttr {
filter: proc_macro2::Ident,
only: Option<Vec<String>>,
except: Option<Vec<String>>,
}
impl FilterAttr {
fn applies_to(&self, action: &str) -> bool {
if let Some(list) = &self.only {
list.iter().any(|a| a == action)
} else if let Some(list) = &self.except {
!list.iter().any(|a| a == action)
} else {
true
}
}
}
fn parse_action_list(tokens: &str, keyword: &str) -> Option<Vec<String>> {
let after = &tokens[tokens.find(keyword)? + keyword.len()..];
let start = after.find('[')? + 1;
let end = after.find(']')?;
Some(
after[start..end]
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
)
}
fn parse_filter_attr(attr: &syn::Attribute) -> Option<FilterAttr> {
let path_ident = attr.meta.path().get_ident()?.to_string();
if path_ident != "before_action" && path_ident != "after_action" {
return None;
}
let Meta::List(list) = &attr.meta else {
return None;
};
let tokens_str = list.tokens.to_string();
let filter_name = tokens_str.split(',').next()?.trim().to_string();
let filter: proc_macro2::Ident = syn::parse_str(&filter_name).ok()?;
let only = if tokens_str.contains("only") {
parse_action_list(&tokens_str, "only")
} else {
None
};
let except = if tokens_str.contains("except") {
parse_action_list(&tokens_str, "except")
} else {
None
};
Some(FilterAttr {
filter,
only,
except,
})
}
fn parse_skip_filters(method: &syn::ImplItemFn) -> Vec<String> {
method
.attrs
.iter()
.filter_map(|attr| {
let attr_name = attr.meta.path().get_ident()?.to_string();
if attr_name != "skip_before_action" {
return None;
}
let Meta::List(list) = &attr.meta else {
return None;
};
let name = list.tokens.to_string();
Some(name.split(',').next()?.trim().to_string())
})
.collect()
}
fn parse_around_filter(method: &syn::ImplItemFn) -> Option<proc_macro2::Ident> {
method.attrs.iter().find_map(|attr| {
let attr_name = attr.meta.path().get_ident()?.to_string();
if attr_name != "around_action" {
return None;
}
let Meta::List(list) = &attr.meta else {
return None;
};
syn::parse_str(list.tokens.to_string().split(',').next()?.trim()).ok()
})
}
pub fn expand_controller(_attr: TokenStream, item: TokenStream) -> Result<TokenStream> {
let mut impl_block: ItemImpl = parse2(item)?;
let self_ty = impl_block.self_ty.clone();
let mut handler_fns: Vec<TokenStream> = Vec::new();
let mut action_fns: Vec<TokenStream> = Vec::new();
for impl_item in &impl_block.items {
let ImplItem::Fn(method) = impl_item else {
continue;
};
if !is_action_method(method) {
continue;
}
let fn_name = &method.sig.ident;
let fn_name_str = fn_name.to_string();
let body = &method.block;
let ret_ty = match &method.sig.output {
syn::ReturnType::Type(_, ty) => quote! { #ty },
syn::ReturnType::Default => quote! { ::axum::response::Response },
};
let action_fn = quote::format_ident!("__doido_action_{}", fn_name);
action_fns.push(quote! {
async fn #action_fn(ctx: &mut ::doido_controller::Context) -> #ret_ty #body
});
let mut before_chain: Vec<TokenStream> = Vec::new();
let mut after_chain: Vec<TokenStream> = Vec::new();
let skipped = parse_skip_filters(method);
for attr in &method.attrs {
let path_name = attr
.meta
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if path_name == "before_action" {
if let Some(filter) = parse_filter_attr(attr) {
let filter_fn = &filter.filter;
let is_skipped = skipped.iter().any(|s| filter_fn == s);
if filter.applies_to(&fn_name_str) && !is_skipped {
before_chain.push(quote! {
if let Err(__early_response) = #filter_fn(&mut ctx).await {
return __early_response;
}
});
}
}
} else if path_name == "after_action" {
if let Some(filter) = parse_filter_attr(attr) {
let filter_fn = &filter.filter;
let is_skipped = skipped.iter().any(|s| filter_fn == s);
if filter.applies_to(&fn_name_str) && !is_skipped {
after_chain.push(quote! {
#filter_fn(&mut ctx).await;
});
}
}
}
}
let invoke = match parse_around_filter(method) {
Some(around_fn) => quote! {
let __response = #around_fn(&mut ctx, async |__ctx: &mut ::doido_controller::Context| {
let __action_result = Self::#action_fn(__ctx).await;
::doido_controller::IntoActionResponse::into_action_response(__action_result)
}).await;
},
None => quote! {
let __action_result = Self::#action_fn(&mut ctx).await;
let __response =
::doido_controller::IntoActionResponse::into_action_response(__action_result);
},
};
handler_fns.push(quote! {
pub async fn #fn_name(
req: ::axum::extract::Request,
) -> ::axum::response::Response {
#[allow(unused_mut)]
let mut ctx = ::doido_controller::Context::build(req).await;
#(#before_chain)*
#invoke
#(#after_chain)*
let mut __response = __response;
ctx.commit_to_response(&mut __response);
__response
}
});
}
impl_block.items.retain(|item| {
if let ImplItem::Fn(method) = item {
return !is_action_method(method);
}
true
});
Ok(quote! {
#impl_block
impl #self_ty {
#(#action_fns)*
#(#handler_fns)*
}
})
}