Overview
bon
is a Rust crate for generating compile-time-checked builders for functions and structs. It also provides idiomatic partial application with optional and named parameters for functions and methods.
If you are new to the concept of builders or named function arguments, and you don't know what problems they may solve for you, then check out the motivational blog post.
Installation
Add this to your Cargo.toml
to use this crate:
[dependencies]
bon = "2.3"
You can opt out of std
and alloc
cargo features with default-features = false
for no_std
environments.
Builder for a function
bon
can turn a function with positional parameters into a function with "named" parameters via a builder. It's as easy as placing the #[builder]
macro on top of it.
Example:
use bon::builder;
#[builder]
fn greet(name: &str, age: u32) -> String {
format!("Hello {name} with age {age}!")
}
let greeting = greet()
.name("Bon")
.age(24)
.call();
assert_eq!(greeting, "Hello Bon with age 24!");
TIP
Many things are customizable with additional attributes. #[builder]
macro reference describes all of them.
Any syntax for functions is supported including async
, fallible, generic functions, impl Trait
, etc. If you find an edge case where bon
doesn't work, please create an issue on GitHub.
Builder for an associated method
You can also generate a builder for associated methods. For this to work you need to add a #[bon]
macro on top of the impl
block additionally.
Example:
use bon::bon;
struct Counter {
val: u32,
}
#[bon] // <- this macro is required on the impl block
impl Counter {
#[builder]
fn new(initial: Option<u32>) -> Self {
Self {
val: initial.unwrap_or_default(),
}
}
#[builder]
fn increment(&mut self, diff: u32) {
self.val += diff;
}
}
let mut counter = Counter::builder()
.initial(3)
.build();
counter
.increment()
.diff(3)
.call();
assert_eq!(counter.val, 6);
Why is that #[bon]
macro on top of the impl
block required? 🤔 (feel free to skip)
There are a couple of technical reasons.
First, it's the lack of surrounding context given to a proc macro in Rust. A proc macro sees only the syntax it is placed on top of. For example, the #[builder]
macro inside of the impl
block can't see the impl Counter
part of the impl block above it. However, it needs that information to tell the actual type of Self
.
Second, the #[builder]
proc macro generates new items such as the builder struct type definition, which it needs to output adjacently to the impl
block itself. However, proc macros in Rust can only modify the part of the syntax they are placed on and generate new items on the same level of nesting. The #[builder]
macro inside of the impl
block can't just break out of it.
Why does it compile without an import of bon::builder
? 🤔 (feel free to skip)
This is because there is no separate #[builder]
proc macro running in this case. Only the #[bon]
macro handles code generation, it's an active attribute, while #[builder]
is a dumb inert data attribute (see the Rust Reference for details about active and inert attributes).
It wouldn't harm if bon::builder
was imported. It won't shadow the inert #[builder]
attribute, but the compiler will report that the import of that macro is unused.
To follow the usual Rust builder naming conventions bon
treats the method named new
inside of the impl block specially. It generates functions with a bit of different names.
If #[builder]
is placed on the method called new
, then the generated functions are called:
Start function | Finish function |
---|---|
builder() -> {T}Builder | build(self) -> T |
For any other methods not called new
and for any free function the naming is a bit different:
Start function | Finish function |
---|---|
{fn_name}() -> {T?}{PascalCaseFnName}Builder | call(self) -> T |
Builder for a struct
bon
supports the classic pattern of annotating a struct to generate a builder with the #[derive(Builder)]
syntax.
Example:
use bon::Builder;
#[derive(Builder)]
struct User {
id: u32,
name: String,
}
let user = User::builder()
.id(1)
.name("Bon".to_owned())
.build();
assert_eq!(user.id, 1);
assert_eq!(user.name, "Bon");
TIP
#[derive(Builder)]
on a struct generates builder API that is fully compatible with placing #[builder]
attribute on the new()
method with a signature similar to the struct's fields.
See compatibility page for details.
In general, both #[derive(Builder)]
on structs and #[builder]
on functions/methods have almost the same API. We'll use both of them throughout the documentation to provide examples. If the example shows only the usage of one syntax (e.g. #[builder]
), it's very likely that the other syntax (e.g. #[derive(Builder)]
) works similarly unless explicitly stated otherwise.
No panics possible
The builders generated by #[builder]
and #[derive(Builder)]
use the typestate pattern to make sure all required parameters are filled, and the same setters aren't called repeatedly to prevent unintentional overwrites and typos. If something is wrong, a compile error will be created. There are no potential panics and unwrap()
calls inside of the builder.
Option<T>
values are optional
If your function argument or struct field (or member for short) is of type Option<T>
, then the generated builder will not enforce setting a value for this member, defaulting to None
.
It also generates two setters: one accepts T
and the other accepts Option<T>
. The first avoids wrapping values with Some()
on the call site. The second allows passing the Option<T>
value directly.
use bon::Builder;
#[derive(Builder)]
struct Projection {
x: Option<u32>,
y: Option<u32>,
// Use an annotation for members of non-`Option` type
#[builder(default)]
z: u32,
}
// Both `x` and `y` will be set to `None`, `z` will be set to `0`
Projection::builder().build();
Projection::builder()
// Pass the value without wrapping it with `Some()`
.x(10)
// Or use a `maybe_`-prefixed setter that accepts `Option`
.maybe_y(Some(20))
// The APIs generated for `#[builder(default)]` and `Option<T>` are equivalent.
// `z` will be set to `0` when `build()` is called.
.maybe_z(None)
.build();
See optional members page for details.
Into
conversions
If you have members of type String
, or PathBuf
, and you need to set them to a hard-coded string literal, then you have to write .to_owned()
or .to_string()
or .into()
.
Example:
use bon::Builder;
use std::path::PathBuf;
#[derive(Builder)]
struct Project {
name: String,
description: String,
path: PathBuf,
}
Project::builder()
.name("Bon".to_owned())
.description("Awesome crate 🐱".to_string())
.path("/path/to/bon".into())
.build();
However, you can ask bon
to generate setters that accept impl Into<T>
to remove the need for manual conversion.
This can be configured with #[builder(into)]
for a single member or with #[builder(on({type}, into))]
for many members at once.
use bon::Builder;
use std::path::PathBuf;
// All setters for members of type `String` will accept `impl Into<String>`
#[derive(Builder)]
#[builder(on(String, into))]
struct Project {
name: String,
description: String,
// The setter only for this member will accept `impl Into<PathBuf>`
#[builder(into)]
path: PathBuf,
}
Project::builder()
// &str is converted to `String` internally
.name("Bon")
.description("Awesome crate 🐱")
// `&str` is converted to `PathBuf` internally
.path("/path/to/your/heart")
.build();
See the "Into Conversions In-Depth" page for more details and important caveats (!).
What's next?
TIP
If you like the idea of this crate and want to say "thank you" or "keep up doing this" consider giving us a star ⭐ on Github. Any support and contribution are appreciated 🐱!
This is just part of what's available in bon
. You may consider reading the rest of the Guide
section to harness the full power of bon
and understand the decisions it makes. Just click on the "Next page" link at the bottom.
However, feel free to skip the docs and just use the #[builder]
and #[derive(Builder)]
in your code. They are designed to be intuitive, so they'll probably do the thing you want them to do already.
If you can't figure something out, consult the docs and maybe use that search 🔍 Search
thing at the top to navigate. You may also create an issue or a discussion in the Github repository for help, or write us a message in Discord (see below).
Socials
Here you can leave feedback, ask questions, report bugs, or just write "thank you". | |
Profile of the maintainer. There are only posts about bon and Rust in general here. |
Acknowledgments
This project was heavily inspired by such awesome crates as buildstructor
, typed-builder
and derive_builder
. This crate was designed with many lessons learned from them.
See alternatives for comparison.