1#![warn(clippy::all)]
9#![forbid(unsafe_code)]
10
11use proc_macro::TokenStream;
12use quote::quote;
13use syn::{
14 parse_macro_input, DeriveInput, Data, Fields, Type, PathArguments, GenericArgument,
15 punctuated::Punctuated, Token,
16};
17
18#[proc_macro_derive(Config, attributes(configulator))]
26pub fn derive_config(input: TokenStream) -> TokenStream {
27 let input = parse_macro_input!(input as DeriveInput);
28 match derive_config_impl(&input) {
29 Ok(tokens) => tokens.into(),
30 Err(err) => err.to_compile_error().into(),
31 }
32}
33
34fn derive_config_impl(input: &DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
35 let name = &input.ident;
36 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
37
38 let fields = extract_named_fields(input)?;
39
40 let mut field_info_tokens = Vec::new();
41 let mut from_map_tokens = Vec::new();
42
43 for field in fields.iter() {
44 let field_ident = field.ident.as_ref().unwrap();
45 let field_name_str = field_ident.to_string();
46 let field_ty = &field.ty;
47
48 let attrs = parse_configulator_attrs(&field.attrs)?;
49
50 let config_name_str = attrs.config_name.unwrap_or_else(|| field_name_str.clone());
51 let field_type_token = field_type_to_tokens(field_ty);
52
53 let default_tokens = match &attrs.default_val {
54 Some(v) => quote! { Some(#v) },
55 None => quote! { None },
56 };
57 let desc_tokens = match &attrs.description {
58 Some(v) => quote! { Some(#v) },
59 None => quote! { None },
60 };
61
62 field_info_tokens.push(quote! {
63 configulator::FieldInfo {
64 field_name: #field_name_str,
65 config_name: #config_name_str,
66 default_value: #default_tokens,
67 description: #desc_tokens,
68 field_type: #field_type_token,
69 }
70 });
71
72 let from_map_field = gen_from_value_map_field(field_ident, &config_name_str, field_ty);
73 from_map_tokens.push(from_map_field);
74 }
75
76 let expanded = quote! {
77 impl #impl_generics configulator::ConfigFields for #name #ty_generics #where_clause {
78 fn configulator_fields() -> Vec<configulator::FieldInfo> {
79 use configulator::ConfiguratorScalar as _;
81 vec![
82 #(#field_info_tokens),*
83 ]
84 }
85 }
86
87 impl #impl_generics configulator::FromValueMap for #name #ty_generics #where_clause {
88 fn from_value_map(
89 map: &configulator::ValueMap,
90 ) -> Result<Self, configulator::ConfigulatorError> {
91 use configulator::ConfiguratorScalar as _;
93 Ok(Self {
94 #(#from_map_tokens),*
95 })
96 }
97 }
98 };
99
100 Ok(expanded)
101}
102
103fn extract_named_fields(
106 input: &DeriveInput,
107) -> Result<&Punctuated<syn::Field, Token![,]>, syn::Error> {
108 match &input.data {
109 Data::Struct(data) => match &data.fields {
110 Fields::Named(fields) => Ok(&fields.named),
111 _ => Err(syn::Error::new_spanned(
112 &input.ident,
113 "Config can only be derived for structs with named fields",
114 )),
115 },
116 _ => Err(syn::Error::new_spanned(
117 &input.ident,
118 "Config can only be derived for structs",
119 )),
120 }
121}
122
123#[derive(Debug)]
124struct FieldConfigAttrs {
125 config_name: Option<String>,
126 default_val: Option<String>,
127 description: Option<String>,
128}
129
130fn parse_configulator_attrs(attrs: &[syn::Attribute]) -> Result<FieldConfigAttrs, syn::Error> {
134 let mut result = FieldConfigAttrs {
135 config_name: None,
136 default_val: None,
137 description: None,
138 };
139 for attr in attrs {
140 if !attr.path().is_ident("configulator") {
141 continue;
142 }
143 attr.parse_nested_meta(|meta| {
144 if meta.path.is_ident("name") {
145 let value = meta.value()?;
146 let lit: syn::LitStr = value.parse()?;
147 result.config_name = Some(lit.value());
148 } else if meta.path.is_ident("default") {
149 let value = meta.value()?;
150 let lit: syn::LitStr = value.parse()?;
151 result.default_val = Some(lit.value());
152 } else if meta.path.is_ident("description") {
153 let value = meta.value()?;
154 let lit: syn::LitStr = value.parse()?;
155 result.description = Some(lit.value());
156 } else {
157 let name = meta.path.get_ident()
158 .map(|id| id.to_string())
159 .unwrap_or_else(|| "?".to_string());
160 return Err(meta.error(format_args!(
161 "unknown configulator attribute `{name}`; \
162 expected `name`, `default`, or `description`",
163 )));
164 }
165 Ok(())
166 })?;
167 }
168 Ok(result)
169}
170
171fn field_type_to_tokens(ty: &Type) -> proc_macro2::TokenStream {
175 if let Type::Path(type_path) = ty {
176 if let Some(segment) = type_path.path.segments.last() {
177 if segment.ident == "bool" {
178 return quote! { configulator::FieldType::Bool };
179 }
180 if segment.ident == "Vec" {
181 if let PathArguments::AngleBracketed(_) = &segment.arguments {
182 return quote! { configulator::FieldType::List };
183 }
184 return quote! {
185 compile_error!("Vec fields must have a type argument, e.g. Vec<String>")
186 };
187 }
188 }
189 }
190 gen_config_detect_tokens(ty)
191}
192
193fn gen_config_detect_tokens(ty: &Type) -> proc_macro2::TokenStream {
197 quote! {
198 {
199 let __m = configulator::ConfigDetect::<#ty>(::std::marker::PhantomData);
200 __m.__configulator_field_type()
201 }
202 }
203}
204
205fn gen_from_value_map_field(
207 field_ident: &syn::Ident,
208 config_name: &str,
209 ty: &Type,
210) -> proc_macro2::TokenStream {
211 let kind = classify_type(ty);
212 match kind {
213 TypeKind::Bool => {
214 quote! {
215 #field_ident: configulator::parse_scalar::<bool>(map, #config_name)?
216 }
217 }
218 TypeKind::Vec(inner_ty) => {
219 quote! {
220 #field_ident: configulator::parse_list::<#inner_ty>(map, #config_name)?
221 }
222 }
223 TypeKind::Other => {
224 quote! {
225 #field_ident: {
226 let __m = configulator::ConfigDetect::<#ty>(::std::marker::PhantomData);
227 __m.__configulator_parse(map, #config_name)?
228 }
229 }
230 }
231 }
232}
233
234#[derive(Debug)]
235enum TypeKind {
236 Bool,
237 Vec(Box<Type>),
238 Other,
239}
240
241fn classify_type(ty: &Type) -> TypeKind {
242 if let Type::Path(type_path) = ty {
243 if let Some(segment) = type_path.path.segments.last() {
244 if segment.ident == "bool" {
245 return TypeKind::Bool;
246 }
247 if segment.ident == "Vec" {
248 if let PathArguments::AngleBracketed(args) = &segment.arguments {
249 if let Some(GenericArgument::Type(inner)) = args.args.first() {
250 return TypeKind::Vec(Box::new(inner.clone()));
251 }
252 }
253 return TypeKind::Other;
255 }
256 }
257 }
258 TypeKind::Other
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use syn::parse_str;
265
266 #[test]
269 fn derive_config_impl_valid_struct() {
270 let input: DeriveInput = parse_str("struct Foo { x: u32 }").unwrap();
271 assert!(derive_config_impl(&input).is_ok());
272 }
273
274 #[test]
275 fn derive_config_impl_rejects_enum() {
276 let input: DeriveInput = parse_str("enum Foo { A, B }").unwrap();
277 let err = derive_config_impl(&input).unwrap_err();
278 assert!(err.to_string().contains("only be derived for structs"));
279 }
280
281 #[test]
282 fn derive_config_impl_rejects_tuple_struct() {
283 let input: DeriveInput = parse_str("struct Foo(u32);").unwrap();
284 let err = derive_config_impl(&input).unwrap_err();
285 assert!(err.to_string().contains("named fields"));
286 }
287
288 #[test]
289 fn derive_config_impl_rejects_bad_attr() {
290 let input: DeriveInput = parse_str(
291 r#"struct Foo { #[configulator(name = 42)] f: String }"#,
292 )
293 .unwrap();
294 assert!(derive_config_impl(&input).is_err());
295 }
296
297 #[test]
300 fn extract_named_fields_accepts_named_struct() {
301 let input: DeriveInput = parse_str("struct Foo { x: u32 }").unwrap();
302 assert!(extract_named_fields(&input).is_ok());
303 }
304
305 #[test]
306 fn extract_named_fields_rejects_tuple_struct() {
307 let input: DeriveInput = parse_str("struct Foo(u32);").unwrap();
308 let err = extract_named_fields(&input).unwrap_err();
309 assert!(
310 err.to_string().contains("named fields"),
311 "expected 'named fields' error, got: {err}"
312 );
313 }
314
315 #[test]
316 fn extract_named_fields_rejects_unit_struct() {
317 let input: DeriveInput = parse_str("struct Foo;").unwrap();
318 let err = extract_named_fields(&input).unwrap_err();
319 assert!(
320 err.to_string().contains("named fields"),
321 "expected 'named fields' error, got: {err}"
322 );
323 }
324
325 #[test]
326 fn extract_named_fields_rejects_enum() {
327 let input: DeriveInput = parse_str("enum Foo { A, B }").unwrap();
328 let err = extract_named_fields(&input).unwrap_err();
329 assert!(
330 err.to_string().contains("only be derived for structs"),
331 "expected 'only be derived for structs' error, got: {err}"
332 );
333 }
334
335 #[test]
338 fn parse_attrs_extracts_all_keys() {
339 let input: DeriveInput = parse_str(
340 r#"struct Foo { #[configulator(name = "n", default = "d", description = "desc")] f: u32 }"#,
341 )
342 .unwrap();
343 let fields = extract_named_fields(&input).unwrap();
344 let attrs = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap();
345 assert_eq!(attrs.config_name.as_deref(), Some("n"));
346 assert_eq!(attrs.default_val.as_deref(), Some("d"));
347 assert_eq!(attrs.description.as_deref(), Some("desc"));
348 }
349
350 #[test]
351 fn parse_attrs_skips_non_configulator() {
352 let input: DeriveInput = parse_str(
353 r#"struct Foo { #[allow(unused)] #[configulator(name = "bar")] f: String }"#,
354 )
355 .unwrap();
356 let fields = extract_named_fields(&input).unwrap();
357 let attrs = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap();
358 assert_eq!(attrs.config_name.as_deref(), Some("bar"));
359 }
360
361 #[test]
362 fn parse_attrs_rejects_unknown_key() {
363 let input: DeriveInput = parse_str(
364 r#"struct Foo { #[configulator(name = "bar", extra)] f: String }"#,
365 )
366 .unwrap();
367 let fields = extract_named_fields(&input).unwrap();
368 let err = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap_err();
369 assert!(
370 err.to_string().contains("unknown configulator attribute"),
371 "expected 'unknown configulator attribute' error, got: {err}"
372 );
373 }
374
375 #[test]
376 fn parse_attrs_error_on_bad_value_type() {
377 let input: DeriveInput = parse_str(
378 r#"struct Foo { #[configulator(name = 42)] f: String }"#,
379 )
380 .unwrap();
381 let fields = extract_named_fields(&input).unwrap();
382 assert!(parse_configulator_attrs(&fields.first().unwrap().attrs).is_err());
383 }
384
385 #[test]
386 fn parse_attrs_no_attrs_returns_none() {
387 let input: DeriveInput = parse_str("struct Foo { f: String }").unwrap();
388 let fields = extract_named_fields(&input).unwrap();
389 let attrs = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap();
390 assert!(attrs.config_name.is_none());
391 assert!(attrs.default_val.is_none());
392 assert!(attrs.description.is_none());
393 }
394
395 #[test]
398 fn classify_type_bool() {
399 let ty: Type = parse_str("bool").unwrap();
400 assert!(matches!(classify_type(&ty), TypeKind::Bool));
401 }
402
403 #[test]
404 fn classify_type_vec_with_inner() {
405 let ty: Type = parse_str("Vec<String>").unwrap();
406 assert!(matches!(classify_type(&ty), TypeKind::Vec(_)));
407 }
408
409 #[test]
410 fn classify_type_scalar_string() {
411 let ty: Type = parse_str("String").unwrap();
412 assert!(matches!(classify_type(&ty), TypeKind::Other));
413 }
414
415 #[test]
416 fn classify_type_reference_is_other() {
417 let ty: Type = parse_str("&str").unwrap();
418 assert!(matches!(classify_type(&ty), TypeKind::Other));
419 }
420
421 #[test]
422 fn classify_type_tuple_is_other() {
423 let ty: Type = parse_str("(i32, i32)").unwrap();
424 assert!(matches!(classify_type(&ty), TypeKind::Other));
425 }
426
427 #[test]
428 fn classify_type_bare_vec_without_type_args() {
429 let ty: Type = parse_str("Vec").unwrap();
430 assert!(matches!(classify_type(&ty), TypeKind::Other));
431 }
432
433 #[test]
436 fn field_type_to_tokens_bool() {
437 let ty: Type = parse_str("bool").unwrap();
438 let tokens = field_type_to_tokens(&ty).to_string();
439 assert!(tokens.contains("FieldType"), "expected FieldType in: {tokens}");
440 assert!(tokens.contains("Bool"), "expected Bool in: {tokens}");
441 }
442
443 #[test]
444 fn field_type_to_tokens_vec() {
445 let ty: Type = parse_str("Vec<u32>").unwrap();
446 let tokens = field_type_to_tokens(&ty).to_string();
447 assert!(tokens.contains("FieldType"), "expected FieldType in: {tokens}");
448 assert!(tokens.contains("List"), "expected List in: {tokens}");
449 }
450
451 #[test]
452 fn field_type_to_tokens_scalar() {
453 let ty: Type = parse_str("String").unwrap();
454 let tokens = field_type_to_tokens(&ty).to_string();
455 assert!(
456 tokens.contains("ConfigDetect"),
457 "expected ConfigDetect dispatch in: {tokens}"
458 );
459 }
460
461 #[test]
462 fn field_type_to_tokens_non_path_fallback() {
463 let ty: Type = parse_str("&str").unwrap();
464 let tokens = field_type_to_tokens(&ty).to_string();
465 assert!(
466 tokens.contains("ConfigDetect"),
467 "expected ConfigDetect dispatch in fallback: {tokens}"
468 );
469 }
470}