Skip to main content

autocli_macros/
lib.rs

1// mod <module>;
2// pub use <module>::*;
3use proc_macro::TokenStream;
4use quote::{format_ident, quote};
5use syn::{parse_macro_input, ItemFn};
6
7#[proc_macro_attribute]
8pub fn auto_cli(_attr: TokenStream, item: TokenStream) -> TokenStream {
9    let input = parse_macro_input!(item as ItemFn);
10    let vis = &input.vis;
11    let sig = &input.sig;
12    let fn_name = &sig.ident;
13
14    // doc comments: #[doc = "..."]
15    let doc_attrs: Vec<String> = input
16    .attrs
17    .iter()
18    .filter_map(|a| {
19        if !a.path().is_ident("doc") {
20            return None;
21        }
22        match &a.meta {
23            syn::Meta::NameValue(nv) => {
24                if let syn::Expr::Lit(expr_lit) = &nv.value {
25                    if let syn::Lit::Str(s) = &expr_lit.lit {
26                        return Some(s.value());
27                    }
28                }
29                None
30            }
31            _ => None,
32        }
33    })
34    .collect();
35    let doc_comment = doc_attrs.join("\n");
36
37    // struct name: <fn>Args (simple)
38    let struct_name = format_ident!("{}Args", fn_name);
39
40    let mut fields = Vec::new();
41    let mut call_args = Vec::new();
42
43    for arg in sig.inputs.iter() {
44        let syn::FnArg::Typed(pat_type) = arg else {
45            return syn::Error::new_spanned(arg, "methods with self not supported")
46            .to_compile_error()
47            .into();
48        };
49
50        let ident = match &*pat_type.pat {
51            syn::Pat::Ident(p) => &p.ident,
52            _ => {
53                return syn::Error::new_spanned(&pat_type.pat, "only ident patterns supported")
54                .to_compile_error()
55                .into();
56            }
57        };
58        let ty = &*pat_type.ty;
59
60        // parse #[arg(default = "...")]
61        let mut default_lit: Option<String> = None;
62        for attr in &pat_type.attrs {
63            if !attr.path().is_ident("arg") {
64                continue;
65            }
66            let _ = attr.parse_nested_meta(|meta| {
67                if meta.path.is_ident("default") {
68                    let value = meta.value()?;
69                    let lit: syn::LitStr = value.parse()?;
70                    default_lit = Some(lit.value());
71                }
72                Ok(())
73            });
74        }
75
76        let clap_attr = if let Some(d) = default_lit {
77            quote! { #[arg(long, default_value = #d)] }
78        } else {
79            quote! { #[arg(long)] }
80        };
81
82        fields.push(quote! {
83            #clap_attr
84            pub #ident: #ty
85        });
86
87        call_args.push(quote! { args.#ident });
88    }
89
90    let fn_name = &sig.ident;
91    let wrapper_name = format_ident!("{}_cli", fn_name);
92
93    let expanded = quote! {
94        #input
95
96        #[derive(Debug, clap::Parser)]
97        #[command(about = #doc_comment)]
98        #vis struct #struct_name {
99        #(#fields,)*
100        }
101
102        #vis fn #wrapper_name() {
103        use clap::Parser;
104        let args = #struct_name::parse();
105        let out = #fn_name(#(#call_args),*);
106        println!("{:?}", out);
107        }
108    };
109
110    expanded.into()
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn it_works() {
119        let result = 4;
120        assert_eq!(result, 4);
121    }
122}