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 | |
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).
This feature isn't available today in bon
, but it's planned for the future. However, it won't be enabled by default; rather, it will be opt-in like it is in derive_builder
.
The problem with this feature is that a setter that pushes an element into a collection like that may confuse the reader if only one element is pushed. This may hide the fact that the member is actually a collection called friends
in the plural. However, this feature is still useful to provide backwards compatibility when changing the type of a member from T
or Option<T>
to Collection<T>
.
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 fields of collection types are considered required by default in bon
, which isn't the case in buildstructor
.