use crate::diagnostic::{MacroError, MacroErrorCode};
pub fn validate_public_async_fn(
item_fn: &syn::ItemFn,
code: MacroErrorCode,
macro_name: &str,
) -> Result<(), MacroError> {
let sig = &item_fn.sig;
if !matches!(item_fn.vis, syn::Visibility::Public(_)) {
return Err(MacroError::new(
code,
sig.ident.span(),
format!("{macro_name} functions must be `pub`"),
));
}
if sig.asyncness.is_none() {
return Err(MacroError::new(
code,
sig.ident.span(),
format!("{macro_name} functions must be `async fn`"),
));
}
if matches!(sig.output, syn::ReturnType::Default) {
return Err(MacroError::new(
code,
sig.ident.span(),
format!("{macro_name} functions must declare a return type"),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
fn parse_fn(tokens: proc_macro2::TokenStream) -> syn::ItemFn {
syn::parse2(tokens).expect("valid fn")
}
#[test]
fn accepts_pub_async_fn_with_return_type() {
let f = parse_fn(quote! { pub async fn handler() -> Response { todo!() } });
assert!(validate_public_async_fn(&f, MacroErrorCode::ArcM007, "#[middleware]").is_ok());
}
#[test]
fn rejects_private_fn() {
let f = parse_fn(quote! { async fn handler() -> Response { todo!() } });
let err =
validate_public_async_fn(&f, MacroErrorCode::ArcM007, "#[middleware]").unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM007);
assert!(err.to_compile_error().to_string().contains("pub"));
}
#[test]
fn rejects_sync_fn() {
let f = parse_fn(quote! { pub fn handler() -> Response { todo!() } });
let err = validate_public_async_fn(&f, MacroErrorCode::ArcM008, "#[listener]").unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM008);
assert!(err.to_compile_error().to_string().contains("async"));
}
#[test]
fn rejects_missing_return_type() {
let f = parse_fn(quote! { pub async fn handler() { } });
let err =
validate_public_async_fn(&f, MacroErrorCode::ArcM010, "#[job_handler]").unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM010);
assert!(err.to_compile_error().to_string().contains("return type"));
}
}