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//! - optional machine introspection and presentation descriptors generated by
11//!   `#[machine]` / `#[transition]` when the `introspection` feature is enabled
12
13use std::borrow::Cow;
14
15#[cfg(doctest)]
16#[doc = include_str!("../README.md")]
17mod readme_doctests {}
18
19#[cfg(feature = "introspection")]
20mod introspection;
21
22pub mod projection;
23#[cfg(feature = "introspection")]
24pub mod testing;
25
26#[doc(hidden)]
27pub mod __private {
28    #[cfg(feature = "introspection")]
29    pub use crate::{
30        MachinePresentation, MachinePresentationDescriptor, RebuildAmbiguity, RebuildAttempt,
31        RebuildInput, RebuildReport, StatePresentation, TransitionPresentation,
32        TransitionPresentationInventory,
33    };
34    #[cfg(feature = "introspection")]
35    pub use linkme;
36
37    #[cfg(feature = "introspection")]
38    #[derive(Debug)]
39    pub struct TransitionToken {
40        _private: u8,
41    }
42
43    #[cfg(feature = "introspection")]
44    impl Default for TransitionToken {
45        fn default() -> Self {
46            Self::new()
47        }
48    }
49
50    #[cfg(feature = "introspection")]
51    impl TransitionToken {
52        pub const fn new() -> Self {
53            Self { _private: 0 }
54        }
55    }
56}
57
58#[cfg(feature = "introspection")]
59pub use introspection::{
60    GraphAuthorityLevel, GraphLintCode, GraphLintFinding, MachineDescriptor, MachineGraph,
61    MachineIntrospection, MachinePresentation, MachinePresentationDescriptor, MachineStateIdentity,
62    MachineTransitionRecorder, RecordedTransition, StableFieldMetadata, StableGraphMetadata,
63    StableGraphMetadataVersion, StableMachineMetadata, StableStateMetadata,
64    StableTransitionMetadata, StateDescriptor, StatePresentation, TransitionDescriptor,
65    TransitionInventory, TransitionPresentation, TransitionPresentationInventory,
66    TransitionTelemetryLabels, UnsupportedGraphMetadataCase,
67};
68
69/// A generated state marker type.
70///
71/// Every `#[state]` variant produces one marker type that implements
72/// `StateMarker`. The associated `Data` type is `()` for unit states and the
73/// tuple payload type for data-bearing states.
74pub trait StateMarker {
75    /// The payload type stored in machines for this state.
76    type Data;
77}
78
79/// A generated state marker with no payload.
80///
81/// Implemented for unit state variants like `Draft` or `Published`.
82pub trait UnitState: StateMarker<Data = ()> {}
83
84/// A generated state marker that carries payload data.
85///
86/// Implemented for tuple variants like `InReview(Assignment)`.
87pub trait DataState: StateMarker {}
88
89/// A machine that can transition directly to `Next`.
90///
91/// This is the stable trait-level view of `self.transition()`.
92pub trait CanTransitionTo<Next> {
93    /// The transition result type.
94    type Output;
95
96    /// Perform the transition.
97    fn transition_to(self) -> Self::Output;
98}
99
100/// A machine that can transition using `Data`.
101///
102/// This is the stable trait-level view of `self.transition_with(data)`.
103pub trait CanTransitionWith<Data> {
104    /// The next state selected by this transition.
105    type NextState;
106    /// The transition result type.
107    type Output;
108
109    /// Perform the transition with payload data.
110    fn transition_with_data(self, data: Data) -> Self::Output;
111}
112
113/// A machine that can transition by mapping its current state data into `Next`.
114///
115/// This is the stable trait-level view of `self.transition_map(...)`.
116pub trait CanTransitionMap<Next: StateMarker> {
117    /// The payload type stored in the current state.
118    type CurrentData;
119    /// The transition result type.
120    type Output;
121
122    /// Perform the transition by consuming the current state data and producing the next payload.
123    fn transition_map<F>(self, f: F) -> Self::Output
124    where
125        F: FnOnce(Self::CurrentData) -> Next::Data;
126}
127
128/// Errors returned by Statum runtime helpers.
129#[derive(Debug)]
130pub enum Error {
131    /// Returned when a runtime check determines the current state is invalid.
132    InvalidState,
133}
134
135/// A first-class two-way branching transition result.
136///
137/// This lets a transition expose two concrete machine targets while keeping the
138/// branch alternatives visible to Statum introspection.
139#[derive(Clone, Debug, Eq, PartialEq)]
140pub enum Branch<A, B> {
141    /// The first legal target branch.
142    First(A),
143    /// The second legal target branch.
144    Second(B),
145}
146
147/// Convenience result alias used by Statum APIs.
148///
149/// # Example
150///
151/// ```
152/// fn ensure_ready(ready: bool) -> statum_core::Result<()> {
153///     if ready {
154///         Ok(())
155///     } else {
156///         Err(statum_core::Error::InvalidState)
157///     }
158/// }
159///
160/// assert!(ensure_ready(true).is_ok());
161/// assert!(ensure_ready(false).is_err());
162/// ```
163pub type Result<T> = core::result::Result<T, Error>;
164
165/// A structured validator rejection captured during typed rehydration.
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub struct Rejection {
168    /// Stable machine-readable reason key for why the validator rejected.
169    pub reason_key: &'static str,
170    /// Optional human-readable message for debugging and reports.
171    pub message: Option<Cow<'static, str>>,
172}
173
174impl Rejection {
175    /// Create a rejection with a stable reason key and no message.
176    pub const fn new(reason_key: &'static str) -> Self {
177        Self {
178            reason_key,
179            message: None,
180        }
181    }
182
183    /// Attach a human-readable message to this rejection.
184    pub fn with_message(self, message: impl Into<Cow<'static, str>>) -> Self {
185        Self {
186            message: Some(message.into()),
187            ..self
188        }
189    }
190}
191
192impl From<&'static str> for Rejection {
193    fn from(reason_key: &'static str) -> Self {
194        Self::new(reason_key)
195    }
196}
197
198impl core::fmt::Display for Rejection {
199    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
200        match &self.message {
201            Some(message) => write!(fmt, "{}: {}", self.reason_key, message),
202            None => write!(fmt, "{}", self.reason_key),
203        }
204    }
205}
206
207impl std::error::Error for Rejection {}
208
209/// An opt-in validator result that carries structured rejection details.
210pub type Validation<T> = core::result::Result<T, Rejection>;
211
212/// One validator evaluation recorded during typed rehydration.
213#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct RebuildAttempt {
215    /// Rust method name of the validator that ran.
216    pub validator: &'static str,
217    /// Rust state-marker name the validator was checking.
218    pub target_state: &'static str,
219    /// Whether this validator accepted the input. In ambiguity-checking reports,
220    /// multiple accepted validators can still leave the final rebuild result invalid.
221    pub matched: bool,
222    /// Stable machine-readable rejection key, when the validator exposed one.
223    pub reason_key: Option<&'static str>,
224    /// Optional human-readable rejection message, when the validator exposed one.
225    pub message: Option<Cow<'static, str>>,
226}
227
228/// Describes the persisted input that a rebuild report evaluated.
229#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct RebuildInput {
231    /// Rust type name of the persisted input shape.
232    pub type_name: &'static str,
233    /// Optional stable identifier for the specific persisted input.
234    pub identifier: Option<Cow<'static, str>>,
235}
236
237/// Ambiguity status for a rebuild report.
238#[derive(Clone, Debug, Eq, PartialEq)]
239pub enum RebuildAmbiguity {
240    /// The generated rebuild surface stopped at the first match and did not scan
241    /// for additional matching validators.
242    NotChecked,
243    /// All candidates were evaluated and at most one matched.
244    Unambiguous,
245    /// Multiple candidates matched the same persisted input.
246    Ambiguous {
247        /// State candidates whose validators accepted the input.
248        matched_states: Vec<&'static str>,
249    },
250}
251
252/// A typed rehydration result plus the validator attempts that produced it.
253#[derive(Debug)]
254#[non_exhaustive]
255pub struct RebuildReport<M> {
256    /// Rust machine type whose validators were evaluated.
257    pub machine: &'static str,
258    /// Persisted input shape and optional stable input identifier.
259    pub input: RebuildInput,
260    /// State candidates considered by the generated rebuild surface.
261    pub candidate_states: Vec<&'static str>,
262    /// Whether this report checked for multiple matching validators.
263    pub ambiguity: RebuildAmbiguity,
264    /// Validator attempts in evaluation order.
265    pub attempts: Vec<RebuildAttempt>,
266    /// Final rebuild result.
267    pub result: Result<M>,
268}
269
270impl<M> RebuildReport<M> {
271    /// Create a structured rebuild report.
272    pub fn new(
273        machine: &'static str,
274        input: RebuildInput,
275        candidate_states: Vec<&'static str>,
276        ambiguity: RebuildAmbiguity,
277        attempts: Vec<RebuildAttempt>,
278        result: Result<M>,
279    ) -> Self {
280        Self {
281            machine,
282            input,
283            candidate_states,
284            ambiguity,
285            attempts,
286            result,
287        }
288    }
289
290    /// Attach a stable persisted-input identifier for logs or admin UIs.
291    pub fn with_input_identifier(mut self, identifier: impl Into<Cow<'static, str>>) -> Self {
292        self.input.identifier = Some(identifier.into());
293        self
294    }
295
296    /// Returns the first matching validator attempt, if any.
297    pub fn matched_attempt(&self) -> Option<&RebuildAttempt> {
298        self.attempts.iter().find(|attempt| attempt.matched)
299    }
300
301    /// Consumes the report and returns the original rebuild result.
302    pub fn into_result(self) -> Result<M> {
303        self.result
304    }
305}
306
307impl<T> From<Error> for core::result::Result<T, Error> {
308    fn from(val: Error) -> Self {
309        Err(val)
310    }
311}
312
313impl core::fmt::Display for Error {
314    fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> {
315        write!(fmt, "{self:?}")
316    }
317}
318
319impl std::error::Error for Error {}