1use 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#[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#[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
46struct Field {
49 name: Ident,
50 key: String,
54 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
71fn expand_struct(input: &DeriveInput, data: &DataStruct) -> syn::Result<TokenStream2> {
74 let name = &input.ident;
75 let partial = format_ident!("Partial{}", name);
76
77 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 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 #[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
193fn 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 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 #[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
318fn 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
346struct 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
399fn 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#[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 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
451fn 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}