`derive_builder`: usage and limitations

Basics

The builder pattern is a well known coding pattern. It helps with object construction by having a dedicated structure to help build the other. It is usually used when many arguments are required to build one.
The example codes are written in Rust, but the concepts behind these can be applied to many languages. I actually found the idea for this article when playing with configuration in the rust rewrite of Tor.
For example of a builder pattern, to create an Object { a: usize, b: String }, one would provide an ObjectBuilder which provides fn a(&mut self, a: usize) and fn b(&mut self, b: String) to choose the values, and a fn build(self) -> Object.
It is sometimes boilerplaty to do so: you have to write a method setting the value for each field. Here comes derive_builder for Rust which generates a builder out of any struct.

use derive_builder::Builder;

#[derive(Debug, Builder)]
pub struct Config {
    how_much_is_needed: usize,
}

#[test]
fn usage() {
    let config = ConfigBuilder::default()
        .how_much_is_needed(9001)
        .build()
        .expect("build config");

    assert_eq!(config.how_much_is_needed, 9001);
}

As you can see, the builder object is generated out of a given struct and provides all the classical methods. You can also set default values, translate input values, add more validation, … It’s quite a powerful crate.

Naively nested

But not powerful enough for me! What I really want to do is to be able to nest builders. It allows for larger, section based configuration. And to show the issue, here goes a working but ugly example to do so.

use derive_builder::Builder;

#[derive(Debug, Builder, Clone)]
pub struct Nested {
    wow_that_much: usize,
}

#[derive(Debug, Builder, Clone)]
pub struct Config {
    nested: Nested,
    indeed_thats_a_lot: usize,
}

#[test]
fn usage() {
    let config = ConfigBuilder::default()
        .indeed_thats_a_lot(666)
        .nested(
            NestedBuilder::default()
                .wow_that_much(242)
                .build()
                .expect("build nested config"),
        )
        .build()
        .expect("build config");

    assert_eq!(config.indeed_thats_a_lot, 666);
    assert_eq!(config.nested.wow_that_much, 242);
}

As you can see, you have to build twice. Also, you have the issue of readability: you lose context when looking at nested(...) content.

Fully nested

So I didn’t stop there, and I found a way to have this feature. Sadly, it adds complexity, but can hopefully be autogenerated by the Builder macro. Here goes my creation

use derive_builder::{Builder, UninitializedFieldError};

#[derive(Debug)]
pub enum ConfigBuilderError {
    Base(UninitializedFieldError),
    Nested(NestedBuilderError),
}

impl From for ConfigBuilderError {
    fn from(nested: NestedBuilderError) -> Self {
        ConfigBuilderError::Nested(nested)
    }
}

#[derive(Debug, Builder, Clone)]
pub struct Nested {
    wow_that_much: usize,
}

#[derive(Debug, Clone)]
pub struct Config {
    nested: Nested,
    indeed_thats_a_lot: usize,
}

#[derive(Default)]
pub struct ConfigBuilder {
    nested: NestedBuilder,
    indeed_thats_a_lot: Option,
}

impl ConfigBuilder {
    pub fn nested(&mut self) -> &mut NestedBuilder {
        &mut self.nested
    }

    pub fn indeed_thats_a_lot(&mut self, how_much: usize) -> &mut Self {
        self.indeed_thats_a_lot = Some(how_much);
        self
    }

    pub fn build(self) -> Result<Config, ConfigBuilderError> {
        Ok(Config {
            nested: self.nested.build()?,

            indeed_thats_a_lot: self.indeed_thats_a_lot.ok_or(ConfigBuilderError::Base(
                UninitializedFieldError::new("indeed_thats_a_lot"),
            ))?,
        })
    }
}

#[test]
fn usage() {
    let mut builder = ConfigBuilder::default();

    builder.indeed_thats_a_lot(666);
    builder.nested().wow_that_much(242);

    let config = builder.build().expect("build config");

    assert_eq!(config.indeed_thats_a_lot, 666);
    assert_eq!(config.nested.wow_that_much, 242);
}

As you can see, that’s not as easy to code anymore. But it’s quite pleasant for the users of the builder.
The Nested struct is actually a normal Builder, but I had to write my own ConfigBuilder. Here, it’s pretty much the same as what is generated by derive_builder except for the build function. This one defers to the nested builder build function. Add some error handling and you’re good to go, you can nest builders.

Now you know what is a builder, how to use Rust’s derive_builder and how to workaround its limitations. If you like the idea behind the third example, you can push it upstream (and link this article).

I’ll see you around in Rustland.

tharvik