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_builder::Builder;
65/// # use derive_deftly::Deftly;
66/// # use std::collections::BTreeMap;
67/// # use tor_config::{ConfigBuildError, define_map_builder, derive_deftly_template_ExtendBuilder};
68/// # use serde::{Serialize, Deserialize};
69/// # use tor_config::extend_builder::{ExtendBuilder,ExtendStrategy};
70/// #[derive(Clone, Debug, Builder, Deftly, Eq, PartialEq)]
71/// #[derive_deftly(ExtendBuilder)]
72/// #[builder(build_fn(error = "ConfigBuildError"))]
73/// #[builder(derive(Debug, Serialize, Deserialize))]
74/// pub struct ConnectionsConfig {
75///     #[builder(sub_builder)]
76///     #[deftly(extend_builder(sub_builder))]
77///     conns: ConnectionMap
78/// }
79///
80/// define_map_builder! {
81///     pub struct ConnectionMapBuilder =>
82///     pub type ConnectionMap = BTreeMap<String, ConnConfig>;
83/// }
84///
85/// #[derive(Clone, Debug, Builder, Deftly, Eq, PartialEq)]
86/// #[derive_deftly(ExtendBuilder)]
87/// #[builder(build_fn(error = "ConfigBuildError"))]
88/// #[builder(derive(Debug, Serialize, Deserialize))]
89/// pub struct ConnConfig {
90///     #[builder(default="true")]
91///     enabled: bool,
92///     port: u16,
93/// }
94///
95/// let defaults: ConnectionsConfigBuilder = toml::from_str(r#"
96/// [conns."socks"]
97/// enabled = true
98/// port = 9150
99///
100/// [conns."http"]
101/// enabled = false
102/// port = 1234
103///
104/// [conns."wombat"]
105/// port = 5050
106/// "#).unwrap();
107/// let user_settings: ConnectionsConfigBuilder = toml::from_str(r#"
108/// [conns."http"]
109/// enabled = false
110/// [conns."quokka"]
111/// enabled = true
112/// port = 9999
113/// "#).unwrap();
114///
115/// let mut cfg = defaults.clone();
116/// cfg.extend_from(user_settings, ExtendStrategy::ReplaceLists);
117/// let cfg = cfg.build().unwrap();
118/// assert_eq!(cfg, ConnectionsConfig {
119///     conns: vec![
120///         ("http".into(), ConnConfig { enabled: false, port: 1234}),
121///         ("quokka".into(), ConnConfig { enabled: true, port: 9999}),
122///         ("socks".into(), ConnConfig { enabled: true, port: 9150}),
123///         ("wombat".into(), ConnConfig { enabled: true, port: 5050}),
124///     ].into_iter().collect(),
125/// });
126/// ```
127///
128/// In the example above, the `derive_map_builder` macro expands to something like:
129///
130/// ```ignore
131/// pub type ConnectionMap = BTreeMap<String, ConnConfig>;
132///
133/// #[derive(Clone,Debug,Serialize,Educe)]
134/// #[educe(Deref,DerefMut)]
135/// pub struct ConnectionMapBuilder(BTreeMap<String, ConnConfigBuilder);
136///
137/// impl ConnectionMapBuilder {
138///     fn build(&self) -> Result<ConnectionMap, ConfigBuildError> {
139///         ...
140///     }
141/// }
142/// impl Default for ConnectionMapBuilder { ... }
143/// impl Deserialize for ConnectionMapBuilder { ... }
144/// impl ExtendBuilder for ConnectionMapBuilder { ... }
145/// ```
146///
147/// # Notes and rationale
148///
149/// We use this macro, instead of using a Map directly in our configuration object,
150/// so that we can have a separate Builder type with a reasonable build() implementation.
151///
152/// We don't support complicated keys; instead we require that the keys implement Deserialize.
153/// If we ever need to support keys with their own builders,
154/// we'll have to define a new macro.
155///
156/// We use `ExtendBuilder` to implement Deserialize with defaults,
157/// so that the provided configuration options can override
158/// only those parts of the default configuration tree
159/// that they actually replace.
160#[macro_export]
161macro_rules! define_map_builder {
162    {
163        $(#[ $b_m:meta ])*
164        $b_v:vis struct $btype:ident =>
165        $(#[ $m:meta ])*
166        $v:vis type $maptype:ident = $coltype:ident < $keytype:ty , $valtype: ty >;
167        $( defaults: $defaults:expr; )?
168    } => {
169        $crate::deps::paste!{$crate::define_map_builder! {
170            $(#[$b_m])*
171            $b_v struct $btype =>
172            $(#[$m])*
173            $v type $maptype = {
174                map: $coltype < $keytype , $valtype >,
175                builder_map: $coltype < $keytype, [<$valtype Builder>] > ,
176            }
177            $( defaults: $defaults; )?
178        }}
179    };
180    // This _undocumented_ internal syntax allows us to take the map types explicitly,
181    // so that we can accept derive-deftly outputs.
182    //
183    // Syntax:
184    // ```
185    //   «#[builder_attrs]»
186    //   «vis» struct «FooMapBuilder» =>
187    //   «#[maptype_attrs]»
188    //   «vis» type «FooMap» = {
189    //       map: «maptype»,
190    //       builder_map: «builder_map_type»,
191    //   }
192    //   ⟦ defaults: «default_expr» ; ⟧
193    // ```
194    //
195    // The defaults line and the attributes are optional.
196    // This (undocumented) syntax is identical to the documented variant above,
197    // except in the braced section after the `=` and before the optional `defaults`.
198    // In that section,
199    // the `maptype` is the type of the map which we are trying to build,
200    // (for example, `HashMap<String, WombatCfg>`),
201    // and the `builder_map` is the type of the map which instantiates the builder
202    // (for example, `HashMap<String, WombatCfgBuilder>`).
203    {
204        $(#[ $b_m:meta ])*
205        $b_v:vis struct $btype:ident =>
206        $(#[ $m:meta ])*
207        $v:vis type $maptype:ident = {
208            map: $mtype:ty,
209            builder_map: $bmtype:ty $(,)?
210        }
211        $( defaults: $defaults:expr; )?
212    } =>
213    {$crate::deps::paste!{
214        $(#[ $m ])*
215        $v type $maptype = $mtype ;
216
217        $(#[ $b_m ])*
218        #[derive(Clone,Debug,$crate::deps::serde::Serialize, $crate::deps::educe::Educe)]
219        #[educe(Deref, DerefMut)]
220        #[serde(transparent)]
221        $b_v struct $btype( $bmtype );
222
223        impl $btype {
224            /// Construct the final map.
225            $b_v fn build(&self) -> ::std::result::Result<$maptype, $crate::ConfigBuildError> {
226                self.0
227                    .iter()
228                    .map(|(k,v)| Ok((k.clone(), v.build()?)))
229                    .collect()
230            }
231        }
232        $(
233            // This section is expanded when we have a defaults_fn().
234            impl ::std::default::Default for $btype {
235                fn default() -> Self {
236                    Self($defaults)
237                }
238            }
239            impl<'de> $crate::deps::serde::Deserialize<'de> for $btype {
240                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241                where
242                    D: $crate::deps::serde::Deserializer<'de> {
243                        use $crate::deps::serde::Deserialize;
244                        // To deserialize into this type, we create a builder holding the defaults,
245                        // and we create a builder holding the values from the deserializer.
246                        // We then use ExtendBuilder to extend the defaults with the deserialized values.
247                        let deserialized = <$bmtype as Deserialize>::deserialize(deserializer)?;
248                        let mut defaults = $btype::default();
249                        $crate::extend_builder::ExtendBuilder::extend_from(
250                            &mut defaults,
251                            Self(deserialized),
252                            $crate::extend_builder::ExtendStrategy::ReplaceLists);
253                        Ok(defaults)
254                    }
255            }
256        )?
257        $crate::define_map_builder!{@if_empty { $($defaults)? } {
258            // This section is expanded when we don't have a defaults_fn().
259            impl ::std::default::Default for $btype {
260                fn default() -> Self {
261                    Self(Default::default())
262                }
263            }
264            // We can't conditionally derive() here, since Rust doesn't like macros that expand to
265            // attributes.
266            impl<'de> $crate::deps::serde::Deserialize<'de> for $btype {
267                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
268                where
269                    D: $crate::deps::serde::Deserializer<'de> {
270                    use $crate::deps::serde::Deserialize;
271                    Ok(Self(<$bmtype as Deserialize>::deserialize(deserializer)?))
272                }
273            }
274        }}
275        impl $crate::extend_builder::ExtendBuilder for $btype
276        {
277            fn extend_from(&mut self, other: Self, strategy: $crate::extend_builder::ExtendStrategy) {
278                $crate::extend_builder::ExtendBuilder::extend_from(&mut self.0, other.0, strategy);
279            }
280        }
281    }};
282    {@if_empty {} {$($x:tt)*}} => {$($x)*};
283    {@if_empty {$($y:tt)*} {$($x:tt)*}} => {};
284}
285
286#[cfg(test)]
287mod test {
288    // @@ begin test lint list maintained by maint/add_warning @@
289    #![allow(clippy::bool_assert_comparison)]
290    #![allow(clippy::clone_on_copy)]
291    #![allow(clippy::dbg_macro)]
292    #![allow(clippy::mixed_attributes_style)]
293    #![allow(clippy::print_stderr)]
294    #![allow(clippy::print_stdout)]
295    #![allow(clippy::single_char_pattern)]
296    #![allow(clippy::unwrap_used)]
297    #![allow(clippy::unchecked_time_subtraction)]
298    #![allow(clippy::useless_vec)]
299    #![allow(clippy::needless_pass_by_value)]
300    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
301
302    use crate::ConfigBuildError;
303    use derive_builder::Builder;
304    use derive_deftly::Deftly;
305    use serde::{Deserialize, Serialize};
306    use std::collections::BTreeMap;
307
308    #[derive(Clone, Debug, Eq, PartialEq, Builder)]
309    #[builder(derive(Deserialize, Serialize, Debug))]
310    #[builder(build_fn(error = "ConfigBuildError"))]
311    struct Outer {
312        #[builder(sub_builder(fn_name = "build"))]
313        #[builder_field_attr(serde(default))]
314        things: ThingMap,
315    }
316
317    #[derive(Clone, Debug, Eq, PartialEq, Builder, Deftly)]
318    #[derive_deftly(ExtendBuilder)]
319    #[builder(derive(Deserialize, Serialize, Debug))]
320    #[builder(build_fn(error = "ConfigBuildError"))]
321    struct Inner {
322        #[builder(default)]
323        fun: bool,
324        #[builder(default)]
325        explosive: bool,
326    }
327
328    define_map_builder! {
329        struct ThingMapBuilder =>
330        type ThingMap = BTreeMap<String, Inner>;
331    }
332
333    #[test]
334    fn parse_and_build() {
335        let builder: OuterBuilder = toml::from_str(
336            r#"
337[things.x]
338fun = true
339explosive = false
340
341[things.yy]
342explosive = true
343fun = true
344"#,
345        )
346        .unwrap();
347
348        let built = builder.build().unwrap();
349        assert_eq!(
350            built.things.get("x").unwrap(),
351            &Inner {
352                fun: true,
353                explosive: false
354            }
355        );
356        assert_eq!(
357            built.things.get("yy").unwrap(),
358            &Inner {
359                fun: true,
360                explosive: true
361            }
362        );
363    }
364
365    #[test]
366    fn build_directly() {
367        let mut builder = OuterBuilder::default();
368        let mut bld = InnerBuilder::default();
369        bld.fun(true);
370        builder.things().insert("x".into(), bld);
371        let built = builder.build().unwrap();
372        assert_eq!(
373            built.things.get("x").unwrap(),
374            &Inner {
375                fun: true,
376                explosive: false
377            }
378        );
379    }
380
381    define_map_builder! {
382        struct ThingMap2Builder =>
383        type ThingMap2 = BTreeMap<String, Inner>;
384        defaults: thingmap2_default();
385    }
386    fn thingmap2_default() -> BTreeMap<String, InnerBuilder> {
387        let mut map = BTreeMap::new();
388        {
389            let mut bld = InnerBuilder::default();
390            bld.fun(true);
391            map.insert("x".to_string(), bld);
392        }
393        {
394            let mut bld = InnerBuilder::default();
395            bld.explosive(true);
396            map.insert("y".to_string(), bld);
397        }
398        map
399    }
400    #[test]
401    fn with_defaults() {
402        let mut tm2 = ThingMap2Builder::default();
403        tm2.get_mut("x").unwrap().explosive(true);
404        let mut bld = InnerBuilder::default();
405        bld.fun(true);
406        tm2.insert("zz".into(), bld);
407        let built = tm2.build().unwrap();
408
409        assert_eq!(
410            built.get("x").unwrap(),
411            &Inner {
412                fun: true,
413                explosive: true
414            }
415        );
416        assert_eq!(
417            built.get("y").unwrap(),
418            &Inner {
419                fun: false,
420                explosive: true
421            }
422        );
423        assert_eq!(
424            built.get("zz").unwrap(),
425            &Inner {
426                fun: true,
427                explosive: false
428            }
429        );
430
431        let tm2: ThingMap2Builder = toml::from_str(
432            r#"
433            [x]
434            explosive = true
435            [zz]
436            fun = true
437            "#,
438        )
439        .unwrap();
440        let built2 = tm2.build().unwrap();
441        assert_eq!(built, built2);
442    }
443}