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 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(:heat_up, :solid, :liquid, :event => :liquified)
21
- transition(:heat_up, :liquid, :gas) # look mam.... two transitions with same name
22
- transition(:cool_down, :gas, :liquid)
23
- transition(:cool_down, :liquid, :solid)
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.heat # the name of the transition is the name of the method
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
- # Export to water.png in the current dir
50
- Water.dot
51
- # Export in another format. (see graphviz documentation for supported file formats)
52
- Water.dot(:format => :foo)
53
- # Change the extension (defaults to the format)
54
- Water.dot(:format => :jpg, :extension => :jpeg)
55
- # Specify a custom file
56
- Water.dot(:outfile => '/afile.png')
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
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 2
4
- :patch: 4
3
+ :minor: 3
4
+ :patch: 0
data/lib/fsm.rb CHANGED
@@ -17,10 +17,10 @@ module FSM
17
17
  end
18
18
  end
19
19
 
20
- def dot(options = {})
20
+ def draw_graph(options = {})
21
21
  machine = Machine[self]
22
22
  raise 'No FSM defined. Call define_fsm first' unless machine
23
- machine.dot(options)
23
+ machine.draw_graph(options)
24
24
  end
25
25
 
26
26
  end
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
- def to_dot
83
- dots = self.transitions.map do |transition|
84
- " #{transition.from.name} -> #{transition.to.name}"
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
- "digraph FSM {\n#{dots.join(";\n")}\n}"
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
- def dot(options = {})
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.fire_event(self, args)
119
- to_state.enter(self)
120
- Machine.set_current_state_name(self, to_state.name)
121
- true # at the moment always return true ... as soon as we have guards or thelike this could be false as well
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('Name is required') unless name
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
@@ -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
@@ -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
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplificator-fsm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - simplificator