ready-active-safe 0.1.3

Lifecycle engine for externally driven systems
Documentation
//! Journal and deterministic replay.
//!
//! Records every transition in a journal, then replays the full history
//! to verify the machine produces identical results. The machine is pure,
//! so replay is just running the same events again.

#![allow(clippy::print_stdout, clippy::enum_glob_use)]

use ready_active_safe::prelude::*;

#[derive(Debug, Clone, PartialEq, Eq)]
enum Mode {
    Ready,
    Active,
    Safe,
}

impl core::fmt::Display for Mode {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{self:?}")
    }
}

#[derive(Debug, Clone)]
enum Event {
    PowerOn,
    SensorReady,
    BeginMeasurement,
    DataCollected,
    AnomalyDetected,
    OperatorReset,
}

#[derive(Debug, Clone, PartialEq)]
enum Command {
    InitializeSensors,
    StartDataAcquisition,
    StoreReading,
    TriggerAlarm,
    ClearAlarm,
    RecalibrateSensors,
}

struct LabInstrument;

impl Machine for LabInstrument {
    type Mode = Mode;
    type Event = Event;
    type Command = Command;

    fn initial_mode(&self) -> Mode {
        Mode::Ready
    }

    fn on_event(
        &self,
        mode: &Self::Mode,
        event: &Self::Event,
    ) -> Decision<Self::Mode, Self::Command> {
        use Command::*;
        use Event::*;
        use Mode::*;

        match (mode, event) {
            (Ready, PowerOn) => stay().emit(InitializeSensors),
            (Ready, BeginMeasurement) => transition(Active).emit(StartDataAcquisition),
            (Active, DataCollected) => stay().emit(StoreReading),
            (Active, AnomalyDetected) => transition(Safe).emit(TriggerAlarm),
            (Safe, OperatorReset) => transition(Ready).emit_all([ClearAlarm, RecalibrateSensors]),
            _ => ignore(),
        }
    }
}

fn main() {
    let instrument = LabInstrument;
    let mut runner = Runner::new(&instrument);

    let events = [
        Event::PowerOn,
        Event::SensorReady,
        Event::BeginMeasurement,
        Event::DataCollected,
        Event::DataCollected,
        Event::AnomalyDetected,
        Event::OperatorReset,
        Event::BeginMeasurement,
    ];

    // Record every transition in the journal
    let mut journal: InMemoryJournal<Mode, Event, Command> = InMemoryJournal::new();

    println!("--- Recording transitions ---\n");
    for event in events {
        let from = runner.mode().clone();
        let commands = runner.feed(&event);
        let to = runner.mode().clone();

        println!("  {from:?} + {event:?} => {to:?}");
        if !commands.is_empty() {
            println!("    commands: {commands:?}");
        }

        journal.record_step(&from, &to, &event, &commands);
    }

    println!("\n--- Journal: {} records ---\n", journal.len());
    for record in journal.records() {
        println!(
            "  step {}: {:?} => {:?} (via {:?})",
            record.step, record.from, record.to, record.event,
        );
    }

    // Replay: feed the same events through a fresh machine and verify
    println!("\n--- Replaying ---\n");
    match journal.replay(&instrument, instrument.initial_mode()) {
        Ok(final_mode) => {
            println!("  replay verified: final mode = {final_mode:?}");
            assert_eq!(final_mode, runner.mode().clone());
        }
        Err(err) => {
            println!("  {err}");
        }
    }
}