statum_core/lib.rs
1//! Core traits and helper types shared by Statum crates.
2//!
3//! Most users reach these through the top-level `statum` crate. This crate
4//! holds the small runtime surface that macro-generated code targets:
5//!
6//! - state marker traits
7//! - transition capability traits
8//! - runtime error and result types
9//! - projection helpers for event-log style rebuilds
10
11use std::borrow::Cow;
12
13#[cfg(doctest)]
14#[doc = include_str!("../README.md")]
15mod readme_doctests {}
16
17mod introspection;
18
19pub mod projection;
20
21#[doc(hidden)]
22pub mod __private {
23 pub use crate::{
24 MachinePresentation, MachinePresentationDescriptor, RebuildAttempt, RebuildReport,
25 StatePresentation, TransitionPresentation, TransitionPresentationInventory,
26 };
27 pub use futures;
28 pub use linkme;
29
30 #[derive(Debug)]
31 pub struct TransitionToken {
32 _private: u8,
33 }
34
35 impl Default for TransitionToken {
36 fn default() -> Self {
37 Self::new()
38 }
39 }
40
41 impl TransitionToken {
42 pub const fn new() -> Self {
43 Self { _private: 0 }
44 }
45 }
46}
47
48pub use introspection::{
49 MachineDescriptor, MachineGraph, MachineIntrospection, MachinePresentation,
50 MachinePresentationDescriptor, MachineStateIdentity, MachineTransitionRecorder,
51 RecordedTransition, StateDescriptor, StatePresentation, TransitionDescriptor,
52 TransitionInventory, TransitionPresentation, TransitionPresentationInventory,
53};
54
55/// A generated state marker type.
56///
57/// Every `#[state]` variant produces one marker type that implements
58/// `StateMarker`. The associated `Data` type is `()` for unit states and the
59/// tuple payload type for data-bearing states.
60pub trait StateMarker {
61 /// The payload type stored in machines for this state.
62 type Data;
63}
64
65/// A generated state marker with no payload.
66///
67/// Implemented for unit state variants like `Draft` or `Published`.
68pub trait UnitState: StateMarker<Data = ()> {}
69
70/// A generated state marker that carries payload data.
71///
72/// Implemented for tuple variants like `InReview(Assignment)`.
73pub trait DataState: StateMarker {}
74
75/// A machine that can transition directly to `Next`.
76///
77/// This is the stable trait-level view of `self.transition()`.
78pub trait CanTransitionTo<Next> {
79 /// The transition result type.
80 type Output;
81
82 /// Perform the transition.
83 fn transition_to(self) -> Self::Output;
84}
85
86/// A machine that can transition using `Data`.
87///
88/// This is the stable trait-level view of `self.transition_with(data)`.
89pub trait CanTransitionWith<Data> {
90 /// The next state selected by this transition.
91 type NextState;
92 /// The transition result type.
93 type Output;
94
95 /// Perform the transition with payload data.
96 fn transition_with_data(self, data: Data) -> Self::Output;
97}
98
99/// A machine that can transition by mapping its current state data into `Next`.
100///
101/// This is the stable trait-level view of `self.transition_map(...)`.
102pub trait CanTransitionMap<Next: StateMarker> {
103 /// The payload type stored in the current state.
104 type CurrentData;
105 /// The transition result type.
106 type Output;
107
108 /// Perform the transition by consuming the current state data and producing the next payload.
109 fn transition_map<F>(self, f: F) -> Self::Output
110 where
111 F: FnOnce(Self::CurrentData) -> Next::Data;
112}
113
114/// Errors returned by Statum runtime helpers.
115#[derive(Debug)]
116pub enum Error {
117 /// Returned when a runtime check determines the current state is invalid.
118 InvalidState,
119}
120
121/// A first-class two-way branching transition result.
122///
123/// This lets a transition expose two concrete machine targets while keeping the
124/// branch alternatives visible to Statum introspection.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub enum Branch<A, B> {
127 /// The first legal target branch.
128 First(A),
129 /// The second legal target branch.
130 Second(B),
131}
132
133/// Convenience result alias used by Statum APIs.
134///
135/// # Example
136///
137/// ```
138/// fn ensure_ready(ready: bool) -> statum_core::Result<()> {
139/// if ready {
140/// Ok(())
141/// } else {
142/// Err(statum_core::Error::InvalidState)
143/// }
144/// }
145///
146/// assert!(ensure_ready(true).is_ok());
147/// assert!(ensure_ready(false).is_err());
148/// ```
149pub type Result<T> = core::result::Result<T, Error>;
150
151/// A structured validator rejection captured during typed rehydration.
152#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct Rejection {
154 /// Stable machine-readable reason key for why the validator rejected.
155 pub reason_key: &'static str,
156 /// Optional human-readable message for debugging and reports.
157 pub message: Option<Cow<'static, str>>,
158}
159
160impl Rejection {
161 /// Create a rejection with a stable reason key and no message.
162 pub const fn new(reason_key: &'static str) -> Self {
163 Self {
164 reason_key,
165 message: None,
166 }
167 }
168
169 /// Attach a human-readable message to this rejection.
170 pub fn with_message(self, message: impl Into<Cow<'static, str>>) -> Self {
171 Self {
172 message: Some(message.into()),
173 ..self
174 }
175 }
176}
177
178impl From<&'static str> for Rejection {
179 fn from(reason_key: &'static str) -> Self {
180 Self::new(reason_key)
181 }
182}
183
184impl core::fmt::Display for Rejection {
185 fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
186 match &self.message {
187 Some(message) => write!(fmt, "{}: {}", self.reason_key, message),
188 None => write!(fmt, "{}", self.reason_key),
189 }
190 }
191}
192
193impl std::error::Error for Rejection {}
194
195/// An opt-in validator result that carries structured rejection details.
196pub type Validation<T> = core::result::Result<T, Rejection>;
197
198/// One validator evaluation recorded during typed rehydration.
199#[derive(Clone, Debug, Eq, PartialEq)]
200pub struct RebuildAttempt {
201 /// Rust method name of the validator that ran.
202 pub validator: &'static str,
203 /// Rust state-marker name the validator was checking.
204 pub target_state: &'static str,
205 /// Whether this validator matched and produced the rebuilt state.
206 pub matched: bool,
207 /// Stable machine-readable rejection key, when the validator exposed one.
208 pub reason_key: Option<&'static str>,
209 /// Optional human-readable rejection message, when the validator exposed one.
210 pub message: Option<Cow<'static, str>>,
211}
212
213/// A typed rehydration result plus the validator attempts that produced it.
214#[derive(Debug)]
215pub struct RebuildReport<M> {
216 /// Validator attempts in evaluation order.
217 pub attempts: Vec<RebuildAttempt>,
218 /// Final rebuild result.
219 pub result: Result<M>,
220}
221
222impl<M> RebuildReport<M> {
223 /// Returns the first matching validator attempt, if any.
224 pub fn matched_attempt(&self) -> Option<&RebuildAttempt> {
225 self.attempts.iter().find(|attempt| attempt.matched)
226 }
227
228 /// Consumes the report and returns the original rebuild result.
229 pub fn into_result(self) -> Result<M> {
230 self.result
231 }
232}
233
234impl<T> From<Error> for core::result::Result<T, Error> {
235 fn from(val: Error) -> Self {
236 Err(val)
237 }
238}
239
240impl core::fmt::Display for Error {
241 fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> {
242 write!(fmt, "{self:?}")
243 }
244}
245
246impl std::error::Error for Error {}