Skip to main content

statime_config_derive/
lib.rs

1//! Derive macro for `statime-config`.
2
3use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::{format_ident, quote};
6use syn::{
7    Data, DataEnum, DataStruct, DeriveInput, Expr, Fields, Ident, LitStr, Token, Type, Variant,
8    parse_macro_input, parse_quote, spanned::Spanned,
9};
10
11/// Generate the partial counterpart of a configuration struct.
12///
13/// A field with no `default` is required, one with `#[config(default = expr)]`
14/// falls back to that expression, and a bare `#[config(default)]` falls back to
15/// the field type's own [`Default`].
16#[proc_macro_derive(Configurable, attributes(config))]
17pub fn derive_configurable(input: TokenStream) -> TokenStream {
18    let input = parse_macro_input!(input as DeriveInput);
19
20    match expand(input) {
21        Ok(tokens) => tokens.into(),
22        Err(error) => error.to_compile_error().into(),
23    }
24}
25
26/// Mark a type as a configuration value the tree does not look inside.
27///
28/// Every traversal stops at it, so this is all a leaf type needs: enums,
29/// newtypes around a duration or an address, and anything else that is a single
30/// value as far as a configuration document is concerned.
31///
32/// ```ignore
33/// #[derive(ConfigurableAtomic)]
34/// pub enum LogLevel { Debug, Info, Warn, Error }
35/// ```
36#[proc_macro_derive(ConfigurableAtomic)]
37pub fn derive_configurable_atomic(input: TokenStream) -> TokenStream {
38    let name = parse_macro_input!(input as DeriveInput).ident;
39
40    quote! {
41        impl ::statime_config::ConfigurableAtomic for #name {}
42    }
43    .into()
44}
45
46/// One field of the configuration struct, and what the traversals need to know
47/// about it.
48struct Field {
49    name: Ident,
50    /// The name this field has in a configuration document. The same string
51    /// reaches serde and the paths in diagnostics, so the two cannot disagree
52    /// about what a document is supposed to say.
53    key: String,
54    /// Set when the key was chosen rather than derived from the field name.
55    renamed: bool,
56    ty: Type,
57    default: Option<Expr>,
58}
59
60fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
61    match &input.data {
62        Data::Struct(data) => expand_struct(&input, data),
63        Data::Enum(data) => expand_enum(&input, data),
64        Data::Union(_) => Err(syn::Error::new(
65            input.span(),
66            "only structs and enums can be configurable",
67        )),
68    }
69}
70
71/// A struct becomes a section: one partial field per field, each of them a node
72/// whose shape the field's own type decides.
73fn expand_struct(input: &DeriveInput, data: &DataStruct) -> syn::Result<TokenStream2> {
74    let name = &input.ident;
75    let partial = format_ident!("Partial{}", name);
76
77    // everything the generated code needs is named in full, so that it neither
78    // collides with, nor depends on, whatever is in scope where it lands
79    let private = quote!(::statime_config::__private);
80
81    let fields = collect_fields(data)?;
82    check_attributes(input)?;
83
84    let declarations = fields.iter().map(|field| {
85        let Field {
86            name,
87            key,
88            renamed,
89            ty,
90            ..
91        } = field;
92        // otherwise the container's `rename_all` already produces this key
93        let rename = renamed.then(|| quote!(#[serde(rename = #key)]));
94
95        quote! {
96            #rename
97            #[serde(skip_serializing_if = "::statime_config::__private::is_effectively_unset")]
98            pub #name: <#ty as ::statime_config::Configurable>::Node
99        }
100    });
101
102    let emptiness = fields.iter().map(
103        |Field { name, .. }| quote!(#private::EffectivelyUnset::is_effectively_unset(&self.#name)),
104    );
105
106    let attributions = fields.iter().map(|Field { name, .. }| {
107        quote!(::statime_config::PartialValue::attribute(&mut self.#name, origin);)
108    });
109
110    let defaults = fields.iter().map(|Field { name, default, .. }| {
111        let supply = default
112            .as_ref()
113            .map(|default| quote!(self.#name.default_to(|| #default);));
114        quote! {
115            #supply
116            ::statime_config::PartialValue::apply_defaults(&mut self.#name);
117        }
118    });
119
120    let merges = fields.iter().map(|Field { name, key, .. }| {
121        quote! {
122            context.at(#key, |context| {
123                ::statime_config::Merge::merge(&mut self.#name, incoming.#name, context)
124            })?;
125        }
126    });
127
128    let resolutions = fields.iter().map(|Field { name, key, .. }| {
129        quote! {
130            #name: path.at(#key, |path| ::statime_config::PartialValue::resolve(self.#name, path))?
131        }
132    });
133
134    let visibility = &input.vis;
135
136    Ok(quote! {
137        /// The partial counterpart of a configuration struct, holding only what
138        /// the configuration documents actually said.
139        #[doc(hidden)]
140        #[derive(Debug, Default, Clone, PartialEq)]
141        #[derive(#private::Deserialize, #private::Serialize)]
142        #[serde(crate = "::statime_config::__private::serde")]
143        #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
144        #visibility struct #partial {
145            #(#declarations,)*
146        }
147
148        impl ::statime_config::Configurable for #name {
149            type Partial = #partial;
150            type Node = ::statime_config::Section<#partial>;
151        }
152
153        impl #private::EffectivelyUnset for #partial {
154            fn is_effectively_unset(&self) -> bool {
155                true #( && #emptiness )*
156            }
157        }
158
159        impl ::statime_config::PartialValue for #partial {
160            type Resolved = #name;
161
162            fn attribute(&mut self, origin: ::statime_config::OriginId) {
163                #(#attributions)*
164            }
165
166            fn apply_defaults(&mut self) {
167                #(#defaults)*
168            }
169
170            fn resolve(
171                self,
172                path: &mut ::statime_config::ConfigPath,
173            ) -> ::core::result::Result<#name, ::statime_config::ConfigError> {
174                ::core::result::Result::Ok(#name {
175                    #(#resolutions,)*
176                })
177            }
178        }
179
180        impl ::statime_config::Merge for #partial {
181            fn merge(
182                &mut self,
183                incoming: Self,
184                context: &mut ::statime_config::MergeContext<'_>,
185            ) -> ::core::result::Result<(), ::statime_config::ConfigError> {
186                #(#merges)*
187                ::core::result::Result::Ok(())
188            }
189        }
190    })
191}
192
193/// An enum becomes a choice between sections, told apart by a tag naming which
194/// one a document means.
195///
196/// It is neither merged nor tested for emptiness: a configurable enum lives in
197/// a vector, which is atomic, so two of them never meet.
198fn expand_enum(input: &DeriveInput, data: &DataEnum) -> syn::Result<TokenStream2> {
199    let name = &input.ident;
200    let partial = format_ident!("Partial{}", name);
201    let private = quote!(::statime_config::__private);
202
203    let tag = enum_tag(input)?;
204    let variants = data
205        .variants
206        .iter()
207        .map(collect_variant)
208        .collect::<syn::Result<Vec<_>>>()?;
209
210    let declarations = variants.iter().map(|EnumVariant { name, ty, rename }| {
211        // otherwise the container's `rename_all` already names this variant
212        let rename = rename.as_ref().map(|key| quote!(#[serde(rename = #key)]));
213
214        quote! {
215            #rename
216            #name(<#ty as ::statime_config::Configurable>::Partial)
217        }
218    });
219
220    let attributions = variants.iter().map(|EnumVariant { name, .. }| {
221        quote!(Self::#name(value) => ::statime_config::PartialValue::attribute(value, origin),)
222    });
223
224    let defaults = variants.iter().map(|EnumVariant { name, .. }| {
225        quote!(Self::#name(value) => ::statime_config::PartialValue::apply_defaults(value),)
226    });
227
228    let variant_names = variants.iter().map(|EnumVariant { name, rename, .. }| {
229        let string_name = rename.clone().unwrap_or_else(|| name.to_string());
230        quote!(Self::#name(value) => #string_name,)
231    });
232
233    let variant_merges = variants.iter().map(|EnumVariant { name, .. }| {
234        quote!((Self::#name(this), Self::#name(incoming)) => ::statime_config::Merge::merge(this, incoming, context),)
235    });
236
237    let resolutions = variants.iter().map(|EnumVariant { name: variant, .. }| {
238        quote! {
239            Self::#variant(value) => ::core::result::Result::Ok(
240                #name::#variant(::statime_config::PartialValue::resolve(value, path)?)
241            ),
242        }
243    });
244
245    let visibility = &input.vis;
246
247    Ok(quote! {
248        /// The partial counterpart of a configuration enum, holding only what
249        /// the configuration documents actually said.
250        #[doc(hidden)]
251        #[derive(Debug, Clone, PartialEq)]
252        #[derive(#private::Deserialize, #private::Serialize)]
253        #[serde(crate = "::statime_config::__private::serde")]
254        #[serde(rename_all = "kebab-case", tag = #tag)]
255        #visibility enum #partial {
256            #(#declarations,)*
257        }
258
259        impl ::statime_config::Configurable for #name {
260            type Partial = #partial;
261            type Node = ::statime_config::EnumSection<#partial>;
262        }
263
264        impl ::statime_config::PartialValue for #partial {
265            type Resolved = #name;
266
267            fn attribute(&mut self, origin: ::statime_config::OriginId) {
268                match self {
269                    #(#attributions)*
270                }
271            }
272
273            fn apply_defaults(&mut self) {
274                match self {
275                    #(#defaults)*
276                }
277            }
278
279            fn resolve(
280                self,
281                path: &mut ::statime_config::ConfigPath,
282            ) -> ::core::result::Result<#name, ::statime_config::ConfigError> {
283                match self {
284                    #(#resolutions)*
285                }
286            }
287        }
288
289        impl #partial {
290            fn variant_name(&self) -> &'static str {
291                match self {
292                    #(#variant_names)*
293                }
294            }
295        }
296
297        impl ::statime_config::Merge for #partial {
298            fn merge(
299                &mut self,
300                incoming: Self,
301                context: &mut ::statime_config::MergeContext<'_>,
302            ) -> ::core::result::Result<(), ::statime_config::ConfigError> {
303                match (self, incoming) {
304                    #(#variant_merges)*
305                    (this, incoming) => match context.policy {
306                        ::statime_config::MergePolicy::RejectOverlap => Err(::statime_config::ConfigError::MismatchedVariants { position: context.path.clone(), current: this.variant_name(), incoming: incoming.variant_name() }),
307                        ::statime_config::MergePolicy::Override => {
308                            *this = incoming;
309                            ::core::result::Result::Ok(())
310                        },
311                    },
312                }
313            }
314        }
315    })
316}
317
318/// The key that says which variant a document means.
319fn enum_tag(input: &DeriveInput) -> syn::Result<String> {
320    let mut tag = None;
321
322    for attribute in &input.attrs {
323        if !attribute.path().is_ident("config") {
324            continue;
325        }
326
327        attribute.parse_nested_meta(|meta| {
328            if meta.path.is_ident("tag") {
329                tag = Some(meta.value()?.parse::<LitStr>()?.value());
330                return Ok(());
331            }
332
333            Err(meta.error("unrecognised configuration attribute"))
334        })?;
335    }
336
337    tag.ok_or_else(|| {
338        syn::Error::new(
339            input.span(),
340            "a configurable enum needs `#[config(tag = \"...\")]`, naming the key \
341             a document uses to say which variant it means",
342        )
343    })
344}
345
346/// One variant of the configuration enum, and the configuration struct it
347/// holds.
348struct EnumVariant {
349    name: Ident,
350    ty: Type,
351    rename: Option<String>,
352}
353
354fn collect_variant(variant: &Variant) -> syn::Result<EnumVariant> {
355    let unnamed = match &variant.fields {
356        Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => &unnamed.unnamed[0],
357        _ => {
358            return Err(syn::Error::new(
359                variant.span(),
360                "a configurable enum variant holds exactly one configuration struct; \
361                 an enum that is itself a single value wants `ConfigurableAtomic` instead",
362            ));
363        }
364    };
365
366    Ok(EnumVariant {
367        name: variant.ident.clone(),
368        ty: unnamed.ty.clone(),
369        rename: variant_rename(variant)?,
370    })
371}
372
373fn collect_fields(data: &DataStruct) -> syn::Result<Vec<Field>> {
374    let Fields::Named(named) = &data.fields else {
375        return Err(syn::Error::new(
376            data.fields.span(),
377            "a configurable struct needs named fields",
378        ));
379    };
380
381    named
382        .named
383        .iter()
384        .map(|field| {
385            let name = field.ident.clone().expect("named fields have a name");
386            let FieldConfig { default, rename } = field_config(field)?;
387
388            Ok(Field {
389                renamed: rename.is_some(),
390                key: rename.unwrap_or_else(|| name.to_string().replace('_', "-")),
391                name,
392                ty: field.ty.clone(),
393                default,
394            })
395        })
396        .collect()
397}
398
399/// Whether this struct is the root of a configuration tree
400fn check_attributes(input: &DeriveInput) -> syn::Result<()> {
401    for attribute in &input.attrs {
402        if !attribute.path().is_ident("config") {
403            continue;
404        }
405
406        attribute
407            .parse_nested_meta(|meta| Err(meta.error("unrecognised configuration attribute")))?;
408    }
409
410    Ok(())
411}
412
413/// What the `#[config(...)]` attributes on a field say.
414#[derive(Default)]
415struct FieldConfig {
416    default: Option<Expr>,
417    rename: Option<String>,
418}
419
420fn field_config(field: &syn::Field) -> syn::Result<FieldConfig> {
421    let mut config = FieldConfig::default();
422
423    for attribute in &field.attrs {
424        if !attribute.path().is_ident("config") {
425            continue;
426        }
427
428        attribute.parse_nested_meta(|meta| {
429            if meta.path.is_ident("default") {
430                // a bare `default` means the type's own `Default`
431                config.default = Some(if meta.input.peek(Token![=]) {
432                    meta.value()?.parse()?
433                } else {
434                    parse_quote!(::core::default::Default::default())
435                });
436                return Ok(());
437            }
438
439            if meta.path.is_ident("rename") {
440                config.rename = Some(meta.value()?.parse::<LitStr>()?.value());
441                return Ok(());
442            }
443
444            Err(meta.error("unrecognised configuration attribute"))
445        })?;
446    }
447
448    Ok(config)
449}
450
451/// The name a variant is known by in a document, when it is not the one the
452/// container's `rename_all` would produce.
453fn variant_rename(variant: &Variant) -> syn::Result<Option<String>> {
454    let mut rename = None;
455
456    for attribute in &variant.attrs {
457        if !attribute.path().is_ident("config") {
458            continue;
459        }
460
461        attribute.parse_nested_meta(|meta| {
462            if meta.path.is_ident("rename") {
463                rename = Some(meta.value()?.parse::<LitStr>()?.value());
464                return Ok(());
465            }
466
467            Err(meta.error("unrecognised configuration attribute"))
468        })?;
469    }
470
471    Ok(rename)
472}