simplificator-fsm 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|