Skip to main content

tor_config/
map_builder.rs

1//! Helper for defining sub-builders that map a serializable type (typically String)
2//! to a configuration type.
3
4/// Define a map type, and an associated builder struct, suitable for use in a configuration object.
5///
6/// We use this macro when we want a configuration structure to contain a key-to-value map,
7/// and therefore we want its associated builder structure to contain
8/// a map from the same key type to a value-builder type.
9///
10/// The key of the map type must implement `Serialize`, `Clone`, and `Debug`.
11/// The value of the map type must have an associated "Builder"
12/// type formed by appending `Builder` to its name.
13/// This Builder type must implement `Serialize`, `Deserialize`, `Clone`, and `Debug`,
14/// and it must have a `build(&self)` method returning `Result<value, ConfigBuildError>`.
15///
16/// # Syntax and behavior
17///
18/// ```ignore
19/// define_map_builder! {
20///     BuilderAttributes
21///     pub struct BuilderName =>
22///
23///     MapAttributes
24///     pub type MapName = ContainerType<KeyType, ValueType>;
25///
26///     defaults: defaults_func(); // <--- this line is optional
27/// }
28/// ```
29///
30/// In the example above,
31///
32/// * BuilderName, MapName, and ContainerType may be replaced with any identifier;
33/// * BuilderAttributes and MapAttributes can be replaced with any set of attributes
34///   (such sa doc comments, `#derive`, and so on);
35/// * The `pub`s may be replaced with any visibility;
36/// * and `KeyType` and `ValueType` may be replaced with any appropriate types.
37///    * `ValueType` must have a corresponding `ValueTypeBuilder`.
38///    * `ValueTypeBuilder` must implement
39///      [`ExtendBuilder`](crate::extend_builder::ExtendBuilder).
40///
41/// Given this syntax, this macro will define "MapType" as an alias for
42/// `Container<KeyType,ValueType>`,
43/// and "BuilderName" as a builder type for that container.
44///
45/// "BuilderName" will implement:
46///  * `Deref` and `DerefMut` with a target type of `Container<KeyType, ValueTypeBuilder>`
47///  * `Default`, `Clone`, and `Debug`.
48///  * `Serialize` and `Deserialize`
49///  * A `build()` function that invokes `build()` on every value in its contained map.
50///
51/// (Note that in order to work as a sub-builder within our configuration system,
52/// "BuilderName" should be the same as "MapName" concatenated with "Builder.")
53///
54/// The `defaults_func()`, if provided, must be
55/// a function returning `ContainerType<KeyType, ValueType>`.
56/// The values returned by `default_func()` map are used to implement
57/// `Default` and `Deserialize` for `BuilderName`,
58/// extending from the defaults with `ExtendStrategy::ReplaceLists`.
59/// If no `defaults_func` is given, `ContainerType::default()` is used.
60///
61/// # Example
62///
63/// ```
64/// # use derive_deftly::Deftly;
65/// # use std::collections::BTreeMap;
66/// # use tor_config::{ConfigBuildError, define_map_builder, derive_deftly_template_ExtendBuilder};
67/// # use tor_config::derive::prelude::*;
68/// # use serde::{Serialize, Deserialize};
69/// # use tor_config::extend_builder::{ExtendBuilder,ExtendStrategy};
70/// #[derive(Clone, Debug, Deftly, Eq, PartialEq)]
71/// #[derive_deftly(TorConfig)]
72/// pub struct ConnectionsConfig {
73///     // Note: Ordinarily, you might choose to `tor_config(map)` instead,
74///     // to automate more of this derivation.
75///     #[deftly(tor_config(sub_builder, no_magic))]
76///     conns: ConnectionMap
77/// }
78///
79/// define_map_builder! {
80///     pub struct ConnectionMapBuilder =>
81///     pub type ConnectionMap = BTreeMap<String, ConnConfig>;
82/// }
83///
84/// #[derive(Clone, Debug, Deftly, Eq, PartialEq)]
85/// #[derive_deftly(TorConfig)]
86/// pub struct ConnConfig {
87///     #[deftly(tor_config(default="true"))]
88///     enabled: bool,
89///     #[deftly(tor_config(default="9999"))]
90///     port: u16,
91/// }
92///
93/// let defaults: ConnectionsConfigBuilder = toml::from_str(r#"
94/// [conns."socks"]
95/// enabled = true
96/// port = 9150
97///
98/// [conns."http"]
99/// enabled = false
100/// port = 1234
101///
102/// [conns."wombat"]
103/// port = 5050
104/// "#).unwrap();
105/// let user_settings: ConnectionsConfigBuilder = toml::from_str(r#"
106/// [conns."http"]
107/// enabled = false
108/// [conns."quokka"]
109/// enabled = true
110/// port = 9999
111/// "#).unwrap();
112///
113/// let mut cfg = defaults.clone();
114/// cfg.extend_from(user_settings, ExtendStrategy::ReplaceLists);
115/// let cfg = cfg.build().unwrap();
116/// assert_eq!(cfg, ConnectionsConfig {
117///     conns: vec![
118///         ("http".into(), ConnConfig { enabled: false, port: 1234}),
119///         ("quokka".into(), ConnConfig { enabled: true, port: 9999}),
120///         ("socks".into(), ConnConfig { enabled: true, port: 9150}),
121///         ("wombat".into(), ConnConfig { enabled: true, port: 5050}),
122///     ].into_iter().collect(),
123/// });
124/// ```
125///
126/// In the example above, the `derive_map_builder` macro expands to something like:
127///
128/// ```ignore
129/// pub type ConnectionMap = BTreeMap<String, ConnConfig>;
130///
131/// #[derive(Clone,Debug,Serialize,Educe)]
132/// #[educe(Deref,DerefMut)]
133/// pub struct ConnectionMapBuilder(BTreeMap<String, ConnConfigBuilder);
134///
135/// impl ConnectionMapBuilder {
136///     fn build(&self) -> Result<ConnectionMap, ConfigBuildError> {
137///         ...
138///     }
139/// }
140/// impl Default for ConnectionMapBuilder { ... }
141/// impl Deserialize for ConnectionMapBuilder { ... }
142/// impl ExtendBuilder for ConnectionMapBuilder { ... }
143/// ```
144///
145/// # Notes and rationale
146///
147/// We use this macro, instead of using a Map directly in our configuration object,
148/// so that we can have a separate Builder type with a reasonable build() implementation.
149///
150/// We don't support complicated keys; instead we require that the keys implement Deserialize.
151/// If we ever need to support keys with their own builders,
152/// we'll have to define a new macro.
153///
154/// We use `ExtendBuilder` to implement Deserialize with defaults,
155/// so that the provided configuration options can override
156/// only those parts of the default configuration tree
157/// that they actually replace.
158#[macro_export]
159macro_rules! define_map_builder {
160    {
161        $(#[ $b_m:meta ])*
162        $b_v:vis struct $btype:ident =>
163        $(#[ $m:meta ])*
164        $v:vis type $maptype:ident = $coltype:ident < $keytype:ty , $valtype: ty >;
165        $( defaults: $defaults:expr; )?
166    } => {
167        $crate::deps::paste!{$crate::define_map_builder! {
168            $(#[$b_m])*
169            $b_v struct $btype =>
170            $(#[$m])*
171            $v type $maptype = {
172                map: $coltype < $keytype , $valtype >,
173                builder_map: $coltype < $keytype, [<$valtype Builder>] > ,
174            }
175            $( defaults: $defaults; )?
176        }}
177    };
178    // This _undocumented_ internal syntax allows us to take the map types explicitly,
179    // so that we can accept derive-deftly outputs.
180    //
181    // Syntax:
182    // ```
183    //   «#[builder_attrs]»
184    //   «vis» struct «FooMapBuilder» =>
185    //   «#[maptype_attrs]»
186    //   «vis» type «FooMap» = {
187    //       map: «maptype»,
188    //       builder_map: «builder_map_type»,
189    //   }
190    //   ⟦ defaults: «default_expr» ; ⟧
191    // ```
192    //
193    // The defaults line and the attributes are optional.
194    // This (undocumented) syntax is identical to the documented variant above,
195    // except in the braced section after the `=` and before the optional `defaults`.
196    // In that section,
197    // the `maptype` is the type of the map which we are trying to build,
198    // (for example, `HashMap<String, WombatCfg>`),
199    // and the `builder_map` is the type of the map which instantiates the builder
200    // (for example, `HashMap<String, WombatCfgBuilder>`).
201    {
202        $(#[ $b_m:meta ])*
203        $b_v:vis struct $btype:ident =>
204        $(#[ $m:meta ])*
205        $v:vis type $maptype:ident = {
206            map: $mtype:ty,
207            builder_map: $bmtype:ty $(,)?
208        }
209        $( defaults: $defaults:expr; )?
210    } =>
211    {$crate::deps::paste!{
212        $(#[ $m ])*
213        $v type $maptype = $mtype ;
214
215        $(#[ $b_m ])*
216        #[derive(Clone,Debug,$crate::deps::serde::Serialize, $crate::deps::educe::Educe)]
217        #[educe(Deref, DerefMut)]
218        #[serde(transparent)]
219        $b_v struct $btype( $bmtype );
220
221        impl $btype {
222            /// Construct the final map.
223            $b_v fn build(&self) -> ::std::result::Result<$maptype, $crate::ConfigBuildError> {
224                self.0
225                    .iter()
226                    .map(|(k,v)| Ok((k.clone(), v.build()?)))
227                    .collect()
228            }
229        }
230        impl $crate::load::Builder for $btype {
231            type Built = $maptype;
232            fn build(&self) -> ::std::result::Result<$maptype, $crate::ConfigBuildError> {
233                $btype :: build(self)
234            }
235        }
236
237        impl $crate::load::ConfigBuilder for $btype {
238            fn apply_defaults(&mut self) -> ::std::result::Result<(), $crate::ConfigBuildError> {
239                #[allow(unused_imports)]
240                use $crate::load::ConfigBuilder as _;
241                for v in self.0.values_mut() {
242                    v.apply_defaults()?;
243                }
244                Ok(())
245            }
246        }
247        $(
248            // This section is expanded when we have a defaults_fn().
249            impl ::std::default::Default for $btype {
250                fn default() -> Self {
251                    Self($defaults)
252                }
253            }
254            impl<'de> $crate::deps::serde::Deserialize<'de> for $btype {
255                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
256                where
257                    D: $crate::deps::serde::Deserializer<'de> {
258                        use $crate::deps::serde::Deserialize;
259                        // To deserialize into this type, we create a builder holding the defaults,
260                        // and we create a builder holding the values from the deserializer.
261                        // We then use ExtendBuilder to extend the defaults with the deserialized values.
262                        let deserialized = <$bmtype as Deserialize>::deserialize(deserializer)?;
263                        let mut defaults = $btype::default();
264                        $crate::extend_builder::ExtendBuilder::extend_from(
265                            &mut defaults,
266                            Self(deserialized),
267                            $crate::extend_builder::ExtendStrategy::ReplaceLists);
268                        Ok(defaults)
269                    }
270            }
271        )?
272        $crate::define_map_builder!{@if_empty { $($defaults)? } {
273            // This section is expanded when we don't have a defaults_fn().
274            impl ::std::default::Default for $btype {
275                fn default() -> Self {
276                    Self(Default::default())
277                }
278            }
279            // We can't conditionally derive() here, since Rust doesn't like macros that expand to
280            // attributes.
281            impl<'de> $crate::deps::serde::Deserialize<'de> for $btype {
282                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283                where
284                    D: $crate::deps::serde::Deserializer<'de> {
285                    use $crate::deps::serde::Deserialize;
286                    Ok(Self(<$bmtype as Deserialize>::deserialize(deserializer)?))
287                }
288            }
289        }}
290        impl $crate::extend_builder::ExtendBuilder for $btype
291        {
292            fn extend_from(&mut self, other: Self, strategy: $crate::extend_builder::ExtendStrategy) {
293                $crate::extend_builder::ExtendBuilder::extend_from(&mut self.0, other.0, strategy);
294            }
295        }
296    }};
297    {@if_empty {} {$($x:tt)*}} => {$($x)*};
298    {@if_empty {$($y:tt)*} {$($x:tt)*}} => {};
299}
300
301#[cfg(test)]
302mod test {
303    // @@ begin test lint list maintained by maint/add_warning @@
304    #![allow(clippy::bool_assert_comparison)]
305    #![allow(clippy::clone_on_copy)]
306    #![allow(clippy::dbg_macro)]
307    #![allow(clippy::mixed_attributes_style)]
308    #![allow(clippy::print_stderr)]
309    #![allow(clippy::print_stdout)]
310    #![allow(clippy::single_char_pattern)]
311    #![allow(clippy::unwrap_used)]
312    #![allow(clippy::unchecked_time_subtraction)]
313    #![allow(clippy::useless_vec)]
314    #![allow(clippy::needless_pass_by_value)]
315    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
316
317    use crate::derive::prelude::*;
318    use derive_deftly::Deftly;
319    use std::collections::BTreeMap;
320
321    #[derive(Clone, Debug, Eq, PartialEq, Deftly)]
322    #[derive_deftly(TorConfig)]
323    struct Outer {
324        #[deftly(tor_config(sub_builder, no_magic))]
325        things: ThingMap,
326    }
327
328    #[derive(Clone, Debug, Eq, PartialEq, Deftly)]
329    #[derive_deftly(TorConfig)]
330    struct Inner {
331        #[deftly(tor_config(default))]
332        fun: bool,
333        #[deftly(tor_config(default))]
334        explosive: bool,
335    }
336
337    impl InnerBuilder {
338        // Testing only. We don't want to use derive_deftly(TorConfig) on Inner
339        // because we are trying to test define_map_builder by hand.
340        #[expect(clippy::unnecessary_wraps)]
341        fn apply_defaults(&mut self) -> Result<(), crate::ConfigBuildError> {
342            self.fun.get_or_insert_default();
343            self.explosive.get_or_insert_default();
344            Ok(())
345        }
346    }
347
348    define_map_builder! {
349        struct ThingMapBuilder =>
350        type ThingMap = BTreeMap<String, Inner>;
351    }
352
353    #[test]
354    fn parse_and_build() {
355        let builder: OuterBuilder = toml::from_str(
356            r#"
357[things.x]
358fun = true
359explosive = false
360
361[things.yy]
362explosive = true
363fun = true
364"#,
365        )
366        .unwrap();
367
368        let built = builder.build().unwrap();
369        assert_eq!(
370            built.things.get("x").unwrap(),
371            &Inner {
372                fun: true,
373                explosive: false
374            }
375        );
376        assert_eq!(
377            built.things.get("yy").unwrap(),
378            &Inner {
379                fun: true,
380                explosive: true
381            }
382        );
383    }
384
385    #[test]
386    fn build_directly() {
387        let mut builder = OuterBuilder::default();
388        let mut bld = InnerBuilder::default();
389        bld.fun(true);
390        builder.things().insert("x".into(), bld);
391        let built = builder.build().unwrap();
392        assert_eq!(
393            built.things.get("x").unwrap(),
394            &Inner {
395                fun: true,
396                explosive: false
397            }
398        );
399    }
400
401    define_map_builder! {
402        struct ThingMap2Builder =>
403        type ThingMap2 = BTreeMap<String, Inner>;
404        defaults: thingmap2_default();
405    }
406    fn thingmap2_default() -> BTreeMap<String, InnerBuilder> {
407        let mut map = BTreeMap::new();
408        {
409            let mut bld = InnerBuilder::default();
410            bld.fun(true);
411            map.insert("x".to_string(), bld);
412        }
413        {
414            let mut bld = InnerBuilder::default();
415            bld.explosive(true);
416            map.insert("y".to_string(), bld);
417        }
418        map
419    }
420    #[test]
421    fn with_defaults() {
422        let mut tm2 = ThingMap2Builder::default();
423        tm2.get_mut("x").unwrap().explosive(true);
424        let mut bld = InnerBuilder::default();
425        bld.fun(true);
426        tm2.insert("zz".into(), bld);
427        let built = tm2.build().unwrap();
428
429        assert_eq!(
430            built.get("x").unwrap(),
431            &Inner {
432                fun: true,
433                explosive: true
434            }
435        );
436        assert_eq!(
437            built.get("y").unwrap(),
438            &Inner {
439                fun: false,
440                explosive: true
441            }
442        );
443        assert_eq!(
444            built.get("zz").unwrap(),
445            &Inner {
446                fun: true,
447                explosive: false
448            }
449        );
450
451        let tm2: ThingMap2Builder = toml::from_str(
452            r#"
453            [x]
454            explosive = true
455            [zz]
456            fun = true
457            "#,
458        )
459        .unwrap();
460        let built2 = tm2.build().unwrap();
461        assert_eq!(built, built2);
462    }
463}