#macro-derive #derive-builder #builder #invariants #derive #validate #macro

quick-builder

compile-time checked builder generator with run-time invariant enforcement

3 releases

0.1.2 Oct 28, 2024
0.1.1 Oct 20, 2024
0.1.0 Oct 4, 2024

#22 in #derive-builder

MIT license

12KB
53 lines

QuickBuilder: Compile-Time Builder with Enforcement of Run-Time Invariants

build tests lints crates maintenance-status

⚠ Deprecation Notice

This crate is deprecated, since its use case of fallible builders is better served by the bon crate.

When Should You Try QuickBuilder?

This crate offers a simple, but powerful, compile-time builder pattern generator. The philosophy is to verify as much as possible at compile-time, while also providing a straightforward way to enforce run-time invariants.

Give QuickBuilder a shot if you want to derive a builder for your struct, that

  • makes it a compile error if you forget to set a field and
  • allows you to specify run-time invariants for your struct that are enforced at run-time and
  • you can live with the more ascetic interface that the builder provides, see the sections on limitations and alternatives.

Motivation

There are great builder crates, like bon or typed-builder, that allow you to create idiomatic builders enforcing that all necessary fields have been set at compile-time. Like those crates, QuickBuilder can generate a builder that only allows to call the final .build() method if all fields were initialized.

use quick_builder::QuickBuilder;

#[derive(QuickBuilder)]
struct ImageRef<'a, T> {
    width: usize,
    height: usize,
    data: &'a [T],
}

fn main() {
    let image_data = &[1, 2, 3, 4, 5, 6];
    let imgref = ImageRef::builder()
        .width(3)
        .height(2)
        .data(image_data)
        .build();
}

However, the example above is not the main usecase for QuickBuilder. If that's all you ever need to do, check out the bon or typed-builder crates. Those offer, among other things, more exhaustive support for idioms like optional and default parameters, as well as great ergonomics. QuickBuilder shines when you additionally need to enforce run-time invariants about your data structure.

Enforcing Run-Time Invariants

In the example above we might want to enforce a couple of invariants about the data structure which we can only check at run-time. The following example shows, how we can use QuickBuilder to enforce that...

  1. ...the width of the image is greater 0 and
  2. the height of the image is an even number greater 0 and
  3. the product of width and height is equal to the length of the given slice.
use quick_builder::QuickBuilder;

#[derive(QuickBuilder)]
#[invariant(|my| my.width * my.height == my.data.len())]
struct ImageRef<'a, T> {
    #[invariant(|w|*w>0)]
    width: usize,
    #[invariant(check_height)]
    height: usize,
    data: &'a [T],
}

// if the conditions to check invariants are too
// unwieldy to put into a closure, you can also
// define a standalone function.
fn check_height(height :&usize) -> bool {
  *height > 0 && *height % 2 == 0
}

fn main() {
    let image_data = &[1, 2, 3, 4, 5, 6];
    let imgref = ImageRef::builder()
        .width(3)
        .height(2)
        .data(image_data)
        .build()
        .unwrap();
}

One (or zero) #[invariant(...)] attributes can be applied to each field or to the struct itself. The attributes take a closure or function name to check if the invariant holds. The function (or closure) must take its argument by reference and return a bool, where true means that the invariant holds and false means it's violated.

As soon as an #[invariant(...)] attribute is encountered, the build function changes its signature. It now returns an optional instance of the original structure, where the optional contains a value if and only if all invariants where upheld during construction.

Limitations

  • Build Order: The builder function must be executed in the order of field declarations in the struct. Typically, IDE support is good enough to provide you with the next allowed option, so you don't have to look up the struct fields. The bon and typed-builder crates allow arbitrary orders, but they don't have a mechanism for enforcing run-time invariants.
  • Default/Optional Arguments: there is no support for default or optional arguments (yet).
  • Weird Generics: The builder structure contains a bit of generic magic and is not meant for passing around.
  • Consuming Builder Pattern Only: The builder uses the consuming pattern always. If you need to set fields conditionally, check out the apply_if crate.

Protecting Field Access: Getters

If care about enforcing invariants about your data, you probably want to provide getters to your fields rather than making them publicly accessible. In that case, you'll be happy to hear that this crate works seamlessly with the popular getset and derive-getters crates. Those offer derive macros for getters and setters.

Alternatives

There is a great overview of builder crates by the bon team. Of the compile-time builders in this list, to my knowledge, only the bon crate provides a way to enforce run-time invariants.

Dependencies

~220–660KB
~16K SLoC