use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::{Attribute, ImplItem, ItemImpl, LitStr};
use crate::attr_args::CommandAttrArgs;
use crate::command_attr::{expand_method, MethodExpansion};
pub fn expand(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
if !attr.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
"#[command_service] takes no arguments",
));
}
let mut item_impl: ItemImpl = syn::parse2(item)?;
if item_impl.trait_.is_some() {
return Err(syn::Error::new_spanned(
&item_impl,
"#[command_service] cannot be applied to a trait impl block",
));
}
if !item_impl.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
&item_impl.generics,
"#[command_service] does not yet support generic host types",
));
}
let host_ty_tokens = item_impl.self_ty.to_token_stream();
let host_ident = host_ident_from_self_ty(&item_impl.self_ty)?;
let mut extra_items = TokenStream::new();
let mut registrations: Vec<(LitStr, syn::Ident)> = Vec::new();
let mut seen_ids: Vec<LitStr> = Vec::new();
for item in item_impl.items.iter_mut() {
let ImplItem::Fn(method) = item else { continue };
let (cmd_attr_idx, cmd_attr) = match find_command_attr(&method.attrs) {
Some(pair) => pair,
None => continue,
};
let args: CommandAttrArgs = cmd_attr.parse_args().map_err(|mut e| {
e.combine(syn::Error::new_spanned(cmd_attr, "in this #[command]"));
e
})?;
if let Some(prev) = seen_ids.iter().find(|p| p.value() == args.id.value()) {
let mut err = syn::Error::new_spanned(
&args.id,
format!(
"duplicate command id `{}` in this #[command_service] block",
args.id.value()
),
);
err.combine(syn::Error::new_spanned(prev, "previously defined here"));
return Err(err);
}
seen_ids.push(args.id.clone());
let id_lit = args.id.clone();
let MethodExpansion {
items: gen_items,
struct_ident,
} = expand_method(args, method, &host_ty_tokens, &host_ident)?;
extra_items.extend(gen_items);
registrations.push((id_lit, struct_ident));
method.attrs.remove(cmd_attr_idx);
}
if registrations.is_empty() {
return Err(syn::Error::new_spanned(
&item_impl,
"#[command_service] impl block contains no #[command(\"...\")] methods",
));
}
let mod_ident = format_ident!("{}", pascal_to_snake(&host_ident.to_string()));
let register_calls = registrations.iter().map(|(_, ident)| {
quote! {
registry
.register_command(
self::#mod_ident::#ident { host: ::std::sync::Arc::clone(&host) }
)
.await?;
}
});
let register_impl = quote! {
impl #host_ty_tokens {
#[allow(clippy::needless_pass_by_value)]
pub async fn register(
self,
registry: &::coralstack_cmd_ipc::CommandRegistry,
) -> ::core::result::Result<(), ::coralstack_cmd_ipc::CommandError> {
let host = ::std::sync::Arc::new(self);
#( #register_calls )*
::core::result::Result::Ok(())
}
}
};
let extra_items_mod = quote! {
#[doc(hidden)]
#[allow(non_snake_case, clippy::module_name_repetitions)]
pub mod #mod_ident {
use super::*;
#extra_items
}
};
Ok(quote! {
#item_impl
#extra_items_mod
#register_impl
})
}
fn pascal_to_snake(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
fn find_command_attr(attrs: &[Attribute]) -> Option<(usize, &Attribute)> {
attrs.iter().enumerate().find(|(_, a)| is_command_attr(a))
}
fn is_command_attr(attr: &Attribute) -> bool {
attr.path().is_ident("command")
}
fn host_ident_from_self_ty(ty: &syn::Type) -> syn::Result<syn::Ident> {
let syn::Type::Path(tp) = ty else {
return Err(syn::Error::new_spanned(
ty,
"#[command_service] host type must be a simple named type",
));
};
tp.path
.segments
.last()
.map(|s| s.ident.clone())
.ok_or_else(|| syn::Error::new_spanned(ty, "empty path for #[command_service] host type"))
}