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,102 @@
|
|
1
|
+
module Statemachine
|
2
|
+
|
3
|
+
class State #:nodoc:
|
4
|
+
|
5
|
+
attr_reader :id, :statemachine
|
6
|
+
attr_accessor :entry_action, :exit_action, :superstate
|
7
|
+
attr_writer :default_transition
|
8
|
+
|
9
|
+
def initialize(id, superstate, state_machine)
|
10
|
+
@id = id
|
11
|
+
@superstate = superstate
|
12
|
+
@statemachine = state_machine
|
13
|
+
@transitions = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(transition)
|
17
|
+
@transitions[transition.event] = transition
|
18
|
+
end
|
19
|
+
|
20
|
+
def transitions
|
21
|
+
return @superstate ? @transitions.merge(@superstate.transitions) : @transitions
|
22
|
+
end
|
23
|
+
|
24
|
+
def non_default_transition_for(event)
|
25
|
+
transition = @transitions[event]
|
26
|
+
if @superstate
|
27
|
+
transition = @superstate.non_default_transition_for(event) if @superstate and @superstate.is_parallel == false and not transition
|
28
|
+
end
|
29
|
+
return transition
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_transition
|
33
|
+
return @default_transition if @default_transition
|
34
|
+
return @superstate.default_transition if @superstate
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def transition_for(event)
|
39
|
+
transition = non_default_transition_for(event)
|
40
|
+
transition = default_transition if not transition
|
41
|
+
return transition
|
42
|
+
end
|
43
|
+
|
44
|
+
def exit(args)
|
45
|
+
messenger = self.statemachine.messenger
|
46
|
+
message_queue = self.statemachine.message_queue
|
47
|
+
@statemachine.trace("\texiting #{self}")
|
48
|
+
@statemachine.invoke_action(@exit_action, args, "exit action for #{self}", messenger, message_queue) if @exit_action
|
49
|
+
@superstate.substate_exiting(self) if @superstate
|
50
|
+
end
|
51
|
+
|
52
|
+
def enter(args=[])
|
53
|
+
messenger = self.statemachine.messenger
|
54
|
+
message_queue = self.statemachine.message_queue
|
55
|
+
@statemachine.trace("\tentering #{self}")
|
56
|
+
@statemachine.invoke_action(@entry_action, args, "entry action for #{self}", messenger, message_queue) if @entry_action
|
57
|
+
end
|
58
|
+
|
59
|
+
def activate
|
60
|
+
@statemachine.state = self
|
61
|
+
if (@statemachine.is_parallel)
|
62
|
+
@statemachine.activation.call(self.id,@statemachine.is_parallel.abstract_states,@statemachine.is_parallel.statemachine.states_id) if @statemachine.activation
|
63
|
+
else
|
64
|
+
|
65
|
+
@statemachine.activation.call(self.id,@statemachine.abstract_states,@statemachine.states_id) if @statemachine.activation
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def concrete?
|
70
|
+
return true
|
71
|
+
end
|
72
|
+
|
73
|
+
def resolve_startstate
|
74
|
+
return self
|
75
|
+
end
|
76
|
+
|
77
|
+
def reset
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
return "'#{id}' state"
|
83
|
+
end
|
84
|
+
|
85
|
+
def has_superstate(id)
|
86
|
+
return false if not @superstate
|
87
|
+
return true if @superstate.id == id
|
88
|
+
return @superstate.has_superstate(id)
|
89
|
+
end
|
90
|
+
|
91
|
+
def abstract_states
|
92
|
+
return [] if not superstate
|
93
|
+
return @superstate.abstract_states if not @superstate.is_parallel
|
94
|
+
[]
|
95
|
+
end
|
96
|
+
|
97
|
+
def is_parallel
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
module Statemachine
|
2
|
+
|
3
|
+
class StatemachineException < Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
class TransitionMissingException < Exception
|
7
|
+
end
|
8
|
+
|
9
|
+
# Used at runtime to execute the behavior of the statemachine.
|
10
|
+
# Should be created by using the Statemachine.build method.
|
11
|
+
#
|
12
|
+
# sm = Statemachine.build do
|
13
|
+
# trans :locked, :coin, :unlocked
|
14
|
+
# trans :unlocked, :pass, :locked:
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# sm.coin
|
18
|
+
# sm.state
|
19
|
+
#
|
20
|
+
# This class will accept any method that corresponds to an event. If the
|
21
|
+
# current state responds to the event, the appropriate transition will be invoked.
|
22
|
+
# Otherwise an exception will be raised.
|
23
|
+
class Statemachine
|
24
|
+
include ActionInvokation
|
25
|
+
|
26
|
+
# The tracer is an IO object. The statemachine will write run time execution
|
27
|
+
# information to the +tracer+. Can be helpful in debugging. Defaults to nil.
|
28
|
+
attr_accessor :tracer
|
29
|
+
|
30
|
+
# Provides access to the +context+ of the statemachine. The context is a object
|
31
|
+
# where all actions will be invoked. This provides a way to separate logic from
|
32
|
+
# behavior. The statemachine is responsible for all the logic and the context
|
33
|
+
# is responsible for all the behavior.
|
34
|
+
attr_reader :context
|
35
|
+
|
36
|
+
attr_reader :root, :states
|
37
|
+
attr_accessor :messenger, :message_queue, :is_parallel #:nodoc:
|
38
|
+
attr_accessor :activation
|
39
|
+
|
40
|
+
# Should not be called directly. Instances of Statemachine::Statemachine are created
|
41
|
+
# through the Statemachine.build method.
|
42
|
+
def initialize(root = Superstate.new(:root, nil, self))
|
43
|
+
@root = root
|
44
|
+
@states = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the id of the startstate of the statemachine.
|
48
|
+
def startstate
|
49
|
+
return @root.startstate_id
|
50
|
+
end
|
51
|
+
|
52
|
+
# Resets the statemachine back to its starting state.
|
53
|
+
def reset(startstate_id=nil)
|
54
|
+
|
55
|
+
if (startstate_id and @root.is_parallel) # called when enterin a parallel state or dierctly entering a child of a parallel state from outside the parallel state
|
56
|
+
@state = get_state(startstate_id)
|
57
|
+
else
|
58
|
+
@state = get_state(@root.startstate_id)
|
59
|
+
end
|
60
|
+
while @state and not @state.concrete?
|
61
|
+
@state = get_state(@state.startstate_id)
|
62
|
+
end
|
63
|
+
raise StatemachineException.new("The state machine doesn't know where to start. Try setting the startstate.") if @state == nil
|
64
|
+
@state.enter
|
65
|
+
@states.values.each { |state|
|
66
|
+
state.reset if not state.is_a? Parallelstate
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def context= c
|
71
|
+
@context = c
|
72
|
+
|
73
|
+
p = get_parallel
|
74
|
+
if p
|
75
|
+
p.context = c
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#Return the id of the current state of the statemachine.
|
80
|
+
def state
|
81
|
+
return @state.id
|
82
|
+
end
|
83
|
+
|
84
|
+
# returns an array with the ids of the current active states of the machine.
|
85
|
+
def states_id(atomic = true)
|
86
|
+
belongs, parallel = belongs_to_parallel(@state.id)
|
87
|
+
if belongs
|
88
|
+
return parallel.states
|
89
|
+
else
|
90
|
+
return [@state.id]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# returns an array with all currently active super states
|
95
|
+
def abstract_states
|
96
|
+
@state.abstract_states
|
97
|
+
end
|
98
|
+
|
99
|
+
# You may change the state of the statemachine by using this method. The parameter should be
|
100
|
+
# the id of the desired state.
|
101
|
+
def state= value
|
102
|
+
if value.is_a? State
|
103
|
+
@state = value
|
104
|
+
elsif @states[value]
|
105
|
+
@state = @states[value]
|
106
|
+
elsif value and @states[value.to_sym]
|
107
|
+
@state = @states[value.to_sym]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def states= values
|
112
|
+
if values.is_a? Array and values.length==1
|
113
|
+
self.state=self.get_state(values.first)
|
114
|
+
else
|
115
|
+
values.each do |v|
|
116
|
+
if @states.has_key? v
|
117
|
+
self.state=v
|
118
|
+
else
|
119
|
+
belongs,parallel = belongs_to_parallel(v)
|
120
|
+
if belongs
|
121
|
+
self.state=parallel.id
|
122
|
+
parallel.state=v
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# The key method to exercise the statemachine. Any extra arguments supplied will be passed into
|
130
|
+
# any actions associated with the transition.
|
131
|
+
#
|
132
|
+
# Alternatively to this method, you may invoke methods, names the same as the event, on the statemachine.
|
133
|
+
# The advantage of using +process_event+ is that errors messages are more informative.
|
134
|
+
def process_event(event, *args)
|
135
|
+
event = event.to_sym
|
136
|
+
trace "Event: #{event}"
|
137
|
+
if @state
|
138
|
+
belongs, parallel = belongs_to_parallel(@state.id)
|
139
|
+
if belongs
|
140
|
+
r = parallel.process_event(event, *args)
|
141
|
+
return true if r
|
142
|
+
end
|
143
|
+
transition = @state.transition_for(event)
|
144
|
+
if transition
|
145
|
+
cond = true
|
146
|
+
if transition.cond!=true and transition.cond.is_a? Proc
|
147
|
+
cond = @state.statemachine.invoke_action(transition.cond, [], "condition from #{@state} invoked by '#{event}' event", nil, nil)
|
148
|
+
else
|
149
|
+
cond = instance_eval(transition.cond) if transition.cond != true and @is_parallel == nil
|
150
|
+
end
|
151
|
+
if cond
|
152
|
+
transition.invoke(@state, self, args)
|
153
|
+
end
|
154
|
+
else
|
155
|
+
raise TransitionMissingException.new("#{@state} does not respond to the '#{event}' event.")
|
156
|
+
end
|
157
|
+
|
158
|
+
else
|
159
|
+
raise StatemachineException.new("The state machine isn't in any state while processing the '#{event}' event.")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def trace(message) #:nodoc:
|
164
|
+
@tracer.puts message if @tracer
|
165
|
+
end
|
166
|
+
|
167
|
+
def belongs_to_parallel(id)
|
168
|
+
@states.each_value do |v|
|
169
|
+
# It doesn't belong to parallel, it is parallel
|
170
|
+
return [true, v] if v.id == id and v.is_a? Parallelstate
|
171
|
+
return [v.has_state(id),v] if v.is_a? Parallelstate
|
172
|
+
end
|
173
|
+
return [false, nil]
|
174
|
+
end
|
175
|
+
|
176
|
+
def get_parallel
|
177
|
+
@states.each_value do |v|
|
178
|
+
return v if v.is_a? Parallelstate
|
179
|
+
end
|
180
|
+
return false
|
181
|
+
end
|
182
|
+
|
183
|
+
def get_state(id) #:nodoc:
|
184
|
+
if @states.has_key? id
|
185
|
+
return @states[id]
|
186
|
+
elsif @is_parallel and @is_parallel.statemachine.get_state(id)
|
187
|
+
return @is_parallel.statemachine.states[id]
|
188
|
+
elsif p = get_parallel and s = p.get_state(id)
|
189
|
+
return s
|
190
|
+
elsif(is_history_state_id?(id))
|
191
|
+
superstate_id = base_id(id)
|
192
|
+
superstate = @states[superstate_id]
|
193
|
+
raise StatemachineException.new("No history exists for #{superstate} since it is not a super state.") if superstate.concrete?
|
194
|
+
return load_history(superstate)
|
195
|
+
elsif @is_parallel and @is_parallel.has_state(id)
|
196
|
+
@is_parallel.get_state(id)
|
197
|
+
else
|
198
|
+
state = State.new(id, @root, self)
|
199
|
+
@states[id] = state
|
200
|
+
return state
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def add_state(state) #:nodoc:
|
205
|
+
@states[state.id] = state
|
206
|
+
end
|
207
|
+
|
208
|
+
def remove_state(state)
|
209
|
+
@states.delete(state.id)
|
210
|
+
end
|
211
|
+
|
212
|
+
def has_state(id) #:nodoc:
|
213
|
+
if(is_history_state_id?(id))
|
214
|
+
return @states.has_key?(base_id(id))
|
215
|
+
else
|
216
|
+
return @states.has_key?(id)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def respond_to?(message)
|
221
|
+
return true if super(message)
|
222
|
+
return true if @state and @state.transition_for(message)
|
223
|
+
return false
|
224
|
+
end
|
225
|
+
|
226
|
+
def method_missing(message, *args) #:nodoc:
|
227
|
+
if @state and @state.transition_for(message)
|
228
|
+
process_event(message.to_sym, *args)
|
229
|
+
# method = self.method(:process_event)
|
230
|
+
# params = [message.to_sym].concat(args)
|
231
|
+
# method.call(*params)
|
232
|
+
else
|
233
|
+
begin
|
234
|
+
super(message, args)
|
235
|
+
rescue NoMethodError
|
236
|
+
process_event(message.to_sym, *args)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def In(id)
|
242
|
+
# check if it is one of the actual states
|
243
|
+
return true if @state.id == id
|
244
|
+
|
245
|
+
# check if it is one of the superstates
|
246
|
+
return true if @state.has_superstate(id)
|
247
|
+
|
248
|
+
# check if it is one of the running parallel states
|
249
|
+
belongs, parallel = belongs_to_parallel(@state.id)
|
250
|
+
if belongs
|
251
|
+
return parallel.In(id)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
def is_history_state_id?(id)
|
258
|
+
return id.to_s[-2..-1] == "_H"
|
259
|
+
end
|
260
|
+
|
261
|
+
def base_id(history_id)
|
262
|
+
return history_id.to_s[0...-2].to_sym
|
263
|
+
end
|
264
|
+
|
265
|
+
def load_history(superstate)
|
266
|
+
100.times do
|
267
|
+
history = superstate.history_id ? get_state(superstate.history_id) : nil
|
268
|
+
raise StatemachineException.new("#{superstate} doesn't have any history yet.") if not history
|
269
|
+
if history.concrete?
|
270
|
+
return history
|
271
|
+
else
|
272
|
+
superstate = history
|
273
|
+
end
|
274
|
+
end
|
275
|
+
raise StatemachineException.new("No history found within 100 levels of nested superstates.")
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Statemachine
|
2
|
+
|
3
|
+
class StubContext
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@verbose = options[:verbose]
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :statemachine
|
10
|
+
|
11
|
+
def method(name)
|
12
|
+
super(name)
|
13
|
+
rescue
|
14
|
+
self.class.class_eval "def #{name}(*args, &block); __generic_method(:#{name}, *args, &block); end"
|
15
|
+
return super(name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def __generic_method(name, *args)
|
19
|
+
if !defined?($IS_TEST)
|
20
|
+
puts "action invoked: #{name}(#{args.join(", ")}) #{block_given? ? "with block" : ""}" if @verbose
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Statemachine
|
2
|
+
|
3
|
+
class Superstate < State #:nodoc:
|
4
|
+
|
5
|
+
attr_accessor :startstate_id
|
6
|
+
attr_reader :history_id
|
7
|
+
|
8
|
+
def initialize(id, superstate, statemachine)
|
9
|
+
super(id, superstate, statemachine)
|
10
|
+
@startstate = nil
|
11
|
+
@history_id = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def concrete?
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
def startstate
|
19
|
+
return @statemachine.get_state(@startstate_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def resolve_startstate
|
23
|
+
return startstate.resolve_startstate
|
24
|
+
end
|
25
|
+
|
26
|
+
def substate_exiting(substate)
|
27
|
+
@history_id = substate.id
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_substates(*substate_ids)
|
31
|
+
do_substate_adding(substate_ids)
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_history=(state_id)
|
35
|
+
@history_id = @default_history_id = state_id
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset
|
39
|
+
@history_id = @default_history_id
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
return "'#{id}' superstate"
|
44
|
+
end
|
45
|
+
|
46
|
+
def abstract_states
|
47
|
+
return [@id] if not @superstate or @superstate.is_parallel
|
48
|
+
([@id] + @superstate.abstract_states).uniq
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|