use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn auto_cli(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let vis = &input.vis;
let sig = &input.sig;
let fn_name = &sig.ident;
let doc_attrs: Vec<String> = input
.attrs
.iter()
.filter_map(|a| {
if !a.path().is_ident("doc") {
return None;
}
match &a.meta {
syn::Meta::NameValue(nv) => {
if let syn::Expr::Lit(expr_lit) = &nv.value {
if let syn::Lit::Str(s) = &expr_lit.lit {
return Some(s.value());
}
}
None
}
_ => None,
}
})
.collect();
let doc_comment = doc_attrs.join("\n");
let struct_name = format_ident!("{}Args", fn_name);
let mut fields = Vec::new();
let mut call_args = Vec::new();
for arg in sig.inputs.iter() {
let syn::FnArg::Typed(pat_type) = arg else {
return syn::Error::new_spanned(arg, "methods with self not supported")
.to_compile_error()
.into();
};
let ident = match &*pat_type.pat {
syn::Pat::Ident(p) => &p.ident,
_ => {
return syn::Error::new_spanned(&pat_type.pat, "only ident patterns supported")
.to_compile_error()
.into();
}
};
let ty = &*pat_type.ty;
let mut default_lit: Option<String> = None;
for attr in &pat_type.attrs {
if !attr.path().is_ident("arg") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
default_lit = Some(lit.value());
}
Ok(())
});
}
let clap_attr = if let Some(d) = default_lit {
quote! { #[arg(long, default_value = #d)] }
} else {
quote! { #[arg(long)] }
};
fields.push(quote! {
#clap_attr
pub #ident: #ty
});
call_args.push(quote! { args.#ident });
}
let fn_name = &sig.ident;
let wrapper_name = format_ident!("{}_cli", fn_name);
let expanded = quote! {
#input
#[derive(Debug, clap::Parser)]
#[command(about = #doc_comment)]
#vis struct #struct_name {
#(#fields,)*
}
#vis fn #wrapper_name() {
use clap::Parser;
let args = #struct_name::parse();
let out = #fn_name(#(#call_args),*);
println!("{:?}", out);
}
};
expanded.into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = 4;
assert_eq!(result, 4);
}
}