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