Fallible Builders
With bon
, you can write a builder that validates its inputs and returns a Result
. It's possible to do this via the function or associated method syntax. Simply write a constructor function with the Result
return type and add a #[builder]
to it.
use anyhow::Error;
use bon::bon;
struct User {
id: u32,
name: String,
}
#[bon]
impl User {
#[builder]
fn new(id: u32, name: String) -> Result<Self, Error> {
if name.is_empty() {
return Err(anyhow::anyhow!("Empty name is disallowed (user id: {id})"));
}
Ok(Self { id, name })
}
}
// The `build()` method returns a `Result`
let result = User::builder()
.id(42)
.name(String::new())
.build();
if let Err(error) = result {
// Handle the error
}
With this approach, the finishing function of the generated builder returns a Result
. Thus, validations are deferred until you invoke the finishing build()
or call()
.
Fallible Setter
You can do validations earlier instead, right when the setter is called. Use #[builder(with)]
with a fallible closure to achieve that. The following example is an excerpt from that attribute's API reference, see more details there in the Fallible Closure section.
use bon::Builder;
use std::num::ParseIntError;
#[derive(Builder)]
struct Example {
#[builder(with = |string: &str| -> Result<_, ParseIntError> {
string.parse()
})]
x1: u32,
}
fn main() -> Result<(), ParseIntError> {
Example::builder()
.x1("99")? // <-- the setter returns a `Result`
.build();
Ok(())
}
use bon::builder;
use std::num::ParseIntError;
#[builder]
fn example(
#[builder(with = |string: &str| -> Result<_, ParseIntError> {
string.parse()
})]
x1: u32,
) -> u32 {
x1
}
fn main() -> Result<(), ParseIntError> {
example()
.x1("99")? // <-- the setter returns a `Result`
.call();
Ok(())
}
use bon::bon;
use std::num::ParseIntError;
struct Example;
#[bon]
impl Example {
#[builder]
fn example(
#[builder(with = |string: &str| -> Result<_, ParseIntError> {
string.parse()
})]
x1: u32,
) -> u32 {
x1
}
}
fn main() -> Result<(), ParseIntError> {
Example::example()
.x1("99")? // <-- the setter returns a `Result`
.call();
Ok(())
}
None Of This Works. Help!
This is very, very(!) unlikely but if you have an elaborate use case where none of the options above are flexible enough, then your last resort is writing a custom method on the builder. You'll need to study the builder's Typestate API to be able to do that. Don't worry, it's rather simple, and you'll gain a lot of power at the end of the day 🐱.
Future Possibilities
If you have some design ideas for an attributes API to do validations with the builder macros, then feel free to post them in this Github issue.