use syn::{Attribute, Error, ImplItem, ImplItemFn, Result, Type};
#[derive(Debug, Clone)]
pub struct HandlerMethod {
pub name: String,
pub ident: syn::Ident,
pub body_type: Option<Type>,
pub has_headers: bool,
pub has_exchange: bool,
pub return_type: Option<Type>,
}
pub fn find_handler_methods(items: &[ImplItem]) -> Result<Vec<HandlerMethod>> {
let mut handlers = Vec::new();
let mut seen_names = std::collections::HashSet::new();
for item in items {
if let ImplItem::Fn(method) = item
&& has_handler_attribute(&method.attrs)
{
let handler = parse_handler_method(method)?;
if !seen_names.insert(handler.name.clone()) {
return Err(Error::new_spanned(
&method.sig,
format!("Duplicate handler method name: '{}'", handler.name),
));
}
handlers.push(handler);
}
}
Ok(handlers)
}
fn has_handler_attribute(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
let path = attr.path();
if path.is_ident("handler") {
return true;
}
let segments = &path.segments;
if let Some(last) = segments.last()
&& last.ident == "handler"
{
return true;
}
false
})
}
fn parse_handler_method(method: &ImplItemFn) -> Result<HandlerMethod> {
let name = method.sig.ident.to_string();
let ident = method.sig.ident.clone();
let mut body_type = None;
let mut has_headers = false;
let mut has_exchange = false;
let mut has_self = false;
let mut param_names = std::collections::HashSet::new();
for input in &method.sig.inputs {
match input {
syn::FnArg::Receiver(receiver) => {
if receiver.reference.is_some() {
has_self = true;
} else {
return Err(Error::new_spanned(
receiver,
"Handler methods must use &self, not self",
));
}
}
syn::FnArg::Typed(pat_type) => {
let param_name = match &*pat_type.pat {
syn::Pat::Ident(pat_ident) => pat_ident.ident.to_string(),
_ => continue,
};
if !param_names.insert(param_name.clone()) {
return Err(Error::new_spanned(
pat_type,
format!("Duplicate parameter name: '{}'", param_name),
));
}
if param_name == "body" {
body_type = Some((*pat_type.ty).clone());
} else if param_name == "headers" {
has_headers = true;
} else if param_name == "exchange" {
has_exchange = true;
}
}
}
}
if !has_self {
return Err(Error::new_spanned(
&method.sig,
"Handler methods must have &self as first parameter",
));
}
if method.sig.asyncness.is_none() {
return Err(Error::new_spanned(
&method.sig,
"Handler methods must be async",
));
}
let return_type = match &method.sig.output {
syn::ReturnType::Default => None, syn::ReturnType::Type(_, ty) => Some((**ty).clone()),
};
Ok(HandlerMethod {
name,
ident,
body_type,
has_headers,
has_exchange,
return_type,
})
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_has_handler_attribute_simple() {
let method: ImplItemFn = parse_quote! {
#[handler]
pub async fn process(&self) {}
};
assert!(has_handler_attribute(&method.attrs));
}
#[test]
fn test_has_handler_attribute_qualified() {
let method: ImplItemFn = parse_quote! {
#[camel_bean::handler]
pub async fn process(&self) {}
};
assert!(has_handler_attribute(&method.attrs));
}
#[test]
fn test_has_handler_attribute_none() {
let method: ImplItemFn = parse_quote! {
pub async fn process(&self) {}
};
assert!(!has_handler_attribute(&method.attrs));
}
#[test]
fn test_parse_handler_method_body_only() {
let method: ImplItemFn = parse_quote! {
#[handler]
pub async fn process(&self, body: String) {}
};
let result = parse_handler_method(&method).unwrap();
assert_eq!(result.name, "process");
assert!(result.body_type.is_some());
assert!(!result.has_headers);
assert!(!result.has_exchange);
}
#[test]
fn test_parse_handler_method_multiple_params() {
let method: ImplItemFn = parse_quote! {
#[handler]
pub async fn process(&self, body: Order, headers: Headers) {}
};
let result = parse_handler_method(&method).unwrap();
assert!(result.body_type.is_some());
assert!(result.has_headers);
assert!(!result.has_exchange);
}
#[test]
fn test_parse_handler_method_exchange() {
let method: ImplItemFn = parse_quote! {
#[handler]
pub async fn process(&self, exchange: &mut Exchange) {}
};
let result = parse_handler_method(&method).unwrap();
assert!(result.body_type.is_none());
assert!(!result.has_headers);
assert!(result.has_exchange);
}
#[test]
fn test_parse_handler_method_non_async_error() {
let method: ImplItemFn = parse_quote! {
#[handler]
pub fn process(&self) {}
};
let result = parse_handler_method(&method);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("must be async"));
}
#[test]
fn test_parse_handler_method_missing_self_error() {
let method: ImplItemFn = parse_quote! {
#[handler]
pub async fn process() {}
};
let result = parse_handler_method(&method);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("&self"));
}
#[test]
fn test_find_handler_methods_multiple() {
let items: Vec<ImplItem> = vec![
parse_quote! {
#[handler]
pub async fn process(&self, body: String) {}
},
parse_quote! {
#[handler]
pub async fn validate(&self, body: String) {}
},
parse_quote! {
pub fn helper(&self) {}
},
];
let result = find_handler_methods(&items).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].name, "process");
assert_eq!(result[1].name, "validate");
}
#[test]
fn test_find_handler_methods_duplicate_name_error() {
let items: Vec<ImplItem> = vec![
parse_quote! {
#[handler]
pub async fn process(&self, body: String) {}
},
parse_quote! {
#[handler]
pub async fn process(&self, body: i32) {}
},
];
let result = find_handler_methods(&items);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Duplicate"));
}
}