use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use crate::diagnostic::{MacroError, MacroErrorCode, MacroResult};
use crate::signature::validate_public_async_fn;
use crate::util::to_pascal_case;
pub fn command(attr: TokenStream, item: TokenStream) -> MacroResult {
let name = parse_name(attr)?;
let item_fn: syn::ItemFn =
syn::parse2(item).map_err(|e| MacroError::from_syn(MacroErrorCode::ArcM001, e))?;
validate_public_async_fn(&item_fn, MacroErrorCode::ArcM011, "#[command(...)]")?;
let fn_ident = &item_fn.sig.ident;
let fn_name = fn_ident.to_string();
let const_ident = syn::Ident::new(
&format!("{}_COMMAND", fn_name.to_uppercase()),
fn_ident.span(),
);
let command_ident = syn::Ident::new(
&format!("{}Command", to_pascal_case(&fn_name)),
fn_ident.span(),
);
let dependency_types = dependency_types(&item_fn)?;
let dependency_idents: Vec<syn::Ident> = (0..dependency_types.len())
.map(|i| format_ident!("__arc_dependency_{i}"))
.collect();
let doc = format!(
"The command type for [`{fn_name}`], generated by \
`#[command(\"{name}\")]`.\n\n\
Hand it to the registry with \
`CommandRegistry::register_command::<{command_ident}>()`; the \
registry then answers to `\"{name}\"`."
);
Ok(quote! {
#item_fn
#[allow(non_upper_case_globals)]
pub const #const_ident: ::arcature::CommandBinding =
::arcature::CommandBinding { name: #name, function: #fn_name };
#[doc = #doc]
#[derive(Debug, Clone, Copy)]
pub struct #command_ident;
impl<S> ::arcature::Command<S> for #command_ident
where
S: ::core::marker::Send + ::core::marker::Sync + 'static,
#(#dependency_types: ::arcature::Resolve<S>,)*
{
const NAME: &'static str = #name;
fn run(state: &S) -> ::arcature::dx::CommandFuture {
#(
let #dependency_idents =
<#dependency_types as ::arcature::Resolve<S>>::resolve(state);
)*
::std::boxed::Box::pin(async move {
let __arc_outcome: ::std::result::Result<(), _> =
#fn_ident(#(#dependency_idents),*).await;
__arc_outcome.map_err(|__arc_error| {
::arcature::CommandError::Failed(
::std::string::ToString::to_string(&__arc_error),
)
})
})
}
}
})
}
fn dependency_types(item_fn: &syn::ItemFn) -> Result<Vec<&syn::Type>, MacroError> {
let sig = &item_fn.sig;
if !sig.generics.params.is_empty() {
return Err(MacroError::new(
MacroErrorCode::ArcM011,
sig.ident.span(),
"#[command(...)] functions must not be generic -- the registry \
stores one handler per name and has nothing to instantiate a \
type parameter with.",
));
}
sig.inputs
.iter()
.map(|input| match input {
syn::FnArg::Typed(pat_type) => match &*pat_type.ty {
syn::Type::Reference(reference) => Err(MacroError::new(
MacroErrorCode::ArcM011,
reference.span(),
"#[command(...)] parameters must be owned values: each is \
resolved from application state via `Resolve<S>` and moved \
into a future the registry outlives.",
)),
ty => Ok(ty),
},
syn::FnArg::Receiver(receiver) => Err(MacroError::new(
MacroErrorCode::ArcM011,
receiver.span(),
"#[command(...)] applies to a free function, not a method -- \
there is no receiver for the registry to supply.",
)),
})
.collect()
}
fn parse_name(attr: TokenStream) -> Result<String, MacroError> {
let lit: syn::LitStr =
syn::parse2(attr).map_err(|e| MacroError::from_syn(MacroErrorCode::ArcM009, e))?;
let name = lit.value();
if name.is_empty() {
return Err(MacroError::new(
MacroErrorCode::ArcM009,
lit.span(),
"#[command(\"...\")] name must not be empty",
));
}
Ok(name)
}
#[cfg(test)]
mod tests {
use super::*;
fn expand(attr: TokenStream, item: TokenStream) -> String {
command(attr, item).unwrap().to_string()
}
#[test]
fn generates_command_binding_const() {
let s = expand(
quote! { "users:prune" },
quote! { pub async fn prune_users() -> Result<()> { Ok(()) } },
);
assert!(s.contains("\"users:prune\""), "got: {s}");
assert!(s.contains("PRUNE_USERS_COMMAND"), "got: {s}");
assert!(s.contains("CommandBinding"), "got: {s}");
}
#[test]
fn generates_a_command_type_named_after_the_function() {
let s = expand(
quote! { "users:prune" },
quote! { pub async fn prune_users() -> Result<()> { Ok(()) } },
);
assert!(s.contains("pub struct PruneUsersCommand"), "got: {s}");
assert!(
s.contains("impl < S > :: arcature :: Command < S > for PruneUsersCommand"),
"got: {s}"
);
assert!(
s.contains("const NAME : & 'static str = \"users:prune\""),
"got: {s}"
);
}
#[test]
fn resolves_each_parameter_from_application_state() {
let s = expand(
quote! { "users:prune" },
quote! {
pub async fn prune_users(users: UserService, db: Db) -> Result<()> { Ok(()) }
},
);
assert!(
s.contains("UserService : :: arcature :: Resolve < S >"),
"got: {s}"
);
assert!(s.contains("Db : :: arcature :: Resolve < S >"), "got: {s}");
assert!(
s.contains("< UserService as :: arcature :: Resolve < S >> :: resolve (state)"),
"got: {s}"
);
assert!(
s.contains("prune_users (__arc_dependency_0 , __arc_dependency_1)"),
"got: {s}"
);
}
#[test]
fn maps_the_functions_error_onto_command_error() {
let s = expand(
quote! { "users:prune" },
quote! { pub async fn prune_users() -> Result<()> { Ok(()) } },
);
assert!(
s.contains(":: arcature :: CommandError :: Failed"),
"got: {s}"
);
}
#[test]
fn emits_the_function_unchanged() {
let s = expand(
quote! { "users:prune" },
quote! { pub async fn prune_users() -> Result<()> { Ok(()) } },
);
assert!(
s.contains("pub async fn prune_users () -> Result < () >"),
"got: {s}"
);
}
#[test]
fn rejects_non_fn_item() {
let err = command(quote! { "test" }, quote! { pub struct Foo {} }).unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM001);
}
#[test]
fn rejects_empty_name() {
let err = command(
quote! { "" },
quote! { pub async fn handle() -> Result<()> { Ok(()) } },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM009);
}
#[test]
fn rejects_missing_name() {
let err = command(
quote! {},
quote! { pub async fn handle() -> Result<()> { Ok(()) } },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM009);
}
#[test]
fn rejects_non_string_name() {
let err = command(
quote! { 42 },
quote! { pub async fn handle() -> Result<()> { Ok(()) } },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM009);
}
#[test]
fn rejects_bad_signature() {
let err = command(
quote! { "test" },
quote! { pub fn handle() -> Result<()> { Ok(()) } },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM011);
}
#[test]
fn rejects_a_generic_function() {
let err = command(
quote! { "test" },
quote! { pub async fn handle<T>(dep: T) -> Result<()> { Ok(()) } },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM011);
assert!(err.to_compile_error().to_string().contains("generic"));
}
#[test]
fn rejects_a_reference_parameter() {
let err = command(
quote! { "test" },
quote! { pub async fn handle(db: &Db) -> Result<()> { Ok(()) } },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM011);
assert!(err.to_compile_error().to_string().contains("owned values"));
}
}