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