Advanced Generics and Type-Level Programming

Type-State Pattern

The type-state pattern encodes an object's runtime lifecycle state as a compile-time generic type parameter (Connection<Disconnected> vs. Connection<Authenticated>). State transition methods consume self by value (fn login(self, ...) -> Connection<Authenticated>) so the old state becomes unreachable after the transition and invalid operation sequences fail at compile time (E0599) rather than returning runtime state errors.

State Machine Architecture

Compile-Time Enforced State Transitions (Move Semantics):

  HttpResponseBuilder<StatusUnset>
        |
        |  .status(200)   (consumes `StatusUnset`, returns `HeadersOpen`)
        v
  HttpResponseBuilder<HeadersOpen> <---+
        |                              | .header(k, v) (allowed only in `HeadersOpen`)
        +------------------------------+
        |
        |  .body("...")   (consumes `HeadersOpen`, returns `ReadyToSend`)
        v
  HttpResponseBuilder<ReadyToSend>
        |
        |  .send()        (available ONLY on `ReadyToSend`!)
        v
     SentResponse

Shared Operations vs. State-Specific Operations

In a type-state design, a single generic struct Builder<State> is paired with multiple impl blocks:

  1. impl<State> Builder<State>: Methods valid in every state (such as inspecting an ID or canceling).
  2. impl<State: CanAddHeaders> Builder<State>: Methods valid in a subset of states sharing a marker trait.
  3. impl Builder<ConcreteState>: Methods valid only in one specific state (Builder<ReadyToSend>).
use std::marker::PhantomData;

// State marker types (uninhabited zero-sized types):
pub enum StatusUnset {}
pub enum HeadersOpen {}
pub enum ReadyToSend {}

pub struct ResponseBuilder<State> {
    status_code: u16,
    headers: Vec<(String, String)>,
    body: String,
    _state: PhantomData<fn() -> State>,
}

impl ResponseBuilder<StatusUnset> {
    pub fn new() -> Self {
        Self {
            status_code: 0,
            headers: Vec::new(),
            body: String::new(),
            _state: PhantomData,
        }
    }

    pub fn status(self, code: u16) -> ResponseBuilder<HeadersOpen> {
        ResponseBuilder {
            status_code: code,
            headers: self.headers,
            body: self.body,
            _state: PhantomData,
        }
    }
}

impl ResponseBuilder<HeadersOpen> {
    pub fn header(mut self, key: &str, value: &str) -> Self {
        self.headers.push((key.to_string(), value.to_string()));
        self
    }

    pub fn body(self, content: &str) -> ResponseBuilder<ReadyToSend> {
        ResponseBuilder {
            status_code: self.status_code,
            headers: self.headers,
            body: content.to_string(),
            _state: PhantomData,
        }
    }
}

impl ResponseBuilder<ReadyToSend> {
    pub fn build(self) -> String {
        format!(
            "HTTP/1.1 {}\nHeaders: {:?}\n\n{}",
            self.status_code, self.headers, self.body
        )
    }
}

fn main() {
    let response = ResponseBuilder::new()
        .status(200)
        .header("content-type", "application/json")
        .body("{\"ok\":true}")
        .build();

    println!("{}", response);
}
HTTP/1.1 200
Headers: [("content-type", "application/json")]

{"ok":true}

If a caller tries to call .build() before setting .status(...) and .body(...), the compiler rejects the call immediately because build is only implemented on ResponseBuilder<ReadyToSend>.

Handling Runtime-Conditional Branching Between States

When the next state depends on a runtime input (for example, authenticate() either transitions to Session<Authenticated> or stays in Session<Unauthenticated>), return a Result<Session<Authenticated>, Session<Unauthenticated>> or an enum enum AuthOutcome { Ok(Session<Authenticated>), Retry(Session<Unauthenticated>) } so both branches retain static type-state guarantees.