Expand description
Type-State Builder Pattern Implementation
A derive macro that generates compile-time safe builders using the type-state pattern. It prevents incomplete object construction by making missing required fields a compile-time error rather than a runtime failure.
For complete documentation, installation instructions, and guides, see the README.
§Design Philosophy
This crate was designed with AI-assisted development in mind. Two principles guided its design:
§Compiler-Enforced Correctness
In AI-assisted development, code generation happens rapidly. By encoding field requirements in the type system, errors are caught immediately by the compiler rather than manifesting as runtime failures. If the code compiles, the builder is correctly configured.
§Actionable Error Messages
Instead of using generic type parameters to track field states (which produce cryptic error messages), this crate generates named structs for each state:
UserBuilder_HasName_MissingEmail // Name set, email missing
UserBuilder_HasName_HasEmail // Both set - build() availableWhen an error occurs, the type name immediately indicates which fields are missing. This is particularly valuable for AI assistants that can parse and act on the error.
§Trade-offs
This approach generates more structs than type-parameter-based solutions, slightly increasing compile time. However, the improved error message clarity is worth this cost. There is no runtime cost due to Rust’s zero-cost abstractions.
§Compatibility
- no_std: Fully compatible. Generated code uses only
coretypes. - MSRV: Rust 1.70.0 or later.
§Overview
The macro automatically selects between two builder patterns:
- Type-State Builder: For structs with required fields, providing compile-time
safety that prevents calling
build()until all required fields are set - Regular Builder: For structs with only optional fields, providing a simple
builder with immediate
build()availability
§Quick Start
Add the derive macro to your struct and mark required fields:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
#[builder(impl_into)] // Setters arguments are now `impl Into<FieldType>`
struct User {
#[builder(required)]
name: String,
#[builder(required)]
email: String,
#[builder(default = Some(30))] // Default value for age
age: Option<u32>,
#[builder(setter_prefix = "is_")] // The setter is now named `is_active`
active: bool, // Will use Default::default() if not set
}
// Usage - this enforces that name and email are set
let user = User::builder()
.name("Alice") // `impl_into` allows to pass any type that can be converted into String
.email("alice@example.com") // `impl_into` allows to pass any type that can be converted into String
// age is not set and defaults to Some(30)
// active is not set and defaults to false (Default::default())
.build(); // The `build()` method is available since all required fields are set
assert_eq!(user.name, "Alice".to_string());
assert_eq!(user.email, "alice@example.com".to_string());
assert_eq!(user.age, Some(30));
assert_eq!(user.active, false);§Supported Attributes
§Struct-level Attributes
#[builder(build_method = "method_name")]- Custom build method name#[builder(setter_prefix = "prefix_")]- Prefix for all setter method names#[builder(impl_into)]- Generate setters withimpl Into<FieldType>parameters#[builder(const)]- Generateconst fnbuilder methods for compile-time construction
§Field-level Attributes
#[builder(required)]- Mark field as required#[builder(setter_name = "name")]- Custom setter method name#[builder(setter_prefix = "prefix_")]- Custom prefix for setter method name#[builder(default = expression)]- Custom default value#[builder(skip_setter)]- Don’t generate setter (requires default)#[builder(impl_into)]- Generate setter withimpl Into<FieldType>parameter#[builder(impl_into = false)]- Override struct-levelimpl_intofor this field#[builder(converter = |param: InputType| -> FieldType { expression })- Custom conversion logic for setter input#[builder(builder_method)]- Use this field’s setter as the builder entry point (replacesbuilder())
§Advanced Examples
§Custom Defaults and Setter Names
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
#[builder(build_method = "create")]
struct Document {
#[builder(required)]
title: String,
#[builder(required, setter_name = "set_content")]
content: String,
#[builder(default = 42)]
page_count: u32,
#[builder(default = String::from("draft"), skip_setter)]
status: String,
}
let doc = Document::builder()
.title("My Document".to_string())
.set_content("Document content here".to_string())
.create(); // Custom build method name§Generic Types and Lifetimes
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct Container<T: Clone>
where
T: Send
{
#[builder(required)]
value: T,
#[builder(required)]
name: String,
tags: Vec<String>,
}
let container = Container::builder()
.value(42)
.name("test".to_string())
.tags(vec!["tag1".to_string()])
.build();§Setter Prefix Examples
use type_state_builder::TypeStateBuilder;
// Struct-level setter prefix applies to all fields
#[derive(TypeStateBuilder)]
#[builder(setter_prefix = "with_")]
struct Config {
#[builder(required)]
host: String,
#[builder(required)]
port: u16,
timeout: Option<u32>,
}
let config = Config::builder()
.with_host("localhost".to_string())
.with_port(8080)
.with_timeout(Some(30))
.build();use type_state_builder::TypeStateBuilder;
// Field-level setter prefix overrides struct-level prefix
#[derive(TypeStateBuilder)]
#[builder(setter_prefix = "with_")]
struct Database {
#[builder(required)]
connection_string: String,
#[builder(required, setter_prefix = "set_")]
credentials: String,
#[builder(setter_name = "timeout_seconds")]
timeout: Option<u32>,
}
let db = Database::builder()
.with_connection_string("postgresql://...".to_string())
.set_credentials("user:pass".to_string())
.with_timeout_seconds(Some(60))
.build();§Ergonomic Conversions with impl_into
The impl_into attribute generates setter methods that accept impl Into<FieldType>
parameters, allowing for more ergonomic API usage by automatically converting
compatible types.
use type_state_builder::TypeStateBuilder;
// Struct-level impl_into applies to all setters
#[derive(TypeStateBuilder)]
#[builder(impl_into)]
struct ApiClient {
#[builder(required)]
base_url: String,
#[builder(required)]
api_key: String,
timeout: Option<u32>,
user_agent: String, // Uses Default::default()
}
// Can now use &str directly instead of String::from() or .to_string()
let client = ApiClient::builder()
.base_url("https://api.example.com") // &str -> String
.api_key("secret-key") // &str -> String
.timeout(Some(30))
.user_agent("MyApp/1.0") // &str -> String
.build();use type_state_builder::TypeStateBuilder;
// Field-level control with precedence rules
#[derive(TypeStateBuilder)]
#[builder(impl_into)] // Default for all fields
struct Document {
#[builder(required)]
title: String, // Inherits impl_into = true
#[builder(required, impl_into = false)]
content: String, // Override: requires String directly
#[builder(impl_into = true)]
category: Option<String>, // Explicit impl_into = true
#[builder(impl_into = false)]
tags: Vec<String>, // Override: requires Vec<String> directly
}
let doc = Document::builder()
.title("My Document") // &str -> String (inherited)
.content("Content".to_string()) // Must use String (override)
.category(Some("tech".to_string())) // impl Into for Option<String>
.tags(vec!["rust".to_string()]) // Must use Vec<String> (override)
.build();Note: impl_into is incompatible with skip_setter since skipped fields
don’t have setter methods generated.
§Complete impl_into Example
use type_state_builder::TypeStateBuilder;
use std::path::PathBuf;
// Demonstrate both struct-level and field-level impl_into usage
#[derive(TypeStateBuilder)]
#[builder(impl_into)] // Struct-level default: enable for all fields
struct CompleteExample {
#[builder(required)]
name: String, // Inherits: accepts impl Into<String>
#[builder(required, impl_into = false)]
id: String, // Override: requires String directly
#[builder(impl_into = true)]
description: Option<String>, // Explicit: accepts impl Into<String>
#[builder(default = PathBuf::from("/tmp"))]
work_dir: PathBuf, // Inherits: accepts impl Into<PathBuf>
#[builder(impl_into = false, default = Vec::new())]
tags: Vec<String>, // Override: requires Vec<String>
}
// Usage demonstrating the different setter behaviors
let example = CompleteExample::builder()
.name("Alice") // &str -> String (inherited impl_into)
.id("user_123".to_string()) // Must use String (override = false)
.description(Some("Engineer".to_string())) // Option<String> required
.work_dir("/home/alice") // &str -> PathBuf (inherited impl_into)
.tags(vec!["rust".to_string()]) // Must use Vec<String> (override = false)
.build();
assert_eq!(example.name, "Alice");
assert_eq!(example.id, "user_123");
assert_eq!(example.description, Some("Engineer".to_string()));
assert_eq!(example.work_dir, PathBuf::from("/home/alice"));
assert_eq!(example.tags, vec!["rust"]);§Custom Conversions with converter
The converter attribute allows you to specify custom conversion logic for field setters,
enabling more powerful transformations than impl_into can provide. Use closure syntax
to define the conversion function with explicit parameter types.
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct User {
#[builder(required, impl_into)]
name: String,
// Normalize email to lowercase and trim whitespace
#[builder(required, converter = |email: &str| email.trim().to_lowercase())]
email: String,
// Parse comma-separated tags into Vec<String>
#[builder(converter = |tags: &str| tags.split(',').map(|s| s.trim().to_string()).collect())]
interests: Vec<String>,
// Parse age from string with fallback
#[builder(converter = |age: &str| age.parse().unwrap_or(0))]
age: u32,
}
let user = User::builder()
.name("Alice")
.email(" ALICE@EXAMPLE.COM ") // Will be normalized to "alice@example.com"
.interests("rust, programming, web") // Parsed to Vec<String>
.age("25") // Parsed from string to u32
.build();
assert_eq!(user.email, "alice@example.com");
assert_eq!(user.interests, vec!["rust", "programming", "web"]);
assert_eq!(user.age, 25);§Complex Converter Examples
use type_state_builder::TypeStateBuilder;
use std::collections::HashMap;
#[derive(TypeStateBuilder)]
struct Config {
// Convert environment-style boolean strings
#[builder(converter = |enabled: &str| {
matches!(enabled.to_lowercase().as_str(), "true" | "1" | "yes" | "on")
})]
debug_enabled: bool,
// Parse key=value pairs into HashMap
#[builder(converter = |pairs: &str| {
pairs.split(',')
.filter_map(|pair| {
let mut split = pair.split('=');
Some((split.next()?.trim().to_string(),
split.next()?.trim().to_string()))
})
.collect()
})]
env_vars: HashMap<String, String>,
// Transform slice to owned Vec
#[builder(converter = |hosts: &[&str]| {
hosts.iter().map(|s| s.to_string()).collect()
})]
allowed_hosts: Vec<String>,
}
let config = Config::builder()
.debug_enabled("true")
.env_vars("LOG_LEVEL=debug,PORT=8080")
.allowed_hosts(&["localhost", "127.0.0.1"])
.build();
assert_eq!(config.debug_enabled, true);
assert_eq!(config.env_vars.get("LOG_LEVEL"), Some(&"debug".to_string()));
assert_eq!(config.allowed_hosts, vec!["localhost", "127.0.0.1"]);§Converter with Generics
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct Container<T: Clone + Default> {
#[builder(required, impl_into)]
name: String,
// Converter works with generic fields too
#[builder(converter = |items: &[T]| items.into_iter().map(|item| item.clone()).collect())]
data: Vec<T>,
}
// Usage with concrete type
let container = Container::builder()
.name("numbers") // Convert &str to String thanks to `impl_into`
.data(&[1, 2, 3, 4, 5]) // Convert &[T] to Vec<T> thanks to `converter`
.build(); // Available only when all required fields are set
assert_eq!(container.data, vec![1, 2, 3, 4, 5]);§Converter vs impl_into Comparison
| Feature | impl_into | converter |
|---|---|---|
| Type conversions | Only Into trait | Any custom logic |
| Parsing strings | Limited | Full support |
| Data validation | No | Custom validation |
| Complex transformations | No | Full support |
| Multiple input formats | Into only | Any input type |
| Performance | Zero-cost | Depends on logic |
| Syntax | Attribute flag | Closure expression |
When to use converter:
- Ergonomic setter generation for
Option<String>fields - Parsing structured data from strings
- Normalizing or validating input data
- Complex data transformations
- Converting between incompatible types
- Custom business logic in setters
Note: converter is incompatible with skip_setter and impl_into since
they represent different approaches to setter generation.
§Optional-Only Structs (Regular Builder)
use type_state_builder::TypeStateBuilder;
// No required fields = regular builder pattern
#[derive(TypeStateBuilder)]
struct Settings {
debug: bool,
max_connections: Option<u32>,
#[builder(default = "default.log".to_string())]
log_file: String,
#[builder(skip_setter, default = 42)]
magic_number: i32,
}
// Can call build() immediately since no required fields
let settings = Settings::builder()
.debug(true)
.max_connections(Some(100))
.build();§Const Builders
The #[builder(const)] attribute generates const fn builder methods, enabling
compile-time constant construction. This is useful for embedded systems, static
configuration, and other scenarios where values must be known at compile time.
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder, Debug, PartialEq)]
#[builder(const)]
struct Config {
#[builder(required)]
name: &'static str,
#[builder(required)]
version: u32,
#[builder(default = 8080)]
port: u16,
}
// Compile-time constant construction
const APP_CONFIG: Config = Config::builder()
.name("my-app")
.version(1)
.port(3000)
.build();
// Also works in static context
static DEFAULT_CONFIG: Config = Config::builder()
.name("default")
.version(0)
.build();
// And in const fn
const fn make_config(name: &'static str) -> Config {
Config::builder()
.name(name)
.version(1)
.build()
}
const CUSTOM: Config = make_config("custom");§Const Builder Requirements
When using #[builder(const)], there are some restrictions:
- Explicit defaults required: Optional fields must use
#[builder(default = expr)]becauseDefault::default()cannot be called in const context - No
impl_into: Theimpl_intoattribute is incompatible with const builders because trait bounds are not supported in const fn - Const-compatible types: Field types must support const construction (e.g.,
&'static strinstead ofString, arrays instead ofVec)
§Const Builders with Converters
Closure converters work with const builders. The macro automatically generates
a const fn from the closure body:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder, Debug, PartialEq)]
#[builder(const)]
struct Data {
// Converter closure is transformed into a const fn
#[builder(required, converter = |s: &'static str| s.len())]
name_length: usize,
#[builder(default = 0, converter = |n: i32| n * 2)]
doubled: i32,
}
const DATA: Data = Data::builder()
.name_length("hello") // Converted to 5
.doubled(21) // Converted to 42
.build();
assert_eq!(DATA.name_length, 5);
assert_eq!(DATA.doubled, 42);Note: The closure body must be const-evaluable. If it contains non-const operations (like heap allocation or trait method calls), the Rust compiler will produce an error.
§Builder Method Entry Point
The #[builder(builder_method)] attribute makes a required field’s setter the
entry point to the builder, replacing the builder() method. This provides a
more ergonomic API when one field is the natural starting point.
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder, Debug, PartialEq)]
struct User {
#[builder(required, builder_method)]
id: u64,
#[builder(required, impl_into)]
name: String,
}
// Instead of User::builder().id(1).name("Alice").build()
let user = User::id(1).name("Alice").build();
assert_eq!(user.id, 1);
assert_eq!(user.name, "Alice".to_string());§Requirements
- Only one field per struct can have
builder_method - The field must be required (not optional)
- Cannot be combined with
skip_setter
§With Const Builders
The builder_method attribute works with const builders:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder, Debug, PartialEq)]
#[builder(const)]
struct Config {
#[builder(required, builder_method)]
name: &'static str,
#[builder(default = 0)]
version: u32,
}
const APP: Config = Config::name("myapp").version(1).build();§Error Prevention
The macro prevents common mistakes at compile time:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct User {
#[builder(required)]
name: String,
}
let user = User::builder().build(); // ERROR: required field not setuse type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct BadConfig {
#[builder(required, default = test)] // ERROR: Invalid combination
name: String,
}§Invalid Attribute Combinations
Several attribute combinations are invalid and will produce compile-time errors with helpful error messages:
§skip_setter + setter_name
You can’t name a setter method that doesn’t exist:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct InvalidSetterName {
#[builder(skip_setter, setter_name = "custom_name")] // ERROR: Conflicting attributes
field: String,
}§skip_setter + setter_prefix
You can’t apply prefixes to non-existent setter methods:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct InvalidSetterPrefix {
#[builder(skip_setter, setter_prefix = "with_")] // ERROR: Conflicting attributes
field: String,
}§skip_setter + impl_into
Skipped setters can’t have parameter conversion rules:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct InvalidImplInto {
#[builder(skip_setter, impl_into = true)] // ERROR: Conflicting attributes
field: String,
}§skip_setter + converter
Skipped setters can’t have custom conversion logic:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct InvalidConverter {
#[builder(skip_setter, converter = |x: String| x)] // ERROR: Conflicting attributes
field: String,
}§converter + impl_into
Custom converters and impl_into are different approaches to parameter handling:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct InvalidConverterImplInto {
#[builder(converter = |x: String| x, impl_into = true)] // ERROR: Conflicting attributes
field: String,
}§skip_setter + required
Required fields must have setter methods:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct InvalidRequired {
#[builder(skip_setter, required)] // ERROR: Conflicting attributes
field: String,
}§Module Organization
The crate is organized into several modules that handle different aspects
of the builder generation process. Most users will only interact with the
TypeStateBuilder derive macro.
For complete documentation, examples, and guides, see the README.
Derive Macros§
- Type
State Builder - Derives a type-safe builder for a struct with compile-time validation of required fields.