#builder #builder-pattern #correct #compile-time #generator

macro tidy-builder

tidy-builder is a builder generator that is compile-time correct

1 unstable release

0.1.0 Aug 2, 2022

#31 in #correct

MIT license

27KB
248 lines

The Builder derive macro creates a compile-time correct builder. It means that it only allows you to build the given struct as long as you provide a value for all of its required fields.

A field is interpreted as required if it's not wrapped in an Option. Any field inside of an Option is not considered required in order to build the given struct. For example in:

pub struct MyStruct {
    foo: String,
    bar: Option<usize>,
}

The foo field is required and bar is optional. Note that although std::option::Option also referes to the same type, for now this macro doesn't recongnize anything other than Option. The builder generated using the Builder macro guarantees correctness by encoding the initialized set using const generics. An example makes it clear. Let's assume we have a struct that has two required fields and an optional one:

pub struct MyStruct {
    req1: String,
    req2: String,
    opt1: Option<String>
}

The generated builder will be:

pub struct MyStructBuilder<const P0: bool, const P1: bool> {
    req1: Option<String>,
    req2: Option<String>,
    opt1: Option<String>,
}

The P0 indicates whether the first required parameter is initialized or not. And similarly, the P1 does the same thing for the second required parameter. The initial state of the builder will be MyStructBuilder<false, false> and the first time a required field is initialized, its corresponding const generic parameter will be set to true which indicates a different state. Setting an optional value does not change the state and consequently keeps the same const generic parameters. When the builder reaches the MyStructBuilder<true, true> and only then you can call the build function on the builder.

So the complete generated code for the given example struct is:

pub struct MyStruct {
    req1: String,
    req2: String,
    opt1: Option<String>
}

pub struct MyStructBuilder<const P0: bool, const P1: bool> {
    req1: Option<String>,
    req2: Option<String>,
    opt1: Option<String>,
}

impl MyStruct {
    pub fn builder() -> MyStructBuilder<false, false> {
        MyStructBuilder {
            req1: None,
            req2: None,
            opt1: None,
        }
    }
}

impl<const P0: bool, const P1: bool> MyStructBuilder<P0, P1> {
    pub fn req1(self, req1: String) -> MyStructBuilder<true, P1> {
        MyStructBuilder {
            req1: Some(req1),
            req2: self.req2,
            opt1: self.opt1,
        }
    }
    pub fn req2(self, req2: String) -> MyStructBuilder<P0, true> {
        MyStructBuilder {
            req1: self.req1,
            req2: Some(req2),
            opt1: self.opt1,
        }
    }
    pub fn opt1(self, opt1: String) -> MyStructBuilder<P0, P1> {
        MyStructBuilder {
            req1: self.req1,
            req2: self.req2,
            opt1: Some(opt1),
        }
    }
}

impl MyStructBuilder<true, true> {
    pub fn build(self) -> MyStruct {
        unsafe {
            MyStruct {
                req1: self.req1.unwrap_unchecked(),
                req2: self.req2.unwrap_unchecked(),
                opt1: self.opt1,
            }
        }
    }
}

Dependencies

~1.5MB
~37K SLoC