barebone-fsm 0.0.2.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +14 -18
- data/README.rdoc +25 -18
- data/example/door.rb +3 -6
- data/example/example_helper.rb +1 -0
- data/example/microwave.rb +2 -1
- data/example/vehicle_class.rb +38 -0
- data/lib/barebone-fsm.rb +371 -45
- data/spec/fsm_spec.rb +113 -0
- data/spec/fsmstate_spec.rb +117 -0
- data/spec/spec_helper.rb +1 -0
- metadata +7 -3
- data/spec/barebone-fsm_spec.rb +0 -58
data/MIT-LICENSE
CHANGED
@@ -1,22 +1,18 @@
|
|
1
1
|
Copyright (c) 2013, Md. Imrul Hassan
|
2
2
|
|
3
|
-
Permission is hereby granted, free of charge, to any person
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
Software is furnished to do so, subject to the following
|
10
|
-
conditions:
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
11
9
|
|
12
|
-
The above copyright notice and this permission notice shall be
|
13
|
-
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
14
12
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
-
OTHER DEALINGS IN THE SOFTWARE.
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
CHANGED
@@ -3,17 +3,19 @@ This module, barebone-fsm, implements a basic finite-state machine (FSM).
|
|
3
3
|
An FSM consists of a finite number of states,
|
4
4
|
with one of them being the current state of the FSM.
|
5
5
|
Transitions are defined between states, which are triggered on events.
|
6
|
-
For details on FSM, see this wiki page: {http://en.wikipedia.org/wiki/Finite-state_machine
|
6
|
+
For details on FSM, see this wiki page: {FSM}[http://en.wikipedia.org/wiki/Finite-state_machine].
|
7
7
|
|
8
|
-
The motivation behind the module was to implement a very basic barebone FSM in Ruby.
|
8
|
+
The motivation behind the module was to implement a very basic barebone {FSM} in Ruby.
|
9
9
|
Features are kept at minimum as well as the code.
|
10
10
|
Only two classes for the FSM are defined as the following:
|
11
|
-
* FSM -> the finite state machine class
|
12
|
-
* FSMState -> the state class
|
11
|
+
* {FSM::FSM} -> the finite state machine class
|
12
|
+
* {FSM::FSMState} -> the state class
|
13
|
+
The {FSM} Module when included in a class also provides few convenience methods.
|
14
|
+
For further details see the README file.
|
13
15
|
|
14
16
|
Author:: Md. Imrul Hassan (mailto:mihassan@gmail.com)
|
15
17
|
Copyright:: Copyright (c) 2013, Md. Imrul Hassan
|
16
|
-
License:: Barebone-fsm is released under the MIT license
|
18
|
+
License:: Barebone-fsm is released under the MIT license
|
17
19
|
|
18
20
|
== Features
|
19
21
|
Apart from having support for states and events, this module offers the following features:
|
@@ -21,7 +23,9 @@ Apart from having support for states and events, this module offers the followin
|
|
21
23
|
2. Default event for each state
|
22
24
|
3. Entry and exit events for each state
|
23
25
|
4. DSL like coding style
|
24
|
-
5. Access to state variables (including @state and @event) inside event blocks
|
26
|
+
5. Access to state variables (including @state and @event) inside event blocks
|
27
|
+
6. The module when included in a Class, provides methods to setup the state machine and trigger events
|
28
|
+
7. Dynamic methods generated for the Class to check states, events and trigger events.
|
25
29
|
|
26
30
|
== Usage
|
27
31
|
The FSM can be setup and triggered Succinctly using Domain Specific Language(DSL) like coding style.
|
@@ -29,13 +33,13 @@ There are two methods to build and run which are defined for both FSM and FSMSta
|
|
29
33
|
These methods, #build and #run, are alias of each other and can be used interchangebly.
|
30
34
|
A basic fintite-state machine for a microwave is simulated in the following example:
|
31
35
|
|
32
|
-
fsm = FSM::FSM.new(:stopped)
|
36
|
+
fsm = FSM::FSM.new(:stopped)
|
33
37
|
|
34
|
-
fsm.build do
|
35
|
-
state :stopped do
|
36
|
-
event :open do
|
38
|
+
fsm.build do
|
39
|
+
state :stopped do
|
40
|
+
event :open do
|
37
41
|
puts "[Stopped]: Door Opened"
|
38
|
-
:open
|
42
|
+
:open
|
39
43
|
end
|
40
44
|
event :start do
|
41
45
|
puts "[Stopped]: Started"
|
@@ -44,40 +48,43 @@ A basic fintite-state machine for a microwave is simulated in the following exam
|
|
44
48
|
end
|
45
49
|
state :open do
|
46
50
|
event :close do
|
47
|
-
puts "[#{@state}]: Door #{@event}"
|
51
|
+
puts "[#{@state}]: Door #{@event}"
|
48
52
|
:stopped
|
49
53
|
end
|
50
54
|
end
|
51
55
|
state :started do
|
52
56
|
event :open do
|
53
|
-
@open_time = Time::now
|
57
|
+
@open_time = Time::now
|
54
58
|
puts "[Started]: Door Opened"
|
55
59
|
:open
|
56
60
|
end
|
57
61
|
event :stop do
|
58
|
-
puts "The door was opened at #{@open_time}"
|
62
|
+
puts "The door was opened at #{@open_time}"
|
59
63
|
puts "[Started]: Door Stopped after #{elapsed_time}"
|
60
64
|
:stopped
|
61
65
|
end
|
62
66
|
end
|
63
67
|
end
|
64
68
|
|
65
|
-
fsm.run do
|
69
|
+
fsm.run do
|
66
70
|
event :start
|
67
71
|
event :open
|
68
72
|
event :close
|
69
73
|
event :start
|
70
74
|
event :stop
|
71
75
|
end
|
72
|
-
|
73
|
-
puts fsm
|
74
76
|
|
75
77
|
== Status
|
76
|
-
The module works as it is,
|
78
|
+
The module works as it is, I have added some testing and documentation; it may be expanded.
|
77
79
|
The api is not stable yet, it may go trhough lots of changes before the first stable version is released.
|
78
80
|
I am open to any suggestion or request to support custom features.
|
79
81
|
|
80
82
|
== Changes
|
83
|
+
* Version: 0.0.3
|
84
|
+
* Added Module level methods for easy access to states and events.
|
85
|
+
* Added documentations for all the methods.
|
86
|
+
* Added Module level dynamic methods to check states, events and to trigger events.
|
87
|
+
* Added some testing codes using RSpec.
|
81
88
|
* Version: 0.0.2
|
82
89
|
* State variables defined in other states can be accessed in the event block as well.
|
83
90
|
* Version: 0.0.1.3
|
data/example/door.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
|
-
|
1
|
+
require_relative 'example_helper'
|
2
2
|
|
3
3
|
fsm = FSM::FSM.new(:default)
|
4
|
-
|
5
4
|
fsm.build do
|
6
|
-
|
5
|
+
@x = 0
|
7
6
|
state :default do
|
8
7
|
event :open do
|
9
8
|
puts "#{@x} transition: default->open"
|
@@ -17,7 +16,6 @@ fsm.build do
|
|
17
16
|
|
18
17
|
state :open do
|
19
18
|
event :close do
|
20
|
-
@x ||= 0
|
21
19
|
@x+=1
|
22
20
|
puts "#{@x} transition: open->close"
|
23
21
|
:close
|
@@ -26,14 +24,13 @@ fsm.build do
|
|
26
24
|
|
27
25
|
state :close do
|
28
26
|
event :open do
|
29
|
-
@x ||= 0
|
30
27
|
@x+=1
|
31
28
|
puts "#{@x} transition: close->open"
|
32
29
|
:open
|
33
30
|
end
|
34
31
|
end
|
35
32
|
|
36
|
-
end
|
33
|
+
end
|
37
34
|
|
38
35
|
puts fsm
|
39
36
|
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../lib/barebone-fsm'
|
data/example/microwave.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'example_helper'
|
2
|
+
|
3
|
+
class Vehicle
|
4
|
+
|
5
|
+
include FSM
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
build do
|
9
|
+
state :parked do event :start => :running, :open => :open end
|
10
|
+
state :running do event :park => :parked end
|
11
|
+
state :open do event :park => :parked end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def show_fsm
|
16
|
+
puts @fsm
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
veh = Vehicle.new
|
21
|
+
|
22
|
+
puts "It is parked" if veh.is_parked?
|
23
|
+
|
24
|
+
puts "It can start" if veh.can_start?
|
25
|
+
|
26
|
+
veh.start
|
27
|
+
|
28
|
+
puts veh.state
|
29
|
+
|
30
|
+
veh.state :turned_off
|
31
|
+
veh.state :parked do event :turn_off => :turned_off end
|
32
|
+
veh.event :turn_off
|
33
|
+
|
34
|
+
puts "It is parked" if veh.is_parked?
|
35
|
+
|
36
|
+
puts "It can start" if veh.can_start?
|
37
|
+
|
38
|
+
#veh.start
|
data/lib/barebone-fsm.rb
CHANGED
@@ -5,42 +5,59 @@
|
|
5
5
|
# Transitions are defined between states, which are triggered on events.
|
6
6
|
# For details on FSM, see this wiki page: {FSM}[http://en.wikipedia.org/wiki/Finite-state_machine].
|
7
7
|
#
|
8
|
-
# The motivation behind the module was to implement a very basic barebone FSM in Ruby.
|
8
|
+
# The motivation behind the module was to implement a very basic barebone {FSM} in Ruby.
|
9
9
|
# Features are kept at minimum as well as the code.
|
10
10
|
# Only two classes for the FSM are defined as the following:
|
11
|
-
# * FSM -> the finite state machine class
|
12
|
-
# * FSMState -> the state class
|
13
|
-
#
|
11
|
+
# * {FSM::FSM} -> the finite state machine class
|
12
|
+
# * {FSM::FSMState} -> the state class
|
13
|
+
# The {FSM} Module when included in a class also provides few convenience methods.
|
14
14
|
# For further details see the README file.
|
15
15
|
#
|
16
16
|
# Author:: Md. Imrul Hassan (mailto:mihassan@gmail.com)
|
17
|
-
# Copyright:: Copyright
|
17
|
+
# Copyright:: Copyright (c) 2013, Md. Imrul Hassan
|
18
|
+
# License:: Barebone-fsm is released under the MIT license
|
18
19
|
#
|
19
20
|
module FSM
|
20
|
-
|
21
|
+
|
22
|
+
##
|
21
23
|
# FSMState class represents a state of the finite state machine.
|
22
24
|
#
|
23
|
-
#
|
24
|
-
# state = FSMState.new :state_name
|
25
|
-
# state.event(:event_name) do
|
25
|
+
# @example Setup the event code for a state through block
|
26
|
+
# state = FSM::FSMState.new :state_name
|
27
|
+
# state.event(:event_name) do
|
26
28
|
# puts "#{@event} triggered on state #{@state}"
|
27
|
-
# :
|
29
|
+
# :next_state
|
28
30
|
# end
|
29
|
-
#
|
30
|
-
#
|
31
|
+
#
|
32
|
+
# @example Setup multiple events specifying only the next state for each event using Hash map
|
33
|
+
# state = FSM::FSMState.new :initial_state
|
34
|
+
# state.event :go_left => :left_state, :go_right => :right_state
|
35
|
+
#
|
36
|
+
# @example Trigger an event for the previous example
|
37
|
+
# state.event :go_left
|
31
38
|
#
|
32
39
|
class FSMState
|
33
40
|
|
34
|
-
|
35
|
-
#
|
41
|
+
##
|
42
|
+
# The name of the state.
|
36
43
|
attr_reader :state
|
44
|
+
##
|
45
|
+
# The events for this state mapped to corresponding event codes.
|
46
|
+
attr_reader :events
|
37
47
|
|
48
|
+
##
|
49
|
+
# @param state_machine [FSM::FSM] the FSM object representing the state machine containing this state
|
50
|
+
# @param state_name [Symbol] the unique name for this state
|
51
|
+
#
|
38
52
|
def initialize(state_machine, state_name)
|
39
53
|
@fsm = state_machine
|
40
54
|
@state = state_name
|
41
55
|
@events = {}
|
42
56
|
end
|
43
|
-
|
57
|
+
|
58
|
+
##
|
59
|
+
# A String representation of the FSMState object.
|
60
|
+
#
|
44
61
|
def to_s()
|
45
62
|
@state.to_s +
|
46
63
|
": [" +
|
@@ -48,12 +65,44 @@ module FSM
|
|
48
65
|
"]"
|
49
66
|
end
|
50
67
|
|
51
|
-
|
52
|
-
#
|
68
|
+
##
|
69
|
+
# Setup or trigger an event.
|
70
|
+
# It sets up a new event when the event_block is provided or event_name is a Hash map.
|
71
|
+
# The event_name is triggered otherwise.
|
53
72
|
# If the event is nil or not already setup, then the default event is triggered.
|
73
|
+
#
|
74
|
+
# @overload event(event_name, &event_block)
|
75
|
+
# Sets up an event for this state with the given event_block parameter.
|
76
|
+
# @param event_name [Symbol] the name of the event to set up
|
77
|
+
# @param event_block [block] the block of code to run when this event will be triggered
|
78
|
+
#
|
79
|
+
# @overload event(events_hash)
|
80
|
+
# Sets up multiple events where each element of the Hash map corresponds to one event.
|
81
|
+
# @param events_hash [Hash<Symbol,Symbol>] the Hash object mapping events to the corresponding next states
|
82
|
+
#
|
83
|
+
# @overload event(event_name)
|
84
|
+
# Triggers the event named event_name for this state.
|
85
|
+
# @param event_name [Symbol] the name of the event to trigger
|
86
|
+
#
|
87
|
+
# @example Setup an event by block
|
88
|
+
# state.event(:event_name) do
|
89
|
+
# puts "#{@event} triggered on state #{@state}"
|
90
|
+
# :next_state
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# @example Setup an event by Hash
|
94
|
+
# state.event :go_left => :left_state, :go_right => :right_state
|
95
|
+
#
|
96
|
+
# @example Trigger an event
|
97
|
+
# state.event :go_left
|
98
|
+
#
|
54
99
|
def event(event_name, &event_block)
|
55
|
-
if
|
100
|
+
if block_given? then
|
56
101
|
@events[event_name] = event_block
|
102
|
+
elsif event_name.is_a?(Hash) then
|
103
|
+
event_name.each{ |ev, st|
|
104
|
+
@events[ev] = Proc.new{st}
|
105
|
+
}
|
57
106
|
elsif event_name and @events.has_key? event_name then
|
58
107
|
@fsm.event = event_name
|
59
108
|
@fsm.instance_eval &@events[event_name]
|
@@ -63,9 +112,27 @@ module FSM
|
|
63
112
|
end
|
64
113
|
end
|
65
114
|
|
66
|
-
|
115
|
+
##
|
116
|
+
# The #build/#run method sets up the events described as DSL code in the build_block.
|
67
117
|
# Only event method is supported within the build_block with the name of the event and an optional block supplied.
|
68
|
-
# The operation for each such line is carried out by the #event method.
|
118
|
+
# The operation for each such line is carried out by the {FSM::FSMState#event} method.
|
119
|
+
# @see FSM::FSMState#event
|
120
|
+
#
|
121
|
+
# @param build_block [block] the block of code with sevaral event methods
|
122
|
+
#
|
123
|
+
# @example Using block to setup events
|
124
|
+
# state.build do
|
125
|
+
# event :event_name do
|
126
|
+
# puts "#{@event} triggered on state #{@state}"
|
127
|
+
# :next_state
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# @example Using block to trigger events
|
132
|
+
# state.build do
|
133
|
+
# event :event_name
|
134
|
+
# end
|
135
|
+
#
|
69
136
|
def build(&build_block)
|
70
137
|
self.instance_eval &build_block
|
71
138
|
end
|
@@ -74,27 +141,51 @@ module FSM
|
|
74
141
|
|
75
142
|
end
|
76
143
|
|
144
|
+
##
|
77
145
|
# This class implements the finite-state machine.
|
78
146
|
# FSM class exposes the states it contains and can trigger an event.
|
79
|
-
# States are created on the fly first time it's referenced through index operator [].
|
147
|
+
# States are created on the fly first time it's referenced through index operator [] or #state method.
|
80
148
|
#
|
81
149
|
# The FSM state transits to the default state if the latest event does not define the next state.
|
82
150
|
# If default state is not set, then state does not change on undefined state.
|
83
151
|
# The initial state of the FSM is the first state mentioned.
|
84
152
|
# This can be either the default state if defined or the first referenced state.
|
85
153
|
#
|
86
|
-
#
|
87
|
-
# fsm = FSM.new
|
88
|
-
# fsm[:default_state].event(:first_event) do
|
154
|
+
# @example Setting up finite state machine and triggering events without blocks
|
155
|
+
# fsm = FSM::FSM.new(:default_state)
|
156
|
+
# fsm[:default_state].event(:first_event) do
|
89
157
|
# puts "The first transition from the default_state to state_name"
|
90
|
-
# :state_name
|
158
|
+
# :state_name
|
159
|
+
# end
|
160
|
+
# fsm.event :first_event
|
161
|
+
#
|
162
|
+
# @example Setting up finite state machine and triggering events with blocks
|
163
|
+
# fsm = FSM::FSM.new
|
164
|
+
# fsm.build do
|
165
|
+
# state :initial_state do
|
166
|
+
# event :an_event => :next_state
|
167
|
+
# end
|
168
|
+
# end
|
169
|
+
# fsm.run do
|
170
|
+
# event :an_event
|
91
171
|
# end
|
92
172
|
#
|
93
173
|
class FSM
|
94
174
|
|
95
|
-
|
175
|
+
##
|
176
|
+
# The list of all the states.
|
177
|
+
attr_reader :states
|
96
178
|
|
97
|
-
|
179
|
+
##
|
180
|
+
# Creates a new FSM object with an optional default state.
|
181
|
+
# @param default_state [Symbol] the name for the default state
|
182
|
+
#
|
183
|
+
# @example Create a new finite state machine without default state
|
184
|
+
# fsm = FSM::FSM.new
|
185
|
+
#
|
186
|
+
# @example Create a new finite state machine with default state
|
187
|
+
# fsm = FSM::FSM.new(:default_state)
|
188
|
+
#
|
98
189
|
def initialize(default_state=nil)
|
99
190
|
@states = {}
|
100
191
|
if default_state then
|
@@ -102,7 +193,10 @@ module FSM
|
|
102
193
|
@states[@default] = FSMState.new(self, @default)
|
103
194
|
end
|
104
195
|
end
|
105
|
-
|
196
|
+
|
197
|
+
##
|
198
|
+
# A String representation of the FSM object.
|
199
|
+
#
|
106
200
|
def to_s()
|
107
201
|
"FSM" +
|
108
202
|
": {" +
|
@@ -112,8 +206,24 @@ module FSM
|
|
112
206
|
"}"
|
113
207
|
end
|
114
208
|
|
115
|
-
|
116
|
-
#
|
209
|
+
##
|
210
|
+
# Creates and/or returns an FSMState object.
|
211
|
+
# If an FSMState object with state_name is present, then returns that object.
|
212
|
+
# Otherwise, it creates a new FSMState object first and then returns that object.
|
213
|
+
# If state_name is not given, then current state object is returned.
|
214
|
+
#
|
215
|
+
# @overload []()
|
216
|
+
# The current FSMState object is returned.
|
217
|
+
#
|
218
|
+
# @overload [](state_name)
|
219
|
+
# Returns the FSMState object with state_name.
|
220
|
+
# If it is not setup yet, a new FSMState object is created first and then returned.
|
221
|
+
#
|
222
|
+
# @example
|
223
|
+
# fsm = FSM::FSM.new
|
224
|
+
# new_state = fsm[:new_state]
|
225
|
+
# print fsm[:new_state].state
|
226
|
+
#
|
117
227
|
def [](state_name=nil)
|
118
228
|
state_name ||= @state
|
119
229
|
if state_name and not @states.has_key? state_name then
|
@@ -123,9 +233,39 @@ module FSM
|
|
123
233
|
@states[state_name]
|
124
234
|
end
|
125
235
|
|
126
|
-
|
127
|
-
#
|
128
|
-
#
|
236
|
+
##
|
237
|
+
# Sets up and/or returns an FSMState object.
|
238
|
+
# It sets up a new state with all its events when the state_block is provided.
|
239
|
+
# If state_block is missing, then it creates and/or returns an FSMState object with state_name.
|
240
|
+
# If state_name is not given, then current state object is returned.
|
241
|
+
#
|
242
|
+
# @overload state(state_name, state_block)
|
243
|
+
# Sets up a state with the given state_block parameter.
|
244
|
+
# @param state_name [Symbol] the name of the state to set up
|
245
|
+
# @param state_block [block] the block of code to setuo the state
|
246
|
+
#
|
247
|
+
# @overload state(state_name)
|
248
|
+
# Create and/or return a state object.
|
249
|
+
# @param state_name [Symbol] the name of the state
|
250
|
+
#
|
251
|
+
# @overload state()
|
252
|
+
# Return current state object.
|
253
|
+
#
|
254
|
+
# @example Setup a state by block
|
255
|
+
# fsm = FSM::FSM.new
|
256
|
+
# new_state = fsm.state(:new_state) do
|
257
|
+
# event :an_event => :next_state
|
258
|
+
# end
|
259
|
+
#
|
260
|
+
# @example Create a new state
|
261
|
+
# fsm = FSM::FSM.new
|
262
|
+
# new_state = fsm.state(:new_state)
|
263
|
+
#
|
264
|
+
# @example Return the current state
|
265
|
+
# fsm = FSM::FSM.new
|
266
|
+
# new_state = fsm.state(:new_state)
|
267
|
+
# print fsm.state.state
|
268
|
+
#
|
129
269
|
def state(state_name=nil, &state_block)
|
130
270
|
if block_given? then
|
131
271
|
self.[](state_name).build &state_block
|
@@ -134,23 +274,55 @@ module FSM
|
|
134
274
|
end
|
135
275
|
end
|
136
276
|
|
137
|
-
|
277
|
+
##
|
278
|
+
# Trigger a series of events.
|
138
279
|
# The :entry and :exit events are called on the leaving state and the entering state.
|
139
280
|
# If the event does not mention the new state, then the state changes to the default state.
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
281
|
+
#
|
282
|
+
# @param event_names [Array<Symbol>] the list of event names
|
283
|
+
#
|
284
|
+
# @example Triggers two events
|
285
|
+
# state.event(:first_event, :second_event)
|
286
|
+
#
|
287
|
+
def event(*event_names)
|
288
|
+
for event_name in event_names do
|
289
|
+
@event = event_name
|
290
|
+
@states[@state].event :exit
|
291
|
+
new_state = @states[@state].event event_name
|
292
|
+
new_state = nil if not @states.has_key? new_state
|
293
|
+
new_state ||= @default
|
294
|
+
new_state ||= @state
|
295
|
+
@state = new_state
|
296
|
+
@states[@state].event :enter
|
297
|
+
end
|
149
298
|
end
|
150
299
|
|
151
|
-
#
|
152
|
-
|
300
|
+
# @private
|
301
|
+
def event=(event_name)
|
302
|
+
@event = event_name
|
303
|
+
end
|
304
|
+
|
305
|
+
##
|
306
|
+
# The #build/#run method sets up the states and events as DSL code in the build_block.
|
307
|
+
# Only state and event methods are supported within the build_block with the name of the state/event and an optional block supplied.
|
153
308
|
# The operation for each such line is carried out by the #state/#event method.
|
309
|
+
# @see FSM::FSM#state
|
310
|
+
# @see FSM::FSM#event
|
311
|
+
#
|
312
|
+
# @example Using block to setup states and trigger events
|
313
|
+
# fsm = FSM::FSM.new
|
314
|
+
# fsm.build do
|
315
|
+
# state :stopped do
|
316
|
+
# event :run => :running
|
317
|
+
# end
|
318
|
+
# state :running do
|
319
|
+
# event :stop => :stopped
|
320
|
+
# end
|
321
|
+
# end
|
322
|
+
# fsm.run do
|
323
|
+
# event :run, :stop
|
324
|
+
# end
|
325
|
+
#
|
154
326
|
def build(&build_block)
|
155
327
|
self.instance_eval &build_block
|
156
328
|
end
|
@@ -159,4 +331,158 @@ module FSM
|
|
159
331
|
|
160
332
|
end
|
161
333
|
|
334
|
+
##
|
335
|
+
# The #build/#run method sets up the states and events as DSL code.
|
336
|
+
# Only state and event methods are supported within the build_block with the name of the state/event and an optional block supplied.
|
337
|
+
# The operation for each such line is carried out by the {FSM::FSM#state}/{FSM::FSM#event} method.
|
338
|
+
# @see FSM::FSM#state
|
339
|
+
# @see FSM::FSM#event
|
340
|
+
#
|
341
|
+
# @example Using block to setup states and trigger events
|
342
|
+
# fsm = FSM::FSM.new
|
343
|
+
# fsm.build do
|
344
|
+
# state :stopped do
|
345
|
+
# event :run => :running
|
346
|
+
# end
|
347
|
+
# state :running do
|
348
|
+
# event :stop => :stopped
|
349
|
+
# end
|
350
|
+
# end
|
351
|
+
# fsm.run do
|
352
|
+
# event :run, :stop
|
353
|
+
# end
|
354
|
+
#
|
355
|
+
def build(&build_block)
|
356
|
+
@fsm ||= FSM.new
|
357
|
+
@fsm.build(&build_block)
|
358
|
+
end
|
359
|
+
alias_method :run, :build
|
360
|
+
|
361
|
+
##
|
362
|
+
# Trigger a series of events.
|
363
|
+
# The :entry and :exit events are called on the leaving state and the entering state.
|
364
|
+
# If the event does not mention the new state, then the state changes to the default state.
|
365
|
+
# @param event_names [Array<Symbol>] the list of event names
|
366
|
+
# @see FSM::FSM#event
|
367
|
+
#
|
368
|
+
def event(*event_names)
|
369
|
+
@fsm.event(*event_names)
|
370
|
+
end
|
371
|
+
|
372
|
+
##
|
373
|
+
# Sets up and/or returns an FSMState object.
|
374
|
+
# It sets up a new state with all its events when the state_block is provided.
|
375
|
+
# If state_block is missing, then it creates and/or returns an FSMState object with state_name.
|
376
|
+
# If state_name is not given, then current state object is returned.
|
377
|
+
# @see FSM::FSM#state
|
378
|
+
#
|
379
|
+
# @overload state(state_name, state_block)
|
380
|
+
# Sets up a state with the given state_block parameter.
|
381
|
+
# @param state_name [Symbol] the name of the state to set up
|
382
|
+
# @param state_block [block] the block of code to setuo the state
|
383
|
+
#
|
384
|
+
# @overload state(state_name)
|
385
|
+
# Create and/or return a state object.
|
386
|
+
# @param state_name [Symbol] the name of the state
|
387
|
+
#
|
388
|
+
# @overload state()
|
389
|
+
# Return current state object.
|
390
|
+
#
|
391
|
+
def state(state_name=nil, &state_block)
|
392
|
+
@fsm.state(state_name, &state_block)
|
393
|
+
end
|
394
|
+
|
395
|
+
# The list of names of all the states
|
396
|
+
def states
|
397
|
+
@fsm.states.keys
|
398
|
+
end
|
399
|
+
|
400
|
+
##
|
401
|
+
# @!method is_state?
|
402
|
+
# Checks if the current state is state.
|
403
|
+
#
|
404
|
+
# @example Vehicle running
|
405
|
+
# class Vehicle
|
406
|
+
# include FSM
|
407
|
+
# def initialize
|
408
|
+
# build do
|
409
|
+
# state :parked do event :start => :running end
|
410
|
+
# state :running do event :park => :parked end
|
411
|
+
# end
|
412
|
+
# end
|
413
|
+
# end
|
414
|
+
# veh = Vehicle.new
|
415
|
+
# puts "Vehicle running." if veh.is_running?
|
416
|
+
# veh.start
|
417
|
+
# puts "Vehicle running." if veh.is_running?
|
418
|
+
|
419
|
+
##
|
420
|
+
# @!method can_trigger?
|
421
|
+
# Checks if trigger is an event on the current state.
|
422
|
+
#
|
423
|
+
# @example Vehicle start if it can
|
424
|
+
# class Vehicle
|
425
|
+
# include FSM
|
426
|
+
# def initialize
|
427
|
+
# build do
|
428
|
+
# state :parked do event :start => :running end
|
429
|
+
# state :running do event :park => :parked end
|
430
|
+
# end
|
431
|
+
# end
|
432
|
+
# end
|
433
|
+
# veh = Vehicle.new
|
434
|
+
# veh.start if veh.can_start?
|
435
|
+
|
436
|
+
##
|
437
|
+
# @!method trigger
|
438
|
+
# Triggers the evnt for current state.
|
439
|
+
# It should be used with #can_trigger? method, as if the method is only defined id it can be triggered.
|
440
|
+
#
|
441
|
+
# @example Vehicle start if it can
|
442
|
+
# class Vehicle
|
443
|
+
# include FSM
|
444
|
+
# def initialize
|
445
|
+
# build do
|
446
|
+
# state :parked do event :start => :running end
|
447
|
+
# state :running do event :park => :parked end
|
448
|
+
# end
|
449
|
+
# end
|
450
|
+
# end
|
451
|
+
# veh = Vehicle.new
|
452
|
+
# # veh.park will generate NoMethodError as vehicle can not park from parked state
|
453
|
+
# veh.park if veh.can_park?
|
454
|
+
# veh.start if veh.can_start?
|
455
|
+
|
456
|
+
def respont_to?(sym)
|
457
|
+
if @fsm then
|
458
|
+
match_can_method?(sym) || match_is_method?(sym) || match_run_method?(sym) || super
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
def method_missing(sym, *args, &block)
|
463
|
+
if match = match_can_method?(sym) then
|
464
|
+
@fsm.state.events.include?(match)
|
465
|
+
elsif match = match_is_method?(sym) then
|
466
|
+
@fsm.state.state == match
|
467
|
+
elsif match = match_run_method?(sym)
|
468
|
+
@fsm.event(match)
|
469
|
+
else
|
470
|
+
super
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
private
|
475
|
+
|
476
|
+
def match_can_method?(sym)
|
477
|
+
sym.to_s =~ /can_(.*)\?/; $1 && $1.to_sym
|
478
|
+
end
|
479
|
+
|
480
|
+
def match_is_method?(sym)
|
481
|
+
sym.to_s =~ /is_(.*)\?/; $1 && $1.to_sym
|
482
|
+
end
|
483
|
+
|
484
|
+
def match_run_method?(sym)
|
485
|
+
@fsm.state.events.include?(sym)
|
486
|
+
end
|
487
|
+
|
162
488
|
end
|
data/spec/fsm_spec.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe FSM::FSM do
|
4
|
+
subject{FSM::FSM.new()}
|
5
|
+
|
6
|
+
describe "#initialize" do
|
7
|
+
context "with default state" do
|
8
|
+
subject{FSM::FSM.new(:default)}
|
9
|
+
its(:states){should have(1).item}
|
10
|
+
end
|
11
|
+
context "without default state" do
|
12
|
+
its(:states){should be_empty}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#to_s" do
|
17
|
+
context "with default state" do
|
18
|
+
subject{FSM::FSM.new(:default)}
|
19
|
+
specify{subject.to_s.should be_eql("FSM: {default: []}")}
|
20
|
+
end
|
21
|
+
context "without default state" do
|
22
|
+
specify{subject.to_s.should be_eql("FSM: {}")}
|
23
|
+
end
|
24
|
+
context "with one state" do
|
25
|
+
before{subject.state(:first_state)}
|
26
|
+
specify{subject.to_s.should be_eql("FSM: {>first_state: []}")}
|
27
|
+
end
|
28
|
+
context "with two states" do
|
29
|
+
before{subject.state(:first_state)}
|
30
|
+
before{subject.state(:second_state)}
|
31
|
+
specify{subject.to_s.should be_eql("FSM: {>first_state: [], second_state: []}")}
|
32
|
+
end
|
33
|
+
context "with states and events" do
|
34
|
+
before{subject.state(:first_state){event :first_event => :second_state}}
|
35
|
+
before{subject.state(:second_state){event :second_event => :first_state}}
|
36
|
+
specify{subject.to_s.should be_eql("FSM: {>first_state: [first_event], second_state: [second_event]}")}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#state" do
|
41
|
+
context "with a state block" do
|
42
|
+
before{subject.state(:first_state){event :first_event => :second_state}}
|
43
|
+
before{subject.state(:second_state){event :second_event => :first_state}}
|
44
|
+
its(:states){should_not be_empty}
|
45
|
+
its(:states){should have(2).items}
|
46
|
+
describe "state" do
|
47
|
+
specify{subject.state.state.should be(:first_state)}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
context "without a state block" do
|
51
|
+
before{subject.state(:first_state)}
|
52
|
+
its(:states){should_not be_empty}
|
53
|
+
its(:states){should have(1).items}
|
54
|
+
describe "state" do
|
55
|
+
specify{subject.state.state.should be(:first_state)}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
context "without any states" do
|
59
|
+
its(:states){should be_empty}
|
60
|
+
its(:state){should be_nil}
|
61
|
+
end
|
62
|
+
context "with some states" do
|
63
|
+
before{subject.state(:first_state)}
|
64
|
+
its(:states){should_not be_empty}
|
65
|
+
its(:states){should have(1).items}
|
66
|
+
its(:state){should_not be_nil}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#event" do
|
71
|
+
before{subject.state(:first_state){event :first_event => :second_state}}
|
72
|
+
before{subject.state(:second_state){event :second_event => :first_state}}
|
73
|
+
before{subject.event :first_event}
|
74
|
+
describe "state" do
|
75
|
+
specify{subject.state.state.should be(:second_state)}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#build" do
|
80
|
+
before{subject.build{state :a_state do event :an_event do :new_state end end}}
|
81
|
+
its(:states){should_not be_empty}
|
82
|
+
its(:states){should have(1).items}
|
83
|
+
its(:state){should_not be_nil}
|
84
|
+
describe "state" do
|
85
|
+
specify{subject.state.state.should be(:a_state)}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#run" do
|
90
|
+
subject{FSM::FSM.new(:default)}
|
91
|
+
before{subject.state(:first_state){event :first_event => :second_state}}
|
92
|
+
before{subject.state(:second_state){event :second_event => :first_state}}
|
93
|
+
|
94
|
+
context "when triggered event exists" do
|
95
|
+
before{subject.run{event :first_event}}
|
96
|
+
describe "state" do
|
97
|
+
specify{subject.state.state.should be(:second_state)}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
context "when triggered event does not exist" do
|
101
|
+
before{subject.run{event :unknown_event}}
|
102
|
+
describe "state" do
|
103
|
+
specify{subject.state.state.should be(:default)}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
context "when neither triggered event nor default state exists" do
|
107
|
+
describe "state" do
|
108
|
+
specify{subject.state.state.should be(:first_state)}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe FSM::FSMState do
|
4
|
+
let(:fsm){FSM::FSM.new}
|
5
|
+
subject{FSM::FSMState.new(fsm, :state_name)}
|
6
|
+
|
7
|
+
describe "#initialize" do
|
8
|
+
context "with parameter :state_name" do
|
9
|
+
its(:state){should be(:state_name)}
|
10
|
+
its(:state){should_not be(:wrong_name)}
|
11
|
+
its(:events){should be_a_kind_of(Hash)}
|
12
|
+
its(:events){should be_empty}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#to_s" do
|
17
|
+
context "with no events" do
|
18
|
+
specify{subject.to_s.should be_eql("state_name: []")}
|
19
|
+
end
|
20
|
+
context "with one event" do
|
21
|
+
before{subject.event(:first_event => :new_state)}
|
22
|
+
specify{subject.to_s.should be_eql("state_name: [first_event]")}
|
23
|
+
end
|
24
|
+
context "with two events" do
|
25
|
+
before{subject.event(:first_event => :new_state, :second_event => :new_state)}
|
26
|
+
specify{subject.to_s.should be_eql("state_name: [first_event, second_event]")}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#event" do
|
31
|
+
context "with an event block" do
|
32
|
+
before{subject.event(:an_event){:new_state}}
|
33
|
+
its(:events){should_not be_empty}
|
34
|
+
its(:events){should include(:an_event)}
|
35
|
+
its(:events){should have(1).item}
|
36
|
+
context "when the event is triggered" do
|
37
|
+
describe "the new state" do
|
38
|
+
specify{subject.event(:an_event).should be(:new_state)}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
context "with a Hash map" do
|
43
|
+
before{subject.event(:an_event=>:new_state)}
|
44
|
+
its(:events){should_not be_empty}
|
45
|
+
its(:events){should include(:an_event)}
|
46
|
+
its(:events){should have(1).item}
|
47
|
+
context "when the event is triggered" do
|
48
|
+
describe "the new state" do
|
49
|
+
specify{subject.event(:an_event).should be(:new_state)}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
context "when triggered event exists" do
|
54
|
+
before{subject.event(:an_event){:new_state}}
|
55
|
+
describe "the new state" do
|
56
|
+
specify{subject.event(:an_event).should be(:new_state)}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
context "when triggered event does not exist" do
|
60
|
+
before{subject.event(:default){:new_state}}
|
61
|
+
it "should trigger default event" do
|
62
|
+
subject.event(:an_event).should be(:new_state)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
context "when neither triggered event nor default event exists" do
|
66
|
+
it "should return nil" do
|
67
|
+
subject.event(:an_event).should be_nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#build" do
|
73
|
+
context "with an event block" do
|
74
|
+
before{subject.build{event :an_event do :new_state end }}
|
75
|
+
its(:events){should_not be_empty}
|
76
|
+
its(:events){should include(:an_event)}
|
77
|
+
its(:events){should have(1).item}
|
78
|
+
context "when the event is triggered" do
|
79
|
+
describe "the new state" do
|
80
|
+
specify{subject.event(:an_event).should be(:new_state)}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
context "with a Hash map" do
|
85
|
+
before{subject.build{event :an_event=>:new_state}}
|
86
|
+
its(:events){should_not be_empty}
|
87
|
+
its(:events){should include(:an_event)}
|
88
|
+
its(:events){should have(1).item}
|
89
|
+
context "when the event is triggered" do
|
90
|
+
describe "the new state" do
|
91
|
+
specify{subject.event(:an_event).should be(:new_state)}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#run" do
|
98
|
+
context "when triggered event exists" do
|
99
|
+
before{subject.event(:an_event){:new_state}}
|
100
|
+
describe "the new state" do
|
101
|
+
specify{subject.run{event :an_event}.should be(:new_state)}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
context "when triggered event does not exist" do
|
105
|
+
before{subject.event(:default){:new_state}}
|
106
|
+
it "should trigger default event" do
|
107
|
+
subject.run{event :an_event}.should be(:new_state)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
context "when neither triggered event nor default event exists" do
|
111
|
+
it "should return nil" do
|
112
|
+
subject.run{event :an_event}.should be_nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../lib/barebone-fsm'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: barebone-fsm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-05-18 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: A barebone implementation of the finite state machine keeping simplicity
|
15
15
|
in mind.
|
@@ -20,8 +20,12 @@ extra_rdoc_files:
|
|
20
20
|
- README.rdoc
|
21
21
|
files:
|
22
22
|
- example/door.rb
|
23
|
+
- example/example_helper.rb
|
23
24
|
- example/microwave.rb
|
24
|
-
-
|
25
|
+
- example/vehicle_class.rb
|
26
|
+
- spec/fsm_spec.rb
|
27
|
+
- spec/fsmstate_spec.rb
|
28
|
+
- spec/spec_helper.rb
|
25
29
|
- lib/barebone-fsm.rb
|
26
30
|
- README.rdoc
|
27
31
|
- MIT-LICENSE
|
data/spec/barebone-fsm_spec.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
require '../lib/barebone-fsm'
|
2
|
-
|
3
|
-
describe FSM::FSM do
|
4
|
-
|
5
|
-
context "with default state" do
|
6
|
-
|
7
|
-
before :each do
|
8
|
-
@fsm = FSM::FSM.new :default
|
9
|
-
end
|
10
|
-
|
11
|
-
it "should set the default state" do
|
12
|
-
@fsm.build do
|
13
|
-
state :start do
|
14
|
-
end
|
15
|
-
end
|
16
|
-
@fsm[].state.should be_eql(:start)
|
17
|
-
@fsm.run do
|
18
|
-
event :undefined
|
19
|
-
end
|
20
|
-
@fsm[].state.should be_eql(:default)
|
21
|
-
end
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
context "without default state" do
|
26
|
-
|
27
|
-
before :each do
|
28
|
-
@fsm = FSM::FSM.new
|
29
|
-
end
|
30
|
-
|
31
|
-
it "should not set the default state" do
|
32
|
-
@fsm.build do
|
33
|
-
state :start do
|
34
|
-
end
|
35
|
-
end
|
36
|
-
@fsm[].state.should be_eql(:start)
|
37
|
-
@fsm.run do
|
38
|
-
event :undefined
|
39
|
-
end
|
40
|
-
@fsm[].state.should be_eql(:start)
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
it "should create states on the fly" do
|
46
|
-
@fsm = FSM::FSM.new
|
47
|
-
@fsm[:new_state].state.should eq(:new_state)
|
48
|
-
@fsm.state(:another_state).state.should eq(:another_state)
|
49
|
-
end
|
50
|
-
|
51
|
-
it "should set current state to the first accessed state" do
|
52
|
-
@fsm = FSM::FSM.new
|
53
|
-
@fsm[:new_state].state.should eq(:new_state)
|
54
|
-
@fsm.state(:another_state).state.should eq(:another_state)
|
55
|
-
@fsm.state().state.should eq(:new_state)
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|