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    {paste::paste!{
170        $(#[ $m ])*
171        $v type $maptype = $coltype < $keytype , $valtype > ;
172
173        $(#[ $b_m ])*
174        #[derive(Clone,Debug,$crate::deps::serde::Serialize, $crate::deps::educe::Educe)]
175        #[educe(Deref, DerefMut)]
176        #[serde(transparent)]
177        $b_v struct $btype( $coltype < $keytype, [<$valtype Builder>] > );
178
179        impl $btype {
180            $b_v fn build(&self) -> ::std::result::Result<$maptype, $crate::ConfigBuildError> {
181                self.0
182                    .iter()
183                    .map(|(k,v)| Ok((k.clone(), v.build()?)))
184                    .collect()
185            }
186        }
187        $(
188            // This section is expanded when we have a defaults_fn().
189            impl ::std::default::Default for $btype {
190                fn default() -> Self {
191                    Self($defaults)
192                }
193            }
194            impl<'de> $crate::deps::serde::Deserialize<'de> for $btype {
195                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196                where
197                    D: $crate::deps::serde::Deserializer<'de> {
198                        // To deserialize into this type, we create a builder holding the defaults,
199                        // and we create a builder holding the values from the deserializer.
200                        // We then use ExtendBuilder to extend the defaults with the deserialized values.
201                        let deserialized = $coltype::<$keytype, [<$valtype Builder>]>::deserialize(deserializer)?;
202                        let mut defaults = $btype::default();
203                        $crate::extend_builder::ExtendBuilder::extend_from(
204                            &mut defaults,
205                            Self(deserialized),
206                            $crate::extend_builder::ExtendStrategy::ReplaceLists);
207                        Ok(defaults)
208                    }
209            }
210        )?
211        $crate::define_map_builder!{@if_empty { $($defaults)? } {
212            // This section is expanded when we don't have a defaults_fn().
213            impl ::std::default::Default for $btype {
214                fn default() -> Self {
215                    Self(Default::default())
216                }
217            }
218            // We can't conditionally derive() here, since Rust doesn't like macros that expand to
219            // attributes.
220            impl<'de> $crate::deps::serde::Deserialize<'de> for $btype {
221                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
222                where
223                    D: $crate::deps::serde::Deserializer<'de> {
224                    Ok(Self($coltype::deserialize(deserializer)?))
225                }
226            }
227        }}
228        impl $crate::extend_builder::ExtendBuilder for $btype
229        {
230            fn extend_from(&mut self, other: Self, strategy: $crate::extend_builder::ExtendStrategy) {
231                $crate::extend_builder::ExtendBuilder::extend_from(&mut self.0, other.0, strategy);
232            }
233        }
234    }};
235    {@if_empty {} {$($x:tt)*}} => {$($x)*};
236    {@if_empty {$($y:tt)*} {$($x:tt)*}} => {};
237}
238
239#[cfg(test)]
240mod test {
241    // @@ begin test lint list maintained by maint/add_warning @@
242    #![allow(clippy::bool_assert_comparison)]
243    #![allow(clippy::clone_on_copy)]
244    #![allow(clippy::dbg_macro)]
245    #![allow(clippy::mixed_attributes_style)]
246    #![allow(clippy::print_stderr)]
247    #![allow(clippy::print_stdout)]
248    #![allow(clippy::single_char_pattern)]
249    #![allow(clippy::unwrap_used)]
250    #![allow(clippy::unchecked_duration_subtraction)]
251    #![allow(clippy::useless_vec)]
252    #![allow(clippy::needless_pass_by_value)]
253    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
254
255    use crate::ConfigBuildError;
256    use derive_builder::Builder;
257    use derive_deftly::Deftly;
258    use serde::{Deserialize, Serialize};
259    use std::collections::BTreeMap;
260
261    #[derive(Clone, Debug, Eq, PartialEq, Builder)]
262    #[builder(derive(Deserialize, Serialize, Debug))]
263    #[builder(build_fn(error = "ConfigBuildError"))]
264    struct Outer {
265        #[builder(sub_builder(fn_name = "build"))]
266        #[builder_field_attr(serde(default))]
267        things: ThingMap,
268    }
269
270    #[derive(Clone, Debug, Eq, PartialEq, Builder, Deftly)]
271    #[derive_deftly(ExtendBuilder)]
272    #[builder(derive(Deserialize, Serialize, Debug))]
273    #[builder(build_fn(error = "ConfigBuildError"))]
274    struct Inner {
275        #[builder(default)]
276        fun: bool,
277        #[builder(default)]
278        explosive: bool,
279    }
280
281    define_map_builder! {
282        struct ThingMapBuilder =>
283        type ThingMap = BTreeMap<String, Inner>;
284    }
285
286    #[test]
287    fn parse_and_build() {
288        let builder: OuterBuilder = toml::from_str(
289            r#"
290[things.x]
291fun = true
292explosive = false
293
294[things.yy]
295explosive = true
296fun = true
297"#,
298        )
299        .unwrap();
300
301        let built = builder.build().unwrap();
302        assert_eq!(
303            built.things.get("x").unwrap(),
304            &Inner {
305                fun: true,
306                explosive: false
307            }
308        );
309        assert_eq!(
310            built.things.get("yy").unwrap(),
311            &Inner {
312                fun: true,
313                explosive: true
314            }
315        );
316    }
317
318    #[test]
319    fn build_directly() {
320        let mut builder = OuterBuilder::default();
321        let mut bld = InnerBuilder::default();
322        bld.fun(true);
323        builder.things().insert("x".into(), bld);
324        let built = builder.build().unwrap();
325        assert_eq!(
326            built.things.get("x").unwrap(),
327            &Inner {
328                fun: true,
329                explosive: false
330            }
331        );
332    }
333
334    define_map_builder! {
335        struct ThingMap2Builder =>
336        type ThingMap2 = BTreeMap<String, Inner>;
337        defaults: thingmap2_default();
338    }
339    fn thingmap2_default() -> BTreeMap<String, InnerBuilder> {
340        let mut map = BTreeMap::new();
341        {
342            let mut bld = InnerBuilder::default();
343            bld.fun(true);
344            map.insert("x".to_string(), bld);
345        }
346        {
347            let mut bld = InnerBuilder::default();
348            bld.explosive(true);
349            map.insert("y".to_string(), bld);
350        }
351        map
352    }
353    #[test]
354    fn with_defaults() {
355        let mut tm2 = ThingMap2Builder::default();
356        tm2.get_mut("x").unwrap().explosive(true);
357        let mut bld = InnerBuilder::default();
358        bld.fun(true);
359        tm2.insert("zz".into(), bld);
360        let built = tm2.build().unwrap();
361
362        assert_eq!(
363            built.get("x").unwrap(),
364            &Inner {
365                fun: true,
366                explosive: true
367            }
368        );
369        assert_eq!(
370            built.get("y").unwrap(),
371            &Inner {
372                fun: false,
373                explosive: true
374            }
375        );
376        assert_eq!(
377            built.get("zz").unwrap(),
378            &Inner {
379                fun: true,
380                explosive: false
381            }
382        );
383
384        let tm2: ThingMap2Builder = toml::from_str(
385            r#"
386            [x]
387            explosive = true
388            [zz]
389            fun = true
390            "#,
391        )
392        .unwrap();
393        let built2 = tm2.build().unwrap();
394        assert_eq!(built, built2);
395    }
396}