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:
impl<State> Builder<State>: Methods valid in every state (such as inspecting an ID or canceling).impl<State: CanAddHeaders> Builder<State>: Methods valid in a subset of states sharing a marker trait.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.