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