use heck::ToUpperCamelCase;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{Ident, ItemEnum, Signature};
#[proc_macro_attribute]
pub fn clap_dispatch(attr: TokenStream, mut item: TokenStream) -> TokenStream {
let generated =
clap_dispatch_gen(&attr, &item).unwrap_or_else(|error| error.to_compile_error().into());
item.extend(generated);
item
}
fn clap_dispatch_gen(attr: &TokenStream, item: &TokenStream) -> Result<TokenStream, syn::Error> {
let item_enum: ItemEnum = syn::parse(item.clone())?;
let signature: Signature = syn::parse(attr.clone())?;
generate(item_enum, signature)
}
fn generate(
item_enum: ItemEnum,
signature: Signature,
) -> Result<TokenStream, syn::Error> {
validity_checks(&item_enum, &signature)?;
let enum_ident = item_enum.ident;
let signature_ident = &signature.ident;
let trait_ident = upper_camel_case(signature_ident);
let call_args = signature.inputs.iter().skip(1).map(|fn_arg| {
if let syn::FnArg::Typed(pat_type) = fn_arg {
&pat_type.pat
} else {
unreachable!()
}
});
let match_arms = item_enum.variants.into_iter().map(|variant| {
let variant_ident = variant.ident;
let call_args = call_args.clone();
quote! {
Self::#variant_ident(args) => self::#trait_ident::#signature_ident(args, #(#call_args),*),
}
});
let generated = quote! {
trait #trait_ident {
#signature;
}
impl #trait_ident for #enum_ident {
#signature {
match self {
#(#match_arms)*
}
}
}
};
Ok(generated.into())
}
fn upper_camel_case(ident: &Ident) -> Ident {
let new_ident = ident.to_string().to_upper_camel_case();
Ident::new(&new_ident, Span::call_site())
}
fn validity_checks(item_enum: &ItemEnum, signature: &Signature) -> Result<(), syn::Error> {
if item_enum.generics.lt_token.is_some() {
return Err(syn::Error::new_spanned(
&item_enum.generics,
"generics are not yet supported by clap-dispatch",
));
}
if signature.generics.lt_token.is_some() {
return Err(syn::Error::new_spanned(
&signature.generics,
"generics are not yet supported by clap-dispatch",
));
}
if signature.variadic.is_some() {
return Err(syn::Error::new_spanned(
&signature.variadic,
"variadics are not yet supported by clap-dispatch",
));
}
match signature.inputs.first() {
Some(fn_arg) => {
if !matches!(fn_arg, syn::FnArg::Receiver(_)) {
return Err(syn::Error::new_spanned(
fn_arg,
"first argument of function must be `self` or `&self` or `&mut self`",
));
}
}
None => {
return Err(syn::Error::new_spanned(
&signature.inputs,
"function needs at least a `self` argument (or `&self` or `&mut self`)",
))
}
}
for variant in item_enum.variants.iter() {
match &variant.fields {
syn::Fields::Named(fields_named) => {
return Err(syn::Error::new_spanned(
fields_named,
"must have unnamed field, not named",
));
}
syn::Fields::Unnamed(fields_unnamed) => {
if fields_unnamed.unnamed.len() != 1 {
return Err(syn::Error::new_spanned(
fields_unnamed,
"number of unnamed fields must be exactly one",
));
}
}
syn::Fields::Unit => {
return Err(syn::Error::new_spanned(
&variant.ident,
"variant must have an unnamed field",
));
}
};
}
Ok(())
}