terminals_core/primitives/
fsm.rs1pub trait StateMachine {
4 type State: Clone + PartialEq + std::fmt::Debug;
5 type Event: Clone + std::fmt::Debug;
6
7 fn state(&self) -> &Self::State;
9
10 fn transition(&mut self, event: &Self::Event) -> &Self::State;
13
14 fn can_transition(&self, event: &Self::Event) -> bool;
16}
17
18type TransitionFn<S, E> = Box<dyn Fn(&S, &E) -> Option<S>>;
20
21pub struct BasicFSM<S, E> {
23 current: S,
24 history: Vec<(S, E, S)>, transition_fn: TransitionFn<S, E>,
26 max_history: usize,
27}
28
29impl<S: Clone + PartialEq + std::fmt::Debug, E: Clone + std::fmt::Debug> BasicFSM<S, E> {
30 pub fn new(initial: S, transition_fn: impl Fn(&S, &E) -> Option<S> + 'static) -> Self {
31 Self {
32 current: initial,
33 history: Vec::new(),
34 transition_fn: Box::new(transition_fn),
35 max_history: 100,
36 }
37 }
38
39 pub fn with_max_history(mut self, max: usize) -> Self {
40 self.max_history = max;
41 self
42 }
43
44 pub fn history(&self) -> &[(S, E, S)] {
45 &self.history
46 }
47
48 pub fn checkpoint(&self) -> S {
50 self.current.clone()
51 }
52
53 pub fn restore(&mut self, state: S) {
55 self.current = state;
56 }
57}
58
59impl<S: Clone + PartialEq + std::fmt::Debug, E: Clone + std::fmt::Debug> StateMachine
60 for BasicFSM<S, E>
61{
62 type State = S;
63 type Event = E;
64
65 fn state(&self) -> &S {
66 &self.current
67 }
68
69 fn transition(&mut self, event: &E) -> &S {
70 if let Some(next) = (self.transition_fn)(&self.current, event) {
71 let from = self.current.clone();
72 self.current = next;
73 let to = self.current.clone();
74 self.history.push((from, event.clone(), to));
75 if self.history.len() > self.max_history {
76 self.history.remove(0);
77 }
78 }
79 &self.current
80 }
81
82 fn can_transition(&self, event: &E) -> bool {
83 (self.transition_fn)(&self.current, event).is_some()
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[derive(Debug, Clone, PartialEq)]
92 enum Light {
93 Red,
94 Yellow,
95 Green,
96 }
97
98 #[derive(Debug, Clone)]
99 enum LightEvent {
100 Timer,
101 Emergency,
102 }
103
104 fn traffic_light() -> BasicFSM<Light, LightEvent> {
105 BasicFSM::new(Light::Red, |state, event| {
106 #[allow(unreachable_patterns)]
107 match (state, event) {
108 (Light::Red, LightEvent::Timer) => Some(Light::Green),
109 (Light::Green, LightEvent::Timer) => Some(Light::Yellow),
110 (Light::Yellow, LightEvent::Timer) => Some(Light::Red),
111 (_, LightEvent::Emergency) => Some(Light::Red),
112 _ => None,
113 }
114 })
115 }
116
117 #[test]
118 fn test_initial_state() {
119 let fsm = traffic_light();
120 assert_eq!(fsm.state(), &Light::Red);
121 }
122
123 #[test]
124 fn test_valid_transition() {
125 let mut fsm = traffic_light();
126 fsm.transition(&LightEvent::Timer);
127 assert_eq!(fsm.state(), &Light::Green);
128 }
129
130 #[test]
131 fn test_full_cycle() {
132 let mut fsm = traffic_light();
133 fsm.transition(&LightEvent::Timer); fsm.transition(&LightEvent::Timer); fsm.transition(&LightEvent::Timer); assert_eq!(fsm.state(), &Light::Red);
137 }
138
139 #[test]
140 fn test_emergency_from_any_state() {
141 let mut fsm = traffic_light();
142 fsm.transition(&LightEvent::Timer); fsm.transition(&LightEvent::Emergency); assert_eq!(fsm.state(), &Light::Red);
145 }
146
147 #[test]
148 fn test_can_transition() {
149 let fsm = traffic_light();
150 assert!(fsm.can_transition(&LightEvent::Timer));
151 }
152
153 #[test]
154 fn test_history_tracking() {
155 let mut fsm = traffic_light();
156 fsm.transition(&LightEvent::Timer);
157 fsm.transition(&LightEvent::Timer);
158 assert_eq!(fsm.history().len(), 2);
159 }
160
161 #[test]
162 fn test_checkpoint_restore() {
163 let mut fsm = traffic_light();
164 fsm.transition(&LightEvent::Timer); let cp = fsm.checkpoint();
166 fsm.transition(&LightEvent::Timer); assert_eq!(fsm.state(), &Light::Yellow);
168 fsm.restore(cp);
169 assert_eq!(fsm.state(), &Light::Green);
170 }
171
172 #[test]
173 fn test_history_max_limit() {
174 let mut fsm = traffic_light().with_max_history(2);
175 fsm.transition(&LightEvent::Timer);
176 fsm.transition(&LightEvent::Timer);
177 fsm.transition(&LightEvent::Timer);
178 assert_eq!(fsm.history().len(), 2); }
180}