simplificator-fsm 0.2.4 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +47 -15
- data/VERSION.yml +2 -2
- data/lib/fsm.rb +2 -2
- data/lib/fsm/machine.rb +19 -10
- data/lib/fsm/state.rb +23 -4
- data/lib/fsm/transition.rb +13 -4
- data/test/state_test.rb +4 -4
- data/test/transition_test.rb +5 -4
- data/test/water_sample_test.rb +40 -8
- metadata +1 -1
data/README.markdown
CHANGED
@@ -9,18 +9,19 @@ FSM is a simple finite state machine
|
|
9
9
|
define_fsm do
|
10
10
|
# now define all the states
|
11
11
|
# you can add :enter / :exit callbacks (callback can be a String, Symbol or Proc)
|
12
|
-
# these callbacks are triggered on any transition from/to this state
|
12
|
+
# these callbacks are triggered on any transition from/to this state.
|
13
13
|
|
14
14
|
state(:gas)
|
15
15
|
state(:liquid)
|
16
16
|
state(:solid, :enter => :on_enter_solid, :exit => :on_exit_solid)
|
17
17
|
|
18
18
|
# define all valid transitions (arguments are name of transition, from state name, to state name)
|
19
|
-
# you can define callbacks which are called only on this transition
|
20
|
-
transition
|
21
|
-
transition(:heat_up, :liquid, :
|
22
|
-
transition(:
|
23
|
-
transition(:cool_down, :liquid, :
|
19
|
+
# you can define callbacks which are called only on this transition as well as guards
|
20
|
+
# guards prevent transition when they return nil/false
|
21
|
+
transition(:heat_up, :solid, :liquid, :event => :on_heat, :guard => :guard_something)
|
22
|
+
transition(:heat_up, :liquid, :gas, :event => :on_heat) # look mam.... two transitions with same name
|
23
|
+
transition(:cool_down, :gas, :liquid, :event => :on_cool)
|
24
|
+
transition(:cool_down, :liquid, :solid, :event => :on_cool)
|
24
25
|
|
25
26
|
# define the attribute which is used to store the state (defaults to :state)
|
26
27
|
state(:state_of_material)
|
@@ -32,28 +33,59 @@ FSM is a simple finite state machine
|
|
32
33
|
private
|
33
34
|
# callbacks here...
|
34
35
|
def ...
|
36
|
+
|
37
|
+
#
|
38
|
+
def guard_something()
|
39
|
+
|
40
|
+
end
|
35
41
|
end
|
36
42
|
|
37
43
|
# then you can call these methods
|
38
44
|
w = Water.new
|
39
|
-
w.
|
45
|
+
w.heat_up # the name of the transition is the name of the method
|
40
46
|
w.reachable_state_names
|
41
47
|
w.available_transition_names
|
42
48
|
w.cool_down # again... it's the name of the transition
|
43
49
|
w.state_of_material
|
44
50
|
|
51
|
+
|
52
|
+
## Guards
|
53
|
+
Guards are methods or Procs which can prevent a transition. To do so they just need to return false/nil. If no guard is specified
|
54
|
+
then the transition is always executed.
|
55
|
+
|
56
|
+
## Callbacks and arguments
|
57
|
+
If the :enter/:exit callbacks are methods, then they are not passed any arguments, if it's a Proc,
|
58
|
+
then a single argument (the caller) is passed.
|
59
|
+
Short: :enter/:exit methods must take 0 arguments, :enter/:exit Procs must take one argument.
|
60
|
+
|
61
|
+
If :event and :guard callbacks are methods then they are passed all the arguments that were passed to the transition method.
|
62
|
+
With Procs for :event and :guard the caller as well as all the arguments passed to the transition methods are passed.
|
63
|
+
|
64
|
+
|
65
|
+
## Order of callbacks/guards calls
|
66
|
+
The callbacks/guards are called in following order if the guard returns __true__:
|
67
|
+
* :exit (state)
|
68
|
+
* :guard (transition)
|
69
|
+
* :event (transition)
|
70
|
+
* :enter (state)
|
71
|
+
|
72
|
+
The callbacks/guards are called in following order if the guard returns __false__:
|
73
|
+
* :exit (state)
|
74
|
+
* :guard (transition)
|
75
|
+
|
76
|
+
|
45
77
|
## Graphviz / Dot format
|
46
78
|
FSM supports the dot format of graphviz (http://www.graphviz.org/).
|
47
79
|
If you have the graphviz tools installed (the dot executable must be on the path) then
|
48
80
|
you can export a graph to png like this
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
81
|
+
# Export to water.png in the current dir
|
82
|
+
Water.graph
|
83
|
+
# Export in another format. (see graphviz documentation for supported file formats)
|
84
|
+
Water.draw_graph(:format => :foo)
|
85
|
+
# Change the extension (defaults to the format)
|
86
|
+
Water.draw_graph(:format => :jpg, :extension => :jpeg)
|
87
|
+
# Specify a custom file
|
88
|
+
Water.draw_graph(:outfile => '/afile.png')
|
57
89
|
|
58
90
|
|
59
91
|
## Copyright
|
data/VERSION.yml
CHANGED
data/lib/fsm.rb
CHANGED
data/lib/fsm/machine.rb
CHANGED
@@ -19,7 +19,7 @@ module FSM
|
|
19
19
|
|
20
20
|
def state(name, options = {})
|
21
21
|
raise "State is already defined: '#{name}'" if self.state_for_name(name, true)
|
22
|
-
self.states << State.new(name, options)
|
22
|
+
self.states << State.new(name, @target_class, options)
|
23
23
|
self.initial_state_name=(name) unless self.initial_state_name
|
24
24
|
end
|
25
25
|
|
@@ -79,14 +79,19 @@ module FSM
|
|
79
79
|
state
|
80
80
|
end
|
81
81
|
|
82
|
-
|
83
|
-
|
84
|
-
|
82
|
+
# Convert this state machine to the dot format of graphviz
|
83
|
+
def to_dot(options = {})
|
84
|
+
s = self.states.map do |state|
|
85
|
+
" #{state.to_dot(options)};"
|
85
86
|
end
|
86
|
-
|
87
|
+
t = self.transitions.map do |transition|
|
88
|
+
" #{transition.to_dot(options)};"
|
89
|
+
end
|
90
|
+
"digraph FSM_#{@target_class.name} {\n#{s.join("\n")}\n\n#{t.join("\n")}\n}"
|
87
91
|
end
|
88
92
|
|
89
|
-
|
93
|
+
#
|
94
|
+
def draw_graph(options = {})
|
90
95
|
format = options[:format] || :png
|
91
96
|
extension = options[:extension] || format
|
92
97
|
file_name = options[:outfile] || "#{@target_class.name.downcase}.#{extension}"
|
@@ -115,10 +120,14 @@ module FSM
|
|
115
120
|
to_state = transition.to
|
116
121
|
|
117
122
|
from_state.exit(self)
|
118
|
-
transition.
|
119
|
-
|
120
|
-
|
121
|
-
|
123
|
+
if transition.fire?(self, args)
|
124
|
+
transition.fire_event(self, args)
|
125
|
+
to_state.enter(self)
|
126
|
+
Machine.set_current_state_name(self, to_state.name)
|
127
|
+
true
|
128
|
+
else
|
129
|
+
false
|
130
|
+
end
|
122
131
|
end
|
123
132
|
end
|
124
133
|
end
|
data/lib/fsm/state.rb
CHANGED
@@ -10,10 +10,11 @@ module FSM
|
|
10
10
|
# options
|
11
11
|
# * :enter : a symbol or string or Proc
|
12
12
|
# * :exit : a symbol or string or Proc
|
13
|
-
def initialize(name, options = {})
|
14
|
-
raise ArgumentError.new('
|
13
|
+
def initialize(name, target_class, options = {})
|
14
|
+
raise ArgumentError.new('name and target_class is required') unless name && target_class
|
15
15
|
assert_options(options, [:enter, :exit])
|
16
16
|
@name = name
|
17
|
+
@target_class = target_class
|
17
18
|
@enter = Executable.new options[:enter] if options.has_key?(:enter)
|
18
19
|
@exit = Executable.new options[:exit] if options.has_key?(:exit)
|
19
20
|
@transitions = {}
|
@@ -40,10 +41,28 @@ module FSM
|
|
40
41
|
def to_states
|
41
42
|
@transitions.map { |to_name, transition| transition.to}
|
42
43
|
end
|
44
|
+
def final?
|
45
|
+
@transitions.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def initial?
|
49
|
+
Machine[@target_class].initial_state_name == self.name
|
50
|
+
end
|
43
51
|
|
44
52
|
def to_s
|
45
|
-
"State '#{self.name}'"
|
53
|
+
"State '#{self.name}' is "
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_dot(options = {})
|
57
|
+
|
58
|
+
if final?
|
59
|
+
attrs = "style=bold"
|
60
|
+
elsif initial?
|
61
|
+
attrs = "style=bold, label=\"#{self.name}\\n(initial)\""
|
62
|
+
else
|
63
|
+
attrs = ""
|
64
|
+
end
|
65
|
+
"#{self.name}[#{attrs}]"
|
46
66
|
end
|
47
|
-
|
48
67
|
end
|
49
68
|
end
|
data/lib/fsm/transition.rb
CHANGED
@@ -1,22 +1,31 @@
|
|
1
1
|
module FSM
|
2
2
|
class Transition
|
3
3
|
include FSM::Options::InstanceMethods
|
4
|
-
attr_accessor(:name, :from, :to, :event)
|
4
|
+
attr_accessor(:name, :from, :to, :event, :guard)
|
5
5
|
def initialize(name, from, to, options = {})
|
6
6
|
raise ArgumentError.new("name, from and to are required but were '#{name}', '#{from}' and '#{to}'") unless name && from && to
|
7
|
-
assert_options(options, [:event])
|
7
|
+
assert_options(options, [:event, :guard])
|
8
8
|
self.name = name
|
9
9
|
self.from = from
|
10
10
|
self.to = to
|
11
11
|
self.event = Executable.new options[:event] if options.has_key?(:event)
|
12
|
+
self.guard = Executable.new options[:guard] if options.has_key?(:guard)
|
12
13
|
end
|
13
14
|
|
14
15
|
def fire_event(target, args)
|
15
16
|
self.event.execute(target, *args) if self.event
|
16
|
-
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def fire?(target, args)
|
20
|
+
self.guard ? self.guard.execute(target, *args) : true
|
21
|
+
end
|
17
22
|
|
18
23
|
def to_s
|
19
24
|
"Transition from #{self.from.name} -> #{self.to.name} with event #{self.event}"
|
20
|
-
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_dot(options = {})
|
28
|
+
"#{self.from.name} -> #{self.to.name} [label=\"#{self.name}\"]"
|
29
|
+
end
|
21
30
|
end
|
22
31
|
end
|
data/test/state_test.rb
CHANGED
@@ -7,15 +7,15 @@ class StateTest < Test::Unit::TestCase
|
|
7
7
|
FSM::State.new(nil, nil, nil)
|
8
8
|
end
|
9
9
|
|
10
|
-
FSM::State.new('bla')
|
10
|
+
FSM::State.new('bla', self)
|
11
11
|
end
|
12
12
|
|
13
13
|
should 'allow only valid options' do
|
14
14
|
assert_raise(ArgumentError) do
|
15
|
-
FSM::State.new('bla', :foo => 12)
|
15
|
+
FSM::State.new('bla', self, :foo => 12)
|
16
16
|
end
|
17
|
-
FSM::State.new('bla', :enter => :some)
|
18
|
-
FSM::State.new('bla', :exit => :some)
|
17
|
+
FSM::State.new('bla', self, :enter => :some)
|
18
|
+
FSM::State.new('bla', self, :exit => :some)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
data/test/transition_test.rb
CHANGED
@@ -5,8 +5,8 @@ class TransitionTest < Test::Unit::TestCase
|
|
5
5
|
|
6
6
|
|
7
7
|
should 'require name, from and to' do
|
8
|
-
bli_state = FSM::State.new('bli')
|
9
|
-
blo_state = FSM::State.new('blo')
|
8
|
+
bli_state = FSM::State.new('bli', self)
|
9
|
+
blo_state = FSM::State.new('blo', self)
|
10
10
|
assert_raise(ArgumentError) do
|
11
11
|
FSM::Transition.new(nil, nil, nil)
|
12
12
|
end
|
@@ -33,12 +33,13 @@ class TransitionTest < Test::Unit::TestCase
|
|
33
33
|
end
|
34
34
|
|
35
35
|
should 'allow only valid options' do
|
36
|
-
bli_state = FSM::State.new('bli')
|
37
|
-
blo_state = FSM::State.new('blo')
|
36
|
+
bli_state = FSM::State.new('bli', self)
|
37
|
+
blo_state = FSM::State.new('blo', self)
|
38
38
|
assert_raise(ArgumentError) do
|
39
39
|
FSM::Transition.new(:name, bli_state, blo_state, :foo => 12)
|
40
40
|
end
|
41
41
|
FSM::Transition.new(:name, bli_state, blo_state, :event => :some)
|
42
|
+
FSM::Transition.new(:name, bli_state, blo_state, :guard => :some)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
end
|
data/test/water_sample_test.rb
CHANGED
@@ -2,6 +2,7 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
class Water
|
4
4
|
attr_accessor(:state_of_material)
|
5
|
+
attr_accessor(:temperature)
|
5
6
|
include FSM
|
6
7
|
define_fsm do
|
7
8
|
# now define all the states
|
@@ -14,10 +15,10 @@ class Water
|
|
14
15
|
# define all valid transitions (name, from, to). This will define a method with the given name.
|
15
16
|
# you can define :event callback which is called only on this transition and receives the arguments passed to the
|
16
17
|
# transition method
|
17
|
-
transition(:heat_up, :solid, :liquid)
|
18
|
-
transition(:heat_up, :liquid, :gas)
|
19
|
-
transition(:cool_down, :gas, :liquid)
|
20
|
-
transition(:cool_down, :liquid, :solid)
|
18
|
+
transition(:heat_up, :solid, :liquid, :event => :on_heat_up, :guard => :guard_solid_to_liquid)
|
19
|
+
transition(:heat_up, :liquid, :gas, :event => :on_heat_up, :guard => :guard_liquid_to_gas)
|
20
|
+
transition(:cool_down, :gas, :liquid, :event => :on_cool_down, :guard => :guard_gas_to_liquid)
|
21
|
+
transition(:cool_down, :liquid, :solid, :event => :on_cool_down, :guard => :guard_liquid_to_solid)
|
21
22
|
|
22
23
|
# define the attribute which is used to store the state (defaults to :state)
|
23
24
|
state_attribute(:state_of_material)
|
@@ -25,12 +26,42 @@ class Water
|
|
25
26
|
# define the initial state (defaults to the first state defined - :gas in this sample)
|
26
27
|
initial(:solid)
|
27
28
|
end
|
29
|
+
|
30
|
+
def initialize(temperature = -20)
|
31
|
+
self.temperature = temperature
|
32
|
+
end
|
33
|
+
|
28
34
|
private
|
29
35
|
def on_enter_solid()
|
30
36
|
end
|
31
37
|
def on_exit_solid()
|
32
38
|
end
|
39
|
+
|
40
|
+
def guard_solid_to_liquid(delta)
|
41
|
+
self.temperature + delta >= 0
|
42
|
+
end
|
43
|
+
def guard_liquid_to_gas(delta)
|
44
|
+
self.temperature + delta >= 100
|
45
|
+
end
|
46
|
+
|
47
|
+
def guard_gas_to_liquid(delta)
|
48
|
+
self.temperature - delta < 100
|
49
|
+
end
|
50
|
+
|
51
|
+
def guard_liquid_to_solid(delta)
|
52
|
+
self.temperature - delta < 0
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_heat_up(delta)
|
56
|
+
self.temperature += delta
|
57
|
+
end
|
58
|
+
|
59
|
+
def on_cool_down(delta)
|
60
|
+
self.temperature -= delta
|
61
|
+
end
|
33
62
|
end
|
63
|
+
# Draw the the state graph
|
64
|
+
#Water.draw_graph(:format => :svg)
|
34
65
|
|
35
66
|
class WaterSampleTest < Test::Unit::TestCase
|
36
67
|
context 'Water' do
|
@@ -38,13 +69,14 @@ class WaterSampleTest < Test::Unit::TestCase
|
|
38
69
|
should 'cycle through material states' do
|
39
70
|
w = Water.new
|
40
71
|
assert_equal(:solid, w.state_of_material)
|
41
|
-
w.heat_up
|
72
|
+
assert w.heat_up(30)
|
42
73
|
assert_equal(:liquid, w.state_of_material)
|
43
|
-
w.heat_up
|
74
|
+
assert !w.heat_up(10)
|
75
|
+
assert w.heat_up(90)
|
44
76
|
assert_equal(:gas, w.state_of_material)
|
45
|
-
w.cool_down
|
77
|
+
assert w.cool_down(50)
|
46
78
|
assert_equal(:liquid, w.state_of_material)
|
47
|
-
w.cool_down
|
79
|
+
assert w.cool_down(70)
|
48
80
|
assert_equal(:solid, w.state_of_material)
|
49
81
|
|
50
82
|
assert_raise(FSM::InvalidStateTransition) do
|