ready-active-safe 0.1.3

Lifecycle engine for externally driven systems
Documentation
//! # ready-active-safe
//!
//! A small lifecycle engine for externally driven systems.
//!
//! Most real systems do not have an "anything can go anywhere" state graph. They have a lifecycle.
//! They start up, run, and shut down. When things go wrong, they go safe and recover.
//!
//! This crate gives you a tiny kernel for that shape of problem.
//! You write one pure function, [`Machine::on_event`], and you get back a [`Decision`].
//! The decision is plain data, not effects. Your runtime stays yours.
//!
//! This crate is not a general purpose state machine framework. It is a lifecycle engine.
//!
//! ## What You Write
//!
//! - `Mode`: lifecycle phases like Ready, Active, Safe
//! - `Event`: input from the outside world
//! - `Command`: work for your runtime to execute
//!
//! ## Feature Flags
//!
//! | Feature   | Default | Requires | Description |
//! |-----------|---------|----------|-------------|
//! | `full`    | Yes     | none     | Enables all features below |
//! | `std`     | No*     | none     | Standard library support |
//! | `runtime` | No*     | `std`    | Event loop and command dispatch |
//! | `time`    | No*     | none     | Clock, instant, and deadline types |
//! | `journal` | No*     | `std`    | Transition recording and replay |
//!
//! *Enabled by default through the `full` feature.
//!
//! ## Module Overview
//!
//! - **Root types** ([`Machine`], [`Decision`], [`ModeChange`], [`Policy`]):
//!   Always available, `no_std`-compatible. These are the core contracts.
//! - [`LifecycleError`]: transition failure errors.
//! - **[`time`]** *(feature `time`)*: Clock and deadline abstractions.
//! - **[`runtime`]** *(feature `runtime`)*: Event acceptance and command dispatch.
//! - **[`journal`]** *(feature `journal`)*: Transition recording and replay.
//!
//! ## Examples
//!
//! Run any example with `cargo run --example <name>`. Suggested reading order:
//!
//! 1. **`basic`** — core API: `Machine`, `Decision`, `stay()`, `transition()`, `emit()`
//! 2. **`openxr_session`** — real-world domain mapping (XR session lifecycle)
//! 3. **`recovery`** — `Policy` enforcement and `apply_checked`
//! 4. **`runner`** — `Runner` event loop with policy denial handling
//! 5. **`recovery_cycle`** — repeated fault/recovery with external retry logic
//! 6. **`channel_runtime`** — multi-threaded event feeding over channels
//! 7. **`metrics`** — observability wrapper around `Runner`
//! 8. **`replay`** — journal recording and deterministic replay
//!
//! ## Quick Start
//!
//! ```rust
//! use ready_active_safe::prelude::*;
//!
//! #[derive(Debug, Clone, PartialEq, Eq)]
//! enum Mode {
//!     Ready,
//!     Active,
//!     Safe,
//! }
//!
//! #[derive(Debug)]
//! enum Event {
//!     Start,
//!     Stop,
//!     Fault,
//! }
//!
//! #[derive(Debug)]
//! enum Command {
//!     Initialize,
//!     Shutdown,
//! }
//!
//! struct System;
//!
//! impl Machine for System {
//!     type Mode = Mode;
//!     type Event = Event;
//!     type Command = Command;
//!
//!     fn initial_mode(&self) -> Mode { Mode::Ready }
//!
//!     fn on_event(&self, mode: &Mode, event: &Event) -> Decision<Mode, Command> {
//!         use Command::*;
//!         use Event::*;
//!         use Mode::*;
//!
//!         match (mode, event) {
//!             (Ready, Start) => transition(Active).emit(Initialize),
//!             (Active, Stop | Fault) => transition(Safe).emit(Shutdown),
//!             _ => ignore(),
//!         }
//!     }
//! }
//!
//! let system = System;
//! let mut mode = system.initial_mode();
//!
//! let decision = system.decide(&mode, &Event::Start);
//! assert!(decision.is_transition());
//! assert_eq!(decision.target_mode(), Some(&Mode::Active));
//! ```

#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]

extern crate alloc;

// Core modules (always available)
mod error;
mod model;

// Feature-gated modules
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
pub mod time;

#[cfg(feature = "runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
pub mod runtime;

#[cfg(feature = "journal")]
#[cfg_attr(docsrs, doc(cfg(feature = "journal")))]
pub mod journal;

// Public API re-exports
pub use error::LifecycleError;
pub use model::{
    apply_decision, apply_decision_checked, ignore, stay, transition, AllowAll, Decision, DenyAll,
    Machine, ModeChange, Policy,
};

/// Convenience re-exports for common imports.
pub mod prelude {
    pub use crate::{
        apply_decision, apply_decision_checked, ignore, stay, transition, AllowAll, Decision,
        DenyAll, LifecycleError, Machine, ModeChange, Policy,
    };

    #[cfg(feature = "runtime")]
    pub use crate::runtime::Runner;

    #[cfg(feature = "journal")]
    pub use crate::journal::{InMemoryJournal, ReplayError, ReplayMismatch, TransitionRecord};

    #[cfg(feature = "time")]
    pub use crate::time::{Clock, Deadline, Instant, ManualClock};

    #[cfg(all(feature = "time", feature = "std"))]
    pub use crate::time::SystemClock;
}

/// Asserts that a decision transitions to the expected mode.
///
/// Produces a clear failure message when the assertion fails.
///
/// # Examples
///
/// ```
/// use ready_active_safe::prelude::*;
/// use ready_active_safe::assert_transitions_to;
///
/// let d: Decision<&str, ()> = transition("active");
/// assert_transitions_to!(d, "active");
/// ```
#[macro_export]
macro_rules! assert_transitions_to {
    ($decision:expr, $expected:expr) => {
        match $decision.target_mode() {
            Some(actual) => {
                assert_eq!(
                    actual, &$expected,
                    "expected transition to {:?}, got transition to {:?}",
                    $expected, actual,
                );
            }
            None => {
                panic!(
                    "expected transition to {:?}, but decision stays in current mode",
                    $expected,
                );
            }
        }
    };
}

/// Asserts that a decision stays in the current mode.
///
/// Produces a clear failure message when the assertion fails.
///
/// # Examples
///
/// ```
/// use ready_active_safe::prelude::*;
/// use ready_active_safe::assert_stays;
///
/// let d: Decision<&str, ()> = stay();
/// assert_stays!(d);
/// ```
#[macro_export]
macro_rules! assert_stays {
    ($decision:expr) => {
        assert!(
            $decision.is_stay(),
            "expected decision to stay, but got transition to {:?}",
            $decision.target_mode(),
        );
    };
}

/// Asserts that a decision emits the expected commands.
///
/// Compares the full command list in order. For partial checks,
/// use standard assertions on `decision.commands()` directly.
///
/// # Examples
///
/// ```
/// use ready_active_safe::prelude::*;
/// use ready_active_safe::assert_emits;
///
/// let d: Decision<(), &str> = stay().emit("init").emit("begin");
/// assert_emits!(d, ["init", "begin"]);
/// ```
#[macro_export]
macro_rules! assert_emits {
    ($decision:expr, []) => {{
        let got = $decision.commands();
        assert!(
            got.is_empty(),
            "expected no commands, got {:?}",
            got,
        );
    }};
    ($decision:expr, [$($cmd:expr),+ $(,)?]) => {{
        let expected = &[$($cmd),+][..];
        let got = $decision.commands();
        assert_eq!(
            got,
            expected,
            "expected commands {:?}, got {:?}",
            expected,
            got,
        );
    }};
}