ready_active_safe/runtime.rs
1//! A small reference runtime.
2//!
3//! The core [`Machine`] trait is pure and does not own mode.
4//! Real systems need a loop that:
5//!
6//! - stores the current mode
7//! - feeds external events into the machine
8//! - applies mode changes (optionally checking a [`Policy`])
9//! - dispatches emitted commands to the outside world
10//!
11//! This module provides a minimal, synchronous helper for that loop.
12//! It does **not** provide an event queue, scheduler, or async runtime.
13
14use alloc::vec::Vec;
15
16use crate::{LifecycleError, Machine, Policy};
17
18/// Owns the current mode and feeds events into a [`Machine`].
19///
20/// `Runner` is intentionally small. It is a convenience layer around the core
21/// contracts, not a framework.
22#[derive(Debug)]
23pub struct Runner<'a, M, E = core::convert::Infallible>
24where
25 M: Machine<E>,
26{
27 machine: &'a M,
28 mode: M::Mode,
29}
30
31impl<'a, M, E> Runner<'a, M, E>
32where
33 M: Machine<E>,
34{
35 /// Creates a new runner starting at `machine.initial_mode()`.
36 #[must_use]
37 pub fn new(machine: &'a M) -> Self {
38 Self {
39 mode: machine.initial_mode(),
40 machine,
41 }
42 }
43
44 /// Creates a runner with an explicit initial mode.
45 #[must_use]
46 pub const fn with_mode(machine: &'a M, mode: M::Mode) -> Self {
47 Self { machine, mode }
48 }
49
50 /// Returns the current mode.
51 #[must_use]
52 pub const fn mode(&self) -> &M::Mode {
53 &self.mode
54 }
55
56 /// Feeds an event into the machine, applies any mode change, and returns emitted commands.
57 ///
58 /// This does not enforce a [`Policy`]. Use [`Runner::feed_checked`]
59 /// when you need explicit transition approval.
60 #[must_use]
61 pub fn feed(&mut self, event: &M::Event) -> Vec<M::Command> {
62 let decision = self.machine.decide(&self.mode, event);
63 let (change, commands) = decision.into_parts();
64
65 if let Some(change) = change {
66 self.mode = change.target;
67 }
68
69 commands
70 }
71
72 /// Like [`Runner::feed`], but checks `policy` before applying a mode change.
73 ///
74 /// # Errors
75 ///
76 /// Returns [`LifecycleError::TransitionDenied`] if the policy rejects the transition.
77 pub fn feed_checked<P>(
78 &mut self,
79 event: &M::Event,
80 policy: &P,
81 ) -> Result<Vec<M::Command>, LifecycleError<M::Mode>>
82 where
83 P: Policy<M::Mode>,
84 M::Mode: Clone,
85 {
86 let decision = self.machine.decide(&self.mode, event);
87 let (change, commands) = decision.into_parts();
88
89 match change {
90 Some(change) => {
91 let target = change.target;
92
93 match policy.check(&self.mode, &target) {
94 Ok(()) => {
95 self.mode = target;
96 Ok(commands)
97 }
98 Err(reason) => Err(LifecycleError::TransitionDenied {
99 from: self.mode.clone(),
100 to: target,
101 reason,
102 }),
103 }
104 }
105 None => Ok(commands),
106 }
107 }
108
109 /// Feeds an event and dispatches each command to `dispatch`.
110 ///
111 /// This is a convenience helper for the common “execute commands immediately”
112 /// pattern. Commands are dispatched in the order they were emitted.
113 pub fn feed_and_dispatch<F>(&mut self, event: &M::Event, mut dispatch: F)
114 where
115 F: FnMut(M::Command),
116 {
117 for command in self.feed(event) {
118 dispatch(command);
119 }
120 }
121
122 /// Like [`Runner::feed_and_dispatch`], but checks `policy` before applying a mode change.
123 ///
124 /// # Errors
125 ///
126 /// Returns [`LifecycleError::TransitionDenied`] if the policy rejects the transition.
127 pub fn feed_checked_and_dispatch<P, F>(
128 &mut self,
129 event: &M::Event,
130 policy: &P,
131 mut dispatch: F,
132 ) -> Result<(), LifecycleError<M::Mode>>
133 where
134 P: Policy<M::Mode>,
135 M::Mode: Clone,
136 F: FnMut(M::Command),
137 {
138 for command in self.feed_checked(event, policy)? {
139 dispatch(command);
140 }
141 Ok(())
142 }
143}