Crate type_state_builder

Source
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 with impl 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 with impl Into<FieldType> parameter
  • #[builder(impl_into = false)] - Override struct-level impl_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

Featureimpl_intoconverter
Type conversionsOnly Into traitAny 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
PerformanceZero-costDepends on logic
SyntaxAttribute flagClosure 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§

TypeStateBuilder
Derives a type-safe builder for a struct with compile-time validation of required fields.