#enforce

macro typestate-builder-macro

Derive-macro-based generator that combines Typestate and Builder patterns

1 unstable release

0.1.1 Sep 25, 2024

#12 in #enforce

Download history 76/week @ 2024-09-19 77/week @ 2024-09-26 11/week @ 2024-10-03

164 downloads per month
Used in typestate-builder

MIT/Apache

24KB
379 lines

typestate-builder

TypestateBuilder is a Rust procedural macro that enables the creation of builder patterns using the typestate design pattern. This macro ensures that your structs are built in a way that enforces compile-time safety, ensuring that required fields are initialized before the struct is created.

Table of Contents

Features

  • Enforced Typestate Pattern: Leveraging Rust's type system, the macro ensures that required fields are set before a struct can be created.
  • No Runtime Checks: The macro generates the necessary types and performs checks at compile time, eliminating the need for Option or Result types.
  • Compile-Time Safety: All required fields must be initialized before creating an instance of the struct, promoting safer code.
  • Support for Named (and Tuple on the way) Structs: Works with named structs, with error messages for unsupported configurations.
  • Fluent and Intuitive Syntax: Offers a simple and idiomatic Rust syntax for creating builders, enhancing code readability and usability.

Example

Here’s a basic example demonstrating how to use the TypestateBuilder macro:

use typestate_builder::TypestateBuilder;

#[derive(Debug, TypestateBuilder)] // `Debug` is not a must.
struct Person {
    name: String,
    age: u32,
    email: Option<String>,
}

let person = Person::builder()
    .name("Alice Johnson".to_string())
    .age(30)
    .email(Some("alice@example.com".to_string()))
    .build();
println!("Created person: {:?}", person);

In this example, the Person struct uses the TypestateBuilder derive macro to create a builder. Each field can be set in a fluent interface style, and the build method assembles the Person instance once all required fields have been set.

How It Works

  • State Management: The macro generates intermediate state structs for each field of the struct being built. Each state struct represents a stage in the building process.

  • Method Chaining: For each field, a method is generated that accepts a value for that field and returns a new builder state, ensuring that only valid transitions are allowed.

  • Final Assembly: The build method assembles the final struct once all required fields have been set, preventing the creation of incomplete instances.

Code Expanded

The expanded version of the above code is like this:

struct PersonBuilderNameAdded(String);
struct PersonBuilderNameEmpty;
struct PersonBuilderAgeAdded(u32);
struct PersonBuilderAgeEmpty;
struct PersonBuilderEmailAdded(Option<String>);
struct PersonBuilderEmailEmpty;
pub struct PersonBuilder<Name, Age, Email> {
    name: Name,
    age: Age,
    email: Email,
}
impl Person {
    pub fn builder() -> PersonBuilder<
        PersonBuilderNameEmpty,
        PersonBuilderAgeEmpty,
        PersonBuilderEmailEmpty,
    > {
        PersonBuilder {
            name: PersonBuilderNameEmpty,
            age: PersonBuilderAgeEmpty,
            email: PersonBuilderEmailEmpty,
        }
    }
}
impl<Age, Email> PersonBuilder<PersonBuilderNameEmpty, Age, Email> {
    pub fn name(
        self,
        name: String,
    ) -> PersonBuilder<PersonBuilderNameAdded, Age, Email> {
        PersonBuilder {
            name: PersonBuilderNameAdded(name),
            age: self.age,
            email: self.email,
        }
    }
}
impl<Name, Email> PersonBuilder<Name, PersonBuilderAgeEmpty, Email> {
    pub fn age(self, age: u32) -> PersonBuilder<Name, PersonBuilderAgeAdded, Email> {
        PersonBuilder {
            name: self.name,
            age: PersonBuilderAgeAdded(age),
            email: self.email,
        }
    }
}
impl<Name, Age> PersonBuilder<Name, Age, PersonBuilderEmailEmpty> {
    pub fn email(
        self,
        email: Option<String>,
    ) -> PersonBuilder<Name, Age, PersonBuilderEmailAdded> {
        PersonBuilder {
            name: self.name,
            age: self.age,
            email: PersonBuilderEmailAdded(email),
        }
    }
}
impl PersonBuilder<
    PersonBuilderNameAdded,
    PersonBuilderAgeAdded,
    PersonBuilderEmailAdded,
> {
    pub fn build(self) -> Person {
        Person {
            name: self.name.0,
            age: self.age.0,
            email: self.email.0,
        }
    }
}

Limitations

In fact, i really can't know (and no one will ever know) where is the final frontier of this crate. But i pasted dozens complex structs here to have them as for setting goals, not all of them can be expected to pass the tests. Therefore, it's impossible to give a guarantee in a universe where black can be found blacker: Just don't get too complicated OR send me a PR :').

License

TypestateBuilder is dual-licensed under the MIT and Apache 2.0 licenses. See the LICENSE-MIT and LICENSE-APACHE files for details.


lib.rs:

This crate provides the TypestateBuilder derive macro for generating a typestate-pattern builder for structs.

Typestate Pattern

The typestate pattern helps ensure that a struct is initialized in steps, enforcing that certain fields must be set before others. This prevents the creation of incomplete structs and provides compile-time safety for field initialization.

Example usage:

#[derive(TypestateBuilder)]
struct Person {
    name: String,
    age: u32,
    email: Option<String>,
}

let person = Person::builder()
    .name("Alice".to_string())
    .age(30)
    .email(Some("alice@example.com".to_string()))
    .build();

Dependencies

~290–740KB
~17K SLoC