statemachine 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|