1 unstable release

0.1.0 Oct 27, 2024

#1975 in Rust patterns

CC0 license

9KB

builder!

A macro, or two, to simplify usage of builder pattern on structs.

use builder_option::Builder;

#[derive(Debug, Builder)]
pub struct Client {
    pub field: bool,
    pub another: Option<bool>,
    pub optional: Option<String>,
}

fn main() -> Result<(), dyn std::error::Error> {
    let client = Client::builder()
        .with_field(true)
        .with_another(false),
        .build()?;
    
    assert!(client.field);
    assert_eq!(client.another, false);
    assert!(client.optional.is_none());
}

LICENSE


lib.rs:

This crate contains two macros to declare a struct and a corresponding builder.

  • builder!: By-example macro that creates a struct and the builder.
  • Builder: Derive macro that creates a builder for annotated struct.

The macro-by-example syntax is inspired by azriel91/builder-macro, but with a different philosophy of treating required vs. optional fields, and therefore a different design choice.

Background

For usage, please skip ahead to the Usage section.

Thic crate was created as a sort-of practice in building macros in Rust, hence 2 similar approaches, one using proc-macro, and one using macro-by-example, that have identical syntax.

The key feature of the macro is that if a client struct defines a field as Option, the builder's API still takes an inner value in setters, but the build process won't enforce those fields, while it'll break if a value for regular, non-Option fields, is not provided.

Returning Result allows the caller to handle failures gracefully.

Usage

builder!

Specify dependency in your crate's Cargo.toml

[dependencies]
builder_option = "0.1.0"

Include the macro inside your crate's lib.rs or main.rs.

use builder_option::builder;

derive(Builder)

The default version of crate defines only the builder! macro-by-example. If you prefer the Builder derive proc-macro version, specify derive feature:

[dependencies]
builder_option = { version = "0.1.0", features = [ "derive" ] }

Include the macro inside your crate's lib.rs or main.rs.

use builder_option::Builder;

Examples

A simpliest way to use the macro is to create a simple struct:

use builder_option::builder;

builder!(ClientBuilder -> Client {
    pub required{bool},
})

fn main() -> Result<(), Box<dyn Error>> {
    let client = Client::builder()
        .with_required(true)
        .build()?;

    assert!(client.required);
}

You can see the funny C++-like initializer syntax for types: required{bool}, this an unfortunate consequence of Rust macro limitations.

A failure to provide a required parameter will cause an error:

use builder_option::builder;

builder!(ClientBuilder -> Client {
    pub required{bool},
})

fn main() {
    let client = Client::builder()
        .build();

    assert!(client.is_err());
}

Fields marked as optional—i.e., wrapped in an Option—are simplified in the builder API by unwrapping one level of Option. This means that for a field declared as optional: Option<bool>, the builder will provide a setter method set_optional(value: bool) that accepts a bool instead of an Option. However, if a field has nested options like optional: Option<Option<bool>>, the builder unwrapping only the first level results in a setter method that accepts Option<bool>.

use builder_option::builder;

builder!(ClientBuilder -> Client {
    pub required{bool},
    pub optional1{Option<bool>},
    pub optional2{Option<Option<bool>>},
    pub optional3{String},
})

fn main() -> Result<(), Box<dyn Error>> {
    let client = Client::builder()
        .with_required(true)
        .with_optional1(true)
        .with_optional2(Some(true))
        .build()?;

    assert!(client.required);
    assert!(client.optional1.unwrap()?);
    assert!(client.optional3.is_none());

}

Same result using derive proc-macro:

use builder_option::Builder;

#[derive(Debug, Builder)]
pub struct Client {
    pub required: bool,
    pub optional1: Option<bool>,
    pub optional2: Option<Option<bool>>,
    pub optional3: Option<String>
}

Dependencies

~105KB