bon

repository·master·Indexed 24 days ago

https://github.com/elastio/bon

A Rust crate for generating compile-time-checked builders for structs and functions. It provides idiomatic partial application with named and optional parameters using the typestate pattern to ensure all required parameters are filled before execution, preventing runtime panics. Includes the #[builder] attribute for functions and methods, #[derive(Builder)] for structs, and helper macros like map! and set!.

Tokens
65.3K
Snippets
181
Records
271
Agent score
83%

What's inside bon

  1. Improved compile times in Bon 2.1

    master
    Bon 2.1 introduced significant optimizations to the code generated by the #[bon::builder] macro. By moving from generic associated type references to separate generic parameters and consolidating multiple impl blocks into a single block for all builder methods, compilation speed for large projects (using many #[builder] annotations) has improved by approximately 36%.
  2. Compare `bon` with other builder crates

    master

    Use the following comparison to understand how bon differs from other popular Rust builder crates like buildstructor, typed-builder, and derive_builder.

    Key advantages of bon include:

    • Versatility: Supports builders for structs, functions, and methods.
    • Panic Safety: All builders are panic-safe.
    • API Stability: Decouples the builder API from the internal struct representation using the function-based builder paradigm.
    • Advanced Features: Supports Option<T> for optional members, Into conversions in setters (opt-in), fallible validation in the finishing function, and fallible setters via the with = closure attribute.
    • Clean Documentation: Uses a trait-based typestate design to avoid the 'noisy generics' problem in generated rustdoc output common in other crates.
  3. Avoid `impl Into` with generic return types

    master

    When a function returns a generic type (like str::parse()), the compiler relies on the usage context to infer the type. If the bon setter uses impl Into<T>, the setter no longer provides a single concrete type hint. This prevents the compiler from deducing the type of the variable, requiring an explicit type annotation.

    use bon::builder;
    use std::net::IpAddr;
    
    #[builder]
    fn connect(#[builder(into)] ip_addr: IpAddr) { /* */ }
    
    let ip_addr = "127.0.0.1".parse().unwrap();
    
    connect()
        .ip_addr(ip_addr)
        .call();
  4. Decide when to use `bon` builders

    master

    Choosing whether to use bon depends on your use case and where the builder is used:

    Use bon for Public APIs

    If you are designing a crate's public API, bon is highly recommended. It focuses on:

    • Breaking change prevention: Ensuring your API remains stable and evolvable (Compatibility).
    • Ergonomics: Providing a convenient and clean API for your users.

    Use bon for Private Modules

    If you are using builders within private modules, you are likely looking for:

    General Rule of Thumb

    Do not blindly add builders to every struct or function. Builders make the most sense for pervasive structs and functions that are constructed or called frequently. If a struct is only used in a single location, a builder is likely unnecessary overhead.

  5. Use the `on` attribute to apply settings to multiple members

    master

    The on(type_pattern, attributes) attribute allows you to apply member attributes to all members that match a specific type pattern. This is useful for reducing boilerplate when you want to apply the same behavior (like into or required) to multiple fields in a struct, function parameters, or method arguments.

    Usage Syntax

    #[builder(on(type_pattern, attributes))]

    You can also specify multiple on clauses, but they must be consecutive (no other attributes allowed between them).

    Supported Attributes

    • into: Allows members to accept impl Into<T>.
    • required: Makes members required (currently only works with the _ type pattern).
    • setters(doc(default(skip))): Skips showing default values in generated documentation.
    • overwritable: (Experimental) Allows calling setters for the same member multiple times. Requires the experimental-overwritable cargo feature.
    #[derive(Builder)]
    #[builder(on(String, into), on(PathBuf, into))]
    struct Example {
        name: String,
        path: PathBuf,
        level: u32,
    }
    
    Example::builder()
        .name("accepts `impl Into<String>`")
        .path("accepts/impl/into/PathBuf")
        .level(100)
        .build();
  6. Use `skip` and `default` expressions with member access

    master

    In bon v2, skip and default expressions can reference other members within the same struct. However, you can only access members that are declared higher in the code (i.e., members that are initialized before the current one).

    use bon::builder;
    
    #[builder]
    struct Example {
        member_1: u32,
    
        // Note that here we don't have access to `member_3` 
        // because it's declared (and thus initialized) later
        #[builder(skip = 2 * member_1)]
        member_2: u32,
    
        #[builder(skip = member_2 + member_1)]
        member_3: u32,
    }
    
    let example = Example::builder()
        .member_1(3)
        .build();
    
    assert_eq!(example.member_1, 3);
    assert_eq!(example.member_2, 6);
    assert_eq!(example.member_3, 9);
  7. How the Typestate API works in Bon 3.0

    master

    Bon 3.0 introduces a redesigned, human-readable typestate system for builders. Unlike other crates that use complex tuples representing all fields, Bon uses a layered typestate that only tracks which fields have been set. This makes the builder's type signature stable, readable, and independent of the number or order of struct fields.

    Key characteristics:

    • Layered Transitions: Each setter call wraps the previous state (e.g., SetX2<SetX1>).
    • Privacy Preserved: The typestate does not expose the internal types or order of the struct's fields.
    • Order Dependent: The type signature reflects the order of calls (e.g., x1().x2() results in SetX2<SetX1>, while x2().x1() results in SetX1<SetX2>).
    • Manual Inspection: You can import type states from the generated module (e.g., use greet_builder::{SetName, SetLevel};) to perform type-level checks or manual annotations.
    #[bon::builder]
    fn greet(level: Option<&str>, name: &str) -> String {
        format!("[{}] {name} says hello!", level.unwrap_or("DEBUG"))
    }
    
    // Import type states from the generated module
    use greet_builder::{SetName, SetLevel};
    
    let builder: GreetBuilder<SetName>           = greet().name("Bon");
    let builder: GreetBuilder<SetX2<SetX1>> = builder.level("INFO");
    
    assert_eq!("[INFO] Bon says hello!", builder.call());
  8. Understand `bon` compilation performance trade-offs

    master

    Using bon introduces more compilation overhead compared to other builder crates like typed-builder or derive_builder. This overhead is primarily due to the complexity of the generated code, which includes additional traits and structs to provide a stable typestate API and cleaner documentation by reducing generic type noise.

    Comparison Summary

    Builder crate10 structs with 50 fields100 structs with 10 fields
    bon2.096s2.340s
    typed-builder2.088s1.831s
    derive_builder0.449s1.026s
    no macros0.112s0.113s

    Why the difference exists?

    • bon vs typed-builder: bon generates more complex code to support a better typestate API and cleaner docs.
    • derive_builder speed: derive_builder is faster because it avoids using generics for typestate, but it must validate required fields at runtime (returning a Result), whereas bon performs these checks at compile-time.
    • Future Improvements: Compile times are expected to improve significantly (estimated 16-58%) once the associated_type_defaults nightly Rust feature reaches stable.
  9. Performance characteristics of #[builder]

    master

    The #[builder] macro generates code designed to be easily optimizable by the compiler. Benchmarks comparing regular positional function calls to bon builder syntax show that in many cases, rustc generates identical assembly code. Even when the assembly differs, the performance impact is typically negligible.

    Important Note: These are microbenchmarks. You should perform your own performance measurements within your specific application under real-world conditions. If you encounter performance issues, please report them via a GitHub issue.

  10. Reference other members in `skip` expressions

    master

    When using #[builder(skip = expression)], you can reference other struct members within the expression.

    Important Constraint: Members are initialized in the order they are declared. Therefore, a skip expression can only reference members that were declared earlier (higher up) in the struct definition. You cannot reference a member that is declared later in the code.

    use bon::Builder;
    
    #[derive(Builder)]
    struct Example {
        x1: u32,
    
        // x2 can reference x1 because x1 is declared earlier
        #[builder(skip = 2 * x1)]
        x2: u32,
    
        // x3 can reference x2 and x1
        #[builder(skip = x2 + x1)]
        x3: u32,
    }
    
    let example = Example::builder()
        .x1(3)
        .build();
    
    assert_eq!(example.x1, 3);
    assert_eq!(example.x2, 6);
    assert_eq!(example.x3, 9);
  11. Understand `const` function limitations in `bon`

    master
    You can apply #[builder] to const fn functions, but the generated builder methods themselves will not be marked const. This is because they rely on Into::into to manage type state transitions, which is a non-const operation. However, the rest of the generated code remains const-compatible.