Macro define_map_builder

Source
macro_rules! define_map_builder {
    {
        $(#[ $b_m:meta ])*
        $b_v:vis struct $btype:ident =>
        $(#[ $m:meta ])*
        $v:vis type $maptype:ident = $coltype:ident < $keytype:ty , $valtype: ty >;
        $( defaults: $defaults:expr; )?
    } => { ... };
    {@if_empty {} {$($x:tt)*}} => { ... };
    {@if_empty {$($y:tt)*} {$($x:tt)*}} => { ... };
}
Expand description

Define a map type, and an associated builder struct, suitable for use in a configuration object.

We use this macro when we want a configuration structure to contain a key-to-value map, and therefore we want its associated builder structure to contain a map from the same key type to a value-builder type.

The key of the map type must implement Serialize, Clone, and Debug. The value of the map type must have an associated “Builder” type formed by appending Builder to its name. This Builder type must implement Serialize, Deserialize, Clone, and Debug, and it must have a build(&self) method returning Result<value, ConfigBuildError>.

§Syntax and behavior

define_map_builder! {
    BuilderAttributes
    pub struct BuilderName =>

    MapAttributes
    pub type MapName = ContainerType<KeyType, ValueType>;

    defaults: defaults_func(); // <--- this line is optional
}

In the example above,

  • BuilderName, MapName, and ContainerType may be replaced with any identifier;
  • BuilderAttributes and MapAttributes can be replaced with any set of attributes (such sa doc comments, #derive, and so on);
  • The pubs may be replaced with any visibility;
  • and KeyType and ValueType may be replaced with any appropriate types.
    • ValueType must have a corresponding ValueTypeBuilder.
    • ValueTypeBuilder must implement ExtendBuilder.

Given this syntax, this macro will define “MapType” as an alias for Container<KeyType,ValueType>, and “BuilderName” as a builder type for that container.

“BuilderName” will implement:

  • Deref and DerefMut with a target type of Container<KeyType, ValueTypeBuilder>
  • Default, Clone, and Debug.
  • Serialize and Deserialize
  • A build() function that invokes build() on every value in its contained map.

(Note that in order to work as a sub-builder within our configuration system, “BuilderName” should be the same as “MapName” concatenated with “Builder.”)

The defaults_func(), if provided, must be a function returning ContainerType<KeyType, ValueType>. The values returned by default_func() map are used to implement Default and Deserialize for BuilderName, extending from the defaults with ExtendStrategy::ReplaceLists. If no defaults_func is given, ContainerType::default() is used.

§Example

#[derive(Clone, Debug, Builder, Deftly, Eq, PartialEq)]
#[derive_deftly(ExtendBuilder)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
pub struct ConnectionsConfig {
    #[builder(sub_builder)]
    #[deftly(extend_builder(sub_builder))]
    conns: ConnectionMap
}

define_map_builder! {
    pub struct ConnectionMapBuilder =>
    pub type ConnectionMap = BTreeMap<String, ConnConfig>;
}

#[derive(Clone, Debug, Builder, Deftly, Eq, PartialEq)]
#[derive_deftly(ExtendBuilder)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
pub struct ConnConfig {
    #[builder(default="true")]
    enabled: bool,
    port: u16,
}

let defaults: ConnectionsConfigBuilder = toml::from_str(r#"
[conns."socks"]
enabled = true
port = 9150

[conns."http"]
enabled = false
port = 1234

[conns."wombat"]
port = 5050
"#).unwrap();
let user_settings: ConnectionsConfigBuilder = toml::from_str(r#"
[conns."http"]
enabled = false
[conns."quokka"]
enabled = true
port = 9999
"#).unwrap();

let mut cfg = defaults.clone();
cfg.extend_from(user_settings, ExtendStrategy::ReplaceLists);
let cfg = cfg.build().unwrap();
assert_eq!(cfg, ConnectionsConfig {
    conns: vec![
        ("http".into(), ConnConfig { enabled: false, port: 1234}),
        ("quokka".into(), ConnConfig { enabled: true, port: 9999}),
        ("socks".into(), ConnConfig { enabled: true, port: 9150}),
        ("wombat".into(), ConnConfig { enabled: true, port: 5050}),
    ].into_iter().collect(),
});

In the example above, the derive_map_builder macro expands to something like:

pub type ConnectionMap = BTreeMap<String, ConnConfig>;

#[derive(Clone,Debug,Serialize,Educe)]
#[educe(Deref,DerefMut)]
pub struct ConnectionMapBuilder(BTreeMap<String, ConnConfigBuilder);

impl ConnectionMapBuilder {
    fn build(&self) -> Result<ConnectionMap, ConfigBuildError> {
        ...
    }
}
impl Default for ConnectionMapBuilder { ... }
impl Deserialize for ConnectionMapBuilder { ... }
impl ExtendBuilder for ConnectionMapBuilder { ... }

§Notes and rationale

We use this macro, instead of using a Map directly in our configuration object, so that we can have a separate Builder type with a reasonable build() implementation.

We don’t support complicated keys; instead we require that the keys implement Deserialize. If we ever need to support keys with their own builders, we’ll have to define a new macro.

We use ExtendBuilder to implement Deserialize with defaults, so that the provided configuration options can override only those parts of the default configuration tree that they actually replace.