fsm_rust_jb/
fsm.rs

1use std::collections::HashMap;
2use std::hash::Hash;
3use std::fmt::Debug;
4use crate::types::{Transition, Effector, StreamData, StatesConnection};
5
6/// Finite state machine with side effects (Mealy automata)
7pub struct FSM<State, Effect>
8    where State: Eq + PartialEq + Copy + Hash,
9          Effect: Copy,
10{
11    /// State at beginning of running through stream
12    initial_state: State,
13    /// Transition graph that connects every state of FSM
14    /// to some next states by transitions
15    transition_table: HashMap<State, Vec<Transition<State, Effect>>>,
16    /// - post_effect: side effect that occurs after proceeding 
17    /// last character of string (ref. as "post-effect") 
18    post_effect: Option<Effect>
19}
20
21/// Error that occurs during initialization or running with FSM
22#[derive(Copy, Clone, Debug)]
23pub enum FSMError<'a, State> 
24    where State: Eq + PartialEq + Copy + Hash + Debug
25{
26    StateDoesNotExist(State),
27    TransDoesNotExist(StatesConnection<State>),
28    NoValidTransition {
29        from: State,
30        input_data: StreamData<'a>
31    }
32}
33
34impl<State, Effect> FSM<State, Effect> 
35    where State: Eq + PartialEq + Copy + Hash + Debug,
36          Effect: Copy,
37{
38    /// Creates new instance of FSM
39    /// - initial_state: starting state,
40    /// - transition_table: transition graph
41    /// - post_effect: post-effect
42    pub fn new<'a>(
43        initial_state: State, 
44        transition_table: HashMap<State, Vec<Transition<State, Effect>>>,
45        post_effect: Option<Effect>
46    ) -> Result<Self, FSMError<'a, State>> {
47        if !transition_table.contains_key(&initial_state) {
48            Err(FSMError::StateDoesNotExist(initial_state))
49        } else {
50            Ok(Self {
51                initial_state,
52                transition_table,
53                post_effect
54            })
55        }
56    }
57
58    /// Merges effects into existing fsm for its states
59    /// aligned to order of transitions for each state
60    /// - effects_map: map from pair of states ("from", "to") to ordered list of effects
61    /// (This method is created mainly for testing, reusing the same states and 
62    /// transition rules (i.e. partial fsm) for different effects configurations.
63    /// For typical cases, 
64    /// it's recommended to build fsm from its initialization (i.e. using "new" method))
65    pub fn merge_effects<'a>(
66        &mut self, 
67        effects_map: &HashMap<StatesConnection<State>, Vec<Effect>>
68    ) -> Result<(), FSMError<'a, State>> {
69        for (conn, effects) in effects_map.iter() {
70            if !self.transition_table.contains_key(&conn.to) {
71                return Err(FSMError::StateDoesNotExist(conn.to));
72            }
73
74            match self.transition_table.get_mut(&conn.from) {
75                Some(transitions) => {
76                    let mut eff_counter: usize = 0;
77
78                    for trans in transitions.iter_mut() {
79                        if conn.to == trans.to {
80                            trans.effect = Some(effects[eff_counter]);
81                            eff_counter += 1;
82                        }
83                    }
84                    
85                    if eff_counter == 0 {
86                        return Err(FSMError::TransDoesNotExist(*conn));
87                    }
88                },
89                None => return Err(
90                    FSMError::StateDoesNotExist(conn.from)
91                )
92            }
93        }
94
95        Ok(())
96    }
97
98    /// Runs some string through FSM to validate it (and apply some effects)
99    /// - string: runnable string,
100    /// - effector: module that mutates some data by effects
101    pub fn proceed<'a>(
102        &self, 
103        string: &'a String,
104        mut effector: Option<&'a mut dyn Effector<Effect>>
105    ) -> Result<(), FSMError<'a, State>> 
106    {
107        let mut curr_state = self.initial_state;
108        let mut char_id: usize = 0;
109
110        for ch in string.chars() {
111            match self.transition_table.get(&curr_state) {
112                Some(transitions) => {
113                    let mut accepted = false;
114                    
115                    for transition in transitions.iter() {
116                        match transition.transit(ch) {
117                            (Some(new_state), effect) => {
118                                curr_state = new_state;
119                                accepted = true;
120                                
121                                if let (Some(effector), Some(effect)) = 
122                                    (effector.as_mut(), effect) 
123                                {
124                                    effector.dispatch(effect, StreamData {
125                                        string,
126                                        index: char_id,
127                                        character: ch
128                                    });    
129                                }
130
131                                break;
132                            },
133                            _ => {}
134                        }
135                    }
136
137                    if !accepted {
138                        return Err(FSMError::NoValidTransition {
139                            from: curr_state,
140                            input_data: StreamData {
141                                string,
142                                index: char_id,
143                                character: ch
144                            }
145                        });
146                    }
147                },
148                None => {
149                    return Err(FSMError::StateDoesNotExist(curr_state));
150                }
151            }
152
153            char_id += 1;
154        }
155
156        if let (Some(effector), Some(effect)) = 
157            (effector, self.post_effect) 
158        {
159            effector.dispatch(effect, StreamData {
160                string,
161                index: string.len(),
162                character: '\0'
163            })
164        }
165
166        Ok(())
167    }
168}