Alternatives โ
There are several other existing alternative crates for generating builders. bon
was designed based on many lessons learned from them. A table that compares the builder crates with some additional explanations is below.
Feature | bon | buildstructor | typed-builder | derive_builder |
---|---|---|---|---|
Builder for structs | โ | โ | โ | โ |
Builder for functions | โ | |||
Builder for methods | โ | โ | ||
Panic safe | โ | โ | โ | build() returns Result |
Option<T> makes members optional | โ | โ | ||
T -> Option<T> is non-breaking | โ docs | โ | via attr strip_option | via attr strip_option |
Generates T::builder() method | โ | โ | โ | only Builder::default() |
Into conversion in setters | opt-in | implicit | opt-in | opt-in |
Validation in the finishing function | โ docs | โ docs | โ docs | |
Validation in setters (fallible setters) | โ
attr with = closure | โ
TryInto via attr try_setter | ||
Custom methods on builder | โ via direct impl block | โ via attr mutators | โ via direct impl block | |
Custom fields on builder | โ
attr field | โ
attr via_mutators | โ
attr field | |
impl Trait , elided lifetimes support | โ | |||
Builder for fn hides original fn | โ | |||
Special setters for collections | (see below) | โ | โ | |
Builder by &self /&mut self | โ | |||
Generates nice docs | โ | โ |
Function Builder Paradigm Shift โ
If you ever hit a wall ๐งฑ with typed-builder
or derive_builder
, you'll have to hack something around their derive attributes syntax on a struct. With bon
or buildstructor
you can simply change the syntax from #[derive(Builder)]
on a struct to a #[builder]
on a function to gain more flexibility at any time ๐คธ. It is guaranteed to preserve compatibility, meaning it's not a breaking change.
Example โ
Suppose you already had a struct like the following with a builder derive:
use bon::Builder;
#[derive(Builder)]
pub struct Line {
x1: u32,
y1: u32,
x2: u32,
y2: u32,
}
// Suppose this is your users' code
Line::builder().x1(1).y1(2).x2(3).y2(4).build();
Then you decided to refactor ๐งน your struct's internal representation by extracting a private utility Point
type:
use bon::Builder;
#[derive(Builder)]
pub struct Line {
point1: Point,
point2: Point,
}
// Private
struct Point {
x: u32,
y: u32,
}
// Suppose this is your users' code (it no longer compiles)
Line::builder().x1(1).y1(2).x2(3).y2(4).build();
// ^^- error[E0599]: no method named `x1` found for struct `LineBuilder`
// available methods: `point1(Point)`, `point2(Point)`
There are two problems with #[derive(Builder)]
syntax in this case:
- This refactoring becomes a breaking change to
Line
's builder API ๐ข. - The private utility
Point
type leaks through the builder API viapoint1
, andpoint2
setters ๐ญ.
The fundamental problem is that the builder's API is coupled โ๏ธ with your struct's internal representation. It's literally derive
d from the fields of your struct.
Suffering โ
If you were using typed-builder
or derive_builder
, you'd be stuck for a while trying to find the magical ๐ช combination of attributes that would let you do this change without breaking compatibility or leakage of the private Point
type.
With no solution in sight ๐ฎโ๐จ, you'd then fall back to writing the same builder manually. You'd probably expand the builder derive macro and edit the generated code directly, which, ugh... hurts ๐ค.
However, that would be especially painful with typed-builder
, which generates a complex typestate that is not human-readable and maintainable enough by hand. It also references some internal #[doc(hidden)]
symbols from the typed-builder
crate. Achoo... ๐คง.
TIP
In contrast, bon
's type state is human-readable, maintainable, and documented ๐
Behold the Function-Based Builder โ
This change is as simple as pie ๐ฅง with bon
or buildstructor
. The code speaks for itself:
use bon::bon;
// No more derives on a struct. Its internal representation is decoupled from the builder.
pub struct Line {
point1: Point,
point2: Point,
}
struct Point {
x: u32,
y: u32,
}
#[bon]
impl Line {
#[builder]
fn new(x1: u32, y1: u32, x2: u32, y2: u32) -> Self {
Self {
point1: Point { x: x1, y: y1 } ,
point2: Point { x: x2, y: y2 } ,
}
}
}
// Suppose this is your users' code (it compiles after this change, yay ๐!)
Line::builder().x1(1).y1(2).x2(3).y2(4).build();
Ah... Isn't this just so simple and beautiful? ๐ The fun part is that the constructor method new
that we originally abandoned comes back to heroically save us โ๏ธ at no cost, other than a star โญ on bon
's Github repo maybe ๐?
And you know what, our old friend new
doesn't feel offended for being abandoned. It doesn't even feel emotions, actually ๐ฟ. But it's happy to help you ๐ซ.
Moreover, it offers you a completely new dimension of flexibility:
- Need some validation? Just change the
new()
method to return aResult
. The generatedbuild()
method will then become fallible. - Need to do an
async
operation in the constructor? Just make your constructorasync
and yourbuild()
will return aFuture
. - Need some adrenaline ๐? Just add
unsafe
, and... you get the idea ๐.
The chances of hitting a wall with function builders are close to zero, and even if you ever do, you still have access to the Typestate API in bon
for even more flexibility ๐ช.
Generated Docs Comparison โ
Here is a table that compares the rustdoc
output for builders generated by different crates based on different syntax.
Underlying syntax | bon | buildstructor | typed-builder | derive_builder |
---|---|---|---|---|
Struct | Link | Link | Link | Link |
Function | Link | |||
Method | Link | Link |
All builders were configured to produce roughly similar builder APIs. The notable exceptions are:
buildstructor
doesn't support#[builder(default)]
and#[builder(into)]
-like annotations;buildstructor
doesn't support doc comments on function arguments;derive_builder
doesn't support typestate-based builders;
Docs generated by typed-builder
and buildstructor
suffer from the problem of noisy generics. This problem significantly worsens with the number of fields/arguments in structs/functions. bon
solves this problem by using a trait-based design for its typestate.
bon
also includes the default values assigned via #[builder(default)]
in the docs (more examples here).
Special Setter Methods for Collections โ
Other builder crates provide a way to generate methods to build collections one element at a time. For example, buildstructor
even generates such methods by default:
#[derive(buildstructor::Builder)]
struct User {
friends: Vec<String>
}
fn main() {
User::builder()
.friend("Foo")
.friend("Bar")
.friend("`String` value is also accepted".to_owned())
.build();
}
TIP
Why is there an explicit main()
function in this code snippet ๐ค? It's a long story explained in a blog post (feel free to skip).
It is possible to implement this with bon
using custom fields and custom setters. See an example of how this can be done here.
Alternatively, bon
provides a separate solution. bon
exposes the following macros that provide convenient syntax to create collections.
Vec<T> | [T; N] | *Map<K, V> | *Set<K, V> |
---|---|---|---|
bon::vec![] | bon::arr![] | bon::map!{} | bon::set![] |
These macros share a common feature that every element of the collection is converted with Into
to shorten the syntax if you, for example, need to initialize a Vec<String>
with items of type &str
. Use these macros only if you need this behaviour, or ignore them if you want to be explicit in code and avoid implicit Into
conversions.
use bon::Builder;
#[derive(Builder)]
struct User {
friends: Vec<String>
}
User::builder()
.friends(bon::vec![
"Foo",
"Bar",
"`String` value is also accepted".to_owned(),
])
.build();
Another difference is that members of collection types are considered required by default in bon
, which isn't the case in buildstructor
.