Skip to main content

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 {}