Skip to main content

terminals_core/primitives/
fsm.rs

1/// Generic state machine trait.
2/// S = state type (typically an enum), E = event type (typically an enum).
3pub trait StateMachine {
4    type State: Clone + PartialEq + std::fmt::Debug;
5    type Event: Clone + std::fmt::Debug;
6
7    /// Current state
8    fn state(&self) -> &Self::State;
9
10    /// Process an event, potentially transitioning to a new state.
11    /// Returns the new state (or current if no transition).
12    fn transition(&mut self, event: &Self::Event) -> &Self::State;
13
14    /// Check if a transition is valid without executing it
15    fn can_transition(&self, event: &Self::Event) -> bool;
16}
17
18/// Transition function type alias for FSM.
19type TransitionFn<S, E> = Box<dyn Fn(&S, &E) -> Option<S>>;
20
21/// A concrete FSM implementation with transition history and callbacks.
22pub struct BasicFSM<S, E> {
23    current: S,
24    history: Vec<(S, E, S)>, // (from, event, to) trace
25    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    /// Checkpoint: snapshot of current state (can be used for rollback)
49    pub fn checkpoint(&self) -> S {
50        self.current.clone()
51    }
52
53    /// Restore to a previous checkpoint
54    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); // Red → Green
134        fsm.transition(&LightEvent::Timer); // Green → Yellow
135        fsm.transition(&LightEvent::Timer); // Yellow → Red
136        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); // → Green
143        fsm.transition(&LightEvent::Emergency); // → Red
144        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); // → Green
165        let cp = fsm.checkpoint();
166        fsm.transition(&LightEvent::Timer); // → Yellow
167        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); // oldest entry evicted
179    }
180}