statum/lib.rs
1//! Compile-time verified typestate workflows for Rust.
2//!
3//! Statum is for workflow and protocol models where representational
4//! correctness matters. It helps keep invalid, undesirable, or not-yet-
5//! validated states out of ordinary code.
6//! In the same spirit as [`Option`] and [`Result`], it uses the type system to
7//! make absence, failure, and workflow legality explicit instead of leaving
8//! them in status fields and guard code. It generates typed state markers,
9//! typed machines, transition helpers, and typed rehydration from stored data.
10//!
11//! # Mental Model
12//!
13//! - [`state`](macro@state) defines the legal phases.
14//! - [`machine`](macro@machine) defines the durable context carried across phases.
15//! - [`transition`](macro@transition) defines the legal edges between phases.
16//! - [`validators`](macro@validators) rebuilds typed machines from persisted data.
17//!
18//! # Quick Start
19//!
20//! ```rust
21//! use statum::{machine, state, transition};
22//!
23//! #[state]
24//! enum CheckoutState {
25//! EmptyCart,
26//! ReadyToPay(OrderDraft),
27//! Paid,
28//! }
29//!
30//! #[derive(Clone)]
31//! struct OrderDraft {
32//! total_cents: u64,
33//! }
34//!
35//! #[machine]
36//! struct Checkout<CheckoutState> {
37//! id: String,
38//! }
39//!
40//! #[transition]
41//! impl Checkout<EmptyCart> {
42//! fn review(self, total_cents: u64) -> Checkout<ReadyToPay> {
43//! self.transition_with(OrderDraft { total_cents })
44//! }
45//! }
46//!
47//! #[transition]
48//! impl Checkout<ReadyToPay> {
49//! fn pay(self) -> Checkout<Paid> {
50//! self.transition()
51//! }
52//! }
53//!
54//! fn main() {
55//! let cart = Checkout::<EmptyCart>::builder()
56//! .id("order-1".to_owned())
57//! .build();
58//!
59//! let ready = cart.review(4200);
60//! assert_eq!(ready.state_data.total_cents, 4200);
61//!
62//! let _paid = ready.pay();
63//! }
64//! ```
65
66//!
67//! # Typed Rehydration
68//!
69//! `#[validators]` lets you rebuild persisted rows back into typed machine
70//! states:
71//!
72//! ```rust
73//! use statum::{machine, state, validators, Error};
74//!
75//! #[state]
76//! enum TaskState {
77//! Draft,
78//! InReview(String),
79//! Published,
80//! }
81//!
82//! #[machine]
83//! struct Task<TaskState> {
84//! id: u64,
85//! }
86//!
87//! struct TaskRow {
88//! id: u64,
89//! status: &'static str,
90//! reviewer: Option<String>,
91//! }
92//!
93//! #[validators(Task)]
94//! impl TaskRow {
95//! fn is_draft(&self) -> statum::Result<()> {
96//! if self.status == "draft" {
97//! Ok(())
98//! } else {
99//! Err(Error::InvalidState)
100//! }
101//! }
102//!
103//! fn is_in_review(&self) -> statum::Result<String> {
104//! if self.status == "in_review" {
105//! self.reviewer.clone().ok_or(Error::InvalidState)
106//! } else {
107//! Err(Error::InvalidState)
108//! }
109//! }
110//!
111//! fn is_published(&self) -> statum::Result<()> {
112//! if self.status == "published" {
113//! Ok(())
114//! } else {
115//! Err(Error::InvalidState)
116//! }
117//! }
118//! }
119//!
120//! fn main() -> statum::Result<()> {
121//! let row = TaskRow {
122//! id: 7,
123//! status: "in_review",
124//! reviewer: Some("alice".to_owned()),
125//! };
126//!
127//! let row_id = row.id;
128//! let machine = Task::rebuild(&row).id(row_id).build()?;
129//! match machine {
130//! task::SomeState::InReview(task) => assert_eq!(task.state_data, "alice"),
131//! _ => panic!("expected in-review task"),
132//! }
133//! Ok(())
134//! }
135//! ```
136//!
137//! If you want explainable rebuild traces, validators can also return
138//! [`Validation`]. Then `.build_report()` and `.build_reports()` populate [`RebuildAttempt::reason_key`] and
139//! [`RebuildAttempt::message`] for failed matches while keeping the normal
140//! `.into_result()` surface.
141//!
142//! # Compile-Time Gating
143//!
144//! Methods only exist on states where you define them.
145//!
146//! ```compile_fail
147//! use statum::{machine, state};
148//!
149//! #[state]
150//! enum LightState {
151//! Off,
152//! On,
153//! }
154//!
155//! #[machine]
156//! struct Light<LightState> {}
157//!
158//! let light = Light::<Off>::builder().build();
159//! let _ = light.switch_off(); // no such method on Light<Off>
160//! ```
161//!
162//! # Machine Introspection
163//!
164//! Statum can also expose the static machine structure as typed metadata.
165//! This is useful when the same machine definition should drive:
166//!
167//! - CLI explainers
168//! - generated docs
169//! - graph exports
170//! - exact transition assertions in tests
171//! - runtime replay or debug tooling
172//!
173//! With the `strict-introspection` feature enabled, the graph is exact at the
174//! transition-site level. A consumer can ask for the legal targets of one
175//! specific method on one specific source state and treat that metadata as the
176//! authoritative static graph surface.
177//!
178//! Strict introspection is derived from locally readable `#[transition]`
179//! method signatures plus any explicit `#[introspect(return = ...)]` escape
180//! hatches. Supported return shapes are direct machine returns plus canonical
181//! wrapper paths around machine types:
182//! `::core::option::Option<Machine<NextState>>`,
183//! `::core::result::Result<Machine<NextState>, E>`, and
184//! `::statum::Branch<Machine<Left>, Machine<Right>>`.
185//! Unsupported custom decision enums, wrapper aliases, and differently-qualified
186//! machine paths are rejected instead of approximated. In the default feature
187//! set, Statum still follows some source-backed aliases for ergonomics, but
188//! that mode should be treated as convenient metadata rather than the strongest
189//! exactness guarantee. Whole-item `#[cfg]` gates are supported, but nested
190//! `#[cfg]` or `#[cfg_attr]` on `#[state]` variants, variant payload fields,
191//! or `#[machine]` fields are rejected because they would otherwise drift the
192//! generated metadata from the active build.
193//!
194//! For small amounts of human-facing metadata, Statum can also generate a
195//! `machine::PRESENTATION` constant from `#[present(...)]` attributes. Add
196//! `#[presentation_types(...)]` on the machine when those attributes should
197//! carry typed `metadata = ...` payloads instead of just labels and
198//! descriptions.
199//!
200//! ```rust
201//! use statum::{
202//! machine, state, transition, MachineIntrospection, MachineTransitionRecorder,
203//! };
204//!
205//! #[state]
206//! enum FlowState {
207//! Fetched,
208//! Accepted,
209//! Rejected,
210//! }
211//!
212//! #[machine]
213//! struct Flow<FlowState> {}
214//!
215//! #[transition]
216//! impl Flow<Fetched> {
217//! fn validate(
218//! self,
219//! accept: bool,
220//! ) -> ::core::result::Result<Flow<Accepted>, Flow<Rejected>> {
221//! if accept {
222//! Ok(self.accept())
223//! } else {
224//! Err(self.reject())
225//! }
226//! }
227//!
228//! fn accept(self) -> Flow<Accepted> {
229//! self.transition()
230//! }
231//!
232//! fn reject(self) -> Flow<Rejected> {
233//! self.transition()
234//! }
235//! }
236//!
237//! fn main() {
238//! let graph = <Flow<Fetched> as MachineIntrospection>::GRAPH;
239//! let validate = graph
240//! .transition_from_method(flow::StateId::Fetched, "validate")
241//! .unwrap();
242//!
243//! assert_eq!(
244//! graph.legal_targets(validate.id).unwrap(),
245//! &[flow::StateId::Accepted, flow::StateId::Rejected]
246//! );
247//!
248//! let event = <Flow<Fetched> as MachineTransitionRecorder>::try_record_transition_to::<
249//! Flow<Accepted>,
250//! >(Flow::<Fetched>::VALIDATE)
251//! .unwrap();
252//!
253//! assert_eq!(event.chosen, flow::StateId::Accepted);
254//! }
255//! ```
256//!
257//! Transition ids are exact and typed, but they are exposed as generated
258//! associated consts on the source-state machine type, such as
259//! `Flow::<Fetched>::VALIDATE`.
260//!
261//! # Where To Look Next
262//!
263//! - Start with [`state`](macro@state), [`machine`](macro@machine), and
264//! [`transition`](macro@transition).
265//! - For stored rows and database rebuilds, read [`validators`](macro@validators).
266//! - For append-only event logs, use [`projection`] before validator rebuilds.
267//! - The repository README and `docs/` directory contain longer guides and
268//! showcase applications.
269
270#[cfg(doctest)]
271#[doc = include_str!("../README.md")]
272mod crate_readme_doctests {}
273
274#[doc(hidden)]
275pub use statum_core::__private;
276#[doc(inline)]
277pub use statum_core::projection;
278#[doc(inline)]
279pub use statum_core::{
280 Branch, CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error,
281 MachineDescriptor, MachineGraph, MachineIntrospection, MachinePresentation,
282 MachinePresentationDescriptor, MachineStateIdentity, MachineTransitionRecorder, RebuildAttempt,
283 RebuildReport, RecordedTransition, Rejection, Result, StateDescriptor, StateMarker,
284 StatePresentation, TransitionDescriptor, TransitionInventory, TransitionPresentation,
285 TransitionPresentationInventory, UnitState, Validation,
286};
287
288/// Define the legal lifecycle phases for a machine.
289///
290/// `#[state]` accepts enums with:
291///
292/// - unit variants like `Draft`
293/// - single-field tuple variants like `InReview(Assignment)`
294/// - named-field variants like `InReview { reviewer: String }`
295///
296/// It generates one marker type per variant plus the trait bounds Statum uses
297/// for typed machines and transitions.
298///
299/// If you need derives, place them below `#[state]`.
300///
301/// ```rust
302/// use statum::state;
303///
304/// #[state]
305/// enum ReviewState {
306/// Draft,
307/// InReview(Reviewer),
308/// Published,
309/// }
310///
311/// #[derive(Clone)]
312/// struct Reviewer {
313/// name: String,
314/// }
315/// ```
316pub use statum_macros::state;
317
318/// Define a typed machine that carries durable context across states.
319///
320/// The machine must be a struct whose first generic parameter is the
321/// `#[state]` enum family:
322///
323/// - `struct Task<TaskState> { ... }`
324/// - `struct Payment<PaymentState> { ... }`
325///
326/// `#[machine]` generates:
327///
328/// - the typed `Machine<State>` surface
329/// - a builder for new machines
330/// - a machine-scoped `machine::SomeState` enum for matching rebuilt machines
331/// - a compatibility alias `machine::State = machine::SomeState`
332/// - a machine-scoped `machine::Fields` struct for heterogeneous batch rebuilds
333///
334/// If you need derives, place them below `#[machine]`.
335///
336/// ```rust
337/// use statum::{machine, state};
338///
339/// #[state]
340/// enum TaskState {
341/// Draft,
342/// Published,
343/// }
344///
345/// #[machine]
346/// struct Task<TaskState> {
347/// id: u64,
348/// }
349///
350/// fn main() {
351/// let task = Task::<Draft>::builder().id(1).build();
352/// assert_eq!(task.id, 1);
353/// }
354/// ```
355pub use statum_macros::machine;
356
357/// Validate and generate legal transitions for one source state.
358///
359/// Apply `#[transition]` to an `impl Machine<CurrentState>` block. Transition
360/// methods consume `self` and return `Machine<NextState>` or exact wrapper
361/// shapes around that same machine path such as
362/// `::core::result::Result<Machine<NextState>, E>`,
363/// `::core::option::Option<Machine<NextState>>`, or
364/// `::statum::Branch<Machine<Left>, Machine<Right>>`.
365/// When the `strict-introspection` feature is enabled, transition graph
366/// semantics must be directly readable from that written return type or from a
367/// local `#[introspect(return = ...)]` escape hatch on the method.
368///
369/// Inside the impl, use:
370///
371/// - `self.transition()` for unit target states
372/// - `self.transition_with(data)` for data-bearing target states
373/// - `self.transition_map(|current| next_data)` when the next payload is built
374/// from the current payload
375///
376/// ```rust
377/// use statum::{machine, state, transition};
378///
379/// #[state]
380/// enum LightState {
381/// Off,
382/// On,
383/// }
384///
385/// #[machine]
386/// struct Light<LightState> {}
387///
388/// #[transition]
389/// impl Light<Off> {
390/// fn switch_on(self) -> Light<On> {
391/// self.transition()
392/// }
393/// }
394///
395/// fn main() {
396/// let _light = Light::<Off>::builder().build().switch_on();
397/// }
398/// ```
399pub use statum_macros::transition;
400
401/// Rebuild typed machines from persisted data.
402///
403/// `#[validators(Machine)]` or an anchored path such as
404/// `#[validators(self::path::Machine)]`,
405/// `#[validators(super::path::Machine)]`, or
406/// `#[validators(crate::path::Machine)]` is attached to an
407/// `impl PersistedRow` block. Statum resolves the state family from the
408/// machine definition. Define one
409/// `is_{state}` method per state variant:
410///
411/// - return `statum::Result<()>` or `statum::Validation<()>` for unit states
412/// - return `statum::Result<StateData>` or `statum::Validation<StateData>` for
413/// data-bearing states
414///
415/// The generated API includes:
416///
417/// - `Task::rebuild(&row)` for single-item rebuilds
418/// - `Task::rebuild_many(rows)` and `.into_machines()` when all items share the same machine fields
419/// - `.into_machines_by(|row| machine::Fields { ... })` when each item needs
420/// different machine fields
421/// - `.build_report()` / `.build_reports()` when you want rebuild attempts and
422/// stable rejection details alongside the normal result
423///
424/// Machine fields are available by name inside validator bodies through
425/// generated bindings. Persisted-row fields still live on `self`. In relaxed
426/// mode, bare multi-segment paths like `#[validators(flow::Machine)]` are
427/// treated as local child-module paths, not imported aliases or re-exports.
428/// If Statum cannot resolve that local path, it emits a compile error asking
429/// for an anchored path instead.
430///
431/// ```rust
432/// use statum::{machine, state, validators, Error};
433///
434/// #[state]
435/// enum TaskState {
436/// Draft,
437/// InReview(String),
438/// Published,
439/// }
440///
441/// #[machine]
442/// struct Task<TaskState> {
443/// id: u64,
444/// }
445///
446/// struct TaskRow {
447/// id: u64,
448/// status: &'static str,
449/// reviewer: Option<String>,
450/// }
451///
452/// #[validators(Task)]
453/// impl TaskRow {
454/// fn is_draft(&self) -> statum::Result<()> {
455/// if self.status == "draft" {
456/// Ok(())
457/// } else {
458/// Err(Error::InvalidState)
459/// }
460/// }
461///
462/// fn is_in_review(&self) -> statum::Result<String> {
463/// if self.status == "in_review" {
464/// self.reviewer.clone().ok_or(Error::InvalidState)
465/// } else {
466/// Err(Error::InvalidState)
467/// }
468/// }
469///
470/// fn is_published(&self) -> statum::Result<()> {
471/// if self.status == "published" {
472/// Ok(())
473/// } else {
474/// Err(Error::InvalidState)
475/// }
476/// }
477/// }
478///
479/// fn main() -> statum::Result<()> {
480/// let row = TaskRow {
481/// id: 7,
482/// status: "draft",
483/// reviewer: None,
484/// };
485///
486/// let row_id = row.id;
487/// let _task = Task::rebuild(&row).id(row_id).build()?;
488/// Ok(())
489/// }
490/// ```
491pub use statum_macros::validators;