A Rust 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.
- Introduction
- Design Philosophy
- Installation
- Quick Start
- Core Concepts
- Features
- Understanding Error Messages
- Compatibility
- How It Works
- License
- Contributing
Traditional builder patterns in Rust typically validate required fields at runtime, returning Result or panicking when
fields are missing:
// Traditional builder - fails at runtime
let user = UserBuilder::new()
.name("Alice")
// Forgot to set email
.build(); // Returns Err or panics at runtimeTypeStateBuilder moves this validation to compile time:
// TypeStateBuilder - fails at compile time
let user = User::builder()
.name("Alice".to_string())
// Forgot to set email
.build(); // Compile error: method `build` not foundThe compiler error message clearly indicates what is missing:
error[E0599]: no method named `build` found for struct
`UserBuilder_HasName_MissingEmail` in the current scope
TypeStateBuilder was designed with AI-assisted development in mind. Two principles guided its design:
In AI-assisted development, code generation happens rapidly. LLMs can produce syntactically correct code that nonetheless contains logical errors, such as forgetting to initialize required fields. By encoding field requirements in the type system, TypeStateBuilder ensures that such errors are caught immediately by the compiler rather than manifesting as runtime failures.
The type system becomes a safety net: if the code compiles, the builder is correctly configured.
Many type-state builder implementations use generic type parameters to track field states:
// Other implementations might generate something like:
UserBuilder<Set, Unset, Set, Unset>
// ^ ^ ^ ^
// What do these mean?When a required field is missing, the resulting error message requires decoding which type parameter corresponds to which field.
TypeStateBuilder takes a different approach. It generates a separate struct for each possible state, with the struct name explicitly describing which fields have been set and which are missing:
UserBuilder_HasName_HasEmail // Both fields set - build() available
UserBuilder_HasName_MissingEmail // Name set, email missing
UserBuilder_MissingName_HasEmail // Email set, name missing
UserBuilder_MissingName_MissingEmail // Neither field set
When an AI assistant encounters an error like UserBuilder_HasName_MissingEmail doesn't have method build, it can
immediately understand that the email field needs to be set. No documentation lookup or type parameter decoding is
required.
This approach generates more structs than a type-parameter-based solution, which increases compile time slightly. However, the improved error message clarity is worth this cost, particularly in AI-assisted workflows where rapid iteration and clear feedback are essential.
Importantly, there is no runtime cost. Rust's zero-cost abstractions ensure that the generated code is as efficient as a hand-written builder.
Add TypeStateBuilder to your Cargo.toml:
[dependencies]
type-state-builder = "0.5.1"TypeStateBuilder is compatible with no_std environments. The generated code uses only core types
(core::option::Option, core::marker::PhantomData, etc.) and does not require the standard library.
TypeStateBuilder requires Rust 1.70.0 or later.
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder, Debug)]
struct User {
#[builder(required)]
name: String,
#[builder(required)]
email: String,
age: Option<u32>,
}
fn main() {
// All required fields must be set before build() is available
let user = User::builder()
.name("Alice".to_string())
.email("alice@example.com".to_string())
.age(Some(30))
.build();
println!("{:?}", user);
// Optional fields can be omitted
let user2 = User::builder()
.name("Bob".to_string())
.email("bob@example.com".to_string())
.build();
println!("{:?}", user2);
}Fields marked with #[builder(required)] must be set before the build() method becomes available. All other fields
are optional and will use their default values if not set.
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct Config {
#[builder(required)]
api_key: String, // Must be set
timeout: Option<u32>, // Optional, defaults to None
retries: u32, // Optional, defaults to 0
}TypeStateBuilder automatically selects the appropriate builder pattern based on your struct:
- Type-state builder: When the struct has required fields. The
build()method is only available after all required fields are set. - Regular builder: When all fields are optional. The
build()method is available immediately.
Each setter method returns a new builder type that reflects the updated state. For a struct with required fields name
and email:
User::builder() // UserBuilder_MissingName_MissingEmail
.name("Alice".to_string()) // UserBuilder_HasName_MissingEmail
.email("alice@example.com".to_string()) // UserBuilder_HasName_HasEmail
.build() // UserOptional fields can be set at any point without affecting the type-state progression.
Mark fields as required using the required attribute:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct DatabaseConfig {
#[builder(required)]
host: String,
#[builder(required)]
database: String,
port: u16, // Optional, defaults to 0
}
let config = DatabaseConfig::builder()
.host("localhost".to_string())
.database("myapp".to_string())
.build();Provide custom default values for optional fields:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct ServerConfig {
#[builder(required)]
host: String,
#[builder(default = 8080)]
port: u16,
#[builder(default = 30)]
timeout_seconds: u32,
#[builder(default = String::from("production"))]
environment: String,
}
let config = ServerConfig::builder()
.host("localhost".to_string())
// port defaults to 8080
// timeout_seconds defaults to 30
// environment defaults to "production"
.build();Some fields should only use their default value without exposing a setter:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct Document {
#[builder(required)]
title: String,
#[builder(default = generate_id(), skip_setter)]
id: String,
#[builder(default = now(), skip_setter)]
created_at: u64,
}
fn generate_id() -> String {
"doc-123".to_string()
}
fn now() -> u64 {
1234567890
}
let doc = Document::builder()
.title("My Document".to_string())
// id and created_at are set automatically, no setters available
.build();Customize individual setter method names:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
struct Person {
#[builder(required, setter_name = "full_name")]
name: String,
#[builder(setter_name = "years_old")]
age: Option<u32>,
}
let person = Person::builder()
.full_name("Alice Smith".to_string())
.years_old(Some(30))
.build();Add consistent prefixes to setter methods at the struct or field level:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
#[builder(setter_prefix = "with_")]
struct ApiClient {
#[builder(required)]
base_url: String,
#[builder(required, setter_prefix = "set_")] // Overrides struct-level prefix
api_key: String,
timeout: Option<u32>,
}
let client = ApiClient::builder()
.with_base_url("https://api.example.com".to_string())
.set_api_key("secret123".to_string()) // Uses field-level prefix
.with_timeout(Some(5000))
.build();The impl_into attribute generates setters that accept impl Into<T>, allowing more ergonomic API usage:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
#[builder(impl_into)] // Apply to all fields
struct Config {
#[builder(required)]
name: String,
#[builder(required)]
description: String,
#[builder(impl_into = false)] // Override: require exact type
id: Option<u64>,
}
let config = Config::builder()
.name("MyApp") // &str converts to String via Into
.description("An application") // &str converts to String via Into
.id(Some(42u64)) // Requires Option<u64> exactly
.build();The converter attribute provides custom transformation logic for setters:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder, Debug)]
struct User {
#[builder(required, converter = |email: &str| email.trim().to_lowercase())]
email: String,
#[builder(converter = |tags: &str| tags.split(',').map(|s| s.trim().to_string()).collect())]
interests: Vec<String>,
#[builder(converter = |value: &str| Some(value.to_string()))]
nickname: Option<String>,
}
let user = User::builder()
.email(" ALICE@EXAMPLE.COM ") // Normalized to "alice@example.com"
.interests("rust, programming, web") // Parsed to Vec<String>
.nickname("ally") // Wrapped in Some
.build();
assert_eq!(user.email, "alice@example.com");
assert_eq!(user.interests, vec!["rust", "programming", "web"]);
assert_eq!(user.nickname, Some("ally".to_string()));The converter must be a closure expression with an explicitly typed parameter:
// Correct
#[builder(converter = |value: &str| value.to_uppercase())]
// Incorrect - function references are not supported
#[builder(converter = str::to_uppercase)]Customize the name of the final build method:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder)]
#[builder(build_method = "create")]
struct Connection {
#[builder(required)]
host: String,
}
let conn = Connection::builder()
.host("localhost".to_string())
.create(); // Uses custom method nameTypeStateBuilder supports generic types, lifetime parameters, and complex bounds:
use type_state_builder::TypeStateBuilder;
#[derive(TypeStateBuilder, Debug)]
struct Container<'a, T, U>
where
T: Clone + Send,
U: std::fmt::Debug,
{
#[builder(required)]
data: T,
#[builder(required)]
metadata: U,
reference: Option<&'a str>,
}
let text = "referenced text";
let container = Container::<String, i32>::builder()
.data("Hello".to_string())
.metadata(42)
.reference(Some(text))
.build();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");Requirements for const builders:
- 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)
Converters with const builders:
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 {
#[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();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:
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();When a required field is missing, the compiler error includes the builder's type name, which explicitly states the current state:
error[E0599]: no method named `build` found for struct
`ConfigBuilder_MissingApi_key_MissingEndpoint` in the current scope
The naming pattern is:
{StructName}Builder_{FieldState1}_{FieldState2}_...
Where each field state is either:
Has{FieldName}- the field has been setMissing{FieldName}- the field has not been set
For example, with a struct having fields api_key and endpoint:
| State | Type Name |
|---|---|
| Neither set | ConfigBuilder_MissingApi_key_MissingEndpoint |
| Only api_key set | ConfigBuilder_HasApi_key_MissingEndpoint |
| Only endpoint set | ConfigBuilder_MissingApi_key_HasEndpoint |
| Both set | ConfigBuilder_HasApi_key_HasEndpoint |
The build() method is only available on the final state where all required fields are set.
TypeStateBuilder generates code that is compatible with no_std environments. The generated code uses:
core::option::Optioninstead ofstd::option::Optioncore::marker::PhantomDatainstead ofstd::marker::PhantomDatacore::fmt::Debuginstead ofstd::fmt::Debugcore::default::Defaultinstead ofstd::default::Default
No feature flags are required; no_std compatibility is the default.
TypeStateBuilder requires Rust 1.70.0 or later. This requirement is driven by:
- Stable proc-macro features used in code generation
- Advanced generic parameter handling
The MSRV is tested in CI and will not be increased without a minor version bump.
TypeStateBuilder implements the type-state pattern using Rust's type system to encode state at compile time.
For a struct with required fields, the macro generates:
- Multiple builder structs, one for each possible combination of set/unset required fields
- Setter methods that transition between states
- A
build()method only on the final state
Each required field contributes to the builder's type name. With n required fields, there are 2^n possible states.
The macro generates a struct for each state, though in practice many states are not reachable through normal usage.
The state tracking is entirely compile-time:
- No runtime state variable
- No runtime validation
- No
Optionwrappers for required fields internally - The final
build()call simply moves values into the target struct
The generated code is equivalent to what you would write by hand, with the type system providing the safety guarantees.
For this input:
#[derive(TypeStateBuilder)]
struct User {
#[builder(required)]
name: String,
age: Option<u32>,
}The macro generates (simplified):
struct UserBuilder_MissingName {
age: Option<u32>,
}
struct UserBuilder_HasName {
name: String,
age: Option<u32>,
}
impl UserBuilder_MissingName {
fn name(self, value: String) -> UserBuilder_HasName {
UserBuilder_HasName {
name: value,
age: self.age,
}
}
fn age(self, value: Option<u32>) -> Self {
Self { age: value, ..self }
}
}
impl UserBuilder_HasName {
fn age(self, value: Option<u32>) -> Self {
Self { age: value, ..self }
}
fn build(self) -> User {
User {
name: self.name,
age: self.age,
}
}
}Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Contributions are welcome. Please open an issue to discuss significant changes before submitting a pull request.
For bug reports and feature requests, use the issue tracker.