Skip to main content

clikeys_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::{quote, quote_spanned};
4use syn::{
5    Attribute, Data, DataStruct, DeriveInput, Fields, Lit, Type, parse_macro_input,
6    spanned::Spanned,
7};
8
9#[proc_macro_derive(CliKeys, attributes(clikey))]
10pub fn derive_cli_keys(input: TokenStream) -> TokenStream {
11    let input = parse_macro_input!(input as DeriveInput);
12    match impl_clikeys(&input) {
13        Ok(ts) => ts.into(),
14        Err(e) => e.into_compile_error().into(),
15    }
16}
17
18struct FieldAttr {
19    rename: Option<String>,
20    help: Option<String>,
21    ns: Option<String>,
22    skip: bool,
23}
24
25fn parse_attrs(attrs: &[Attribute], field_name: &str) -> syn::Result<FieldAttr> {
26    let mut out = FieldAttr {
27        rename: None,
28        help: None,
29        ns: None,
30        skip: false,
31    };
32
33    for attr in attrs {
34        if !attr.path().is_ident("clikey") {
35            continue;
36        }
37        attr.parse_nested_meta(|meta| {
38            if meta.path.is_ident("rename") {
39                let value: Lit = meta.value()?.parse()?;
40                if let Lit::Str(s) = value {
41                    out.rename = Some(s.value());
42                    Ok(())
43                } else {
44                    Err(syn::Error::new(value.span(), "rename must be string"))
45                }
46            } else if meta.path.is_ident("help") {
47                let value: Lit = meta.value()?.parse()?;
48                if let Lit::Str(s) = value {
49                    out.help = Some(s.value());
50                    Ok(())
51                } else {
52                    Err(syn::Error::new(value.span(), "help must be string"))
53                }
54            } else if meta.path.is_ident("ns") {
55                if meta.input.peek(syn::Token![=]) {
56                    let value: Lit = meta.value()?.parse()?;
57                    if let Lit::Str(s) = value {
58                        out.ns = Some(s.value());
59                        Ok(())
60                    } else {
61                        Err(syn::Error::new(value.span(), "ns must be string"))
62                    }
63                } else {
64                    out.ns = Some(field_name.to_string());
65                    Ok(())
66                }
67            } else if meta.path.is_ident("skip") {
68                out.skip = true;
69                Ok(())
70            } else {
71                Err(meta.error("unknown attribute"))
72            }
73        })?;
74    }
75
76    Ok(out)
77}
78
79fn is_leaf_type(ty: &Type) -> Option<&'static str> {
80    match ty {
81        Type::Path(tp) => {
82            let last = tp.path.segments.last()?.ident.to_string();
83            match last.as_str() {
84                "bool" => Some("bool"),
85                "usize" => Some("usize"),
86                "u32" => Some("u32"),
87                "u64" => Some("u64"),
88                "i32" => Some("i32"),
89                "i64" => Some("i64"),
90                "f32" => Some("f32"),
91                "f64" => Some("f64"),
92                "String" => Some("String"),
93                _ => None,
94            }
95        }
96        _ => None,
97    }
98}
99
100fn impl_clikeys(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
101    let name = &input.ident;
102
103    let ds = match &input.data {
104        Data::Struct(DataStruct {
105            fields: Fields::Named(fields),
106            ..
107        }) => fields,
108        _ => {
109            return Err(syn::Error::new(
110                input.span(),
111                "#[derive(CliKeys)] supports only structs with named fields",
112            ));
113        }
114    };
115
116    let mut apply_stmts = Vec::new();
117    let mut meta_stmts = Vec::new();
118
119    for f in &ds.named {
120        let ident = f.ident.as_ref().unwrap();
121        let fspan = f.span();
122        let FieldAttr {
123            rename,
124            help,
125            ns,
126            skip,
127        } = parse_attrs(&f.attrs, &ident.to_string())?;
128
129        if skip {
130            continue;
131        }
132
133        let field_key = rename.unwrap_or_else(|| ident.to_string());
134        let help_lit = help.unwrap_or_default();
135        let fty = &f.ty;
136
137        if let Some(tyname) = is_leaf_type(fty) {
138            let tyname_str = syn::LitStr::new(tyname, Span::call_site());
139            let key_lit = syn::LitStr::new(&field_key, Span::call_site());
140
141            apply_stmts.push(quote_spanned! {fspan=>
142                if key == #key_lit {
143                    let parsed = <#fty as ::clikeys::ParseFromStr>::parse_str(value)
144                        .map_err(|msg| ::clikeys::NsError::ParseError {
145                            key: key.to_string(),
146                            value: value.to_string(),
147                            msg,
148                        })?;
149                    self.#ident = parsed;
150                    return Ok(());
151                }
152            });
153
154            meta_stmts.push(quote_spanned! {fspan=>
155                meta.push(::clikeys::OptionMeta::with_default(
156                    #key_lit,
157                    #tyname_str,
158                    #help_lit,
159                    default.#ident.to_string()
160                ));
161            });
162        } else {
163            let ns_str = ns.unwrap_or_else(|| field_key.clone());
164            let ns_lit = syn::LitStr::new(&ns_str, Span::call_site());
165
166            apply_stmts.push(quote_spanned! {fspan=>
167                if let Some((seg, rest)) = ::clikeys::split_once(key, '.') {
168                    if seg == #ns_lit {
169                        return ::clikeys::CliKeys::apply_kv(&mut self.#ident, rest, value);
170                    }
171                }
172            });
173
174            meta_stmts.push(quote_spanned! {fspan=>
175                {
176                    let child = <#fty as ::clikeys::CliKeys>::options_meta();
177                    let child = ::clikeys::prefix_meta(#ns_lit, child);
178                    meta.extend(child);
179                }
180            });
181        }
182    }
183
184    apply_stmts.push(quote! {
185        Err(::clikeys::NsError::UnknownKey(key.to_string()))
186    });
187
188    let tokens = quote! {
189        impl ::clikeys::CliKeys for #name {
190            fn options_meta() -> ::std::vec::Vec<::clikeys::OptionMeta> {
191                let default: Self = <Self as ::std::default::Default>::default();
192                let mut meta = ::std::vec::Vec::new();
193                #(#meta_stmts)*
194                meta
195            }
196
197            fn apply_kv(&mut self, key: &str, value: &str)
198                -> ::std::result::Result<(), ::clikeys::NsError>
199            {
200                #(#apply_stmts)*
201            }
202        }
203
204        impl #name {
205            pub fn new_with_options<I, S>(options: I)
206                -> ::std::result::Result<Self, ::clikeys::NsError>
207            where
208                I: ::std::iter::IntoIterator<Item = S>,
209                S: ::std::convert::AsRef<str>,
210            {
211                let mut cfg: Self = ::std::default::Default::default();
212                for opt in options {
213                    let opt = opt.as_ref();
214                    let Some((key, value)) = ::clikeys::split_once(opt, '=') else {
215                        return Err(::clikeys::NsError::ParseError {
216                            key: opt.to_string(),
217                            value: ::std::string::String::new(),
218                            msg: ::std::string::String::from("expected KEY=VALUE"),
219                        });
220                    };
221                    ::clikeys::CliKeys::apply_kv(&mut cfg, key, value)?;
222                }
223                Ok(cfg)
224            }
225        }
226    };
227
228    Ok(tokens)
229}