Typestate Pattern
The Typestate pattern encodes the runtime lifecycle state of an object directly into its compile-time type parameter (Connection<Disconnected>, Connection<Authenticated>, Transaction<Committed>). State transition methods consume self by value in the old state (fn login(self) -> Connection<Authenticated>), making it a compile-time error to call an operation in the wrong state or to reuse a stale state handle!
State Machine Enforced at Compile Time (`0` Bytes of Runtime State Tag!):
HttpResponse<BuildingHeaders>
| Can call `.header(k, v)`
| Cannot call `.send()` yet!
v `.body(bytes)` (Consumes `BuildingHeaders` by value!)
HttpResponse<ReadyToSend>
| Cannot call `.header(k, v)` anymore!
v `.send()` (Consumes `ReadyToSend` by value!)
SentReceipt
How Zero-Sized State Markers Achieve Zero Runtime Cost
Each state marker (pub struct BuildingHeaders;, pub struct ReadyToSend;) is a Zero-Sized Type (ZST) held inside the generic struct via PhantomData<State> (or directly as a ZST field state: State when state-specific data is carried).
Because size_of::<PhantomData<State>>() == 0:
HttpResponse<BuildingHeaders>andHttpResponse<ReadyToSend>have the exact same memory layout as the underlying fields withoutState.- There is no runtime
match self.statecheck orInvalidStateErrorbranch—invalid state transitions simply do not exist as methods on that type!
use std::marker::PhantomData;
use std::mem::size_of;
// 1. Define Zero-Sized Type (ZST) state markers:
pub struct BuildingHeaders;
pub struct ReadyToSend;
pub struct HttpResponse<State> {
status: u16,
headers: Vec<(String, String)>,
body: Vec<u8>,
_state: PhantomData<State>,
}
// 2. Methods available ONLY in the `BuildingHeaders` state:
impl HttpResponse<BuildingHeaders> {
pub fn new(status: u16) -> Self {
Self {
status,
headers: Vec::new(),
body: Vec::new(),
_state: PhantomData,
}
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((key.into(), value.into()));
self
}
/// Transitions from `BuildingHeaders` -> `ReadyToSend` by consuming `self`!
/// Automatically appends the exact `Content-Length` header so headers can no longer change.
pub fn body(mut self, payload: impl Into<Vec<u8>>) -> HttpResponse<ReadyToSend> {
let body = payload.into();
self.headers
.push(("Content-Length".to_string(), body.len().to_string()));
HttpResponse {
status: self.status,
headers: self.headers,
body,
_state: PhantomData,
}
}
}
// 3. Methods available ONLY in the `ReadyToSend` state:
impl HttpResponse<ReadyToSend> {
pub fn send(self) -> String {
format!(
"HTTP/1.1 {} ({} headers, {} body bytes)",
self.status,
self.headers.len(),
self.body.len()
)
}
}
// 4. Methods available in ANY state `<S>`:
impl<S> HttpResponse<S> {
pub fn status(&self) -> u16 {
self.status
}
}
fn main() {
// Verify `PhantomData<State>` adds 0 bytes to the struct size:
assert_eq!(
size_of::<HttpResponse<BuildingHeaders>>(),
size_of::<HttpResponse<ReadyToSend>>()
);
let receipt = HttpResponse::new(200)
.header("Content-Type", "application/json")
.body(b"{\"ok\":true}".to_vec())
// `.header(...)` here would be a compile-time error because we are now `ReadyToSend`!
.send();
println!("Sent typestate response: {receipt}");
}
Sent typestate response: HTTP/1.1 200 (2 headers, 11 body bytes)
Sealing Typestate Marker Traits
When you write generic methods over impl<S: ResponseState> HttpResponse<S>, seal the ResponseState trait using a private supertrait (mod private { pub trait Sealed {} }) so external callers cannot invent fake state marker structs outside your state machine.