statemachine 0.2.0 → 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/CHANGES +20 -0
- data/README +38 -0
- data/Rakefile +8 -42
- data/TODO +3 -2
- data/lib/statemachine.rb +20 -2
- data/lib/statemachine/action_invokation.rb +1 -1
- data/lib/statemachine/builder.rb +153 -9
- data/lib/statemachine/state.rb +13 -3
- data/lib/statemachine/{state_machine.rb → statemachine.rb} +47 -10
- data/lib/statemachine/{super_state.rb → superstate.rb} +9 -1
- data/lib/statemachine/transition.rb +1 -1
- data/lib/statemachine/version.rb +2 -2
- data/spec/builder_spec.rb +25 -9
- data/spec/default_transition_spec.rb +65 -0
- data/spec/history_spec.rb +74 -0
- data/spec/sm_entry_exit_actions_spec.rb +13 -2
- data/spec/sm_odds_n_ends_spec.rb +5 -30
- metadata +12 -6
data/CHANGES
CHANGED
@@ -1,5 +1,25 @@
|
|
1
1
|
= Statemachine Changelog
|
2
2
|
|
3
|
+
== Version 0.3.0
|
4
|
+
|
5
|
+
Feature enhancements
|
6
|
+
* added default transitions
|
7
|
+
* added default history for superstates
|
8
|
+
* the context method in the builder will set the context's statemachine variable if the context respond_to?(:statemachine=)
|
9
|
+
|
10
|
+
Behavior Fixes
|
11
|
+
* the entry action of the startstate is called when the statemachine starts or is reset.
|
12
|
+
* resetting the statemachine will reset the history state for each superstate.
|
13
|
+
|
14
|
+
== Version 0.2.2
|
15
|
+
|
16
|
+
Minor plugin update
|
17
|
+
* introduced before_event and after_event hooks for controllers
|
18
|
+
|
19
|
+
== Version 0.2.1
|
20
|
+
|
21
|
+
Rails Plugin.
|
22
|
+
* Rails plugin introduced
|
3
23
|
|
4
24
|
== Version 0.2.0
|
5
25
|
|
data/README
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= Statemachine Gem
|
2
|
+
|
3
|
+
This Ruby Library enables simple creation of full-features Finite Statemachines.
|
4
|
+
|
5
|
+
== API
|
6
|
+
|
7
|
+
Where to start:
|
8
|
+
|
9
|
+
* Statemachine.build
|
10
|
+
* Statemachine::SuperstateBuilding
|
11
|
+
* Statemachine::StateBuilding
|
12
|
+
* Statemachine::Statemachine
|
13
|
+
|
14
|
+
== Documentation
|
15
|
+
|
16
|
+
Some documentation is available here in this RDOC documentation.
|
17
|
+
You may also find useful documentation on the Statemachine website: http://statemachine.rubyforge.org
|
18
|
+
|
19
|
+
A detailed tutorial and overview of Finite State Machines and this library can be found
|
20
|
+
at http://blog.8thlight.com/articles/2006/11/17/understanding-statemachines-part-1-states-and-transitions.
|
21
|
+
|
22
|
+
== License
|
23
|
+
|
24
|
+
Copyright (C) 2006 Micah Martin
|
25
|
+
|
26
|
+
This library is free software; you can redistribute it and/or
|
27
|
+
modify it under the terms of the GNU Lesser General Public
|
28
|
+
License as published by the Free Software Foundation; either
|
29
|
+
version 2.1 of the License, or (at your option) any later version.
|
30
|
+
|
31
|
+
This library is distributed in the hope that it will be useful,
|
32
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
33
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
34
|
+
Lesser General Public License for more details.
|
35
|
+
|
36
|
+
You should have received a copy of the GNU Lesser General Public
|
37
|
+
License along with this library; if not, write to the Free Software
|
38
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
data/Rakefile
CHANGED
@@ -7,13 +7,6 @@ require 'rake/rdoctask'
|
|
7
7
|
require 'spec/rake/spectask'
|
8
8
|
require 'statemachine'
|
9
9
|
|
10
|
-
# Some of the tasks are in separate files since they are also part of the website documentation
|
11
|
-
"load File.dirname(__FILE__) + '/tasks/examples.rake'
|
12
|
-
load File.dirname(__FILE__) + '/tasks/examples_specdoc.rake'
|
13
|
-
load File.dirname(__FILE__) + '/tasks/examples_with_rcov.rake'
|
14
|
-
load File.dirname(__FILE__) + '/tasks/failing_examples_with_html.rake'
|
15
|
-
load File.dirname(__FILE__) + '/tasks/verify_rcov.rake'"
|
16
|
-
|
17
10
|
PKG_NAME = "statemachine"
|
18
11
|
PKG_VERSION = Statemachine::VERSION::STRING
|
19
12
|
PKG_TAG = Statemachine::VERSION::TAG
|
@@ -22,7 +15,6 @@ PKG_FILES = FileList[
|
|
22
15
|
'[A-Z]*',
|
23
16
|
'lib/**/*.rb',
|
24
17
|
'spec/**/*.rb'
|
25
|
-
# 'examples/**/*',
|
26
18
|
]
|
27
19
|
|
28
20
|
task :default => :spec
|
@@ -30,52 +22,26 @@ task :default => :spec
|
|
30
22
|
desc "Run all specs"
|
31
23
|
Spec::Rake::SpecTask.new do |t|
|
32
24
|
t.spec_files = FileList['spec/**/*_spec.rb']
|
33
|
-
# t.spec_opts = ['--diff','--color']
|
34
|
-
# t.rcov = true
|
35
|
-
# t.rcov_dir = 'doc/output/coverage'
|
36
|
-
# t.rcov_opts = ['--exclude', 'spec\/spec,bin\/spec']"
|
37
25
|
end
|
38
26
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
# raise "ERROR while running webgen: #{output}" if output =~ /ERROR/n || $? != 0
|
47
|
-
# end
|
48
|
-
#end
|
49
|
-
|
50
|
-
#desc 'Generate RDoc'
|
51
|
-
#rd = Rake::RDocTask.new do |rdoc|
|
52
|
-
# rdoc.rdoc_dir = 'doc/output/rdoc'
|
53
|
-
# rdoc.options << '--title' << 'RSpec' << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
54
|
-
# rdoc.rdoc_files.include('README', 'CHANGES', 'EXAMPLES.rd', 'lib/**/*.rb')
|
55
|
-
#end
|
56
|
-
#task :rdoc => :examples_specdoc # We generate EXAMPLES.rd
|
27
|
+
desc 'Generate RDoc'
|
28
|
+
rd = Rake::RDocTask.new do |rdoc|
|
29
|
+
rdoc.rdoc_dir = 'doc/website/output/rdoc'
|
30
|
+
rdoc.options << '--title' << 'Statemachine' << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
31
|
+
rdoc.rdoc_files.include('README', 'CHANGES', 'lib/**/*.rb')
|
32
|
+
end
|
33
|
+
task :rdoc
|
57
34
|
|
58
35
|
spec = Gem::Specification.new do |s|
|
59
36
|
s.name = PKG_NAME
|
60
37
|
s.version = PKG_VERSION
|
61
38
|
s.summary = Statemachine::VERSION::DESCRIPTION
|
62
|
-
s.description =
|
63
|
-
Statemachine is a ruby library for building Finite State Machines (FSM), also known as Finite State Automata (FSA).
|
64
|
-
EOF
|
65
|
-
|
39
|
+
s.description = "Statemachine is a ruby library for building Finite State Machines (FSM), also known as Finite State Automata (FSA)."
|
66
40
|
s.files = PKG_FILES.to_a
|
67
41
|
s.require_path = 'lib'
|
68
|
-
|
69
|
-
# s.has_rdoc = true
|
70
|
-
# s.rdoc_options = rd.options
|
71
|
-
# s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$|^EXAMPLES.rd$/ }.to_a
|
72
|
-
|
73
42
|
s.test_files = Dir.glob('spec/*_spec.rb')
|
74
43
|
s.require_path = 'lib'
|
75
44
|
s.autorequire = 'statemachine'
|
76
|
-
# s.bindir = "bin"
|
77
|
-
# s.executables = ["spec"]
|
78
|
-
# s.default_executable = "spec"
|
79
45
|
s.author = "Micah Martin"
|
80
46
|
s.email = "statemachine-devel@rubyforge.org"
|
81
47
|
s.homepage = "http://statemachine.rubyforge.org"
|
data/TODO
CHANGED
data/lib/statemachine.rb
CHANGED
@@ -1,7 +1,25 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C) 2006 Micah Martin
|
3
|
+
#
|
4
|
+
# This library is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of the GNU Lesser General Public
|
6
|
+
# License as published by the Free Software Foundation; either
|
7
|
+
# version 2.1 of the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This library is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
12
|
+
# Lesser General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU Lesser General Public
|
15
|
+
# License along with this library; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
#++
|
18
|
+
|
1
19
|
require 'statemachine/action_invokation'
|
2
20
|
require 'statemachine/state'
|
3
|
-
require 'statemachine/
|
21
|
+
require 'statemachine/superstate'
|
4
22
|
require 'statemachine/transition'
|
5
|
-
require 'statemachine/
|
23
|
+
require 'statemachine/statemachine'
|
6
24
|
require 'statemachine/builder'
|
7
25
|
require 'statemachine/version'
|
data/lib/statemachine/builder.rb
CHANGED
@@ -1,13 +1,36 @@
|
|
1
1
|
module Statemachine
|
2
|
-
|
2
|
+
|
3
|
+
# The starting point for building instances of Statemachine.
|
4
|
+
# The block passed in should contain all the declarations for all
|
5
|
+
# states, events, and actions with in the statemachine.
|
6
|
+
#
|
7
|
+
# Sample: Turnstyle
|
8
|
+
#
|
9
|
+
# sm = Statemachine.build do
|
10
|
+
# trans :locked, :coin, :unlocked, :unlock
|
11
|
+
# trans :unlocked, :pass, :locked, :lock
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# An optional statemachine paramter may be passed in to modify
|
15
|
+
# an existing statemachine instance.
|
16
|
+
#
|
17
|
+
# Actions:
|
18
|
+
# Where ever an action paramter is used, it may take on one of three forms:
|
19
|
+
# 1. Symbols: will execute a method by the same name on the _context_
|
20
|
+
# 2. String: Ruby code that will be executed within the binding of the _context_
|
21
|
+
# 3. Proc: Will be executed within the binding of the _context_
|
22
|
+
#
|
23
|
+
# See Statemachine::SuperstateBuilding
|
24
|
+
# See Statemachine::StateBuilding
|
25
|
+
#
|
3
26
|
def self.build(statemachine = nil, &block)
|
4
27
|
builder = statemachine ? StatemachineBuilder.new(statemachine) : StatemachineBuilder.new
|
5
28
|
builder.instance_eval(&block)
|
6
29
|
builder.statemachine.reset
|
7
30
|
return builder.statemachine
|
8
31
|
end
|
9
|
-
|
10
|
-
class Builder
|
32
|
+
|
33
|
+
class Builder #:nodoc:
|
11
34
|
attr_reader :statemachine
|
12
35
|
|
13
36
|
def initialize(statemachine)
|
@@ -29,54 +52,164 @@ module Statemachine
|
|
29
52
|
return state
|
30
53
|
end
|
31
54
|
end
|
32
|
-
|
55
|
+
|
56
|
+
# The builder module used to declare states.
|
33
57
|
module StateBuilding
|
34
58
|
attr_reader :subject
|
35
59
|
|
60
|
+
# Declares that the state responds to the spcified event.
|
61
|
+
# The +event+ paramter should be a Symbol.
|
62
|
+
# The +destination_id+, which should also be a Symbol, is the id of the state
|
63
|
+
# that will event will transition into.
|
64
|
+
#
|
65
|
+
# The 3rd +action+ paramter is optional
|
66
|
+
#
|
67
|
+
# sm = Statemachine.build do
|
68
|
+
# state :locked do
|
69
|
+
# event :coin, :unlocked, :unlock
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
36
73
|
def event(event, destination_id, action = nil)
|
37
74
|
@subject.add(Transition.new(@subject.id, destination_id, event, action))
|
38
75
|
end
|
39
76
|
|
77
|
+
# Declare the entry action for the state.
|
78
|
+
#
|
79
|
+
# sm = Statemachine.build do
|
80
|
+
# state :locked do
|
81
|
+
# on_entry :lock
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
40
85
|
def on_entry(entry_action)
|
41
86
|
@subject.entry_action = entry_action
|
42
87
|
end
|
43
|
-
|
88
|
+
|
89
|
+
# Declare the exit action for the state.
|
90
|
+
#
|
91
|
+
# sm = Statemachine.build do
|
92
|
+
# state :locked do
|
93
|
+
# on_exit :unlock
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
#
|
44
97
|
def on_exit(exit_action)
|
45
98
|
@subject.exit_action = exit_action
|
46
99
|
end
|
100
|
+
|
101
|
+
# Declare a default transition for the state. Any event that is not already handled
|
102
|
+
# by the state will be handled by this transition.
|
103
|
+
#
|
104
|
+
# sm = Statemachine.build do
|
105
|
+
# state :locked do
|
106
|
+
# default :unlock, :action
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
def default(destination_id, action = nil)
|
111
|
+
@subject.default_transition = Transition.new(@subject.id, destination_id, nil, action)
|
112
|
+
end
|
47
113
|
end
|
48
114
|
|
115
|
+
# The builder module used to declare superstates.
|
49
116
|
module SuperstateBuilding
|
50
117
|
attr_reader :subject
|
51
|
-
|
118
|
+
|
119
|
+
# Define a state within the statemachine or superstate.
|
120
|
+
#
|
121
|
+
# sm = Statemachine.build do
|
122
|
+
# state :locked do
|
123
|
+
# #define the state
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
#
|
52
127
|
def state(id, &block)
|
53
128
|
builder = StateBuilder.new(id, @subject, @statemachine)
|
54
129
|
builder.instance_eval(&block) if block
|
55
130
|
end
|
56
|
-
|
131
|
+
|
132
|
+
# Define a superstate within the statemachine or superstate.
|
133
|
+
#
|
134
|
+
# sm = Statemachine.build do
|
135
|
+
# superstate :operational do
|
136
|
+
# #define superstate
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
57
140
|
def superstate(id, &block)
|
58
141
|
builder = SuperstateBuilder.new(id, @subject, @statemachine)
|
59
142
|
builder.instance_eval(&block)
|
60
143
|
end
|
61
|
-
|
144
|
+
|
145
|
+
# Declares a transition within the superstate or statemachine.
|
146
|
+
# The +origin_id+, a Symbol, identifies the starting state for this transition. The state
|
147
|
+
# identified by +origin_id+ will be created within the statemachine or superstate which this
|
148
|
+
# transition is declared.
|
149
|
+
# The +event+ paramter should be a Symbol.
|
150
|
+
# The +destination_id+, which should also be a Symbol, is the id of the state that will
|
151
|
+
# event will transition into. This method will not create destination states within the
|
152
|
+
# current statemachine of superstate. If the state destination state should exist here,
|
153
|
+
# that declare with with the +state+ method or declare a transition starting at the state.
|
154
|
+
#
|
155
|
+
# sm = Statemachine.build do
|
156
|
+
# trans :locked, :coin, :unlocked, :unlock
|
157
|
+
# end
|
158
|
+
#
|
62
159
|
def trans(origin_id, event, destination_id, action = nil)
|
63
160
|
origin = acquire_state_in(origin_id, @subject)
|
64
161
|
origin.add(Transition.new(origin_id, destination_id, event, action))
|
65
162
|
end
|
66
163
|
|
164
|
+
# Specifies the startstate for the statemachine or superstate. The state must
|
165
|
+
# exist within the scope.
|
166
|
+
#
|
167
|
+
# sm = Statemachine.build do
|
168
|
+
# startstate :locked
|
169
|
+
# end
|
170
|
+
#
|
67
171
|
def startstate(startstate_id)
|
68
172
|
@subject.startstate_id = startstate_id
|
69
173
|
end
|
70
174
|
|
175
|
+
# Allows the declaration of entry actions without using the +state+ method. +id+ is identifies
|
176
|
+
# the state to which the entry action will be added.
|
177
|
+
#
|
178
|
+
# sm = Statemachine.build do
|
179
|
+
# trans :locked, :coin, :unlocked
|
180
|
+
# on_entry_of :unlocked, :unlock
|
181
|
+
# end
|
182
|
+
#
|
71
183
|
def on_entry_of(id, action)
|
72
184
|
@statemachine.get_state(id).entry_action = action
|
73
185
|
end
|
74
186
|
|
187
|
+
# Allows the declaration of exit actions without using the +state+ method. +id+ is identifies
|
188
|
+
# the state to which the exit action will be added.
|
189
|
+
#
|
190
|
+
# sm = Statemachine.build do
|
191
|
+
# trans :locked, :coin, :unlocked
|
192
|
+
# on_exit_of :locked, :unlock
|
193
|
+
# end
|
194
|
+
#
|
75
195
|
def on_exit_of(id, action)
|
76
196
|
@statemachine.get_state(id).exit_action = action
|
77
197
|
end
|
198
|
+
|
199
|
+
# Used to specify the default state held by the history pseudo state of the superstate.
|
200
|
+
#
|
201
|
+
# sm = Statemachine.build do
|
202
|
+
# superstate :operational do
|
203
|
+
# default_history :state_id
|
204
|
+
# end
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
def default_history(id)
|
208
|
+
@subject.default_history = @statemachine.get_state(id)
|
209
|
+
end
|
78
210
|
end
|
79
211
|
|
212
|
+
# Builder class used to define states. Creates by SuperstateBuilding#state
|
80
213
|
class StateBuilder < Builder
|
81
214
|
include StateBuilding
|
82
215
|
|
@@ -85,7 +218,8 @@ module Statemachine
|
|
85
218
|
@subject = acquire_state_in(id, superstate)
|
86
219
|
end
|
87
220
|
end
|
88
|
-
|
221
|
+
|
222
|
+
# Builder class used to define superstates. Creates by SuperstateBuilding#superstate
|
89
223
|
class SuperstateBuilder < Builder
|
90
224
|
include StateBuilding
|
91
225
|
include SuperstateBuilding
|
@@ -98,6 +232,7 @@ module Statemachine
|
|
98
232
|
end
|
99
233
|
end
|
100
234
|
|
235
|
+
# Created by Statemachine.build as the root context for building the statemachine.
|
101
236
|
class StatemachineBuilder < Builder
|
102
237
|
include SuperstateBuilding
|
103
238
|
|
@@ -106,8 +241,17 @@ module Statemachine
|
|
106
241
|
@subject = @statemachine.root
|
107
242
|
end
|
108
243
|
|
244
|
+
# Used the set the context of the statemahine within the builder.
|
245
|
+
#
|
246
|
+
# sm = Statemachine.build do
|
247
|
+
# ...
|
248
|
+
# context MyContext.new
|
249
|
+
# end
|
250
|
+
#
|
251
|
+
# Statemachine.context may also be used.
|
109
252
|
def context(a_context)
|
110
253
|
@statemachine.context = a_context
|
254
|
+
a_context.statemachine = @statemachine if a_context.respond_to?(:statemachine=)
|
111
255
|
end
|
112
256
|
end
|
113
257
|
|
data/lib/statemachine/state.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Statemachine
|
2
2
|
|
3
|
-
class State
|
3
|
+
class State #:nodoc:
|
4
4
|
|
5
5
|
attr_reader :id, :statemachine, :superstate
|
6
|
-
attr_accessor :entry_action, :exit_action
|
6
|
+
attr_accessor :entry_action, :exit_action, :default_transition
|
7
7
|
|
8
8
|
def initialize(id, superstate, state_machine)
|
9
9
|
@id = id
|
@@ -20,13 +20,20 @@ module Statemachine
|
|
20
20
|
return @superstate ? @transitions.merge(@superstate.transitions) : @transitions
|
21
21
|
end
|
22
22
|
|
23
|
+
def transition_for(event)
|
24
|
+
transition = @transitions[event]
|
25
|
+
transition = @default_transition if not transition
|
26
|
+
transition = @superstate.transition_for(event) if @superstate and not transition
|
27
|
+
return transition
|
28
|
+
end
|
29
|
+
|
23
30
|
def exit(args)
|
24
31
|
@statemachine.trace("\texiting #{self}")
|
25
32
|
@statemachine.invoke_action(@exit_action, args, "exit action for #{self}") if @exit_action
|
26
33
|
@superstate.substate_exiting(self) if @superstate
|
27
34
|
end
|
28
35
|
|
29
|
-
def enter(args)
|
36
|
+
def enter(args=[])
|
30
37
|
@statemachine.trace("\tentering #{self}")
|
31
38
|
@statemachine.invoke_action(@entry_action, args, "entry action for #{self}") if @entry_action
|
32
39
|
end
|
@@ -38,6 +45,9 @@ module Statemachine
|
|
38
45
|
def is_concrete?
|
39
46
|
return true
|
40
47
|
end
|
48
|
+
|
49
|
+
def reset
|
50
|
+
end
|
41
51
|
|
42
52
|
def to_s
|
43
53
|
return "'#{id}' state"
|
@@ -3,34 +3,66 @@ module Statemachine
|
|
3
3
|
class StatemachineException < Exception
|
4
4
|
end
|
5
5
|
|
6
|
+
# Used at runtime to execute the behavior of the statemachine.
|
7
|
+
# Should be created by using the Statemachine.build method.
|
8
|
+
#
|
9
|
+
# sm = Statemachine.build do
|
10
|
+
# trans :locked, :coin, :unlocked
|
11
|
+
# trans :unlocked, :pass, :locked:
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# sm.coin
|
15
|
+
# sm.state
|
16
|
+
#
|
17
|
+
# This class will accept any method that corresponds to an event. If the
|
18
|
+
# current state respons to the event, the appropriate transtion will be invoked.
|
19
|
+
# Otherwise an exception will be raised.
|
6
20
|
class Statemachine
|
7
|
-
|
8
21
|
include ActionInvokation
|
9
22
|
|
10
|
-
|
11
|
-
|
23
|
+
# The tracer is an IO object. The statemachine will write run time execution
|
24
|
+
# information to the +tracer+. Can be helpful in debugging. Defaults to nil.
|
25
|
+
attr_accessor :tracer
|
26
|
+
|
27
|
+
# Provides access to the +context+ of the statemachine. The context is a object
|
28
|
+
# where all actions will be invoked. This provides a way to separate logic from
|
29
|
+
# behavior. The statemachine is responsible for all the logic and the context
|
30
|
+
# is responsible for all the behavior.
|
31
|
+
attr_accessor :context
|
32
|
+
|
33
|
+
attr_reader :root #:nodoc:
|
12
34
|
|
35
|
+
# Should not be called directly. Instances of Statemachine::Statemachine are created
|
36
|
+
# through the Statemachine.build method.
|
13
37
|
def initialize(root = Superstate.new(:root, nil, self))
|
14
38
|
@root = root
|
15
39
|
@states = {}
|
16
40
|
end
|
17
41
|
|
42
|
+
# Returns the id of the startstate of the statemachine.
|
18
43
|
def startstate
|
19
44
|
return @root.startstate_id
|
20
45
|
end
|
21
46
|
|
47
|
+
# Resets the statemachine back to its starting state.
|
22
48
|
def reset
|
23
49
|
@state = get_state(@root.startstate_id)
|
24
50
|
while @state and not @state.is_concrete?
|
25
51
|
@state = get_state(@state.startstate_id)
|
26
52
|
end
|
27
53
|
raise StatemachineException.new("The state machine doesn't know where to start. Try setting the startstate.") if @state == nil
|
54
|
+
@state.enter
|
55
|
+
|
56
|
+
@states.values.each { |state| state.reset }
|
28
57
|
end
|
29
58
|
|
59
|
+
# Return the id of the current state of the statemachine.
|
30
60
|
def state
|
31
61
|
return @state.id
|
32
62
|
end
|
33
63
|
|
64
|
+
# You may change the state of the statemachine by using this method. The parameter should be
|
65
|
+
# the id of the desired state.
|
34
66
|
def state= value
|
35
67
|
if value.is_a? State
|
36
68
|
@state = value
|
@@ -41,11 +73,16 @@ module Statemachine
|
|
41
73
|
end
|
42
74
|
end
|
43
75
|
|
76
|
+
# The key method to exercise the statemachine. Any extra arguments supplied will be passed into
|
77
|
+
# any actions associated with the transition.
|
78
|
+
#
|
79
|
+
# Alternatively to this method, you may invoke methods, names the same as the event, on the statemachine.
|
80
|
+
# The advantage of using +process_event+ is that errors messages are more informative.
|
44
81
|
def process_event(event, *args)
|
45
82
|
event = event.to_sym
|
46
83
|
trace "Event: #{event}"
|
47
84
|
if @state
|
48
|
-
transition = @state.
|
85
|
+
transition = @state.transition_for(event)
|
49
86
|
if transition
|
50
87
|
transition.invoke(@state, self, args)
|
51
88
|
else
|
@@ -56,11 +93,11 @@ module Statemachine
|
|
56
93
|
end
|
57
94
|
end
|
58
95
|
|
59
|
-
def trace(message)
|
96
|
+
def trace(message) #:nodoc:
|
60
97
|
@tracer.puts message if @tracer
|
61
98
|
end
|
62
99
|
|
63
|
-
def get_state(id)
|
100
|
+
def get_state(id) #:nodoc:
|
64
101
|
if @states.has_key? id
|
65
102
|
return @states[id]
|
66
103
|
elsif(is_history_state_id?(id))
|
@@ -76,11 +113,11 @@ module Statemachine
|
|
76
113
|
end
|
77
114
|
end
|
78
115
|
|
79
|
-
def add_state(state)
|
116
|
+
def add_state(state) #:nodoc:
|
80
117
|
@states[state.id] = state
|
81
118
|
end
|
82
119
|
|
83
|
-
def has_state(id)
|
120
|
+
def has_state(id) #:nodoc:
|
84
121
|
if(is_history_state_id?(id))
|
85
122
|
return @states.has_key?(base_id(id))
|
86
123
|
else
|
@@ -88,8 +125,8 @@ module Statemachine
|
|
88
125
|
end
|
89
126
|
end
|
90
127
|
|
91
|
-
def method_missing(message, *args)
|
92
|
-
if @state and @state.
|
128
|
+
def method_missing(message, *args) #:nodoc:
|
129
|
+
if @state and @state.transition_for(message)
|
93
130
|
method = self.method(:process_event)
|
94
131
|
params = [message.to_sym].concat(args)
|
95
132
|
method.call(*params)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Statemachine
|
2
2
|
|
3
|
-
class Superstate < State
|
3
|
+
class Superstate < State #:nodoc:
|
4
4
|
|
5
5
|
attr_accessor :startstate_id
|
6
6
|
attr_reader :history
|
@@ -22,6 +22,14 @@ module Statemachine
|
|
22
22
|
def add_substates(*substate_ids)
|
23
23
|
do_substate_adding(substate_ids)
|
24
24
|
end
|
25
|
+
|
26
|
+
def default_history=(state)
|
27
|
+
@history = @default_history = state
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset
|
31
|
+
@history = @default_history
|
32
|
+
end
|
25
33
|
|
26
34
|
def to_s
|
27
35
|
return "'#{id}' superstate"
|
data/lib/statemachine/version.rb
CHANGED
data/spec/builder_spec.rb
CHANGED
@@ -39,6 +39,7 @@ context "Builder" do
|
|
39
39
|
end
|
40
40
|
|
41
41
|
specify "Adding a superstate to the switch" do
|
42
|
+
the_context = self
|
42
43
|
sm = Statemachine.build do
|
43
44
|
superstate :operation do
|
44
45
|
event :admin, :testing, lambda { @log << "testing" }
|
@@ -48,8 +49,8 @@ context "Builder" do
|
|
48
49
|
end
|
49
50
|
trans :testing, :resume, :operation, lambda { @log << "resuming" }
|
50
51
|
startstate :off
|
52
|
+
context the_context
|
51
53
|
end
|
52
|
-
sm.context = self
|
53
54
|
|
54
55
|
sm.state.should_be :off
|
55
56
|
sm.toggle
|
@@ -61,6 +62,7 @@ context "Builder" do
|
|
61
62
|
end
|
62
63
|
|
63
64
|
specify "entry exit actions" do
|
65
|
+
the_context = self
|
64
66
|
sm = Statemachine.build do
|
65
67
|
state :off do
|
66
68
|
on_entry Proc.new { @log << "enter off" }
|
@@ -68,18 +70,19 @@ context "Builder" do
|
|
68
70
|
on_exit Proc.new { @log << "exit off" }
|
69
71
|
end
|
70
72
|
trans :on, :toggle, :off, lambda { @log << "toggle off" }
|
73
|
+
context the_context
|
71
74
|
end
|
72
|
-
sm.context = self
|
73
75
|
|
74
76
|
sm.toggle
|
75
77
|
sm.state.should_be :on
|
76
78
|
sm.toggle
|
77
79
|
sm.state.should_be :off
|
78
80
|
|
79
|
-
@log.join(",").should_eql "exit off,toggle on,toggle off,enter off"
|
81
|
+
@log.join(",").should_eql "enter off,exit off,toggle on,toggle off,enter off"
|
80
82
|
end
|
81
83
|
|
82
84
|
specify "History state" do
|
85
|
+
the_context = self
|
83
86
|
sm = Statemachine.build do
|
84
87
|
superstate :operation do
|
85
88
|
event :admin, :testing, lambda { @log << "testing" }
|
@@ -92,29 +95,30 @@ context "Builder" do
|
|
92
95
|
end
|
93
96
|
trans :testing, :resume, :operation_H, lambda { @log << "resuming" }
|
94
97
|
startstate :off
|
98
|
+
context the_context
|
95
99
|
end
|
96
|
-
sm.context = self
|
97
100
|
|
98
101
|
sm.admin
|
99
102
|
sm.resume
|
100
103
|
sm.state.should_be :off
|
101
104
|
|
102
|
-
@log.join(",").should_eql "testing,resuming,enter off"
|
105
|
+
@log.join(",").should_eql "enter off,testing,resuming,enter off"
|
103
106
|
end
|
104
107
|
|
105
108
|
specify "entry and exit action created from superstate builder" do
|
109
|
+
the_context = self
|
106
110
|
sm = Statemachine.build do
|
107
111
|
trans :off, :toggle, :on, Proc.new { @log << "toggle on" }
|
108
112
|
on_entry_of :off, Proc.new { @log << "entering off" }
|
109
113
|
trans :on, :toggle, :off, Proc.new { @log << "toggle off" }
|
110
114
|
on_exit_of :on, Proc.new { @log << "exiting on" }
|
115
|
+
context the_context
|
111
116
|
end
|
112
|
-
sm.context = self
|
113
117
|
|
114
118
|
sm.toggle
|
115
119
|
sm.toggle
|
116
120
|
|
117
|
-
@log.join(",").should_eql "toggle on,exiting on,toggle off,entering off"
|
121
|
+
@log.join(",").should_eql "entering off,toggle on,exiting on,toggle off,entering off"
|
118
122
|
|
119
123
|
end
|
120
124
|
|
@@ -172,10 +176,22 @@ context "Builder" do
|
|
172
176
|
context widget
|
173
177
|
end
|
174
178
|
|
175
|
-
sm.context.
|
179
|
+
sm.context.should be(widget)
|
176
180
|
end
|
177
181
|
|
178
|
-
|
182
|
+
specify "statemachine will be set on context if possible" do
|
183
|
+
class Widget
|
184
|
+
attr_accessor :statemachine
|
185
|
+
end
|
186
|
+
widget = Widget.new
|
187
|
+
|
188
|
+
sm = Statemachine.build do
|
189
|
+
context widget
|
190
|
+
end
|
191
|
+
|
192
|
+
sm.context.should be(widget)
|
193
|
+
widget.statemachine.should be(sm)
|
194
|
+
end
|
179
195
|
|
180
196
|
end
|
181
197
|
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
context "Default Transition" do
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@sm = Statemachine.build do
|
7
|
+
trans :default_state, :start, :test_state
|
8
|
+
|
9
|
+
state :test_state do
|
10
|
+
default :default_state
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
specify "the default transition is set" do
|
16
|
+
test_state = @sm.get_state(:test_state)
|
17
|
+
test_state.default_transition.should_not be(nil)
|
18
|
+
test_state.transition_for(:fake_event).should_not be(nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
specify "Should go to the default_state with any event" do
|
22
|
+
@sm.start
|
23
|
+
@sm.fake_event
|
24
|
+
|
25
|
+
@sm.state.should eql(:default_state)
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "default transition can have actions" do
|
29
|
+
me = self
|
30
|
+
@sm = Statemachine.build do
|
31
|
+
trans :default_state, :start, :test_state
|
32
|
+
|
33
|
+
state :test_state do
|
34
|
+
default :default_state, :hi
|
35
|
+
end
|
36
|
+
context me
|
37
|
+
end
|
38
|
+
|
39
|
+
@sm.start
|
40
|
+
@sm.blah
|
41
|
+
|
42
|
+
@sm.state.should eql(:default_state)
|
43
|
+
@hi.should eql(true)
|
44
|
+
end
|
45
|
+
|
46
|
+
def hi
|
47
|
+
@hi = true
|
48
|
+
end
|
49
|
+
|
50
|
+
specify "superstate supports the default" do
|
51
|
+
@sm = Statemachine.build do
|
52
|
+
superstate :test_superstate do
|
53
|
+
default :default_state
|
54
|
+
|
55
|
+
state :start_state
|
56
|
+
state :default_state
|
57
|
+
end
|
58
|
+
|
59
|
+
startstate :start_state
|
60
|
+
end
|
61
|
+
|
62
|
+
@sm.blah
|
63
|
+
@sm.state.should eql(:default_state)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
context "History States" do
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@sm = Statemachine.build do
|
7
|
+
superstate :operate do
|
8
|
+
trans :on, :toggle, :off
|
9
|
+
trans :off, :toggle, :on
|
10
|
+
event :fiddle, :middle
|
11
|
+
end
|
12
|
+
trans :middle, :fiddle, :operate_H
|
13
|
+
trans :middle, :dream, :on_H
|
14
|
+
trans :middle, :faddle, :on
|
15
|
+
startstate :middle
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
specify "no history allowed for concrete states" do
|
20
|
+
lambda {
|
21
|
+
@sm.dream
|
22
|
+
}.should_raise(Statemachine::StatemachineException, "No history exists for 'on' state since it is not a super state.")
|
23
|
+
end
|
24
|
+
|
25
|
+
specify "error when trying to use history that doesn't exist yet" do
|
26
|
+
lambda {
|
27
|
+
@sm.fiddle
|
28
|
+
}.should_raise(Statemachine::StatemachineException, "'operate' superstate doesn't have any history yet.")
|
29
|
+
end
|
30
|
+
|
31
|
+
specify "reseting the statemachine resets history" do
|
32
|
+
@sm.faddle
|
33
|
+
@sm.fiddle
|
34
|
+
@sm.get_state(:operate).history.id.should eql(:on)
|
35
|
+
|
36
|
+
@sm.reset
|
37
|
+
@sm.get_state(:operate).history.should eql(nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
context "History Default" do
|
43
|
+
|
44
|
+
setup do
|
45
|
+
@sm = Statemachine.build do
|
46
|
+
superstate :operate do
|
47
|
+
trans :on, :toggle, :off
|
48
|
+
trans :off, :toggle, :on
|
49
|
+
event :fiddle, :middle
|
50
|
+
default_history :on
|
51
|
+
end
|
52
|
+
trans :middle, :fiddle, :operate_H
|
53
|
+
startstate :middle
|
54
|
+
trans :middle, :faddle, :on
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
specify "default history" do
|
59
|
+
@sm.fiddle
|
60
|
+
@sm.state.should eql(:on)
|
61
|
+
end
|
62
|
+
|
63
|
+
specify "reseting the statemachine resets history" do
|
64
|
+
@sm.faddle
|
65
|
+
@sm.toggle
|
66
|
+
@sm.fiddle
|
67
|
+
@sm.get_state(:operate).history.id.should eql(:off)
|
68
|
+
|
69
|
+
@sm.reset
|
70
|
+
@sm.get_state(:operate).history.id.should eql(:on)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
@@ -59,12 +59,12 @@ context "State Machine Entry and Exit Actions" do
|
|
59
59
|
Statemachine.build(@sm) do
|
60
60
|
superstate :off_super do
|
61
61
|
on_exit Proc.new {@log << @sm.state}
|
62
|
-
|
62
|
+
state :off
|
63
63
|
event :toggle, :on, Proc.new { @log << "super_on" }
|
64
64
|
end
|
65
65
|
superstate :on_super do
|
66
66
|
on_entry Proc.new { @log << @sm.state }
|
67
|
-
|
67
|
+
state :on
|
68
68
|
event :toggle, :off, Proc.new { @log << "super_off" }
|
69
69
|
end
|
70
70
|
startstate :off
|
@@ -83,6 +83,17 @@ context "State Machine Entry and Exit Actions" do
|
|
83
83
|
@sm.state.should_be :off
|
84
84
|
end
|
85
85
|
|
86
|
+
specify "startstate's entry action should be called when the statemachine starts" do
|
87
|
+
the_context = self
|
88
|
+
@sm = Statemachine.build do
|
89
|
+
trans :a, :b, :c
|
90
|
+
on_entry_of :a, Proc.new { @log << "entering a" }
|
91
|
+
context the_context
|
92
|
+
end
|
93
|
+
|
94
|
+
@log.join(",").should eql("entering a")
|
95
|
+
end
|
96
|
+
|
86
97
|
|
87
98
|
|
88
99
|
end
|
data/spec/sm_odds_n_ends_spec.rb
CHANGED
@@ -28,40 +28,15 @@ context "State Machine Odds And Ends" do
|
|
28
28
|
@sm.state.should_be :on
|
29
29
|
end
|
30
30
|
|
31
|
-
end
|
32
|
-
|
33
|
-
context "Special States" do
|
34
|
-
|
35
|
-
setup do
|
36
|
-
@sm = Statemachine.build do |s|
|
37
|
-
s.superstate :operate do |o|
|
38
|
-
o.trans :on, :toggle, :off
|
39
|
-
o.trans :off, :toggle, :on
|
40
|
-
o.event :fiddle, :middle
|
41
|
-
end
|
42
|
-
s.trans :middle, :fiddle, :operate_H
|
43
|
-
s.trans :middle, :push, :stuck
|
44
|
-
s.trans :middle, :dream, :on_H
|
45
|
-
s.startstate :middle
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
31
|
specify "states without transitions are valid" do
|
32
|
+
@sm = Statemachine.build do
|
33
|
+
trans :middle, :push, :stuck
|
34
|
+
startstate :middle
|
35
|
+
end
|
36
|
+
|
50
37
|
@sm.push
|
51
38
|
@sm.state.should_be :stuck
|
52
39
|
end
|
53
|
-
|
54
|
-
specify "no history allowed for concrete states" do
|
55
|
-
lambda {
|
56
|
-
@sm.dream
|
57
|
-
}.should_raise(Statemachine::StatemachineException, "No history exists for 'on' state since it is not a super state.")
|
58
|
-
end
|
59
|
-
|
60
|
-
specify "error when trying to use history that doesn't exist yet" do
|
61
|
-
lambda {
|
62
|
-
@sm.fiddle
|
63
|
-
}.should_raise(Statemachine::StatemachineException, "'operate' superstate doesn't have any history yet.")
|
64
|
-
end
|
65
40
|
|
66
41
|
end
|
67
42
|
|
metadata
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.
|
2
|
+
rubygems_version: 0.9.1
|
3
3
|
specification_version: 1
|
4
4
|
name: statemachine
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date:
|
8
|
-
summary: Statemachine-0.
|
6
|
+
version: 0.3.0
|
7
|
+
date: 2007-04-18 00:00:00 -05:00
|
8
|
+
summary: Statemachine-0.3.0 - Statemachine Library for Ruby http://statemachine.rubyforge.org/
|
9
9
|
require_paths:
|
10
10
|
- lib
|
11
11
|
email: statemachine-devel@rubyforge.org
|
@@ -25,23 +25,27 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
25
25
|
platform: ruby
|
26
26
|
signing_key:
|
27
27
|
cert_chain:
|
28
|
+
post_install_message:
|
28
29
|
authors:
|
29
30
|
- Micah Martin
|
30
31
|
files:
|
31
32
|
- CHANGES
|
32
33
|
- LICENSE
|
33
34
|
- Rakefile
|
35
|
+
- README
|
34
36
|
- TODO
|
35
37
|
- lib/statemachine.rb
|
36
38
|
- lib/statemachine/action_invokation.rb
|
37
39
|
- lib/statemachine/builder.rb
|
38
40
|
- lib/statemachine/state.rb
|
39
|
-
- lib/statemachine/
|
40
|
-
- lib/statemachine/
|
41
|
+
- lib/statemachine/statemachine.rb
|
42
|
+
- lib/statemachine/superstate.rb
|
41
43
|
- lib/statemachine/transition.rb
|
42
44
|
- lib/statemachine/version.rb
|
43
45
|
- spec/action_invokation_spec.rb
|
44
46
|
- spec/builder_spec.rb
|
47
|
+
- spec/default_transition_spec.rb
|
48
|
+
- spec/history_spec.rb
|
45
49
|
- spec/sm_action_parameterization_spec.rb
|
46
50
|
- spec/sm_entry_exit_actions_spec.rb
|
47
51
|
- spec/sm_odds_n_ends_spec.rb
|
@@ -53,6 +57,8 @@ files:
|
|
53
57
|
test_files:
|
54
58
|
- spec/action_invokation_spec.rb
|
55
59
|
- spec/builder_spec.rb
|
60
|
+
- spec/default_transition_spec.rb
|
61
|
+
- spec/history_spec.rb
|
56
62
|
- spec/sm_action_parameterization_spec.rb
|
57
63
|
- spec/sm_entry_exit_actions_spec.rb
|
58
64
|
- spec/sm_odds_n_ends_spec.rb
|