Skip to content

Commit

Permalink
simplified transitions, added a few tests
Browse files Browse the repository at this point in the history
  • Loading branch information
tinybike committed Aug 22, 2014
1 parent 8df2335 commit 53b6678
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 140 deletions.
17 changes: 12 additions & 5 deletions src/FiniteStateMachine.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ module FiniteStateMachine
export StateMachine, state_machine

type StateMachine
initial::Dict{String,String}
events::Array{Dict{String,String},1}
map::Dict
callbacks::Dict{String,Function}
events::Dict
current::String
function StateMachine(events::Dict)
current = events["current"]
new(events, current)
end
end

include("constants.jl")
const RESULT = (ASCIIString => Int8)[
"SUCCEEDED" => 1,
"NOTRANSITION" => 2,
"CANCELLED" => 3,
"PENDING" => 4,
]

include("state_machine.jl")

Expand Down
14 changes: 0 additions & 14 deletions src/constants.jl

This file was deleted.

126 changes: 50 additions & 76 deletions src/state_machine.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
function state_machine(cfg::Dict)
function add(event::Dict)
if haskey(event, "from")
if isa(event["from"], Array)
from = event["from"]
else
from = String[event["from"]]
end
end
if !haskey(_map, event["name"])
_map[event["name"]] = Dict()
end
for f in from
if haskey(event, "to")
_map[event["name"]][f] = event["to"]
else
# No-op transition
_map[event["name"]][f] = f
end
end
end
fsm = Dict()
_map = Dict()
# Initial state
if haskey(cfg, "initial")
if typeof(cfg["initial"]) <: String
if isa(cfg["initial"], String)
initial = (String => String)["state" => cfg["initial"]]
else
initial = cfg["initial"]
Expand Down Expand Up @@ -39,28 +59,22 @@ function state_machine(cfg::Dict)
"name" => initial["event"],
"from" => "none",
"to" => initial["state"],
], _map)
])
end
for event in events
add(event, _map)
add(event)
end
for (name, target) in _map
fsm[name] = build_event(fsm, name, target)
for (name, minimap) in _map
fsm[name] = build_event(fsm, name, minimap)
end
for (name, cb) in callbacks
fsm[name] = cb
end
fsm["current"] = "none"
fsm["is"] = state -> isa(state, Array) ?
findfirst(state, fsm["current"] > 0) : (fsm["current"] == state)
fsm["can"] = event -> !fsm["transition"] &&
(haskey(_map[event], fsm["current"]) || haskey(_map[event], "*"))
fsm["can"] = event -> !haskey(fsm, "transition") && haskey(_map[event], fsm["current"])
fsm["cannot"] = event -> !fsm["can"](event)
if haskey(cfg, "error")
fsm["error"] = cfg["error"]
else
fsm["error"] = (name, from, to, args, error, msg, exc) -> error(exc)
end
fsm["isfinished"] = () -> fsm["is"](terminal)
if initial != nothing
if haskey(initial, "defer")
Expand All @@ -69,84 +83,44 @@ function state_machine(cfg::Dict)
end
end
end
StateMachine(initial, events, _map, callbacks)
end

function add(event::Dict, _map::Dict)
if haskey(event, "from")
if typeof(event["from"]) <: Array
from = event["from"]
else
from = String[event["from"]]
end
else
# Wildcard transition
from = "*"
end
if !haskey(_map, event["name"])
_map[event["name"]] = Dict()
end
for f in from
if haskey(event, "to")
_map[event["name"]][f] = event["to"]
else
# No-op transition
_map[event["name"]][f] = f
end
end
StateMachine(fsm)
end

function build_event(fsm::Dict, name::String, _map::Dict)
function ()
from = fsm["current"]
if haskey(_map, "from")
to = _map["from"]
elseif haskey(_map, "*")
to = _map["*"]
if haskey(_map, from)
to = _map[from]
else
to = from
end
if fsm["transition"]
return error(name * " inappropriate because previous transition did not complete")
end
if fsm["cannot"](name)
return error(name * " inappropriate in current state " * fsm["current"])
end
if before_event(fsm, name, from, to)
return RESULT["CANCELLED"]
end
if from == to
after_event(fsm, name, from, to)
return RESULT["NOTRANSITION"]
end
fsm["transition"] = function ()
if haskey(fsm, "transition")
delete!(fsm, "transition")
end
fsm["current"] = to
enter_state(fsm, name, from, to)
change_state(fsm, name, from, to)
after_event(fsm, name, from, to)
RESULT["SUCCEEDED"]
end
# Cancel asynchronous transition on request
fsm["cancel_transition"] = function ()
if haskey(fsm, "transition")
delete!(fsm, "transition")
end
after_event(fsm, name, from, to)
end
leave = leave_state(fsm, name, from, to)
if leave == false
if haskey(fsm, "transition")
delete!(fsm, "transition")
end
RESULT["CANCELLED"]
elseif leave == ASYNC
RESULT["PENDING"]
RESULT["NOTRANSITION"]
else
if fsm["transition"]
fsm["transition"]()
fsm["transition"] = function ()
if haskey(fsm, "transition")
delete!(fsm, "transition")
end
fsm["current"] = to
enter_state(fsm, name, from, to)
change_state(fsm, name, from, to)
after_event(fsm, name, from, to)
RESULT["SUCCEEDED"]
end
leave = leave_state(fsm, name, from, to)
if leave == false
if haskey(fsm, "transition")
delete!(fsm, "transition")
end
RESULT["CANCELLED"]
else
if haskey(fsm, "transition")
fsm["transition"]()
end
end
end
end
Expand Down
101 changes: 66 additions & 35 deletions src/transitions.jl
Original file line number Diff line number Diff line change
@@ -1,64 +1,95 @@
function callback(fsm::Dict, func::Function,
state::String, from::String, to::String)
if isdefined(func)
try
func(fsm, state, from, to)
catch
error("Invalid callback")
end
end
end

# Before event
function before_event(fsm::Dict, state::String, from::String, to::String)
specific = before_this_event(fsm, state, from, to)
general = before_any_event(fsm, state, from, to)
(~specific || ~general) ? false : ASYNC
before_this_event(fsm, state, from, to)
before_any_event(fsm, state, from, to)
end

before_this_event(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onbefore"] + state, state, from, to)
function before_this_event(fsm::Dict, state::String, from::String, to::String)
fname = "onbefore" * state
if haskey(fsm, fname)
fsm[fname](fsm, state, from, to)
end
end

before_any_event(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onbeforeevent"], state, from, to)
function before_any_event(fsm::Dict, state::String, from::String, to::String)
fname = "onbeforeevent"
if haskey(fsm, fname)
fsm[fname](fsm, state, from, to)
end
end

# After event
function after_event(fsm::Dict, state::String, from::String, to::String)
after_this_event(fsm, state, from, to)
after_any_event(fsm, state, from, to)
end

after_this_event(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onafter"] + state, state, from, to)
function after_this_event(fsm::Dict, state::String, from::String, to::String)
fname = "onafter" * state
if haskey(fsm, fname)
fsm[fname](fsm, state, from, to)
end
end

after_any_event(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onafterevent"] || fsm["onevent"], state, from, to)
function after_any_event(fsm::Dict, state::String, from::String, to::String)
if haskey(fsm, "onafterevent")
fname = "onafterevent"
elseif haskey(fsm, "onevent")
fname = "onevent"
else
return
end
fsm[fname](fsm, state, from, to)
end

# Change of state
change_state(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onchangestate"], state, from, to)
function change_state(fsm::Dict, state::String, from::String, to::String)
fname = "onchangestate"
if haskey(fsm, fname)
fsm[fname](fsm, state, from, to)
end
end

# Entering a state
function enter_state(fsm::Dict, state::String, from::String, to::String)
enter_this_state(fsm, state, from, to)
enter_any_state(fsm, state, from, to)
end

enter_this_state(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onenter"] + to, state, from, to)
function enter_this_state(fsm::Dict, state::String, from::String, to::String)
fname = "onenter" * to
if haskey(fsm, fname)
fsm[fname](fsm, state, from, to)
end
end

enter_any_state(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onenterstate"] || fsm["onstate"], state, from, to)
function enter_any_state(fsm::Dict, state::String, from::String, to::String)
if haskey(fsm, "onenterstate")
fname = "onenterstate"
elseif haskey(fsm, "onstate")
fname = "onstate"
else
return
end
fsm[fname](fsm, state, from, to)
end

# Leaving a state
function leave_state(fsm::Dict, state::String, from::String, to::String)
specific = leave_this_state(fsm, state, from, to)
general = leave_any_state(fsm, state, from, to)
(~specific || ~general) ? false : ASYNC
leave_this_state(fsm, state, from, to)
leave_any_state(fsm, state, from, to)
end

leave_this_state(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onleave"] + from, state, from, to)
function leave_this_state(fsm::Dict, state::String, from::String, to::String)
fname = "onleave" * from
if haskey(fsm, fname)
fsm[fname](fsm, state, from, to)
end
end

leave_any_state(fsm::Dict, state::String, from::String, to::String) =
callback(fsm, fsm["onleavestate"], state, from, to)
function leave_any_state(fsm::Dict, state::String, from::String, to::String)
fname = "onleavestate"
if haskey(fsm, fname)
fsm[fname](fsm, state, from, to)
end
end
24 changes: 14 additions & 10 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ cfg = {
],
],
"callbacks" => (String => Function)[
"onfirst" => () -> println("boom!"),
"onbeforeevent" => () -> println("boom!"),
],
}

fsm = state_machine(cfg)

@test fsm.initial["event"] == "startup"
@test fsm.initial["state"] == "first"
@test fsm.map["startup"] == {"none"=>"first"}
@test fsm.map["skip"] == {"second"=>"third"}
@test fsm.map["jump"] == {"third"=>"fourth"}
@test fsm.map["hop"] == {"first"=>"second"}
@test fsm.events[1] == ["name"=>"hop","to"=>"second","from"=>"first"]
@test fsm.events[2] == ["name"=>"skip","to"=>"third","from"=>"second"]
@test fsm.events[3] == ["name"=>"jump","to"=>"fourth","from"=>"third"]
@test fsm.current == "none"

fsm.events["startup"]()
@test fsm.events["current"] == "first"

fsm.events["hop"]()
@test fsm.events["current"] == "second"

fsm.events["skip"]()
@test fsm.events["current"] == "third"

fsm.events["jump"]()
@test fsm.events["current"] == "fourth"

0 comments on commit 53b6678

Please sign in to comment.