Expand description
Type-State Builder Pattern Implementation
This crate provides a derive macro for implementing the Type-State Builder pattern in Rust, enabling compile-time validation of required fields and zero-cost builder abstractions.
§Overview
The Type-State Builder pattern uses Rust’s type system to enforce compile-time validation of required fields. It 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
§Key Features
- Zero Runtime Cost: All validation happens at compile time
- Automatic Pattern Selection: Chooses the best builder pattern for your struct
- Comprehensive Generic Support: Handles complex generic types and lifetimes
- Flexible Configuration: Extensive attribute-based customization
- Excellent Error Messages: Clear guidance for fixing configuration issues
§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
§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 = "value|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_into
for this field#[builder(converter = |param: InputType| -> FieldType { expression })
- Custom conversion logic for setter input
§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)]
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".to_string())
.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();
§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 set
use 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.