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}