MINT-statemachine 1.2.2
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 +135 -0
- data/LICENSE +16 -0
- data/MINT-statemachine.gemspec +27 -0
- data/README.rdoc +69 -0
- data/Rakefile +88 -0
- data/TODO +2 -0
- data/lib/statemachine.rb +26 -0
- data/lib/statemachine/action_invokation.rb +83 -0
- data/lib/statemachine/builder.rb +383 -0
- data/lib/statemachine/generate/dot_graph.rb +1 -0
- data/lib/statemachine/generate/dot_graph/dot_graph_statemachine.rb +127 -0
- data/lib/statemachine/generate/java.rb +1 -0
- data/lib/statemachine/generate/java/java_statemachine.rb +265 -0
- data/lib/statemachine/generate/src_builder.rb +48 -0
- data/lib/statemachine/generate/util.rb +50 -0
- data/lib/statemachine/parallelstate.rb +196 -0
- data/lib/statemachine/state.rb +102 -0
- data/lib/statemachine/statemachine.rb +279 -0
- data/lib/statemachine/stub_context.rb +26 -0
- data/lib/statemachine/superstate.rb +53 -0
- data/lib/statemachine/transition.rb +76 -0
- data/lib/statemachine/version.rb +17 -0
- data/spec/action_invokation_spec.rb +101 -0
- data/spec/builder_spec.rb +243 -0
- data/spec/default_transition_spec.rb +111 -0
- data/spec/generate/dot_graph/dot_graph_stagemachine_spec.rb +27 -0
- data/spec/generate/java/java_statemachine_spec.rb +349 -0
- data/spec/history_spec.rb +107 -0
- data/spec/noodle.rb +23 -0
- data/spec/sm_action_parameterization_spec.rb +99 -0
- data/spec/sm_activation_spec.rb +116 -0
- data/spec/sm_entry_exit_actions_spec.rb +99 -0
- data/spec/sm_odds_n_ends_spec.rb +67 -0
- data/spec/sm_parallel_state_spec.rb +207 -0
- data/spec/sm_simple_spec.rb +26 -0
- data/spec/sm_super_state_spec.rb +55 -0
- data/spec/sm_turnstile_spec.rb +76 -0
- data/spec/spec_helper.rb +121 -0
- data/spec/transition_spec.rb +107 -0
- metadata +115 -0
@@ -0,0 +1,383 @@
|
|
1
|
+
module Statemachine
|
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
|
+
#
|
26
|
+
def self.build(statemachine = nil, &block)
|
27
|
+
builder = statemachine ? StatemachineBuilder.new(statemachine) : StatemachineBuilder.new
|
28
|
+
builder.instance_eval(&block)
|
29
|
+
builder.statemachine.reset
|
30
|
+
return builder.statemachine
|
31
|
+
end
|
32
|
+
|
33
|
+
class Builder #:nodoc:
|
34
|
+
attr_reader :statemachine
|
35
|
+
|
36
|
+
def initialize(statemachine)
|
37
|
+
@statemachine = statemachine
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
def acquire_state_in(state_id, context)
|
42
|
+
return nil if state_id == nil
|
43
|
+
return state_id if state_id.is_a? State
|
44
|
+
state = nil
|
45
|
+
if @statemachine.has_state(state_id)
|
46
|
+
state = @statemachine.get_state(state_id)
|
47
|
+
else
|
48
|
+
state = State.new(state_id, context, @statemachine)
|
49
|
+
@statemachine.add_state(state)
|
50
|
+
end
|
51
|
+
context.startstate_id = state_id if context.startstate_id == nil
|
52
|
+
return state
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class ParallelBuilder
|
57
|
+
attr_reader :parallel_statemachine
|
58
|
+
|
59
|
+
def initialize(statemachines)
|
60
|
+
@parallel_statemachine = ParallelStatemachine.new statemachines
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# The builder module used to declare states.
|
67
|
+
module StateBuilding
|
68
|
+
attr_reader :subject
|
69
|
+
|
70
|
+
# Declares that the state responds to the spcified event.
|
71
|
+
# The +event+ paramter should be a Symbol.
|
72
|
+
# The +destination_id+, which should also be a Symbol, is the id of the state
|
73
|
+
# that will event will transition into.
|
74
|
+
#
|
75
|
+
# The 3rd +action+ paramter is optional
|
76
|
+
#
|
77
|
+
# sm = Statemachine.build do
|
78
|
+
# state :locked do
|
79
|
+
# event :coin, :unlocked, :unlock
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
def event(event, destination_id, action = nil, cond = true)
|
84
|
+
@subject.add(Transition.new(@subject.id, destination_id, event, action, cond))
|
85
|
+
end
|
86
|
+
|
87
|
+
def on_event(event, options)
|
88
|
+
self.event(event, options[:transition_to], options[:and_perform])
|
89
|
+
end
|
90
|
+
|
91
|
+
# Declare the entry action for the state.
|
92
|
+
#
|
93
|
+
# sm = Statemachine.build do
|
94
|
+
# state :locked do
|
95
|
+
# on_entry :lock
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
def on_entry(entry_action)
|
100
|
+
@subject.entry_action = entry_action
|
101
|
+
end
|
102
|
+
|
103
|
+
# Declare the exit action for the state.
|
104
|
+
#
|
105
|
+
# sm = Statemachine.build do
|
106
|
+
# state :locked do
|
107
|
+
# on_exit :unlock
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
def on_exit(exit_action)
|
112
|
+
@subject.exit_action = exit_action
|
113
|
+
end
|
114
|
+
|
115
|
+
# Declare a default transition for the state. Any event that is not already handled
|
116
|
+
# by the state will be handled by this transition.
|
117
|
+
#
|
118
|
+
# sm = Statemachine.build do
|
119
|
+
# state :locked do
|
120
|
+
# default :unlock, :action
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
def default(destination_id, action = nil, cond = true)
|
125
|
+
@subject.default_transition = Transition.new(@subject.id, destination_id, nil, action, cond)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# The builder module used to declare superstates.
|
130
|
+
module SuperstateBuilding
|
131
|
+
attr_reader :subject
|
132
|
+
|
133
|
+
# Define a state within the statemachine or superstate.
|
134
|
+
#
|
135
|
+
# sm = Statemachine.build do
|
136
|
+
# state :locked do
|
137
|
+
# #define the state
|
138
|
+
# end
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
def state(id, &block)
|
142
|
+
builder = StateBuilder.new(id, @subject, @statemachine)
|
143
|
+
builder.instance_eval(&block) if block
|
144
|
+
end
|
145
|
+
|
146
|
+
# Define a superstate within the statemachine or superstate.
|
147
|
+
#
|
148
|
+
# sm = Statemachine.build do
|
149
|
+
# superstate :operational do
|
150
|
+
# #define superstate
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
def superstate(id, &block)
|
155
|
+
builder = SuperstateBuilder.new(id, @subject, @statemachine)
|
156
|
+
builder.instance_eval(&block)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Declares a transition within the superstate or statemachine.
|
160
|
+
# The +origin_id+, a Symbol, identifies the starting state for this transition. The state
|
161
|
+
# identified by +origin_id+ will be created within the statemachine or superstate which this
|
162
|
+
# transition is declared.
|
163
|
+
# The +event+ paramter should be a Symbol.
|
164
|
+
# The +destination_id+, which should also be a Symbol, is the id of the state that will
|
165
|
+
# event will transition into. This method will not create destination states within the
|
166
|
+
# current statemachine of superstate. If the state destination state should exist here,
|
167
|
+
# that declare with with the +state+ method or declare a transition starting at the state.
|
168
|
+
#
|
169
|
+
# sm = Statemachine.build do
|
170
|
+
# trans :locked, :coin, :unlocked, :unlock
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
def trans(origin_id, event, destination_id, action = nil, cond = true)
|
174
|
+
origin = acquire_state_in(origin_id, @subject)
|
175
|
+
origin.add(Transition.new(origin_id, destination_id, event, action, cond))
|
176
|
+
end
|
177
|
+
|
178
|
+
def transition_from(origin_id, options)
|
179
|
+
trans(origin_id, options[:on_event], options[:transition_to], options[:and_perform])
|
180
|
+
end
|
181
|
+
|
182
|
+
# Specifies the startstate for the statemachine or superstate. The state must
|
183
|
+
# exist within the scope.
|
184
|
+
#
|
185
|
+
# sm = Statemachine.build do
|
186
|
+
# startstate :locked
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
def startstate(startstate_id)
|
190
|
+
@subject.startstate_id = startstate_id
|
191
|
+
end
|
192
|
+
|
193
|
+
# Allows the declaration of entry actions without using the +state+ method. +id+ is identifies
|
194
|
+
# the state to which the entry action will be added.
|
195
|
+
#
|
196
|
+
# sm = Statemachine.build do
|
197
|
+
# trans :locked, :coin, :unlocked
|
198
|
+
# on_entry_of :unlocked, :unlock
|
199
|
+
# end
|
200
|
+
#
|
201
|
+
def on_entry_of(id, action)
|
202
|
+
@statemachine.get_state(id).entry_action = action
|
203
|
+
end
|
204
|
+
|
205
|
+
# Allows the declaration of exit actions without using the +state+ method. +id+ is identifies
|
206
|
+
# the state to which the exit action will be added.
|
207
|
+
#
|
208
|
+
# sm = Statemachine.build do
|
209
|
+
# trans :locked, :coin, :unlocked
|
210
|
+
# on_exit_of :locked, :unlock
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
def on_exit_of(id, action)
|
214
|
+
@statemachine.get_state(id).exit_action = action
|
215
|
+
end
|
216
|
+
|
217
|
+
# Used to specify the default state held by the history pseudo state of the superstate.
|
218
|
+
#
|
219
|
+
# sm = Statemachine.build do
|
220
|
+
# superstate :operational do
|
221
|
+
# default_history :state_id
|
222
|
+
# end
|
223
|
+
# end
|
224
|
+
#
|
225
|
+
def default_history(id)
|
226
|
+
@subject.default_history = id
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Builder class used to define states. Creates by SuperstateBuilding#state
|
231
|
+
class StateBuilder < Builder
|
232
|
+
include StateBuilding
|
233
|
+
|
234
|
+
def initialize(id, superstate, statemachine)
|
235
|
+
super statemachine
|
236
|
+
@subject = acquire_state_in(id, superstate)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
module ParallelstateBuilding
|
241
|
+
attr_reader :subject
|
242
|
+
|
243
|
+
def parallel (id, &block)
|
244
|
+
builder = ParallelStateBuilder.new(id, @subject, @statemachine)
|
245
|
+
builder.instance_eval(&block)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Builder class used to define superstates. Creates by SuperstateBuilding#superstate
|
250
|
+
class SuperstateBuilder < Builder
|
251
|
+
include StateBuilding
|
252
|
+
include SuperstateBuilding
|
253
|
+
include ParallelstateBuilding
|
254
|
+
|
255
|
+
def initialize(id, superstate, statemachine)
|
256
|
+
super statemachine
|
257
|
+
@subject = Superstate.new(id, superstate, statemachine)
|
258
|
+
superstate.startstate_id = id if superstate.startstate_id == nil
|
259
|
+
|
260
|
+
# small patch to support redefinition of already existing states without
|
261
|
+
# loosing the already existing transformations. Used to overwrite states
|
262
|
+
# with superstates.
|
263
|
+
|
264
|
+
s = statemachine.get_state(id)
|
265
|
+
if (s)
|
266
|
+
s.transitions.each {|k,v|
|
267
|
+
@subject.add(v)
|
268
|
+
}
|
269
|
+
end
|
270
|
+
statemachine.add_state(@subject)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
|
276
|
+
|
277
|
+
# Created by Statemachine.build as the root context for building the statemachine.
|
278
|
+
class StatemachineBuilder < Builder
|
279
|
+
include SuperstateBuilding
|
280
|
+
include ParallelstateBuilding
|
281
|
+
|
282
|
+
def initialize(statemachine = Statemachine.new)
|
283
|
+
super statemachine
|
284
|
+
@subject = @statemachine.root
|
285
|
+
end
|
286
|
+
|
287
|
+
# Used the set the context of the statemahine within the builder.
|
288
|
+
#
|
289
|
+
# sm = Statemachine.build do
|
290
|
+
# ...
|
291
|
+
# context MyContext.new
|
292
|
+
# end
|
293
|
+
#
|
294
|
+
# Statemachine.context may also be used.
|
295
|
+
def context(a_context)
|
296
|
+
@statemachine.context = a_context
|
297
|
+
a_context.statemachine = @statemachine if a_context.respond_to?(:statemachine=)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Stubs the context. This makes statemachine immediately useable, even if functionless.
|
301
|
+
# The stub will print all the actions called so it's nice for trial runs.
|
302
|
+
#
|
303
|
+
# sm = Statemachine.build do
|
304
|
+
# ...
|
305
|
+
# stub_context :verbose => true
|
306
|
+
# end
|
307
|
+
#
|
308
|
+
# Statemachine.context may also be used.
|
309
|
+
def stub_context(options={})
|
310
|
+
require 'statemachine/stub_context'
|
311
|
+
context StubContext.new(options)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
# The builder module used to declare statemachines.
|
317
|
+
module StatemachineBuilding
|
318
|
+
attr_reader :subject
|
319
|
+
|
320
|
+
def statemachine (id, &block)
|
321
|
+
builder = StatemachineBuilder.new(Statemachine.new(@subject))
|
322
|
+
#builder = StatemachineBuilder.new
|
323
|
+
builder.instance_eval(&block) if block
|
324
|
+
if not @subject.is_a? Parallelstate
|
325
|
+
# Only reset statemachine if it's the root one. Otherwise
|
326
|
+
# the inital states on_entry function would be called!
|
327
|
+
builder.statemachine.reset
|
328
|
+
end
|
329
|
+
# puts "build statemachine #{builder.statemachine.inspect}"
|
330
|
+
|
331
|
+
@subject.add_statemachine builder.statemachine
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
class ParallelStateBuilder < Builder
|
336
|
+
include StatemachineBuilding
|
337
|
+
def initialize(id, superstate, statemachine)
|
338
|
+
super statemachine
|
339
|
+
@subject = Parallelstate.new(id, superstate, statemachine)
|
340
|
+
superstate.startstate_id = id if superstate.startstate_id == nil
|
341
|
+
statemachine.add_state(@subject)
|
342
|
+
#puts "added #{@subject.inspect}"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Created by Statemachine.build as the root context for building the statemachine.
|
347
|
+
class ParallelStatemachineBuilder < ParallelBuilder
|
348
|
+
include StatemachineBuilding
|
349
|
+
|
350
|
+
def initialize
|
351
|
+
super []
|
352
|
+
#@subject = @statemachine
|
353
|
+
end
|
354
|
+
|
355
|
+
# used the set the context of the statemahine within the builder.
|
356
|
+
#
|
357
|
+
# sm = Statemachine.build do
|
358
|
+
# ...
|
359
|
+
# context MyContext.new
|
360
|
+
# end
|
361
|
+
#
|
362
|
+
# Statemachine.context may also be used.
|
363
|
+
def context(a_context)
|
364
|
+
@statemachine.context = a_context
|
365
|
+
a_context.statemachine = @statemachine if a_context.respond_to?(:statemachine=)
|
366
|
+
end
|
367
|
+
|
368
|
+
# Stubs the context. This makes statemachine immediately useable, even if functionless.
|
369
|
+
# The stub will print all the actions called so it's nice for trial runs.
|
370
|
+
#
|
371
|
+
# sm = Statemachine.build do
|
372
|
+
# ...
|
373
|
+
# stub_context :verbose => true
|
374
|
+
# end
|
375
|
+
#
|
376
|
+
# Statemachine.context may also be used.
|
377
|
+
def stub_context(options={})
|
378
|
+
require 'statemachine/stub_context'
|
379
|
+
context StubContext.new(options)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'statemachine/generate/dot_graph/dot_graph_statemachine'
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'statemachine/generate/util'
|
2
|
+
require 'statemachine/generate/src_builder'
|
3
|
+
|
4
|
+
module Statemachine
|
5
|
+
class Statemachine
|
6
|
+
|
7
|
+
attr_reader :states
|
8
|
+
|
9
|
+
def to_dot(options = {})
|
10
|
+
generator = Generate::DotGraph::DotGraphStatemachine.new(self, options)
|
11
|
+
generator.generate!
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
module Generate
|
17
|
+
module DotGraph
|
18
|
+
|
19
|
+
class DotGraphStatemachine
|
20
|
+
|
21
|
+
include Generate::Util
|
22
|
+
|
23
|
+
def initialize(sm, options)
|
24
|
+
@sm = sm
|
25
|
+
@output_dir = options[:output]
|
26
|
+
raise "Please specify an output directory. (:output => 'where/you/want/your/code')" if @output_dir.nil?
|
27
|
+
raise "Output dir '#{@output_dir}' doesn't exist." if !File.exist?(@output_dir)
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate!
|
31
|
+
explore_sm
|
32
|
+
save_output(src_file("main"), build_full_graph)
|
33
|
+
@sm.states.values.each do |state|
|
34
|
+
save_output(src_file("#{state.id}"), build_state_graph(state))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def explore_sm
|
41
|
+
@nodes = []
|
42
|
+
@transitions = []
|
43
|
+
@sm.states.values.each { |state|
|
44
|
+
state.transitions.values.each { |transition|
|
45
|
+
@nodes << transition.origin_id
|
46
|
+
@nodes << transition.destination_id
|
47
|
+
@transitions << transition
|
48
|
+
}
|
49
|
+
}
|
50
|
+
@nodes = @nodes.uniq
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_full_graph
|
54
|
+
builder = Generate::SrcBuilder.new
|
55
|
+
|
56
|
+
add_graph_header(builder, "main")
|
57
|
+
|
58
|
+
@nodes.each { |node| add_node(builder, node) }
|
59
|
+
builder << endl
|
60
|
+
|
61
|
+
@transitions.each do |transition|
|
62
|
+
add_transition(builder, transition)
|
63
|
+
end
|
64
|
+
|
65
|
+
add_graph_footer(builder)
|
66
|
+
|
67
|
+
return builder.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_state_graph(state)
|
71
|
+
builder = Generate::SrcBuilder.new
|
72
|
+
|
73
|
+
add_graph_header(builder, state.id)
|
74
|
+
|
75
|
+
state.transitions.values.each do |transition|
|
76
|
+
add_transition(builder, transition)
|
77
|
+
end
|
78
|
+
|
79
|
+
add_graph_footer(builder)
|
80
|
+
|
81
|
+
return builder.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_graph_header(builder, graph_name)
|
85
|
+
builder << "digraph #{graph_name} {" << endl
|
86
|
+
builder.indent!
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_graph_footer(builder)
|
90
|
+
builder.undent!
|
91
|
+
builder << "}" << endl
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_node(builder, node)
|
95
|
+
builder << node
|
96
|
+
builder << " [ href = \"#{node}.svg\"]"
|
97
|
+
builder << endl
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_transition(builder, transition)
|
101
|
+
builder << transition.origin_id
|
102
|
+
builder << " -> "
|
103
|
+
builder << transition.destination_id
|
104
|
+
builder << " [ "
|
105
|
+
builder << "label = #{transition.event} "
|
106
|
+
builder << "]"
|
107
|
+
builder << endl
|
108
|
+
end
|
109
|
+
|
110
|
+
def src_file(name)
|
111
|
+
return name if @output_dir.nil?
|
112
|
+
path = @output_dir
|
113
|
+
answer = File.join(path, "#{name}.dot")
|
114
|
+
return answer
|
115
|
+
end
|
116
|
+
|
117
|
+
def save_output(filename, content)
|
118
|
+
if @output_dir.nil?
|
119
|
+
say "Writing to file: #{filename}"
|
120
|
+
else
|
121
|
+
create_file(filename, content)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|