type_cli_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{self, Attribute, Item};
4
5macro_rules! crate_path {
6    ($typ: tt) => {{
7        let crate_name = proc_macro_crate::crate_name("type-cli")
8            .expect("`type-cli` is present in `Cargo.toml`");
9        let crate_name = quote::format_ident!("{}", crate_name);
10        quote::quote! { ::#crate_name::$typ }
11    }};
12    () => {{
13        let crate_name = proc_macro_crate::crate_name("type-cli")
14            .expect("`type-cli` is present in `Cargo.toml`");
15        let crate_name = quote::format_ident!("{}", crate_name);
16        quote::quote! { ::#crate_name }
17    }};
18}
19
20macro_rules! try_help {
21    ($iter: expr) => {{
22        let mut iter = $iter;
23        if let Some(help) = iter.find(|a| a.path.is_ident("help")) {
24            match $crate::parse_help(help) {
25                Ok(help) => Some(help),
26                Err(e) => return e.to_compile_error().into(),
27            }
28        } else {
29            None
30        }
31    }};
32}
33
34mod enum_cmd;
35mod struct_cmd;
36
37#[proc_macro_derive(CLI, attributes(help, named, flag, optional, variadic))]
38pub fn cli(item: TokenStream) -> TokenStream {
39    let parse_ty = crate_path!(Parse);
40    let err_ty = crate_path!(Error);
41    let cli_ty = crate_path!(CLI);
42
43    let input: Item = syn::parse(item).expect("failed to parse");
44
45    let iter_ident = format_ident!("ARGS_ITER");
46    let cmd_ident;
47
48    let body = match input {
49        Item::Enum(item) => {
50            cmd_ident = item.ident;
51            enum_cmd::parse(&cmd_ident, item.attrs, item.variants, &iter_ident)
52        }
53        Item::Struct(item) => {
54            cmd_ident = item.ident.clone();
55            struct_cmd::parse(item.ident, item.attrs, item.fields, &iter_ident)
56        }
57        _ => panic!("Only allowed on structs and enums."),
58    };
59
60    let ret = quote! {
61        impl #cli_ty for #cmd_ident {
62            fn parse(mut #iter_ident : impl std::iter::Iterator<Item=String>) -> Result<#parse_ty<#cmd_ident>, #err_ty> {
63                let _ = #iter_ident.next();
64                let ret = {
65                    #body
66                };
67                Ok(#parse_ty::Success(ret))
68            }
69        }
70    };
71    ret.into()
72}
73
74fn parse_help(help: &Attribute) -> syn::Result<String> {
75    match help.parse_meta()? {
76        syn::Meta::NameValue(meta) => {
77            if let syn::Lit::Str(help) = meta.lit {
78                Ok(help.value())
79            } else {
80                Err(syn::Error::new_spanned(
81                    help.tokens.clone(),
82                    "Help message must be a string literal",
83                ))
84            }
85        }
86        _ => Err(syn::Error::new_spanned(
87            help.tokens.clone(),
88            r#"Help must be formatted as #[help = "msg"]"#,
89        )),
90    }
91}
92
93fn to_snake(ident: &impl ToString) -> String {
94    let ident = ident.to_string();
95    let mut val = String::with_capacity(ident.len());
96    for (i, ch) in ident.chars().enumerate() {
97        if ch.is_uppercase() {
98            if i > 0 {
99                val.push('-');
100            }
101            val.push(ch.to_ascii_lowercase());
102        } else {
103            val.push(ch);
104        }
105    }
106    val
107}