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}