Skip to content

Conditional Building

On this page, we'll review a case when you have multiple branches in your code that need to set different values for different builder members. Since the builders generated by bon use the type-state pattern and the setters consume self, it is a bit more complicated for conditional code to use them. But, not until you know the patterns described below 🐱. So let's learn how to write better conditional code 📚.

The patterns described here aren't mutually exclusive. You can combine them as you see necessary to keep your code clean.

Shared partial builder

If your conditional code needs to set the same values for the same members, consider extracting this shared code into a variable that holds a partial builder.

Example:

rust
use bon::Builder;

#[derive(Builder)]
#[builder(on(String, into))]
struct User {
    name: String,
    tags: Vec<String>,
    alias: Option<String>,
    description: Option<String>,
}

// Common part is here
let user = User::builder()
    .name("Bon")
    .tags(vec!["confectioner".to_owned()]);

// Differences are in each branch
let user = if 2 + 2 == 4 {
    user
        .alias("Good girl")
        .description("Knows mathematics 🐱")
        .build()
} else {
    user
        .description("Skipped math classes 😿")
        .build()
};

It's important to call the build() method inside each of the branches of conditional code. This way every branch returns the value of the same type.

This also works with match expressions. Just remember to call .build() in every arm.

Deep Thought 👀

This pattern of branching and converging on the same code path can be described using the terms "upcasting" or "context loss".

The part of the code inside of the conditional branches has more context than the code that comes after. The code inside of the conditional block has a strongly typed builder that encodes the info that, for example, the alias member was set (and thus we can no longer call this setter).

Once the build() method is called, we no longer have the context of how exactly the User was built.

Shared total builder

In contrast to the shared partial builder, here we are going to use the builder strictly after the conditional code. The conditional code needs to create the variables that hold the component values for the builder beforehand.

Example:

rust
use bon::Builder;

#[derive(Builder)]
#[builder(on(String, into))]
struct User {
    name: String,
    tags: Vec<String>,
    alias: Option<String>,
    description: Option<String>,
}

let knows_math = 2 + 2 == 4;

// Differences are in branches for each variable
let alias = if knows_math {
    Some("Good girl")
} else {
    None
};
// ^^^^ This can be reduced to a single line with `bool::then_some()`
// (try it yourself)

let description = if knows_math {
    "Knows mathematics 🐱"
} else {
    "Skipped math classes 😿"
};

// Common part is here
let user = User::builder()
    .name("Bon")
    .tags(vec!["confectioner".to_owned()])
    // Use the `maybe_` setter to provide an `Option<T>`
    .maybe_alias(alias)
    // If the description could be `None`, we'd use `maybe_description`,
    // but in this case, all branches set the `description`, so we can
    // use this shorter setter that wraps with `Some()` internally
    .description(description)
    .build();

In this case, we create a variable for each conditional member beforehand and initialize them separately, then we just pass the results to the builder. We benefit from the maybe_ setters for optional members such that we can pass the Option values directly.

NOTE

However, creating separate variables is not strictly required. You can inline the usage of the variables in such a simple code like here, where each branch of the if takes a single line. Anyhow, branches can be much bigger in real code.

If you'd like to avoid checking the same condition multiple times, you can write a single if that returns a tuple of values for several members:

rust
let (alias, description) = if 2 + 2 == 4 {
    (Some("Good girl"), "Knows mathematics 🐱")
} else {
    (None, "Skipped math classes 😿")
};

However, when the number of members in the conditional logic grows, such an if with tuples becomes hard to read. In such a case, consider defining variables outside of the if or using the shared partial builder pattern instead.

Example:

rust
let mut alias = None;
let description;

if 2 + 2 == 4 {
    alias = Some("Good girl");
    description = "Knows mathematics 🐱";
} else {
    description = "Skipped math classes 😿";
}

// Pass the variables to the builder now
// ...