use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderState {
Pending,
Paid,
Shipped,
Delivered,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderEvent {
Pay,
Ship,
Deliver,
Cancel,
}
#[derive(Debug, PartialEq, Eq)]
pub enum TransitionError {
Illegal { from: OrderState, event: OrderEvent },
}
impl fmt::Display for TransitionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransitionError::Illegal { from, event } => {
write!(f, "cannot apply {:?} while {:?}", event, from)
}
}
}
}
impl OrderState {
pub fn apply(self, event: OrderEvent) -> Result<OrderState, TransitionError> {
use OrderEvent::*;
use OrderState::*;
match (self, event) {
(Pending, Pay) => Ok(Paid),
(Pending, Cancel) => Ok(Cancelled),
(Paid, Ship) => Ok(Shipped),
(Paid, Cancel) => Ok(Cancelled),
(Shipped, Deliver) => Ok(Delivered),
(from, event) => Err(TransitionError::Illegal { from, event }),
}
}
pub fn is_terminal(self) -> bool {
matches!(self, OrderState::Delivered | OrderState::Cancelled)
}
}
mod sealed {
pub trait Sealed {}
}
pub trait State: sealed::Sealed {}
pub struct Pending;
pub struct Paid;
pub struct Shipped;
impl sealed::Sealed for Pending {}
impl sealed::Sealed for Paid {}
impl sealed::Sealed for Shipped {}
impl State for Pending {}
impl State for Paid {}
impl State for Shipped {}
pub struct Order<S: State> {
pub id: u64,
pub total_cents: u64,
_state: std::marker::PhantomData<S>,
}
impl Order<Pending> {
pub fn new(id: u64, total_cents: u64) -> Self {
Order { id, total_cents, _state: std::marker::PhantomData }
}
pub fn pay(self) -> Order<Paid> {
Order { id: self.id, total_cents: self.total_cents, _state: std::marker::PhantomData }
}
}
impl Order<Paid> {
pub fn ship(self, tracking: &str) -> Order<Shipped> {
println!("order {} shipped via {}", self.id, tracking);
Order { id: self.id, total_cents: self.total_cents, _state: std::marker::PhantomData }
}
}
impl Order<Shipped> {
pub fn tracking_ready(&self) -> bool {
true
}
}
mod order_state;
mod typed_order;
use order_state::{OrderEvent, OrderState};
use typed_order::Order;
fn run_machine(events: &[OrderEvent]) -> Result<OrderState, String> {
let mut state = OrderState::Pending;
for &event in events {
state = state.apply(event).map_err(|e| e.to_string())?;
if state.is_terminal() {
break;
}
}
Ok(state)
}
fn main() {
let good = [OrderEvent::Pay, OrderEvent::Ship, OrderEvent::Deliver];
match run_machine(&good) {
Ok(state) => println!("final state: {:?}", state),
Err(msg) => eprintln!("transition failed: {}", msg),
}
let bad = [OrderEvent::Ship];
assert!(run_machine(&bad).is_err());
// Type-state path: only compiles because the order is paid before shipping.
let order = Order::new(42, 1999);
let shipped = order.pay().ship("UPS-1Z999");
assert!(shipped.tracking_ready());
println!("typed order {} reached shipped", shipped.id);
}
This snippet models an order lifecycle as an explicit state machine using Rust enums, with a runtime-checked API and a compile-time type-state layer on top. The core idea is that an order can only be in one of a fixed set of states, and only certain transitions are legal — the code enforces that in two complementary ways so illegal transitions become impossible rather than merely discouraged.
In order_state.rs, the OrderState enum enumerates every stage — Pending, Paid, Shipped, Delivered, Cancelled — and the OrderEvent enum enumerates the things that can happen. The apply method is the heart of the runtime machine: it pattern-matches on the (state, event) pair and returns either the next state or a TransitionError. This total-match style is why enums shine here — the compiler forces every combination to be considered, so an unhandled pairing falls into the catch-all arm that produces TransitionError::Illegal rather than silently doing nothing. Terminal states like Delivered and Cancelled simply have no outgoing arms, which is how the model expresses finality.
Because a runtime error is still a bug that reaches production, typed_order.rs lifts the same rules into the type system. Zero-sized marker structs (Pending, Paid, Shipped) implement the sealed State trait, and Order<S> carries the state as a phantom-like type parameter. Methods such as pay and ship are defined only on the specific impl Order<Pending> or impl Order<Paid> block, and they consume self and return a new Order<Paid>. Calling ship on an unpaid order is not a runtime failure — it does not compile, because that method does not exist for Order<Pending>.
The trade-off is real: the type-state approach gives the strongest guarantees but is rigid, so it fits linear happy-path flows, while the enum machine in apply handles dynamic, data-driven transitions (like events arriving from a queue). main.rs exercises both, showing the runtime machine folding events and the typed builder chaining pay().ship(). A common pitfall is trying to store heterogeneous Order<S> values in one collection; when that is needed, the enum representation is the pragmatic choice. Using both together lets a system validate untrusted input at the boundary and then move into type-safe territory.
Related snips
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
class CreateDeadJobs < ActiveRecord::Migration[7.1]
def change
create_table :dead_jobs do |t|
t.string :jid, null: false
t.string :queue, null: false
t.string :klass, null: false
Background Job Dead Letter Queue (DLQ) Table
import axios from 'axios';
export type NormalizedErrors = {
fields: Record<string, string>;
formLevel: string | null;
};
Frontend: normalize and display server validation errors
use anyhow::{Context, Result};
use std::fs;
fn load_config(path: &str) -> Result<String> {
fs::read_to_string(path)
.with_context(|| format!("failed to read config from {}", path))
anyhow::Context for adding error context without custom types
module ApiErrorHandler
extend ActiveSupport::Concern
included do
rescue_from StandardError, with: :handle_standard_error
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
Structured JSON error responses
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
Send and Sync traits for safe concurrency guarantees
Share this code
Here's the card — post it anywhere.