#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Handshake,
Active,
Closed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event {
Hello,
Data,
Bye,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IllegalTransition {
pub state: State,
pub event: Event,
}
impl State {
pub fn start() -> State {
State::Handshake
}
pub fn on(self, event: Event) -> Result<State, IllegalTransition> {
let to_active = (self == State::Handshake && event == Event::Hello)
|| (self == State::Active && event == Event::Data);
if to_active {
Ok(State::Active)
} else if self == State::Active && event == Event::Bye {
Ok(State::Closed)
} else {
Err(IllegalTransition { state: self, event })
}
}
pub fn is_closed(self) -> bool {
self == State::Closed
}
pub fn accepts_data(self) -> bool {
self == State::Active
}
}
#[cfg(test)]
mod tests {
use super::*;
const STATES: [State; 3] = [State::Handshake, State::Active, State::Closed];
const EVENTS: [Event; 3] = [Event::Hello, Event::Data, Event::Bye];
fn expected(state: State, event: Event) -> Option<State> {
match (state, event) {
(State::Handshake, Event::Hello) => Some(State::Active),
(State::Active, Event::Data) => Some(State::Active),
(State::Active, Event::Bye) => Some(State::Closed),
_ => None,
}
}
#[test]
fn every_transition_matches_the_specification_table() {
for &s in &STATES {
for &e in &EVENTS {
match expected(s, e) {
Some(next) => assert_eq!(
s.on(e),
Ok(next),
"legal {s:?} --{e:?}--> {next:?} must be allowed"
),
None => assert_eq!(
s.on(e),
Err(IllegalTransition { state: s, event: e }),
"illegal {s:?} --{e:?}--> must be refused"
),
}
}
}
}
#[test]
fn start_is_handshake_and_a_normal_session_reaches_closed() {
let s = State::start();
assert_eq!(s, State::Handshake);
let s = s.on(Event::Hello).unwrap();
assert_eq!(s, State::Active);
let s = s.on(Event::Data).unwrap();
assert_eq!(s, State::Active); let s = s.on(Event::Bye).unwrap();
assert_eq!(s, State::Closed);
}
#[test]
fn data_before_handshake_is_the_classic_out_of_sync_bug_and_is_refused() {
assert_eq!(
State::start().on(Event::Data),
Err(IllegalTransition {
state: State::Handshake,
event: Event::Data
})
);
}
#[test]
fn nothing_is_accepted_after_close_no_deadlock_no_reopen() {
let closed = State::Closed;
for &e in &EVENTS {
assert!(closed.on(e).is_err(), "{e:?} after close must be refused");
}
}
#[test]
fn is_closed_and_accepts_data_track_the_state_exactly() {
assert!(!State::Handshake.is_closed());
assert!(!State::Active.is_closed());
assert!(State::Closed.is_closed());
assert!(!State::Handshake.accepts_data());
assert!(State::Active.accepts_data());
assert!(!State::Closed.accepts_data());
}
}