ready_active_safe/lib.rs
1//! # ready-active-safe
2//!
3//! A small lifecycle engine for externally driven systems.
4//!
5//! Most real systems do not have an "anything can go anywhere" state graph. They have a lifecycle.
6//! They start up, run, and shut down. When things go wrong, they go safe and recover.
7//!
8//! This crate gives you a tiny kernel for that shape of problem.
9//! You write one pure function, [`Machine::on_event`], and you get back a [`Decision`].
10//! The decision is plain data, not effects. Your runtime stays yours.
11//!
12//! This crate is not a general purpose state machine framework. It is a lifecycle engine.
13//!
14//! ## What You Write
15//!
16//! - `Mode`: lifecycle phases like Ready, Active, Safe
17//! - `Event`: input from the outside world
18//! - `Command`: work for your runtime to execute
19//!
20//! ## Feature Flags
21//!
22//! | Feature | Default | Requires | Description |
23//! |-----------|---------|----------|-------------|
24//! | `full` | Yes | none | Enables all features below |
25//! | `std` | No* | none | Standard library support |
26//! | `runtime` | No* | `std` | Event loop and command dispatch |
27//! | `time` | No* | none | Clock, instant, and deadline types |
28//! | `journal` | No* | `std` | Transition recording and replay |
29//!
30//! *Enabled by default through the `full` feature.
31//!
32//! ## Module Overview
33//!
34//! - **Root types** ([`Machine`], [`Decision`], [`ModeChange`], [`Policy`]):
35//! Always available, `no_std`-compatible. These are the core contracts.
36//! - [`LifecycleError`]: transition failure errors.
37//! - **[`time`]** *(feature `time`)*: Clock and deadline abstractions.
38//! - **[`runtime`]** *(feature `runtime`)*: Event acceptance and command dispatch.
39//! - **[`journal`]** *(feature `journal`)*: Transition recording and replay.
40//!
41//! ## Examples
42//!
43//! Run any example with `cargo run --example <name>`. Suggested reading order:
44//!
45//! 1. **`basic`** — core API: `Machine`, `Decision`, `stay()`, `transition()`, `emit()`
46//! 2. **`openxr_session`** — real-world domain mapping (XR session lifecycle)
47//! 3. **`recovery`** — `Policy` enforcement and `apply_checked`
48//! 4. **`runner`** — `Runner` event loop with policy denial handling
49//! 5. **`recovery_cycle`** — repeated fault/recovery with external retry logic
50//! 6. **`channel_runtime`** — multi-threaded event feeding over channels
51//! 7. **`metrics`** — observability wrapper around `Runner`
52//! 8. **`replay`** — journal recording and deterministic replay
53//!
54//! ## Quick Start
55//!
56//! ```rust
57//! use ready_active_safe::prelude::*;
58//!
59//! #[derive(Debug, Clone, PartialEq, Eq)]
60//! enum Mode {
61//! Ready,
62//! Active,
63//! Safe,
64//! }
65//!
66//! #[derive(Debug)]
67//! enum Event {
68//! Start,
69//! Stop,
70//! Fault,
71//! }
72//!
73//! #[derive(Debug)]
74//! enum Command {
75//! Initialize,
76//! Shutdown,
77//! }
78//!
79//! struct System;
80//!
81//! impl Machine for System {
82//! type Mode = Mode;
83//! type Event = Event;
84//! type Command = Command;
85//!
86//! fn initial_mode(&self) -> Mode { Mode::Ready }
87//!
88//! fn on_event(&self, mode: &Mode, event: &Event) -> Decision<Mode, Command> {
89//! use Command::*;
90//! use Event::*;
91//! use Mode::*;
92//!
93//! match (mode, event) {
94//! (Ready, Start) => transition(Active).emit(Initialize),
95//! (Active, Stop | Fault) => transition(Safe).emit(Shutdown),
96//! _ => ignore(),
97//! }
98//! }
99//! }
100//!
101//! let system = System;
102//! let mut mode = system.initial_mode();
103//!
104//! let decision = system.decide(&mode, &Event::Start);
105//! assert!(decision.is_transition());
106//! assert_eq!(decision.target_mode(), Some(&Mode::Active));
107//! ```
108
109#![cfg_attr(not(feature = "std"), no_std)]
110#![cfg_attr(docsrs, feature(doc_cfg))]
111
112extern crate alloc;
113
114// Core modules (always available)
115mod error;
116mod model;
117
118// Feature-gated modules
119#[cfg(feature = "time")]
120#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
121pub mod time;
122
123#[cfg(feature = "runtime")]
124#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
125pub mod runtime;
126
127#[cfg(feature = "journal")]
128#[cfg_attr(docsrs, doc(cfg(feature = "journal")))]
129pub mod journal;
130
131// Public API re-exports
132pub use error::LifecycleError;
133pub use model::{
134 apply_decision, apply_decision_checked, ignore, stay, transition, AllowAll, Decision, DenyAll,
135 Machine, ModeChange, Policy,
136};
137
138/// Convenience re-exports for common imports.
139pub mod prelude {
140 pub use crate::{
141 apply_decision, apply_decision_checked, ignore, stay, transition, AllowAll, Decision,
142 DenyAll, LifecycleError, Machine, ModeChange, Policy,
143 };
144
145 #[cfg(feature = "runtime")]
146 pub use crate::runtime::Runner;
147
148 #[cfg(feature = "journal")]
149 pub use crate::journal::{InMemoryJournal, ReplayError, ReplayMismatch, TransitionRecord};
150
151 #[cfg(feature = "time")]
152 pub use crate::time::{Clock, Deadline, Instant, ManualClock};
153
154 #[cfg(all(feature = "time", feature = "std"))]
155 pub use crate::time::SystemClock;
156}
157
158/// Asserts that a decision transitions to the expected mode.
159///
160/// Produces a clear failure message when the assertion fails.
161///
162/// # Examples
163///
164/// ```
165/// use ready_active_safe::prelude::*;
166/// use ready_active_safe::assert_transitions_to;
167///
168/// let d: Decision<&str, ()> = transition("active");
169/// assert_transitions_to!(d, "active");
170/// ```
171#[macro_export]
172macro_rules! assert_transitions_to {
173 ($decision:expr, $expected:expr) => {
174 match $decision.target_mode() {
175 Some(actual) => {
176 assert_eq!(
177 actual, &$expected,
178 "expected transition to {:?}, got transition to {:?}",
179 $expected, actual,
180 );
181 }
182 None => {
183 panic!(
184 "expected transition to {:?}, but decision stays in current mode",
185 $expected,
186 );
187 }
188 }
189 };
190}
191
192/// Asserts that a decision stays in the current mode.
193///
194/// Produces a clear failure message when the assertion fails.
195///
196/// # Examples
197///
198/// ```
199/// use ready_active_safe::prelude::*;
200/// use ready_active_safe::assert_stays;
201///
202/// let d: Decision<&str, ()> = stay();
203/// assert_stays!(d);
204/// ```
205#[macro_export]
206macro_rules! assert_stays {
207 ($decision:expr) => {
208 assert!(
209 $decision.is_stay(),
210 "expected decision to stay, but got transition to {:?}",
211 $decision.target_mode(),
212 );
213 };
214}
215
216/// Asserts that a decision emits the expected commands.
217///
218/// Compares the full command list in order. For partial checks,
219/// use standard assertions on `decision.commands()` directly.
220///
221/// # Examples
222///
223/// ```
224/// use ready_active_safe::prelude::*;
225/// use ready_active_safe::assert_emits;
226///
227/// let d: Decision<(), &str> = stay().emit("init").emit("begin");
228/// assert_emits!(d, ["init", "begin"]);
229/// ```
230#[macro_export]
231macro_rules! assert_emits {
232 ($decision:expr, []) => {{
233 let got = $decision.commands();
234 assert!(
235 got.is_empty(),
236 "expected no commands, got {:?}",
237 got,
238 );
239 }};
240 ($decision:expr, [$($cmd:expr),+ $(,)?]) => {{
241 let expected = &[$($cmd),+][..];
242 let got = $decision.commands();
243 assert_eq!(
244 got,
245 expected,
246 "expected commands {:?}, got {:?}",
247 expected,
248 got,
249 );
250 }};
251}