This page collects the common Statum patterns that are useful after the quick start. The examples below are intentionally short. For executable coverage, see ../statum-examples/tests/patterns.rs.
Each pattern is in service of the same goal: legal states should be explicit, and undesirable states should not be smuggled through ordinary APIs.
A transition site can expose two explicit legal targets when that branch is
part of the stable protocol surface. Use statum::Branch<Machine<A>, Machine<B>> for that narrow case:
#[transition]
impl ProcessMachine<Init> {
fn decide(
self,
event: Event,
) -> statum::Branch<ProcessMachine<NextState>, ProcessMachine<OtherState>> {
match event {
Event::Go => statum::Branch::First(self.transition()),
Event::Alternative => statum::Branch::Second(self.transition()),
}
}
}If the choice needs a richer domain enum, more than two branches, or a lot of policy logic, keep that branching in a normal helper and dispatch into explicit transition methods from there.
Keep preconditions in normal methods and transition only after the guard passes:
impl Machine<Pending> {
fn try_activate(self) -> statum::Result<Machine<Active>> {
if self.can_activate() {
Ok(self.activate())
} else {
Err(statum::Error::InvalidState)
}
}
}Attach data only to states where it is actually valid. Transition into those
states with transition_with(data):
#[state]
enum ReviewState {
Draft,
InReview(ReviewData),
Published,
}
#[transition]
impl Document<Draft> {
fn submit_for_review(self, reviewer: String) -> Document<InReview> {
self.transition_with(ReviewData { reviewer })
}
}Examples: ../statum-examples/src/toy_demos/07-state-data.rs, ../statum-examples/src/toy_demos/08-transition-with-data.rs
When the next state's payload should be derived by consuming the current
state's payload, use transition_map(...) instead of cloning fields into a new
value first:
#[transition]
impl Order<Packed> {
fn ship(self, tracking: String) -> Order<Shipped> {
self.transition_map(|packed| ShippedData {
order_id: packed.order_id,
tracking,
})
}
}Example: ../statum-examples/src/toy_demos/15-transition-map.rs
Keep side effects in async methods and call a synchronous transition at the end:
#[transition]
impl Job<Queued> {
fn start(self) -> Job<Running> {
self.transition()
}
}
impl Job<Queued> {
async fn start_with_effects(self) -> Job<Running> {
do_io().await;
self.start()
}
}Example: ../statum-examples/src/toy_demos/06-async-transitions.rs
Use a machine as state data when a parent workflow owns a child workflow:
#[state]
enum ParentState {
NotStarted,
InProgress(SubMachine<Running>),
Done,
}Example: ../statum-examples/src/toy_demos/11-hierarchical-machines.rs
If you need undo or history, carry the prior state's data into the next state or add an explicit rollback transition:
#[transition]
impl Machine<Draft> {
fn publish(self) -> Machine<Published> {
let previous = self.state_data.clone();
self.transition_with(PublishData { previous })
}
}Examples: ../statum-examples/src/toy_demos/12-rollbacks.rs, ../statum-examples/tests/patterns.rs
Use .into_machines_by(...) when each persisted row needs different machine
fields during rebuild:
let machines = rows
.into_machines_by(|row| workflow_machine::Fields {
tenant: row.tenant.clone(),
workflow_name: row.workflow_name.clone(),
})
.build();Example: ../statum-examples/src/toy_demos/14-batch-machine-fields.rs
For append-only storage, project events into validator rows first and then rehydrate typed machines:
use statum::projection::{ProjectionReducer, reduce_grouped};
let rows = reduce_grouped(events, |event| event.order_id, &OrderProjector)?;
let machines = rows.into_machines().build();Example: ../statum-examples/src/showcases/sqlite_event_log_rebuild.rs
Not every Statum workflow is persistence-driven. Session and protocol lifecycles work well when legal frame order matters and method availability should change by phase.
Example: ../statum-examples/src/showcases/tokio_websocket_session.rs
Statum works best when the stable core of a protocol is known up front. If most of your logic is runtime branching, user-authored graphs, or rapidly changing states, keep that part in normal runtime validation and use typestate only around the small stable core.