ready-active-safe 0.1.3

Lifecycle engine for externally driven systems
Documentation
//! A small reference runtime.
//!
//! The core [`Machine`] trait is pure and does not own mode.
//! Real systems need a loop that:
//!
//! - stores the current mode
//! - feeds external events into the machine
//! - applies mode changes (optionally checking a [`Policy`])
//! - dispatches emitted commands to the outside world
//!
//! This module provides a minimal, synchronous helper for that loop.
//! It does **not** provide an event queue, scheduler, or async runtime.

use alloc::vec::Vec;

use crate::{LifecycleError, Machine, Policy};

/// Owns the current mode and feeds events into a [`Machine`].
///
/// `Runner` is intentionally small. It is a convenience layer around the core
/// contracts, not a framework.
#[derive(Debug)]
pub struct Runner<'a, M, E = core::convert::Infallible>
where
    M: Machine<E>,
{
    machine: &'a M,
    mode: M::Mode,
}

impl<'a, M, E> Runner<'a, M, E>
where
    M: Machine<E>,
{
    /// Creates a new runner starting at `machine.initial_mode()`.
    #[must_use]
    pub fn new(machine: &'a M) -> Self {
        Self {
            mode: machine.initial_mode(),
            machine,
        }
    }

    /// Creates a runner with an explicit initial mode.
    #[must_use]
    pub const fn with_mode(machine: &'a M, mode: M::Mode) -> Self {
        Self { machine, mode }
    }

    /// Returns the current mode.
    #[must_use]
    pub const fn mode(&self) -> &M::Mode {
        &self.mode
    }

    /// Feeds an event into the machine, applies any mode change, and returns emitted commands.
    ///
    /// This does not enforce a [`Policy`]. Use [`Runner::feed_checked`]
    /// when you need explicit transition approval.
    #[must_use]
    pub fn feed(&mut self, event: &M::Event) -> Vec<M::Command> {
        let decision = self.machine.decide(&self.mode, event);
        let (change, commands) = decision.into_parts();

        if let Some(change) = change {
            self.mode = change.target;
        }

        commands
    }

    /// Like [`Runner::feed`], but checks `policy` before applying a mode change.
    ///
    /// # Errors
    ///
    /// Returns [`LifecycleError::TransitionDenied`] if the policy rejects the transition.
    pub fn feed_checked<P>(
        &mut self,
        event: &M::Event,
        policy: &P,
    ) -> Result<Vec<M::Command>, LifecycleError<M::Mode>>
    where
        P: Policy<M::Mode>,
        M::Mode: Clone,
    {
        let decision = self.machine.decide(&self.mode, event);
        let (change, commands) = decision.into_parts();

        match change {
            Some(change) => {
                let target = change.target;

                match policy.check(&self.mode, &target) {
                    Ok(()) => {
                        self.mode = target;
                        Ok(commands)
                    }
                    Err(reason) => Err(LifecycleError::TransitionDenied {
                        from: self.mode.clone(),
                        to: target,
                        reason,
                    }),
                }
            }
            None => Ok(commands),
        }
    }

    /// Feeds an event and dispatches each command to `dispatch`.
    ///
    /// This is a convenience helper for the common “execute commands immediately”
    /// pattern. Commands are dispatched in the order they were emitted.
    pub fn feed_and_dispatch<F>(&mut self, event: &M::Event, mut dispatch: F)
    where
        F: FnMut(M::Command),
    {
        for command in self.feed(event) {
            dispatch(command);
        }
    }

    /// Like [`Runner::feed_and_dispatch`], but checks `policy` before applying a mode change.
    ///
    /// # Errors
    ///
    /// Returns [`LifecycleError::TransitionDenied`] if the policy rejects the transition.
    pub fn feed_checked_and_dispatch<P, F>(
        &mut self,
        event: &M::Event,
        policy: &P,
        mut dispatch: F,
    ) -> Result<(), LifecycleError<M::Mode>>
    where
        P: Policy<M::Mode>,
        M::Mode: Clone,
        F: FnMut(M::Command),
    {
        for command in self.feed_checked(event, policy)? {
            dispatch(command);
        }
        Ok(())
    }
}