Skip to main content

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}