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}