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 core;
54pub mod doc;
55pub mod instance;
56pub mod macros;
57pub mod query;
58
59// Re-export public interface
60pub use core::StateMachine;
61pub use doc::StateMachineDoc;
62pub use instance::StateMachineInstance;
63pub use query::StateMachineQuery;
64
65/// Default maximum history size
66pub const DEFAULT_MAX_HISTORY_SIZE: usize = 512;
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    // Test traffic light state machine
73    define_state_machine! {
74        name: TrafficLight,
75        states: { Red, Yellow, Green },
76        inputs: { Timer, Emergency },
77        initial: Red,
78        transitions: {
79            Red + Timer => Green,
80            Green + Timer => Yellow,
81            Yellow + Timer => Red,
82            Red + Emergency => Yellow,
83            Green + Emergency => Red,
84            Yellow + Emergency => Red
85        }
86    }
87
88    // Test state machine with hidden inputs
89    mod test_machine {
90        use super::super::*;
91
92        define_state_machine! {
93            name: TestMachine,
94            states: { StateA, StateB },
95            inputs: { Action, _HiddenAction, _Debug },
96            initial: StateA,
97            transitions: {
98                StateA + Action => StateB,
99                StateB + Action => StateA,
100                StateA + _HiddenAction => StateA,
101                StateB + _HiddenAction => StateB,
102                StateA + _Debug => StateA,
103                StateB + _Debug => StateB
104            }
105        }
106    }
107
108    #[test]
109    fn test_deterministic_state_machine_basic() {
110        let mut sm = StateMachineInstance::<TrafficLight>::new();
111        assert_eq!(*sm.current_state(), State::Red);
112
113        // Test valid transition
114        let result = sm.transition(Input::Timer);
115        assert!(result.is_ok());
116        assert_eq!(*sm.current_state(), State::Green);
117        assert_eq!(result.unwrap(), State::Green);
118
119        // Continue transition
120        let result = sm.transition(Input::Timer);
121        assert!(result.is_ok());
122        assert_eq!(*sm.current_state(), State::Yellow);
123    }
124
125    #[test]
126    fn test_invalid_transition() {
127        let mut sm = StateMachineInstance::<TrafficLight>::new();
128        assert_eq!(*sm.current_state(), State::Red);
129
130        // Test emergency transition - Red can transition to Yellow via Emergency input
131        let result = sm.transition(Input::Emergency);
132        assert!(result.is_ok());
133        assert_eq!(*sm.current_state(), State::Yellow);
134    }
135
136    #[test]
137    fn test_state_machine_instance_methods() {
138        let mut sm = StateMachineInstance::<TrafficLight>::new();
139
140        // Test initial state
141        assert_eq!(*sm.current_state(), State::Red);
142        assert!(sm.history_is_empty());
143        assert_eq!(sm.history_len(), 0);
144
145        // Test valid input checking
146        assert!(sm.can_accept(&Input::Timer));
147        assert!(sm.can_accept(&Input::Emergency));
148
149        // Test valid input list
150        let valid_inputs = sm.valid_inputs();
151        assert!(valid_inputs.contains(&Input::Timer));
152        assert!(valid_inputs.contains(&Input::Emergency));
153
154        // Execute transition
155        sm.transition(Input::Timer).unwrap();
156        assert_eq!(sm.history_len(), 1);
157        assert!(!sm.history_is_empty());
158
159        // Test reset
160        sm.reset();
161        assert_eq!(*sm.current_state(), State::Red);
162        assert!(sm.history_is_empty());
163    }
164
165    #[test]
166    fn test_query_functions() {
167        let reachable = StateMachineQuery::<TrafficLight>::reachable_states(&State::Red);
168        assert!(reachable.contains(&State::Green));
169        assert!(reachable.contains(&State::Yellow));
170
171        let leading_to_red = StateMachineQuery::<TrafficLight>::states_leading_to(&State::Red);
172        assert!(leading_to_red.contains(&State::Yellow));
173        assert!(leading_to_red.contains(&State::Green));
174
175        // Test path finding
176        assert!(StateMachineQuery::<TrafficLight>::has_path(
177            &State::Red,
178            &State::Green
179        ));
180
181        // Test shortest path
182        let path = StateMachineQuery::<TrafficLight>::shortest_path(&State::Red, &State::Green);
183        assert!(path.is_some());
184        let path = path.unwrap();
185        assert_eq!(path[0], State::Red);
186        assert_eq!(path[1], State::Green);
187    }
188
189    #[test]
190    fn test_mermaid_generation() {
191        let mermaid = StateMachineDoc::<TrafficLight>::generate_mermaid();
192        assert!(mermaid.contains("stateDiagram-v2"));
193        assert!(mermaid.contains("Red"));
194        assert!(mermaid.contains("Green"));
195        assert!(mermaid.contains("Yellow"));
196        assert!(mermaid.contains("Timer"));
197        assert!(mermaid.contains("Emergency"));
198    }
199
200    #[test]
201    fn test_history_size_limit() {
202        let mut sm = StateMachineInstance::<TrafficLight>::with_max_history(2);
203        assert_eq!(sm.max_history_size(), 2);
204
205        // Execute multiple transitions
206        sm.transition(Input::Timer).unwrap(); // Red -> Green
207        sm.transition(Input::Timer).unwrap(); // Green -> Yellow
208        sm.transition(Input::Timer).unwrap(); // Yellow -> Red
209
210        // History should only contain the last 2 transitions
211        assert_eq!(sm.history().len(), 2);
212        assert_eq!(sm.history()[0], (State::Green, Input::Timer));
213        assert_eq!(sm.history()[1], (State::Yellow, Input::Timer));
214    }
215
216    #[test]
217    fn test_history_size_default() {
218        let sm = StateMachineInstance::<TrafficLight>::new();
219        assert_eq!(sm.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
220
221        let sm_default = StateMachineInstance::<TrafficLight>::default();
222        assert_eq!(sm_default.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
223    }
224
225    #[test]
226    fn test_underscore_inputs_excluded_from_docs() {
227        let mermaid = StateMachineDoc::<test_machine::TestMachine>::generate_mermaid();
228
229        // Should contain normal actions
230        assert!(mermaid.contains("Action"));
231
232        // Should not contain underscore-prefixed actions
233        assert!(!mermaid.contains("_HiddenAction"));
234        assert!(!mermaid.contains("_Debug"));
235
236        let table = StateMachineDoc::<test_machine::TestMachine>::generate_transition_table();
237
238        // Should contain normal actions
239        assert!(table.contains("Action"));
240
241        // Should not contain underscore-prefixed actions
242        assert!(!table.contains("_HiddenAction"));
243        assert!(!table.contains("_Debug"));
244    }
245
246    #[test]
247    fn test_underscore_inputs_still_functional() {
248        use test_machine::{Input, State, TestMachine};
249
250        let mut sm = StateMachineInstance::<TestMachine>::new();
251        assert_eq!(*sm.current_state(), State::StateA);
252
253        // Test that underscore inputs are still valid
254        let valid_inputs = sm.valid_inputs();
255        assert!(valid_inputs.contains(&Input::Action));
256        assert!(valid_inputs.contains(&Input::_HiddenAction));
257        assert!(valid_inputs.contains(&Input::_Debug));
258
259        // Test underscore input transition functionality
260        let result = sm.transition(Input::_HiddenAction);
261        assert!(result.is_ok());
262        assert_eq!(*sm.current_state(), State::StateA);
263
264        let result = sm.transition(Input::_Debug);
265        assert!(result.is_ok());
266        assert_eq!(*sm.current_state(), State::StateA);
267
268        // Test normal transition
269        let result = sm.transition(Input::Action);
270        assert!(result.is_ok());
271        assert_eq!(*sm.current_state(), State::StateB);
272    }
273
274    #[test]
275    fn test_display_implementation() {
276        assert_eq!(State::Red.to_string(), "Red");
277        assert_eq!(Input::Timer.to_string(), "Timer");
278    }
279
280    #[test]
281    fn test_documentation_generation() {
282        let stats = StateMachineDoc::<TrafficLight>::generate_statistics();
283        assert!(stats.contains("Number of States"));
284        assert!(stats.contains("Number of Transitions"));
285
286        let full_doc = StateMachineDoc::<TrafficLight>::generate_full_documentation();
287        assert!(full_doc.contains("State Machine Documentation"));
288        assert!(full_doc.contains("State Transition Table"));
289        assert!(full_doc.contains("State Diagram"));
290    }
291
292    #[cfg(feature = "serde")]
293    #[test]
294    fn test_serde_serialization() {
295        // Test state serialization
296        let state = State::Red;
297        let serialized = serde_json::to_string(&state).unwrap();
298        assert_eq!(serialized, "\"Red\"");
299
300        let deserialized: State = serde_json::from_str(&serialized).unwrap();
301        assert_eq!(deserialized, State::Red);
302
303        // Test input serialization
304        let input = Input::Timer;
305        let serialized = serde_json::to_string(&input).unwrap();
306        assert_eq!(serialized, "\"Timer\"");
307
308        let deserialized: Input = serde_json::from_str(&serialized).unwrap();
309        assert_eq!(deserialized, Input::Timer);
310
311        // Test multiple states
312        let states = vec![State::Red, State::Yellow, State::Green];
313        let serialized = serde_json::to_string(&states).unwrap();
314        let deserialized: Vec<State> = serde_json::from_str(&serialized).unwrap();
315        assert_eq!(deserialized, states);
316    }
317}