#[macro_use]
extern crate custom_derive;
extern crate dot;
#[macro_use]
extern crate enum_derive;
extern crate itertools;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate slog;
extern crate uuid;
use dot::LabelText;
use itertools::Itertools;
use slog::Logger;
use std::cell::{Ref, RefCell, RefMut};
use std::cmp::Ordering;
use std::collections::{HashMap, VecDeque, HashSet};
use std::default::Default;
use std::fmt::Debug;
use std::fs;
use std::hash::Hash;
use std::io;
use std::iter::Iterator;
use std::mem::swap;
use std::rc::Rc;
use uuid::Uuid;
#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub enum EntryExit {
EntryTransition,
ExitTransition,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Errors<EventType, StateType, ErrorType> {
OK,
InternalError(EventType, StateType, ErrorType),
NoTransition(EventType, StateType),
TransitionFailure,
}
pub type OptionalFnArg<TransitionFnArguments> = Option<TransitionFnArguments>;
pub type EventQueue<EventType, TransitionFnArguments> =
VecDeque<(EventType, OptionalFnArg<TransitionFnArguments>)>;
pub type TransitionResult<EventType, StateType, TransitionFnArguments, ErrorType> = Result<
Option<Vec<(EventType, OptionalFnArg<TransitionFnArguments>)>>,
Errors<EventType, StateType, ErrorType>,
>;
pub type TransitionFn<ExtendedState, EventType, StateType, TransitionFnArguments, ErrorType> =
dyn Fn(RefMut<Box<ExtendedState>>, EventType, OptionalFnArg<TransitionFnArguments>)
-> TransitionResult<EventType, StateType, TransitionFnArguments, ErrorType>;
pub type EntryExitTransitionFn<
ExtendedState,
EventType,
StateType,
TransitionFnArguments,
ErrorType,
> = dyn Fn(RefMut<Box<ExtendedState>>, Option<StateType>, Option<EventType>)
-> TransitionResult<EventType, StateType, TransitionFnArguments, ErrorType>;
pub struct FSM<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
where
StateType: Clone + Eq + Hash + Sized,
EventType: Clone + Eq + Hash + Sized,
{
pub extended_state: RefCell<Box<ExtendedState>>,
name: String,
start_state: StateType,
current_state: StateType,
event_queue: EventQueue<EventType, TransitionFnArguments>,
transitions: Rc<
RefCell<
TransitionTable<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>,
>,
>,
statetransitions: Rc<
RefCell<
EntryExitTransitionTable<
ExtendedState,
StateType,
EventType,
TransitionFnArguments,
ErrorType,
>,
>,
>,
log: Option<Logger>,
dotgraph: Rc<RefCell<DotGraph<StateType>>>,
last_state: Option<StateType>,
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct ColorGroupedTransitions<StateType>
where
StateType: Clone + Sized + Eq + Hash,
{
color: DotColor,
source: StateType,
target: StateType, }
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum DotEdgeKey<StateType>
where
StateType: Clone + Sized + Eq + Hash,
{
TransitionsSet(ColorGroupedTransitions<StateType>),
EntryExit(EntryExitKey<StateType>),
}
impl<StateType> DotEdgeKey<StateType>
where
StateType: Clone + Eq + Hash + Sized,
{
pub fn new_set(color: DotColor, source: StateType, target: StateType) -> DotEdgeKey<StateType> {
DotEdgeKey::TransitionsSet(ColorGroupedTransitions {
color: color,
source: source,
target: target,
})
}
pub fn new_entryexit(into: EntryExitKey<StateType>) -> DotEdgeKey<StateType> {
DotEdgeKey::EntryExit(into)
}
}
custom_derive! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord,
IterVariants(ColorVariants), IterVariantNames(ColorNames))]
#[allow(non_camel_case_types)]
pub enum DotColor {
red,
green,
blue,
yellow,
black,
gray,
cyan,
gold
}
}
lazy_static! {
static ref COLORS: HashMap<DotColor, &'static str> = zipenumvariants(
Box::new(DotColor::iter_variants()),
Box::new(DotColor::iter_variant_names())
);
}
fn zipenumvariants<ET>(
i1: Box<dyn Iterator<Item=ET>>,
i2: Box<dyn Iterator<Item=&'static str>>,
) -> HashMap<ET, &'static str>
where
ET: Sized + Eq + Hash,
{
i1.zip(i2).collect::<HashMap<_, _>>()
}
impl Into<&'static str> for DotColor {
fn into(self) -> &'static str {
COLORS.get(&self).expect("dot color cannot be translated")
}
}
#[derive(Clone, PartialEq, Eq)]
struct DotEdge<StateType>
where
StateType: Clone + Sized + Eq + Hash,
{
key: DotEdgeKey<StateType>,
style: dot::Style,
label: String,
color: DotColor,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct DotNodeKey<StateType: Clone + Sized + Eq + Hash> {
entryexit: Option<EntryExit>,
state: StateType,
}
impl<StateType: Clone + Sized + Eq + Hash> DotNodeKey<StateType> {
pub fn new(entryexit: Option<EntryExit>, state: StateType) -> DotNodeKey<StateType> {
DotNodeKey {
entryexit: entryexit,
state: state,
}
}
}
#[derive(Clone, PartialEq, Eq)]
struct DotNode<StateType>
where
StateType: Clone + Sized + Eq + Hash,
{
key: DotNodeKey<StateType>,
id: Uuid,
shape: Option<String>,
style: dot::Style,
label: String,
}
struct DotGraph<StateType: Clone + Sized + Eq + Hash> {
nodes: HashMap<DotNodeKey<StateType>, DotNode<StateType>>,
edges: HashMap<DotEdgeKey<StateType>, DotEdge<StateType>>,
id: Uuid,
start_state: Option<StateType>,
}
impl<StateType> Default for DotGraph<StateType>
where
StateType: Clone + Sized + Eq + Hash,
{
fn default() -> DotGraph<StateType> {
DotGraph {
nodes: HashMap::new(),
edges: HashMap::new(),
id: Uuid::new_v4(),
start_state: None,
}
}
}
impl<'a, ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
dot::GraphWalk<'a, DotNodeKey<StateType>, DotEdgeKey<StateType>>
for FSM<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
where
StateType: Clone + PartialEq + Eq + Hash + Sized,
EventType: Clone + PartialEq + Eq + Hash + Sized,
{
fn nodes(&'a self) -> dot::Nodes<'a, DotNodeKey<StateType>> {
self.dotgraph.borrow().nodes.keys().cloned().collect()
}
fn edges(&'a self) -> dot::Edges<'a, DotEdgeKey<StateType>> {
self.dotgraph.borrow().edges.keys().cloned().collect()
}
fn source(&self, e: &DotEdgeKey<StateType>) -> DotNodeKey<StateType> {
match *e {
DotEdgeKey::EntryExit(ref eek) => {
if eek.entryexit == EntryExit::EntryTransition {
DotNodeKey::new(Some(eek.entryexit.clone()), eek.state.clone())
} else {
if let Some(_) = self.statetransitions.borrow().get(eek) {
DotNodeKey::new(None, eek.state.clone())
} else {
unreachable!();
}
}
}
DotEdgeKey::TransitionsSet(ref tk) => DotNodeKey::new(None, tk.source.clone()),
}
}
fn target(&self, e: &DotEdgeKey<StateType>) -> DotNodeKey<StateType> {
match *e {
DotEdgeKey::EntryExit(ref eek) => {
if eek.entryexit == EntryExit::ExitTransition {
DotNodeKey::new(Some(eek.entryexit.clone()), eek.state.clone())
} else {
if let Some(_) = self.statetransitions.borrow().get(eek) {
DotNodeKey::new(None, eek.state.clone())
} else {
unreachable!();
}
}
}
DotEdgeKey::TransitionsSet(ref tk) => DotNodeKey::new(None, tk.target.clone()),
}
}
}
impl<'a, ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
dot::Labeller<'a, DotNodeKey<StateType>, DotEdgeKey<StateType>>
for FSM<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
where
StateType: Clone + PartialEq + Eq + Hash + Sized,
EventType: Clone + PartialEq + Eq + Hash + Sized,
{
fn graph_id(&'a self) -> dot::Id<'a> {
let gid = format!("G{:X}", self.dotgraph.borrow().id.as_u128());
dot::Id::new(gid).unwrap()
}
fn node_id(&'a self, n: &DotNodeKey<StateType>) -> dot::Id<'a> {
match self.dotgraph.borrow().nodes.get(n) {
Some(realnode) => {
let fid = format!("N{:X}", realnode.id.as_u128());
dot::Id::new(fid).unwrap()
}
None => unreachable!(),
}
}
fn node_shape(&'a self, n: &DotNodeKey<StateType>) -> Option<dot::LabelText<'a>> {
let borrowed = self.dotgraph.borrow();
match borrowed.nodes.get(n) {
Some(realnode) => {
if let Some(ref r) = realnode.shape {
let v = r.clone();
Some(dot::LabelText::LabelStr(v.into()))
} else {
Some(dot::LabelText::LabelStr("oval".into()))
}
}
None => unreachable!(),
}
}
fn node_style(&'a self, n: &DotNodeKey<StateType>) -> dot::Style {
match self.dotgraph.borrow().nodes.get(n) {
Some(realnode) => realnode.style,
None => unreachable!(),
}
}
fn edge_end_arrow(&'a self, _e: &DotEdgeKey<StateType>) -> dot::Arrow {
dot::Arrow::normal()
}
fn edge_start_arrow(&'a self, _e: &DotEdgeKey<StateType>) -> dot::Arrow {
dot::Arrow::none()
}
fn edge_style(&'a self, _e: &DotEdgeKey<StateType>) -> dot::Style {
dot::Style::None
}
fn node_label<'b>(&'b self, n: &DotNodeKey<StateType>) -> dot::LabelText<'b> {
match self.dotgraph.borrow().nodes.get(n) {
Some(ref realnode) => dot::LabelText::LabelStr(realnode.label.clone().into()),
None => unreachable!(),
}
}
fn edge_label<'b>(&'b self, ek: &DotEdgeKey<StateType>) -> dot::LabelText<'b> {
match self.dotgraph.borrow().edges.get(ek) {
Some(realedge) => dot::LabelText::LabelStr(realedge.label.clone().into()),
None => unreachable!(),
}
}
fn edge_color(&'a self, ek: &DotEdgeKey<StateType>) -> Option<LabelText<'a>> {
match self.dotgraph.borrow().edges.get(ek) {
Some(realedge) => {
let cs: &str = realedge.color.into();
Some(dot::LabelText::LabelStr(String::from(cs).into()))
}
None => unreachable!(),
}
}
}
pub trait RunsFSM<EventType, StateType, TransitionFnArguments, ErrorType> {
fn add_events(
&mut self,
events: &mut Vec<(EventType, OptionalFnArg<TransitionFnArguments>)>,
) -> Result<u32, Errors<EventType, StateType, ErrorType>>;
fn extend_events<I>(&mut self, iter: I)
where
I: IntoIterator<Item=(EventType, std::option::Option<TransitionFnArguments>)>;
fn process_event_queue(&mut self) -> Result<u32, Errors<EventType, StateType, ErrorType>>;
}
impl<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
FSM<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
where
StateType: Clone + Eq + Hash + Sized,
EventType: Clone + Eq + Hash + Sized,
{
pub fn new(
start_state: StateType,
extended_init: Box<ExtendedState>,
name: &str,
log: Option<Logger>,
) -> Self {
let mut g = DotGraph::default();
g.start_state = Some(start_state.clone());
FSM {
log,
name: String::from(name),
current_state: start_state.clone(),
start_state,
event_queue: VecDeque::<(EventType, OptionalFnArg<TransitionFnArguments>)>::new(),
transitions: Rc::new(RefCell::new(TransitionTable::new())),
statetransitions: Rc::new(RefCell::new(EntryExitTransitionTable::new())),
extended_state: RefCell::new(extended_init),
dotgraph: Rc::new(RefCell::new(g)),
last_state: None,
}
}
pub fn flyweight(
&self,
extended_init: Box<ExtendedState>,
name: &str,
log: Option<Logger>,
) -> Self {
FSM {
log,
name: String::from(name),
current_state: self.start_state.clone(),
start_state: self.start_state.clone(),
event_queue: VecDeque::<(EventType, OptionalFnArg<TransitionFnArguments>)>::new(),
transitions: self.transitions.clone(),
statetransitions: self.statetransitions.clone(),
extended_state: RefCell::new(extended_init),
dotgraph: self.dotgraph.clone(),
last_state: None,
}
}
pub fn add_transition(
&mut self,
from: TransitionSource<StateType, EventType>,
to: TransitionTarget<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>,
) -> bool {
self.transitions.borrow_mut().insert(from, to).is_none()
}
pub fn transitions(
&self,
) -> Ref<TransitionTable<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>>
{
self.transitions.borrow()
}
pub fn entry_exit_transitions(
&self,
) -> Ref<
EntryExitTransitionTable<
ExtendedState,
StateType,
EventType,
TransitionFnArguments,
ErrorType,
>,
> {
self.statetransitions.borrow()
}
pub fn add_enter_transition(
&mut self,
case: (StateType, EntryExit),
trans: EntryExitTransition<
ExtendedState,
StateType,
EventType,
TransitionFnArguments,
ErrorType,
>,
) -> bool {
self.statetransitions
.borrow_mut()
.insert(
EntryExitKey {
state: case.0,
entryexit: case.1,
},
trans,
).is_none()
}
pub fn name(&self) -> &String {
&self.name
}
pub fn extended_state(&self) -> Ref<Box<ExtendedState>> {
self.extended_state.borrow()
}
pub fn current_state(&self) -> StateType {
self.current_state.clone()
}
pub fn events_pending(&self) -> bool {
self.event_queue.len() > 0
}
}
impl<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
FSM<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
where
StateType: Clone + Eq + Ord + Hash + Sized,
EventType: Clone + Eq + Ord + Hash + Sized,
{
pub fn dotfile(
&self,
filename: Option<String>,
state2name: &HashMap<StateType, &'static str>,
event2name: &HashMap<EventType, &'static str>,
omitstates: Option<&HashSet<StateType>>,
omitevents: Option<&HashSet<EventType>>,
) -> Result<(), io::Error> {
let fileattempt = if let Some(fname) = filename {
fs::File::create(fname).map(|f| Some(f))
} else {
Ok(None)
};
fn omitstate<ST: Eq + Hash>(omitstates: &Option<&HashSet<ST>>, n: &ST, ) -> bool {
omitstates
.map_or(false,
|os| os.contains(n))
}
fn omitevent<EV: Eq + Hash>(omitevents: &Option<&HashSet<EV>>, n: &EV, ) -> bool {
omitevents
.map_or(false,
|os| os.contains(n))
}
if let Ok(maybef) = fileattempt {
let sout = io::stdout();
let sv = state2name.keys().cloned().collect::<Vec<_>>();
{
let mut dotgraphwork = self.dotgraph.borrow_mut();
for n in sv.iter() {
let key = DotNodeKey::new(None, n.clone());
let shape = if let Some(ref sn) = dotgraphwork.start_state {
if sn == n {
Some(String::from("diamond"))
} else {
None
}
} else {
None
};
if !omitstate(&omitstates, n) {
dotgraphwork.nodes.insert(
key.clone(),
DotNode {
key: key,
id: Uuid::new_v4(),
shape: shape,
style: dot::Style::None,
label: String::from(*state2name.get(n).unwrap_or(&"?")),
},
);
for t in &[EntryExit::EntryTransition, EntryExit::ExitTransition] {
let eek = EntryExitKey {
state: n.clone(),
entryexit: t.clone(),
};
match self.statetransitions.borrow().get(&eek) {
None => {}
Some(st) => {
let label = match t {
&EntryExit::EntryTransition => "Enter".into(),
&EntryExit::ExitTransition => "Exit".into(),
};
let key = DotNodeKey::new(
Some(t.clone()), n.clone());
dotgraphwork.nodes.insert(
key.clone(),
DotNode {
key: key,
id: Uuid::new_v4(),
shape: Some(String::from("plain")),
style: if st.is_visible() {
dot::Style::Dashed
} else {
dot::Style::Invisible
},
label: label,
},
);
}
}
}
}
}
for (target, pertargetsource) in self
.transitions
.borrow()
.iter()
.sorted_by(|&(_, e1t), &(_, e2t)| e1t.endstate.cmp(&e2t.endstate))
.into_iter()
.group_by(|&(_, to)| to.endstate.clone())
.into_iter()
.filter(|(tgt, _)| !omitstate(&omitstates, tgt))
{
for (source, pertargetsource) in pertargetsource
.into_iter()
.filter(|e1| !omitevent(&omitevents, &e1.0.event))
.sorted_by(|e1, e2|
match e1.0.state.cmp(&e2.0.state) {
Ordering::Equal =>
e1.0.event.cmp(&e2.0.event),
v @ _ => v,
})
.into_iter()
.group_by(|&(from, _)|
from.state.clone())
.into_iter()
.filter(|(src, _)| !omitstate(&omitstates, src)) {
for (color, pertargetsourcecolor) in pertargetsource
.into_iter()
.sorted_by(|&(_, e1t), &(_, e2t)| e1t.color.cmp(&e2t.color))
.into_iter()
.filter(|&(_, to)| to.is_visible())
.group_by(|&(_, to)| to.color)
.into_iter()
{
let key = DotEdgeKey::new_set(color, source.clone(), target.clone());
dotgraphwork.edges.insert(
key.clone(),
DotEdge {
key: key,
style: dot::Style::None,
label: pertargetsourcecolor
.into_iter()
.map(|(source, dest)| {
format!(
"{}|{}|",
dest.name
.as_ref()
.map(|n| if n.len() > 0 {
format!("{}\n", n)
} else {
"".into()
}).unwrap_or("".into()),
event2name
.get(&source.event.clone())
.unwrap_or(&"")
)
}).collect::<Vec<_>>()
.join("\n"),
color: color,
},
);
}
}
}
for (tk, tv) in self
.statetransitions
.borrow()
.iter()
.filter(|&(st, _)| !omitstate(&omitstates, &st.state))
.filter(|&(_, tv)| tv.is_visible())
{
let key: DotEdgeKey<StateType> = DotEdgeKey::new_entryexit(tk.clone());
dotgraphwork.edges.insert(
key.clone(),
DotEdge {
key: key,
style: dot::Style::None,
label: format!("{}", tv.get_name().clone().unwrap_or(String::from(""))),
color: tv.get_color(),
},
);
}
}
let render = move |mut mf, mut sout| {
match &mut mf {
&mut Some(ref mut f) => dot::render(self, f),
_ => dot::render(self, &mut sout), }
};
render(maybef, sout)
} else {
Err(fileattempt.err().unwrap()) }
}
}
#[derive(Hash, Eq, PartialEq, Clone)]
pub struct TransitionSource<StateType, EventType> {
state: StateType,
event: EventType,
}
impl<StateType, EventType> TransitionSource<StateType, EventType> {
pub fn new(state: StateType, event: EventType) -> TransitionSource<StateType, EventType> {
TransitionSource {
state: state,
event: event,
}
}
pub fn state(&self) -> &StateType {
&self.state
}
pub fn event(&self) -> &EventType {
&self.event
}
}
pub trait Annotated
where
Self: std::marker::Sized,
{
fn name(self, _name: &str) -> Self;
fn description(self, _name: &str) -> Self;
fn color(self, _color: DotColor) -> Self;
fn visible(self, _visibility: bool) -> Self;
fn get_name(&self) -> &Option<String>;
fn get_description(&self) -> &Option<String>;
fn get_color(&self) -> DotColor;
fn is_visible(&self) -> bool {
true
}
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EntryExitKey<StateType> {
state: StateType,
entryexit: EntryExit,
}
impl<StateType> EntryExitKey<StateType> {
pub fn state(&self) -> &StateType {
&self.state
}
pub fn entry(&self) -> bool {
self.entryexit == EntryExit::EntryTransition
}
}
pub struct TransitionTarget<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType> {
endstate: StateType,
transfn:
Box<TransitionFn<ExtendedState, EventType, StateType, TransitionFnArguments, ErrorType>>,
name: Option<String>,
description: Option<String>,
visible: bool,
color: DotColor,
}
impl<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
TransitionTarget<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
{
pub fn new(
endstate: StateType,
transfn: Box<
TransitionFn<ExtendedState, EventType, StateType, TransitionFnArguments, ErrorType>,
>,
) -> Self {
TransitionTarget {
endstate: endstate,
transfn: transfn,
name: None,
description: None,
visible: true,
color: DotColor::black,
}
}
pub fn state(&self) -> &StateType {
&self.endstate
}
}
impl<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType> Annotated
for TransitionTarget<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
{
fn name(mut self, name: &str) -> Self {
self.name = Some(name.into());
self
}
fn description(mut self, desc: &str) -> Self {
self.description = Some(desc.into());
self
}
fn color(mut self, color: DotColor) -> Self {
self.color = color;
self
}
fn visible(mut self, vis: bool) -> Self {
self.visible = vis;
self
}
fn get_name(&self) -> &Option<String> {
&self.name
}
fn get_description(&self) -> &Option<String> {
&self.description
}
fn get_color(&self) -> DotColor {
self.color
}
fn is_visible(&self) -> bool {
self.visible
}
}
type TransitionTable<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType> =
HashMap<
TransitionSource<StateType, EventType>,
TransitionTarget<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>,
>;
pub struct EntryExitTransition<
ExtendedState,
StateType,
EventType,
TransitionFnArguments,
ErrorType,
> {
transfn: Box<
EntryExitTransitionFn<
ExtendedState,
EventType,
StateType,
TransitionFnArguments,
ErrorType,
>,
>,
name: Option<String>,
description: Option<String>,
visible: bool,
color: DotColor,
}
impl<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
EntryExitTransition<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
{
pub fn new(
transfn: Box<
EntryExitTransitionFn<
ExtendedState,
EventType,
StateType,
TransitionFnArguments,
ErrorType,
>,
>,
) -> Self {
EntryExitTransition {
transfn: transfn,
name: None,
description: None,
color: DotColor::black,
visible: true,
}
}
}
impl<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType> Annotated
for EntryExitTransition<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
{
fn name(mut self, name: &str) -> Self {
self.name = Some(name.into());
self
}
fn description(mut self, desc: &str) -> Self {
self.description = Some(desc.into());
self
}
fn color(mut self, color: DotColor) -> Self {
self.color = color;
self
}
fn visible(mut self, vis: bool) -> Self {
self.visible = vis;
self
}
fn get_name(&self) -> &Option<String> {
&self.name
}
fn get_description(&self) -> &Option<String> {
&self.description
}
fn get_color(&self) -> DotColor {
self.color
}
fn is_visible(&self) -> bool {
self.visible
}
}
type EntryExitTransitionTable<
ExtendedState,
StateType,
EventType,
TransitionFnArguments,
ErrorType,
> = HashMap<
EntryExitKey<StateType>,
EntryExitTransition<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>,
>;
impl<ExtendedState, EventType, StateType, TransitionFnArguments, ErrorType>
RunsFSM<EventType, StateType, TransitionFnArguments, ErrorType>
for FSM<ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType>
where
StateType: Clone + PartialEq + Eq + Hash + Debug + Sized,
EventType: Clone + PartialEq + Eq + Hash + Debug + Sized + Debug,
ErrorType: Debug,
{
fn add_events(
&mut self,
events: &mut Vec<(EventType, OptionalFnArg<TransitionFnArguments>)>,
) -> Result<u32, Errors<EventType, StateType, ErrorType>> {
let el = events.len();
if let Some(ref l) = self.log {
debug!(
l,
"FSM {} adding {} events: {:?}",
self.name,
el,
events.iter().map(|e| e.0.clone()).collect::<Vec<_>>()
);
}
self.event_queue.extend(events.drain(..));
Ok(el as u32)
}
fn extend_events<I>(&mut self, events: I)
where
I: IntoIterator<Item=(EventType, std::option::Option<TransitionFnArguments>)>,
{
if let Some(ref l) = self.log {
debug!(l, "FSM {} adding events from iterator", self.name);
}
self.event_queue.extend(events)
}
fn process_event_queue(&mut self) -> Result<u32, Errors<EventType, StateType, ErrorType>> {
let mut evs = VecDeque::new();
swap(&mut evs, &mut self.event_queue);
let nrev = evs.len() as u32;
let mut lr: Vec<Errors<EventType, StateType, ErrorType>> = evs
.drain(..)
.map(|e| {
let state = self.current_state.clone();
let event = e.0.clone();
let entryexittransitions = self.statetransitions.borrow();
let transitions = self.transitions.borrow();
let transition =
transitions.get(&TransitionSource::new(state.clone(), event.clone()));
let ref mut q = self.event_queue;
let name = &self.name;
if let Some(ref l) = self.log {
debug!(l, "FSM {} processing event {:?}/{:?}", name, event, state);
}
fn entryexit<
ExtendedState,
EventType,
StateType,
TransitionFnArguments,
ErrorType,
>(
log: Option<&Logger>,
extended_state: RefMut<Box<ExtendedState>>,
name: &str,
on_state: StateType,
direction: EntryExit,
event_queue: &mut EventQueue<EventType, TransitionFnArguments>,
transitions: &Ref<
EntryExitTransitionTable<
ExtendedState,
StateType,
EventType,
TransitionFnArguments,
ErrorType,
>,
>,
last_state: Option<StateType>,
last_event: Option<EventType>,
) -> Errors<EventType, StateType, ErrorType>
where
StateType: Clone + PartialEq + Eq + Hash + Debug,
EventType: Clone + PartialEq + Eq + Hash + Debug,
ErrorType: Debug,
{
match transitions.get(&EntryExitKey {
state: on_state.clone(),
entryexit: direction,
}) {
None => Errors::OK,
Some(ref entryexittrans) => {
let ref func = entryexittrans.transfn;
let ref tname = entryexittrans.get_name();
if let Some(ref l) = log {
debug!(
l,
"FSM {} exit/entry state transition for {:?} {:?}",
name,
on_state,
tname
);
}
match func(extended_state, last_state, last_event) {
Err(v) => v,
Ok(v) => match v {
Some(mut eventset) => {
event_queue.extend(eventset.drain(..));
Errors::OK
}
None => Errors::OK,
},
}
}
}
}
match transition {
Some(itrans) => {
let endstate = itrans.endstate.clone();
let transfn = &itrans.transfn;
let mut res = Errors::OK;
let lls = self.last_state.clone();
res = if state == endstate.clone() {
res
} else {
let extstate = self.extended_state.borrow_mut();
entryexit(
self.log.as_ref(),
extstate,
name,
state.clone(),
EntryExit::ExitTransition,
q,
&entryexittransitions,
lls.clone(),
Some(event.clone()),
)
};
res = match res {
Errors::OK => {
let extstate = self.extended_state.borrow_mut();
match transfn(extstate, e.0, e.1) {
Err(v) => v,
Ok(v) => {
match v {
None => {}
Some(mut eventset) => {
q.extend(eventset.drain(..));
}
}
if let Some(ref l) = self.log {
debug!(
l,
"FSM {} moving machine to {:?}", name, endstate
);
}
self.last_state = Some(self.current_state.clone());
self.current_state = endstate.clone();
Errors::OK
}
}
}
r => r,
};
match res {
Errors::OK => {
if state == endstate.clone() {
res
} else {
let extstate = self.extended_state.borrow_mut();
entryexit(
self.log.as_ref(),
extstate,
name,
endstate.clone(),
EntryExit::EntryTransition,
q,
&entryexittransitions,
lls,
Some(event.clone()),
)
}
}
r => r,
}
}
None => Errors::NoTransition(event, state),
}
}).filter(|e| match *e {
Errors::OK => false,
_ => true,
}).take(1)
.collect::<Vec<_>>();
match lr.pop() {
Some(x) => {
if let Some(ref l) = self.log {
debug!(
l,
"FSM {} filter on transition failures yields {:?}", self.name, &x
);
}
Err(x)
}
_ => Ok(nrev),
}
}
}
#[cfg(test)]
mod tests {
extern crate slog;
extern crate slog_async;
extern crate slog_atomic;
extern crate slog_term;
use std::cell::RefMut;
use std::collections::HashMap;
use self::slog_atomic::*;
use slog::*;
use std;
use std::borrow::Borrow;
use super::{
zipenumvariants, Annotated, DotColor, EntryExit, EntryExitTransition, Errors, RunsFSM,
TransitionSource, TransitionTarget, FSM,
};
fn build_logger(level: Level) -> Logger {
let decorator = slog_term::PlainDecorator::new(std::io::stdout());
let drain = slog_term::CompactFormat::new(decorator).build().fuse();
let drain = slog_async::Async::new(drain).build().fuse();
let drain = AtomicSwitch::new(drain);
Logger::root(
LevelFilter::new(drain, level).fuse(),
o!("version" => env!("CARGO_PKG_VERSION"),),
)
}
#[derive(Debug, Clone)]
enum StillCoinType {
Good,
Bad,
}
#[derive(Debug, Clone)]
enum StillArguments {
Coin(StillCoinType),
}
custom_derive! {
#[derive(IterVariants(StillStateVariants), IterVariantNames(StillStateNames),
Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
enum StillStates {
ClosedWaitForMoney,
CheckingMoney,
OpenWaitForTimeOut,
}
}
custom_derive! {
#[derive(IterVariants(StillEventVariants), IterVariantNames(StillEventNames),
Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
enum StillEvents {
GotCoin,
AcceptMoney,
RejectMoney,
Timeout,
}
}
#[derive(Debug)]
enum StillErrors {
CoinArgumentMissing,
}
struct StillExtState {
coincounter: u32,
opened: u32,
closed: u32,
exitedon: Vec<(Option<StillStates>, Option<StillEvents>)>,
enteredon: Vec<(Option<StillStates>, Option<StillEvents>)>,
}
type CoinStillFSM = FSM<StillExtState, StillStates, StillEvents, StillArguments, StillErrors>;
fn coin_fsm_extstate() -> Box<StillExtState> {
Box::new(StillExtState {
coincounter: 0,
opened: 0,
closed: 0,
exitedon: vec![],
enteredon: vec![],
})
}
fn build_coin_fsm() -> CoinStillFSM {
let mainlog = build_logger(Level::Info);
let mut still_fsm =
FSM::<StillExtState, StillStates, StillEvents, StillArguments, StillErrors>::new(
StillStates::ClosedWaitForMoney,
coin_fsm_extstate(),
"coin_still",
Some(mainlog),
);
let check_money = move |_extstate: RefMut<Box<StillExtState>>,
_ev: StillEvents,
arg: Option<StillArguments>| {
match arg {
None => Err(Errors::InternalError(
StillEvents::GotCoin,
StillStates::ClosedWaitForMoney,
StillErrors::CoinArgumentMissing,
)),
Some(arg) => match arg {
StillArguments::Coin(t) => match t {
StillCoinType::Good => Ok(Some(vec![(StillEvents::AcceptMoney, None)])),
StillCoinType::Bad => Ok(Some(vec![(StillEvents::RejectMoney, None)])),
},
},
}
};
assert!(
still_fsm.add_transition(
TransitionSource::new(StillStates::ClosedWaitForMoney, StillEvents::GotCoin),
TransitionTarget::new(StillStates::CheckingMoney, Box::new(check_money))
.name("ProcessCoin")
.color(DotColor::green),
)
);
assert!(
still_fsm.add_transition(
TransitionSource::new(StillStates::CheckingMoney, StillEvents::RejectMoney),
TransitionTarget::new(
StillStates::ClosedWaitForMoney,
Box::new(|_, _, _| Ok(None))
).name("Rejected")
.color(DotColor::red),
)
);
assert!(
still_fsm.add_transition(
TransitionSource::new(StillStates::CheckingMoney, StillEvents::GotCoin),
TransitionTarget::new(StillStates::CheckingMoney, Box::new(|_, _, _| Ok(None)))
.name("IgnoreAnotherCoin")
.color(DotColor::red)
)
);
assert!(
still_fsm.add_transition(
TransitionSource::new(StillStates::CheckingMoney, StillEvents::AcceptMoney),
TransitionTarget::new(
StillStates::OpenWaitForTimeOut,
Box::new(|mut estate: RefMut<Box<StillExtState>>, _, _| {
estate.coincounter += 1;
Ok(None)
})
).name("Accepted")
.color(DotColor::green)
)
);
assert!(
still_fsm.add_transition(
TransitionSource::new(StillStates::OpenWaitForTimeOut, StillEvents::GotCoin),
TransitionTarget::new(
StillStates::OpenWaitForTimeOut,
Box::new(|_, _, _| Ok(Some(vec![(StillEvents::RejectMoney, None)]))),
).name("Reject")
.color(DotColor::red),
)
);
assert!(
still_fsm.add_transition(
TransitionSource::new(StillStates::OpenWaitForTimeOut, StillEvents::RejectMoney),
TransitionTarget::new(
StillStates::OpenWaitForTimeOut,
Box::new(|_, _, _| Ok(None))
).name("Rejected")
.color(DotColor::red)
)
);
assert!(
still_fsm.add_transition(
TransitionSource::new(StillStates::OpenWaitForTimeOut, StillEvents::Timeout),
TransitionTarget::new(
StillStates::ClosedWaitForMoney,
Box::new(|_, _, _| Ok(None))
).name("TimeOut")
.color(DotColor::blue)
)
);
assert!(
still_fsm.add_enter_transition(
(StillStates::OpenWaitForTimeOut, EntryExit::EntryTransition),
EntryExitTransition::new(Box::new(
|mut estate: RefMut<Box<StillExtState>>, laststate, lastevent| {
estate.opened += 1;
estate.enteredon.push((laststate, lastevent));
Ok(None)
}
)).name("CountOpens")
.color(DotColor::gold)
)
);
assert!(
still_fsm.add_enter_transition(
(StillStates::OpenWaitForTimeOut, EntryExit::ExitTransition),
EntryExitTransition::new(Box::new(
|mut estate: RefMut<Box<StillExtState>>, laststate, lastevent| {
estate.closed += 1;
estate.exitedon.push((laststate, lastevent));
Ok(None)
}
)).name("CountClose")
.color(DotColor::gold)
)
);
still_fsm
}
#[test]
fn coin_machine_test() {
let mut still_fsm = build_coin_fsm();
let mut onevec = vec![(StillEvents::Timeout, None)];
still_fsm.extend_events(onevec.drain(..));
match still_fsm.process_event_queue() {
Ok(v) => panic!("failed with {:?} # processed tokens as Ok(_)", v),
Err(v) => match v {
Errors::NoTransition(StillEvents::Timeout, StillStates::ClosedWaitForMoney) => (),
_ => panic!("failed with wrong FSM error"),
},
}
let goodcoin = StillArguments::Coin(StillCoinType::Good);
let badcoin = StillArguments::Coin(StillCoinType::Bad);
let mut still_fsm = build_coin_fsm();
assert_eq!(
still_fsm
.add_events(&mut vec![
(StillEvents::GotCoin, Some(goodcoin.clone())),
(StillEvents::GotCoin, Some(badcoin.clone())),
(StillEvents::GotCoin, Some(goodcoin.clone())),
(StillEvents::GotCoin, Some(goodcoin.clone())),
]).unwrap(),
4
);
while still_fsm.events_pending() {
assert!(!still_fsm.process_event_queue().is_err());
}
assert!(still_fsm.current_state() == StillStates::OpenWaitForTimeOut);
assert_eq!(
still_fsm
.add_events(&mut vec![(StillEvents::Timeout, None)])
.unwrap(),
1
);
while still_fsm.events_pending() {
assert!(!still_fsm.process_event_queue().is_err());
}
assert!(still_fsm.current_state() == StillStates::ClosedWaitForMoney);
let es = still_fsm.extended_state();
assert!(es.borrow().coincounter == 1);
assert!(es.borrow().opened == 1);
assert!(es.borrow().closed == 1);
assert_eq!(
es.borrow().exitedon,
vec![(Some(StillStates::CheckingMoney), Some(StillEvents::Timeout))]
);
assert_eq!(
es.borrow().enteredon,
vec![(
Some(StillStates::CheckingMoney),
Some(StillEvents::AcceptMoney)
)]
);
}
#[test]
fn coin_machine_dot() {
let still_fsm = build_coin_fsm();
for fname in vec![None, Some("target/tmp.dot".into())] {
still_fsm
.dotfile(
fname,
&zipenumvariants(
Box::new(StillStates::iter_variants()),
Box::new(StillStates::iter_variant_names()),
),
&zipenumvariants(
Box::new(StillEvents::iter_variants()),
Box::new(StillEvents::iter_variant_names()),
),
None,
None,
).expect("cannot dotfile");
}
}
#[derive(Debug, Clone)]
enum DotTestArguments {}
custom_derive! {
#[derive(IterVariants(StateVariants), IterVariantNames(StateNames),
Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
enum DotTestStates {
Init,
One,
Two,
Three,
}
}
custom_derive! {
#[derive(IterVariants(EventVariants), IterVariantNames(EventNames),
Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
enum DotTestEvents {
Event1,
Event2,
Event3,
Event4,
RedEvent1,
RedEvent2,
RedEvent3,
CyanEvent1,
CyanEvent2,
CyanEvent3,
InvisibleEvent,
}
}
#[derive(Debug)]
enum DotTestErrors {}
struct DotTestExtState {}
type DotTestFSM =
FSM<DotTestExtState, DotTestStates, DotTestEvents, DotTestArguments, DotTestErrors>;
fn build_dottest_fsm() -> DotTestFSM {
let mainlog = build_logger(Level::Debug);
let mut dottest_fsm = FSM::<
DotTestExtState,
DotTestStates,
DotTestEvents,
DotTestArguments,
DotTestErrors,
>::new(
DotTestStates::Init,
Box::new(DotTestExtState {}),
"DotTest",
Some(mainlog),
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::Init, DotTestEvents::Event1),
TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None)))
.name("Init2One-GRAY")
.color(DotColor::gray),
)
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::One, DotTestEvents::Event1),
TransitionTarget::new(DotTestStates::Two, Box::new(|_, _, _| Ok(None)))
.name("One2Two-GREEN")
.color(DotColor::green)
)
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::Two, DotTestEvents::Event1),
TransitionTarget::new(DotTestStates::Three, Box::new(|_, _, _| Ok(None)))
.name("Two2Three-BLUE")
.color(DotColor::blue)
)
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::Three, DotTestEvents::Event1),
TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None)))
.name("Three2One-1-RED")
.color(DotColor::red)
)
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::Three, DotTestEvents::Event2),
TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None)))
.name("Three2One-2-RED")
.color(DotColor::red)
)
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::Three, DotTestEvents::Event3),
TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None)))
.name("Three2One-3-BLUE")
.color(DotColor::blue)
)
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::Three, DotTestEvents::Event4),
TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None)))
.name("Three2One-4-BLUE")
.color(DotColor::blue)
)
);
assert!(
dottest_fsm.add_transition(
TransitionSource::new(DotTestStates::Three, DotTestEvents::InvisibleEvent),
TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None)))
.name("Three2One-INVISIBLE-BLUE")
.color(DotColor::blue)
.visible(false)
)
);
assert!(
dottest_fsm.add_enter_transition(
(DotTestStates::Three, EntryExit::EntryTransition),
EntryExitTransition::new(Box::new(|_, _, _| Ok(None)))
.name("Three3-INVISIBLE-ENTRY")
.color(DotColor::gold)
.visible(false)
)
);
for e in &[
DotTestEvents::RedEvent1,
DotTestEvents::RedEvent2,
DotTestEvents::RedEvent3,
] {
for s in &[DotTestStates::One, DotTestStates::Two, DotTestStates::Three] {
assert!(
dottest_fsm.add_transition(
TransitionSource::new(*s, *e),
TransitionTarget::new(*s, Box::new(|_, _, _| Ok(None)))
.name(&format!("Self2Self-{}", DOTTESTEVENTS.get(e).unwrap()))
.color(DotColor::red)
.description("simple description"),
)
);
}
}
for e in &[
DotTestEvents::CyanEvent1,
DotTestEvents::CyanEvent2,
DotTestEvents::CyanEvent3,
] {
for s in &[DotTestStates::One, DotTestStates::Two, DotTestStates::Three] {
assert!(
dottest_fsm.add_transition(
TransitionSource::new(*s, *e),
TransitionTarget::new(*s, Box::new(|_, _, _| Ok(None)))
.name("Self2Self-RED")
.color(DotColor::cyan)
)
);
}
}
dottest_fsm
}
lazy_static! {
static ref DOTTESTEVENTS: HashMap<DotTestEvents, &'static str> = zipenumvariants(
Box::new(DotTestEvents::iter_variants()),
Box::new(DotTestEvents::iter_variant_names())
);
}
#[test]
fn dottest_fsm_dot() {
let dottest_fsm = build_dottest_fsm();
for fname in vec![None, Some("target/dottest.dot".into())] {
dottest_fsm
.dotfile(
fname,
&zipenumvariants(
Box::new(DotTestStates::iter_variants()),
Box::new(DotTestStates::iter_variant_names()),
),
&DOTTESTEVENTS,
None, None,
).expect("cannot dotfile");
}
}
#[test]
fn flyweight() {
let mut c1 = build_coin_fsm();
let mut c2 = c1.flyweight(
coin_fsm_extstate(),
"coin_still flyweight",
Some(build_logger(Level::Info)),
);
let goodcoin = StillArguments::Coin(StillCoinType::Good);
assert_eq!(
c1.add_events(&mut vec![(StillEvents::GotCoin, Some(goodcoin.clone())), ])
.unwrap(),
1
);
while c1.events_pending() {
assert!(!c1.process_event_queue().is_err());
}
assert!(c1.current_state() == StillStates::OpenWaitForTimeOut);
assert!(c2.current_state() == StillStates::ClosedWaitForMoney);
assert_eq!(
c1.add_events(&mut vec![(StillEvents::Timeout, None)])
.unwrap(),
1
);
while c1.events_pending() {
assert!(!c1.process_event_queue().is_err());
}
assert!(c1.current_state() == StillStates::ClosedWaitForMoney);
let es = c1.extended_state();
assert!(es.borrow().coincounter == 1);
assert!(es.borrow().opened == 1);
assert!(es.borrow().closed == 1);
assert_eq!(
c2.add_events(&mut vec![(StillEvents::GotCoin, Some(goodcoin.clone())), ])
.unwrap(),
1
);
while c2.events_pending() {
assert!(!c2.process_event_queue().is_err());
}
let es = c2.extended_state();
assert!(es.borrow().closed == 0);
assert!(es.borrow().coincounter == 1);
}
}