yasm/
lib.rs

1//! # YASM (Yet Another State Machine)
2//!
3//! A modern, efficient deterministic state machine library designed for Rust 2024 edition.
4//!
5//! ## Features
6//!
7//! - **Deterministic State Machine**: Each state+input combination has at most one possible next state
8//! - **Type Safety**: Leverage Rust's type system to ensure state machine correctness
9//! - **Macro Support**: Use declarative macros to quickly define state machines
10//! - **History Tracking**: Automatically maintain state transition history for debugging and analysis
11//! - **Query Functions**: Rich state machine analysis capabilities
12//! - **Documentation Generation**: Automatically generate Mermaid diagrams and transition tables
13//! - **Serde Support**: Optional serialization and deserialization support
14//!
15//! ## Basic Usage
16//!
17//! ```rust
18//! use yasm::*;
19//!
20//! // Define state machine
21//! define_state_machine! {
22//!     name: TrafficLight,
23//!     states: { Red, Yellow, Green },
24//!     inputs: { Timer, Emergency },
25//!     initial: Red,
26//!     transitions: {
27//!         Red + Timer => Green,
28//!         Green + Timer => Yellow,
29//!         Yellow + Timer => Red,
30//!         Red + Emergency => Yellow,
31//!         Green + Emergency => Red,
32//!         Yellow + Emergency => Red
33//!     }
34//! }
35//!
36//! // Create state machine instance
37//! let mut traffic_light = StateMachineInstance::<TrafficLight>::new();
38//!
39//! // Execute state transition
40//! traffic_light.transition(Input::Timer).unwrap();
41//! assert_eq!(*traffic_light.current_state(), State::Green);
42//! ```
43//!
44//! ## Module Structure
45//!
46//! - [`core`][]: Core trait and type definitions
47//! - [`instance`][]: State machine instance implementation
48//! - [`query`][]: State machine query and analysis functionality
49//! - [`doc`][]: Documentation generation functionality
50//! - [`macros`][]: Macro definitions
51
52// Module declarations
53pub mod callbacks;
54pub mod core;
55pub mod doc;
56pub mod instance;
57pub mod macros;
58pub mod query;
59
60// Re-export public interface
61pub use callbacks::CallbackRegistry;
62pub use core::StateMachine;
63pub use doc::StateMachineDoc;
64pub use instance::StateMachineInstance;
65pub use query::StateMachineQuery;
66
67/// Default maximum history size
68pub const DEFAULT_MAX_HISTORY_SIZE: usize = 512;
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    // Test traffic light state machine
75    define_state_machine! {
76        name: TrafficLight,
77        states: { Red, Yellow, Green },
78        inputs: { Timer, Emergency },
79        initial: Red,
80        transitions: {
81            Red + Timer => Green,
82            Green + Timer => Yellow,
83            Yellow + Timer => Red,
84            Red + Emergency => Yellow,
85            Green + Emergency => Red,
86            Yellow + Emergency => Red
87        }
88    }
89
90    // Test state machine with hidden inputs
91    mod test_machine {
92        use super::super::*;
93
94        define_state_machine! {
95            name: TestMachine,
96            states: { StateA, StateB },
97            inputs: { Action, _HiddenAction, _Debug },
98            initial: StateA,
99            transitions: {
100                StateA + Action => StateB,
101                StateB + Action => StateA,
102                StateA + _HiddenAction => StateA,
103                StateB + _HiddenAction => StateB,
104                StateA + _Debug => StateA,
105                StateB + _Debug => StateB
106            }
107        }
108    }
109
110    #[test]
111    fn test_deterministic_state_machine_basic() {
112        let mut sm = StateMachineInstance::<TrafficLight>::new();
113        assert_eq!(*sm.current_state(), State::Red);
114
115        // Test valid transition
116        let result = sm.transition(Input::Timer);
117        assert!(result.is_ok());
118        assert_eq!(*sm.current_state(), State::Green);
119        assert_eq!(result.unwrap(), State::Green);
120
121        // Continue transition
122        let result = sm.transition(Input::Timer);
123        assert!(result.is_ok());
124        assert_eq!(*sm.current_state(), State::Yellow);
125    }
126
127    #[test]
128    fn test_invalid_transition() {
129        let mut sm = StateMachineInstance::<TrafficLight>::new();
130        assert_eq!(*sm.current_state(), State::Red);
131
132        // Test emergency transition - Red can transition to Yellow via Emergency input
133        let result = sm.transition(Input::Emergency);
134        assert!(result.is_ok());
135        assert_eq!(*sm.current_state(), State::Yellow);
136    }
137
138    #[test]
139    fn test_state_machine_instance_methods() {
140        let mut sm = StateMachineInstance::<TrafficLight>::new();
141
142        // Test initial state
143        assert_eq!(*sm.current_state(), State::Red);
144        assert!(sm.history_is_empty());
145        assert_eq!(sm.history_len(), 0);
146
147        // Test valid input checking
148        assert!(sm.can_accept(&Input::Timer));
149        assert!(sm.can_accept(&Input::Emergency));
150
151        // Test valid input list
152        let valid_inputs = sm.valid_inputs();
153        assert!(valid_inputs.contains(&Input::Timer));
154        assert!(valid_inputs.contains(&Input::Emergency));
155
156        // Execute transition
157        sm.transition(Input::Timer).unwrap();
158        assert_eq!(sm.history_len(), 1);
159        assert!(!sm.history_is_empty());
160
161        // Test reset
162        sm.reset();
163        assert_eq!(*sm.current_state(), State::Red);
164        assert!(sm.history_is_empty());
165    }
166
167    #[test]
168    fn test_query_functions() {
169        let reachable = StateMachineQuery::<TrafficLight>::reachable_states(&State::Red);
170        assert!(reachable.contains(&State::Green));
171        assert!(reachable.contains(&State::Yellow));
172
173        let leading_to_red = StateMachineQuery::<TrafficLight>::states_leading_to(&State::Red);
174        assert!(leading_to_red.contains(&State::Yellow));
175        assert!(leading_to_red.contains(&State::Green));
176
177        // Test path finding
178        assert!(StateMachineQuery::<TrafficLight>::has_path(
179            &State::Red,
180            &State::Green
181        ));
182
183        // Test shortest path
184        let path = StateMachineQuery::<TrafficLight>::shortest_path(&State::Red, &State::Green);
185        assert!(path.is_some());
186        let path = path.unwrap();
187        assert_eq!(path[0], State::Red);
188        assert_eq!(path[1], State::Green);
189    }
190
191    #[test]
192    fn test_mermaid_generation() {
193        let mermaid = StateMachineDoc::<TrafficLight>::generate_mermaid();
194        assert!(mermaid.contains("stateDiagram-v2"));
195        assert!(mermaid.contains("Red"));
196        assert!(mermaid.contains("Green"));
197        assert!(mermaid.contains("Yellow"));
198        assert!(mermaid.contains("Timer"));
199        assert!(mermaid.contains("Emergency"));
200    }
201
202    #[test]
203    fn test_history_size_limit() {
204        let mut sm = StateMachineInstance::<TrafficLight>::with_max_history(2);
205        assert_eq!(sm.max_history_size(), 2);
206
207        // Execute multiple transitions
208        sm.transition(Input::Timer).unwrap(); // Red -> Green
209        sm.transition(Input::Timer).unwrap(); // Green -> Yellow
210        sm.transition(Input::Timer).unwrap(); // Yellow -> Red
211
212        // History should only contain the last 2 transitions
213        assert_eq!(sm.history().len(), 2);
214        assert_eq!(sm.history()[0], (State::Green, Input::Timer));
215        assert_eq!(sm.history()[1], (State::Yellow, Input::Timer));
216    }
217
218    #[test]
219    fn test_history_size_default() {
220        let sm = StateMachineInstance::<TrafficLight>::new();
221        assert_eq!(sm.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
222
223        let sm_default = StateMachineInstance::<TrafficLight>::default();
224        assert_eq!(sm_default.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
225    }
226
227    #[test]
228    fn test_underscore_inputs_excluded_from_docs() {
229        let mermaid = StateMachineDoc::<test_machine::TestMachine>::generate_mermaid();
230
231        // Should contain normal actions
232        assert!(mermaid.contains("Action"));
233
234        // Should not contain underscore-prefixed actions
235        assert!(!mermaid.contains("_HiddenAction"));
236        assert!(!mermaid.contains("_Debug"));
237
238        let table = StateMachineDoc::<test_machine::TestMachine>::generate_transition_table();
239
240        // Should contain normal actions
241        assert!(table.contains("Action"));
242
243        // Should not contain underscore-prefixed actions
244        assert!(!table.contains("_HiddenAction"));
245        assert!(!table.contains("_Debug"));
246    }
247
248    #[test]
249    fn test_underscore_inputs_still_functional() {
250        use test_machine::{Input, State, TestMachine};
251
252        let mut sm = StateMachineInstance::<TestMachine>::new();
253        assert_eq!(*sm.current_state(), State::StateA);
254
255        // Test that underscore inputs are still valid
256        let valid_inputs = sm.valid_inputs();
257        assert!(valid_inputs.contains(&Input::Action));
258        assert!(valid_inputs.contains(&Input::_HiddenAction));
259        assert!(valid_inputs.contains(&Input::_Debug));
260
261        // Test underscore input transition functionality
262        let result = sm.transition(Input::_HiddenAction);
263        assert!(result.is_ok());
264        assert_eq!(*sm.current_state(), State::StateA);
265
266        let result = sm.transition(Input::_Debug);
267        assert!(result.is_ok());
268        assert_eq!(*sm.current_state(), State::StateA);
269
270        // Test normal transition
271        let result = sm.transition(Input::Action);
272        assert!(result.is_ok());
273        assert_eq!(*sm.current_state(), State::StateB);
274    }
275
276    #[test]
277    fn test_display_implementation() {
278        assert_eq!(State::Red.to_string(), "Red");
279        assert_eq!(Input::Timer.to_string(), "Timer");
280    }
281
282    #[test]
283    fn test_documentation_generation() {
284        let stats = StateMachineDoc::<TrafficLight>::generate_statistics();
285        assert!(stats.contains("Number of States"));
286        assert!(stats.contains("Number of Transitions"));
287
288        let full_doc = StateMachineDoc::<TrafficLight>::generate_full_documentation();
289        assert!(full_doc.contains("State Machine Documentation"));
290        assert!(full_doc.contains("State Transition Table"));
291        assert!(full_doc.contains("State Diagram"));
292    }
293
294    #[test]
295    fn test_state_from_str() {
296        // Test valid state strings
297        let red_state = State::from("Red");
298        assert_eq!(red_state, State::Red);
299
300        let yellow_state = State::from("Yellow");
301        assert_eq!(yellow_state, State::Yellow);
302
303        let green_state = State::from("Green");
304        assert_eq!(green_state, State::Green);
305    }
306
307    #[test]
308    #[should_panic(expected = "Invalid state")]
309    fn test_state_from_str_invalid() {
310        // Test invalid state string - should panic
311        let _ = State::from("InvalidState");
312    }
313
314    #[test]
315    fn test_input_from_str() {
316        // Test valid input strings
317        let timer_input = Input::from("Timer");
318        assert_eq!(timer_input, Input::Timer);
319
320        let emergency_input = Input::from("Emergency");
321        assert_eq!(emergency_input, Input::Emergency);
322    }
323
324    #[test]
325    #[should_panic(expected = "Invalid input")]
326    fn test_input_from_str_invalid() {
327        // Test invalid input string - should panic
328        let _ = Input::from("InvalidInput");
329    }
330
331    #[cfg(feature = "serde")]
332    #[test]
333    fn test_serde_serialization() {
334        // Test state serialization
335        let state = State::Red;
336        let serialized = serde_json::to_string(&state).unwrap();
337        assert_eq!(serialized, "\"Red\"");
338
339        let deserialized: State = serde_json::from_str(&serialized).unwrap();
340        assert_eq!(deserialized, State::Red);
341
342        // Test input serialization
343        let input = Input::Timer;
344        let serialized = serde_json::to_string(&input).unwrap();
345        assert_eq!(serialized, "\"Timer\"");
346
347        let deserialized: Input = serde_json::from_str(&serialized).unwrap();
348        assert_eq!(deserialized, Input::Timer);
349
350        // Test multiple states
351        let states = vec![State::Red, State::Yellow, State::Green];
352        let serialized = serde_json::to_string(&states).unwrap();
353        let deserialized: Vec<State> = serde_json::from_str(&serialized).unwrap();
354        assert_eq!(deserialized, states);
355    }
356}