oxihuman_core/
state_machine_v2.rs1#![allow(dead_code)]
4
5pub type GuardFn = fn(&str, &str) -> bool;
9
10#[allow(dead_code)]
12#[derive(Clone)]
13pub struct Transition {
14 pub from: String,
15 pub to: String,
16 pub event: String,
17 pub guard: Option<GuardFn>,
18}
19
20#[allow(dead_code)]
22pub struct StateMachineV2 {
23 current: String,
24 states: Vec<String>,
25 transitions: Vec<Transition>,
26 history: Vec<String>,
27 fire_count: u64,
28}
29
30#[allow(dead_code)]
31impl StateMachineV2 {
32 pub fn new(initial: &str) -> Self {
33 Self {
34 current: initial.to_string(),
35 states: vec![initial.to_string()],
36 transitions: Vec::new(),
37 history: vec![initial.to_string()],
38 fire_count: 0,
39 }
40 }
41
42 pub fn add_state(&mut self, name: &str) {
43 if !self.states.contains(&name.to_string()) {
44 self.states.push(name.to_string());
45 }
46 }
47
48 pub fn add_transition(&mut self, from: &str, to: &str, event: &str) {
49 self.transitions.push(Transition {
50 from: from.to_string(),
51 to: to.to_string(),
52 event: event.to_string(),
53 guard: None,
54 });
55 }
56
57 pub fn add_guarded_transition(&mut self, from: &str, to: &str, event: &str, guard: GuardFn) {
58 self.transitions.push(Transition {
59 from: from.to_string(),
60 to: to.to_string(),
61 event: event.to_string(),
62 guard: Some(guard),
63 });
64 }
65
66 pub fn fire(&mut self, event: &str) -> bool {
67 let current = self.current.clone();
68 for t in &self.transitions {
69 if t.from != current || t.event != event {
70 continue;
71 }
72 let allowed = t.guard.is_none_or(|g| g(¤t, &t.to));
73 if allowed {
74 self.current = t.to.clone();
75 self.history.push(t.to.clone());
76 self.fire_count += 1;
77 return true;
78 }
79 }
80 false
81 }
82
83 pub fn current(&self) -> &str {
84 &self.current
85 }
86
87 pub fn state_count(&self) -> usize {
88 self.states.len()
89 }
90
91 pub fn transition_count(&self) -> usize {
92 self.transitions.len()
93 }
94
95 pub fn history(&self) -> &[String] {
96 &self.history
97 }
98
99 pub fn fire_count(&self) -> u64 {
100 self.fire_count
101 }
102
103 pub fn is_in(&self, state: &str) -> bool {
104 self.current == state
105 }
106
107 pub fn can_fire(&self, event: &str) -> bool {
108 let current = &self.current;
109 self.transitions.iter().any(|t| {
110 t.from == *current && t.event == event && t.guard.is_none_or(|g| g(current, &t.to))
111 })
112 }
113
114 pub fn reachable_events(&self) -> Vec<String> {
115 let current = &self.current;
116 let mut evts: Vec<String> = self
117 .transitions
118 .iter()
119 .filter(|t| t.from == *current)
120 .map(|t| t.event.clone())
121 .collect();
122 evts.sort();
123 evts.dedup();
124 evts
125 }
126}
127
128pub fn new_state_machine(initial: &str) -> StateMachineV2 {
129 StateMachineV2::new(initial)
130}
131
132pub fn sm_add_state(sm: &mut StateMachineV2, name: &str) {
133 sm.add_state(name);
134}
135
136pub fn sm_add_transition(sm: &mut StateMachineV2, from: &str, to: &str, event: &str) {
137 sm.add_transition(from, to, event);
138}
139
140pub fn sm_fire(sm: &mut StateMachineV2, event: &str) -> bool {
141 sm.fire(event)
142}
143
144pub fn sm_current(sm: &StateMachineV2) -> &str {
145 sm.current()
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn traffic_light() -> StateMachineV2 {
153 let mut sm = new_state_machine("red");
154 sm.add_state("green");
155 sm.add_state("yellow");
156 sm_add_transition(&mut sm, "red", "green", "go");
157 sm_add_transition(&mut sm, "green", "yellow", "slow");
158 sm_add_transition(&mut sm, "yellow", "red", "stop");
159 sm
160 }
161
162 #[test]
163 fn initial_state() {
164 let sm = traffic_light();
165 assert_eq!(sm_current(&sm), "red");
166 }
167
168 #[test]
169 fn valid_transition() {
170 let mut sm = traffic_light();
171 assert!(sm_fire(&mut sm, "go"));
172 assert_eq!(sm_current(&sm), "green");
173 }
174
175 #[test]
176 fn invalid_event_returns_false() {
177 let mut sm = traffic_light();
178 assert!(!sm_fire(&mut sm, "stop"));
179 assert_eq!(sm_current(&sm), "red");
180 }
181
182 #[test]
183 fn full_cycle() {
184 let mut sm = traffic_light();
185 sm_fire(&mut sm, "go");
186 sm_fire(&mut sm, "slow");
187 sm_fire(&mut sm, "stop");
188 assert!(sm.is_in("red"));
189 }
190
191 #[test]
192 fn history_tracks_states() {
193 let mut sm = traffic_light();
194 sm_fire(&mut sm, "go");
195 sm_fire(&mut sm, "slow");
196 assert_eq!(sm.history().len(), 3);
197 }
198
199 #[test]
200 fn fire_count() {
201 let mut sm = traffic_light();
202 sm_fire(&mut sm, "go");
203 sm_fire(&mut sm, "slow");
204 assert_eq!(sm.fire_count(), 2);
205 }
206
207 #[test]
208 fn can_fire() {
209 let sm = traffic_light();
210 assert!(sm.can_fire("go"));
211 assert!(!sm.can_fire("slow"));
212 }
213
214 #[test]
215 fn reachable_events() {
216 let sm = traffic_light();
217 assert_eq!(sm.reachable_events(), vec!["go".to_string()]);
218 }
219
220 #[test]
221 fn guarded_transition_blocked() {
222 let mut sm = new_state_machine("idle");
223 sm.add_state("active");
224 sm.add_guarded_transition("idle", "active", "start", |_, _| false);
225 assert!(!sm_fire(&mut sm, "start"));
226 assert!(sm.is_in("idle"));
227 }
228
229 #[test]
230 fn state_count() {
231 let sm = traffic_light();
232 assert_eq!(sm.state_count(), 3);
233 }
234}