Skip to content

Commit

Permalink
prettier syntax, improved coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
tinybike committed Aug 26, 2014
1 parent 035aa76 commit fdd3cc0
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 45 deletions.
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,27 @@ A simple finite state machine library for Julia, based on Jake Gordon's [javascr

julia> using FiniteStateMachine

julia> fsm = state_machine((String => Any)[
julia> fsm = state_machine({
"initial" => "first",
"final" => "fourth",
"events" => Dict{String,String}[
(String => String)[
"events" => [{
"name" => "hop",
"from" => "first",
"to" => "second",
],
(String => String)[
}, {
"name" => "skip",
"from" => "second",
"to" => "third",
],
(String => String)[
}, {
"name" => "jump",
"from" => "third",
"to" => "fourth",
],
},
],
"callbacks" => (String => Function)[
"callbacks" => {
"onbeforeevent" => (fsm::StateMachine, args...) -> 1+1,
],
])
},
})

Events are called using the `fire` function:

Expand Down Expand Up @@ -66,6 +63,14 @@ Unless other specified, a special "startup" event fires automatically when the s

The "initial" field can be either a string, or a dict specifying: `state` (the name of the state in which to start), `event` (the startup event), and/or `defer` (set to `true` if the startup event should *not* be fired automatically when the state machine is created).

Supports multiple source states, for example, the following code allows "hop" to take place from either the "first" or "third" states:

"events" => [{
"name" => "hop",
"from" => ["first", "third"],
"to" => "second",
},

### Tests

Unit tests are located in `test/`. To run the tests:
Expand Down
8 changes: 3 additions & 5 deletions src/FiniteStateMachine.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ module FiniteStateMachine
export StateMachine, state_machine, fire, callback, before_event, before_this_event, before_any_event, after_event, after_this_event, after_any_event, change_state, enter_state, enter_this_state, enter_any_state, leave_state, leave_this_state, leave_any_state

type StateMachine
map::Dict
actions::Dict
current::String
terminal::Union(String, Nothing)
map::Dict{String,Dict{String,String}}
StateMachine() = new("none", nothing, (String => Dict{String,String})[])
StateMachine() = new(Dict(), Dict(), "none", nothing)
end

# All events and callbacks are stored here
actions = (String => Function)[]

include("state_machine.jl")

include("fire.jl")
Expand Down
4 changes: 2 additions & 2 deletions src/fire.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ function fire(fsm::StateMachine, name::String, args...)
fsm.current == fsm.terminal

# User-defined events and callbacks
elseif haskey(actions, name)
actions[name]()
elseif haskey(fsm.actions, name)
fsm.actions[name]()

# Throw an error if there weren't any matching names
else
Expand Down
18 changes: 10 additions & 8 deletions src/state_machine.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# Create a finite state machine, using model supplied by the user.
function state_machine(model::Dict{String,Any})
function state_machine(model::Dict)

fsm = StateMachine()

# Write events and their associated states to the state machine's map
function add(event::Dict)

# Multiple source states are allowed
if haskey(event, "from")
if isa(event["from"], Array)
from = event["from"]
else
from = String[event["from"]]
from = [event["from"]]
end
end
if !haskey(fsm.map, event["name"])
Expand All @@ -33,10 +35,10 @@ function state_machine(model::Dict{String,Any})
# Initial state can be specified as a string or dict.
# If the initial event has not been specified, default to "startup".
if isa(model["initial"], String)
initial = (String => String)[
initial = {
"state" => model["initial"],
"event" => "startup",
]
}
else
initial = model["initial"]
if !haskey(initial, "event")
Expand All @@ -45,11 +47,11 @@ function state_machine(model::Dict{String,Any})
end

# Add the startup event to the map
add((String => String)[
add({
"name" => initial["event"],
"from" => "none",
"to" => initial["state"],
])
})
end

# Terminal (final) state
Expand All @@ -68,7 +70,7 @@ function state_machine(model::Dict{String,Any})

# Set up the callable events (functions), which can be invoked by fire()
for (name, minimap) in fsm.map
actions[name] = () -> begin
fsm.actions[name] = () -> begin
from = fsm.current
to = haskey(minimap, from) ? minimap[from] : from
if !fire(fsm, "can", name)
Expand All @@ -88,7 +90,7 @@ function state_machine(model::Dict{String,Any})
# Set up user-specified callbacks, if any were provided
if haskey(model, "callbacks")
for (name, callback) in model["callbacks"]
actions[name] = callback
fsm.actions[name] = callback
end
end

Expand Down
12 changes: 6 additions & 6 deletions src/transitions.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Transition callback
function callback(fsm::StateMachine,
action::String, name::String, from::String, to::String)
if haskey(actions, action)
actions[action](fsm, name, from, to)
if haskey(fsm.actions, action)
fsm.actions[action](fsm, name, from, to)
end
end

Expand All @@ -28,9 +28,9 @@ after_this_event(fsm::StateMachine, name::String, from::String, to::String) =
callback(fsm, "onafter" * name, name, from, to)

function after_any_event(fsm::StateMachine, name::String, from::String, to::String)
if haskey(actions, "onafterevent")
if haskey(fsm.actions, "onafterevent")
action = "onafterevent"
elseif haskey(actions, "onevent")
elseif haskey(fsm.actions, "onevent")
action = "onevent"
else
return
Expand All @@ -52,9 +52,9 @@ enter_this_state(fsm::StateMachine, name::String, from::String, to::String) =
callback(fsm, "onenter" * to, name, from, to)

function enter_any_state(fsm::StateMachine, name::String, from::String, to::String)
if haskey(actions, "onenterstate")
if haskey(fsm.actions, "onenterstate")
action = "onenterstate"
elseif haskey(actions, "onstate")
elseif haskey(fsm.actions, "onstate")
action = "onstate"
else
return
Expand Down
41 changes: 41 additions & 0 deletions test/test_fire.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
@test fire(dfsm, "can", "dance") == true
@test fire(dfsm, "can", "hop") == false
@test_throws ErrorException fire(dfsm, "skip")
@test_throws ErrorException fire(dfsm, "startup")

fire(dfsm, "dance")

@test dfsm.current == "first"
@test fire(dfsm, "can", "dance") == false
@test fire(dfsm, "can", "hop") == true
@test_throws ErrorException fire(dfsm, "skip")
@test_throws ErrorException fire(dfsm, "dance")

fire(dfsm, "hop")

@test dfsm.current == "second"

fire(dfsm, "skip")

@test dfsm.current == "third"

fire(dfsm, "sleep")

@test dfsm.current == "third"

@test fire(fsm, "is", "first") == true
@test fire(fsm, "can", "hop") == true
@test fire(fsm, "can", "jump") == false
Expand Down Expand Up @@ -44,3 +69,19 @@ fire(fsm, "jump")
@test_throws ErrorException fire(fsm, "hop")
@test_throws ErrorException fire(fsm, "startup")
@test_throws ErrorException fire(fsm, "humblebrag")

fire(mfsm, "hop")

@test mfsm.current == "second"

fire(mfsm, "skip")

@test mfsm.current == "third"

fire(mfsm, "sleep")

@test mfsm.current == "third"

fire(mfsm, "hop")

@test mfsm.current == "second"
78 changes: 65 additions & 13 deletions test/test_state_machine.jl
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
fsm = state_machine((String => Any)[
fsm = state_machine({
"initial" => "first",
"final" => "fourth",
"events" => Dict{String,String}[
(String => String)[
"events" => [{
"name" => "hop",
"from" => "first",
"to" => "second",
],
(String => String)[
}, {
"name" => "skip",
"from" => "second",
"to" => "third",
],
(String => String)[
}, {
"name" => "jump",
"from" => "third",
"to" => "fourth",
],
},
],
"callbacks" => (String => Function)[
"callbacks" => {
"onbeforeevent" => (fsm::StateMachine, args...) -> 1,
"onafterevent" => (fsm::StateMachine, args...) -> 1 + 1,
"onbeforejump" => (fsm::StateMachine, args...) -> 1 + 2,
Expand All @@ -28,14 +25,69 @@ fsm = state_machine((String => Any)[
"onentersecond" => (fsm::StateMachine, args...) -> 8 + 13,
"onleavestate" => (fsm::StateMachine, args...) -> 13 + 21,
"onleavefirst" => (fsm::StateMachine, args...) -> 21 + 34,
],
])
},
})

@test fsm.current == "first"
@test fsm.terminal == "fourth"
@test fsm.map == (String => Dict{String, String})[
@test fsm.map == {
"startup" => ["none" => "first"],
"skip" => ["second" => "third"],
"jump" => ["third" => "fourth"],
"hop" => ["first" => "second"],
]
}

dfsm = state_machine({
"initial" => {
"state" => "first",
"event" => "dance",
"defer" => true,
},
"terminal" => "fourth",
"events" => [{
"name" => "hop",
"from" => "first",
"to" => "second",
}, {
"name" => "skip",
"from" => "second",
"to" => "third",
}, {
"name" => "sleep",
"from" => "third",
}, {
"name" => "coupdegrace",
"from" => "third",
"to" => "fourth",
}
],
})

@test dfsm.current == "none"
@test dfsm.terminal == "fourth"

mfsm = state_machine({
"initial" => {
"state" => "first",
"defer" => false,
},
"events" => [{
"name" => "hop",
"from" => ["first", "third"],
"to" => "second",
}, {
"name" => "skip",
"from" => "second",
"to" => "third",
}, {
"name" => "sleep",
"from" => "third",
},
],
"callbacks" => {
"onevent" => (fsm::StateMachine, args...) -> 1,
"onstate" => (fsm::StateMachine, args...) -> -1,
},
})

@test mfsm.current == "first"
3 changes: 3 additions & 0 deletions test/test_transitions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@
@test enter_this_state(fsm, "hop", "first", "second") == 21
@test leave_any_state(fsm, "hop", "first", "second") == 34
@test leave_this_state(fsm, "hop", "first", "second") == 55

@test after_any_event(mfsm, "hop", "first", "second") == 1
@test enter_any_state(mfsm, "hop", "first", "second") == -1

0 comments on commit fdd3cc0

Please sign in to comment.