stativus 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +54 -0
- data/lib/stativus.rb +590 -0
- data/tests/add_state.rb +40 -0
- data/tests/complex_states.rb +67 -0
- data/tests/events.rb +94 -0
- data/tests/test_states.rb +28 -0
- data/tests/tests.rb +4 -0
- metadata +70 -0
data/README.rdoc
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
= stativus-rb
|
2
|
+
|
3
|
+
by Mike Ball
|
4
|
+
http://github.com/onkis/stativus-rb
|
5
|
+
|
6
|
+
== DESCRIPTION:
|
7
|
+
A port of the stativus.js statechart library. Primarly designed to work with
|
8
|
+
ruby motion projects
|
9
|
+
|
10
|
+
== USAGE:
|
11
|
+
|
12
|
+
#create statechart
|
13
|
+
statechart = Stativus.Statechart.new
|
14
|
+
|
15
|
+
#To Define a state
|
16
|
+
class MyState < Stativus::State
|
17
|
+
def enter
|
18
|
+
#do your enter state stuff here
|
19
|
+
end
|
20
|
+
|
21
|
+
def exit
|
22
|
+
#do your exit stuff here
|
23
|
+
end
|
24
|
+
|
25
|
+
#actions
|
26
|
+
def my_action
|
27
|
+
self.goto_state('Another')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
#add state to statechart
|
31
|
+
#TODO: make this more automagic this is ruby...
|
32
|
+
statechart.add_state(MyState)
|
33
|
+
|
34
|
+
#start the statechart
|
35
|
+
statechart.start('MyState') #pass the initial state
|
36
|
+
|
37
|
+
Look in the tests directory to see more examples of
|
38
|
+
* parallel substates
|
39
|
+
* send_event
|
40
|
+
* parent => child relationship definitions
|
41
|
+
|
42
|
+
== INSTALL:
|
43
|
+
|
44
|
+
gem install stativus
|
45
|
+
|
46
|
+
== LICENSE:
|
47
|
+
|
48
|
+
Copyright (c) 2012 Mike Ball
|
49
|
+
|
50
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
51
|
+
|
52
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
53
|
+
|
54
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/stativus.rb
ADDED
@@ -0,0 +1,590 @@
|
|
1
|
+
module Stativus
|
2
|
+
class State
|
3
|
+
attr_accessor :global_concurrent_state,
|
4
|
+
:local_concurrent_state,
|
5
|
+
:statechart,
|
6
|
+
:has_concurrent_substates,
|
7
|
+
:parent_state,
|
8
|
+
:initial_substate,
|
9
|
+
:substates,
|
10
|
+
:states,
|
11
|
+
:history
|
12
|
+
|
13
|
+
def initialize(statechart)
|
14
|
+
@statechart = statechart
|
15
|
+
@substates = []
|
16
|
+
|
17
|
+
@has_concurrent_substates = self.respond_to?(:_has_concurrent_substates) ? self._has_concurrent_substates : false
|
18
|
+
@global_concurrent_state = self.respond_to?(:_global_concurrent_state) ? self._global_concurrent_state : DEFAULT_TREE
|
19
|
+
@states = self.respond_to?(:_states) ? self._states : []
|
20
|
+
@initial_substate = self.respond_to?(:_initial_substate) ? self._initial_substate : nil
|
21
|
+
@parent_state = self.respond_to?(:_parent_state) ? self._parent_state : nil
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def goto_state(name)
|
26
|
+
sc = @statechart
|
27
|
+
if(sc)
|
28
|
+
sc.goto_state(name, @global_concurrent_state, @local_concurrent_state)
|
29
|
+
else
|
30
|
+
raise "State has no statechart. Therefore no gotoState"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def goto_history_state(name)
|
35
|
+
sc = @statechart
|
36
|
+
if(sc)
|
37
|
+
sc.gotoHistroyState(name, @global_concurrent_state, @local_concurrent_state)
|
38
|
+
else
|
39
|
+
raise "State has no statechart. Therefore no History State"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def send_event(evt, *args)
|
44
|
+
sc = @statechart
|
45
|
+
if(sc)
|
46
|
+
sc.send_event(evt, args)
|
47
|
+
else
|
48
|
+
raise "can't send event cause state doesn't have a statechart"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
alias :send_action :send_event
|
52
|
+
|
53
|
+
#
|
54
|
+
# My ruby foo is weak and i really with there was another way to setup
|
55
|
+
# data on a class other than calling send :define_method then checking
|
56
|
+
# for the existence of this method...
|
57
|
+
def self.has_concurrent_substates(value)
|
58
|
+
send :define_method, :_has_concurrent_substates do
|
59
|
+
return value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.global_concurrent_state(state)
|
64
|
+
send :define_method, :_global_concurrent_state do
|
65
|
+
return state
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.states(*states)
|
70
|
+
send :define_method, :_states do
|
71
|
+
return states
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.initial_substate(state)
|
76
|
+
send :define_method, :_initial_substate do
|
77
|
+
return state
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.parent_state(state)
|
82
|
+
send :define_method, :_parent_state do
|
83
|
+
return state
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def name
|
88
|
+
return self.class.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
return self.class.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
DEFAULT_TREE = "default"
|
98
|
+
SUBSTATE_DELIM = "SUBSTATE:"
|
99
|
+
class Statechart
|
100
|
+
attr_accessor :all_states,
|
101
|
+
:states_with_concurrent_substates,
|
102
|
+
:current_subtrees,
|
103
|
+
:current_state,
|
104
|
+
:goto_state_locked,
|
105
|
+
:send_event_locked,
|
106
|
+
:pending_state_transitions,
|
107
|
+
:pending_events,
|
108
|
+
:active_subtrees
|
109
|
+
|
110
|
+
def initialize()
|
111
|
+
@all_states = {}
|
112
|
+
@all_states[DEFAULT_TREE] = {}
|
113
|
+
@states_with_concurrent_substates = {}
|
114
|
+
@current_subsates = {}
|
115
|
+
@current_state = {}
|
116
|
+
@current_state[DEFAULT_TREE] = nil
|
117
|
+
@goto_state = false
|
118
|
+
@send_event_locked = false
|
119
|
+
@pending_state_transitions = []
|
120
|
+
@pending_events = []
|
121
|
+
@active_subtrees = {}
|
122
|
+
@goto_state_locked = false
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_state(state_class)
|
126
|
+
state = state_class.new(self)
|
127
|
+
tree = state.global_concurrent_state
|
128
|
+
parent_state = state.parent_state
|
129
|
+
current_tree = @states_with_concurrent_substates[tree]
|
130
|
+
|
131
|
+
if(state.has_concurrent_substates)
|
132
|
+
obj = @states_with_concurrent_substates[tree] || {}
|
133
|
+
obj[state.name] = true
|
134
|
+
@states_with_concurrent_substates[tree] = obj
|
135
|
+
end
|
136
|
+
|
137
|
+
if(parent_state and current_tree and current_tree[parent_state])
|
138
|
+
parent_state = @all_states[tree][parent_state]
|
139
|
+
if(parent_state)
|
140
|
+
parent_state.substates.push(state.name)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
obj = @all_states[tree] || {}
|
145
|
+
|
146
|
+
obj[state.name] = state
|
147
|
+
|
148
|
+
sub_states = state.states || []
|
149
|
+
|
150
|
+
sub_states.each do |sub_state|
|
151
|
+
sub_state.parent_state = state
|
152
|
+
sub_state.global_concurrent_state = tree
|
153
|
+
add_state(sub_state)
|
154
|
+
end
|
155
|
+
end #add_state
|
156
|
+
|
157
|
+
# call this in your programs main
|
158
|
+
# state is the initial state of the application
|
159
|
+
# in the default tree
|
160
|
+
def start(state)
|
161
|
+
@in_initial_setup = true
|
162
|
+
self.goto_state(state, DEFAULT_TREE)
|
163
|
+
@in_initial_setup = false
|
164
|
+
flush_pending_events
|
165
|
+
end
|
166
|
+
|
167
|
+
def goto_state(requested_state_name, tree, concurrent_tree=nil)
|
168
|
+
all_states = @all_states[tree]
|
169
|
+
|
170
|
+
#First, find the current tree off of the concurrentTree, then the main tree
|
171
|
+
curr_state = concurrent_tree ? @current_state[concurrent_tree] : @current_state[tree]
|
172
|
+
|
173
|
+
requested_state = all_states[requested_state_name]
|
174
|
+
|
175
|
+
# if the current state is the same as the requested state do nothing
|
176
|
+
return if(check_all_current_states(requested_state, concurrent_tree || tree))
|
177
|
+
|
178
|
+
|
179
|
+
if(@goto_state_locked)
|
180
|
+
#There is a state transition currently happening. Add this requested
|
181
|
+
#state transition to the queue of pending state transitions. The req
|
182
|
+
#will be invoked after the current state transition is finished
|
183
|
+
@pending_state_transitions.push({
|
184
|
+
:requested_state => requested_state_name,
|
185
|
+
:tree => tree
|
186
|
+
})
|
187
|
+
return
|
188
|
+
end
|
189
|
+
# Lock for the current state transition, so that it all gets sorted out
|
190
|
+
# in the right order
|
191
|
+
@goto_state_locked = true
|
192
|
+
|
193
|
+
# Get the parent states for the current state and the registered state.
|
194
|
+
# we will use them to find the commen parent state
|
195
|
+
enter_states = parent_states_with_root(requested_state)
|
196
|
+
exit_states = curr_state ? parent_states_with_root(curr_state) : []
|
197
|
+
#
|
198
|
+
# continue by finding the common parent state for the current and
|
199
|
+
# requested states:
|
200
|
+
#
|
201
|
+
# At most, this takes O(m^2) time, where m is the maximum depth from the
|
202
|
+
# root of the tree to either the requested state or the current state.
|
203
|
+
# Will always be less than or equal to O(n^2), where n is the number
|
204
|
+
# of states in the tree
|
205
|
+
enter_match_index = nil
|
206
|
+
exit_match_index = 0
|
207
|
+
exit_states.each_index do |idx|
|
208
|
+
exit_match_index = idx
|
209
|
+
enter_match_index = enter_states.index(exit_states[idx])
|
210
|
+
break if(enter_match_index != nil)
|
211
|
+
end
|
212
|
+
|
213
|
+
# In the case where we don't find a common parent state, we
|
214
|
+
# must enter from the root state
|
215
|
+
enter_match_index = enter_states.length()-1 if(enter_match_index == nil)
|
216
|
+
|
217
|
+
#setup the enter state sequence
|
218
|
+
@enter_states = enter_states
|
219
|
+
@enter_state_match_index = enter_match_index
|
220
|
+
@enter_state_concurrent_tree = concurrent_tree
|
221
|
+
@enter_state_tree = tree
|
222
|
+
|
223
|
+
# Now, we will exit all the underlying states till we reach the common
|
224
|
+
# parent state. We do not exit the parent state because we transition
|
225
|
+
# within it.
|
226
|
+
@exit_state_stack = []
|
227
|
+
full_exit_from_substates(tree, curr_state) if(curr_state != nil and curr_state.has_concurrent_substates)
|
228
|
+
|
229
|
+
if exit_match_index == nil || exit_match_index-1 < 0
|
230
|
+
exit_match_index = 0
|
231
|
+
else
|
232
|
+
0.upto(exit_match_index-1) do |i|
|
233
|
+
@exit_state_stack.push(exit_states[i])
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
#Now that we have the full stack of states to exit
|
238
|
+
#we can exit them...
|
239
|
+
|
240
|
+
unwind_exit_state_stack();
|
241
|
+
|
242
|
+
end #end goto_state
|
243
|
+
|
244
|
+
def send_event(evt, *args)
|
245
|
+
|
246
|
+
# We want to prevent any events from occurring until
|
247
|
+
# we have completed the state transitions and events
|
248
|
+
if @in_initial_setup or @goto_state_locked or @send_event_locked
|
249
|
+
@pending_events.push({
|
250
|
+
:evt => evt,
|
251
|
+
:args => args
|
252
|
+
})
|
253
|
+
return
|
254
|
+
end
|
255
|
+
|
256
|
+
@send_event_locked = true
|
257
|
+
|
258
|
+
structure_crawl(evt, args)
|
259
|
+
|
260
|
+
# Now, that the states have a chance to process the first action
|
261
|
+
# we can go ahead and flush the queued events
|
262
|
+
@send_event_locked = false;
|
263
|
+
|
264
|
+
flush_pending_events() unless @in_initial_setup
|
265
|
+
|
266
|
+
end
|
267
|
+
#
|
268
|
+
# Private functions
|
269
|
+
#
|
270
|
+
private
|
271
|
+
|
272
|
+
def structure_crawl(evt, args)
|
273
|
+
current_states = @current_state
|
274
|
+
ss = Stativus::SUBSTATE_DELIM
|
275
|
+
|
276
|
+
for tree in current_states.keys
|
277
|
+
next unless tree
|
278
|
+
handled = false
|
279
|
+
s_tree = nil
|
280
|
+
responder = current_states[tree]
|
281
|
+
|
282
|
+
next if(!responder or tree.slice(0, ss.length()) == ss)
|
283
|
+
|
284
|
+
all_states = @all_states[tree]
|
285
|
+
|
286
|
+
next unless all_states
|
287
|
+
|
288
|
+
a_trees = @active_subtrees[tree] || []
|
289
|
+
|
290
|
+
#0.upto(a_trees.length()-1) do |i|
|
291
|
+
a_trees.each do |s_tree|
|
292
|
+
#s_tree = a_trees[i]
|
293
|
+
s_responder = current_states[s_tree]
|
294
|
+
tmp = handled ? [true, true] : cascade_events(evt, args, s_responder, all_states, s_tree)
|
295
|
+
handled = tmp[0]
|
296
|
+
#if (DEBUG_MODE) found = tmp[1];
|
297
|
+
end
|
298
|
+
if(not handled)
|
299
|
+
tmp = cascade_events(evt, args, responder, all_states, nil)
|
300
|
+
handled = tmp[0]
|
301
|
+
# if (DEBUG_MODE){
|
302
|
+
# if (!found) found = tmp[1];
|
303
|
+
# }
|
304
|
+
|
305
|
+
end
|
306
|
+
# if (DEBUG_MODE){
|
307
|
+
# if(!found) console.log(['ACTION/EVENT:{'+evt+'} with', args.length || 0, 'argument(s)','found NO state to handle this'].join(' '));
|
308
|
+
# }
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def cascade_events(evt, args, responder, all_states, tree)
|
313
|
+
|
314
|
+
found = false
|
315
|
+
handled = nil
|
316
|
+
if(tree)
|
317
|
+
trees = tree.split('=>')
|
318
|
+
ss_name = trees.last
|
319
|
+
end
|
320
|
+
|
321
|
+
while(not handled and responder)
|
322
|
+
if(responder.respond_to?(evt))
|
323
|
+
method = responder.method(evt)
|
324
|
+
#if (DEBUG_MODE) console.log(['EVENT:',responder.name,'fires','['+evt+']', 'with', args.length || 0, 'argument(s)'].join(' '));
|
325
|
+
if(method.arity != 0)
|
326
|
+
handled = method.call(args)
|
327
|
+
else
|
328
|
+
handled = method.call()
|
329
|
+
end
|
330
|
+
#ruby has implict returns therefore you actually
|
331
|
+
#have to explicitly return true
|
332
|
+
handled = handled == false ? false : true
|
333
|
+
found = true
|
334
|
+
end
|
335
|
+
|
336
|
+
#check to see if we're at the end of the tree
|
337
|
+
return [handled, found] if(tree and ss_name == responder.name)
|
338
|
+
responder = (!handled && responder.parent_state) ? all_states[responder.parent_state] : nil
|
339
|
+
end
|
340
|
+
|
341
|
+
return [handled, found]
|
342
|
+
end
|
343
|
+
# this function exits all items next on the exit state stack
|
344
|
+
def unwind_exit_state_stack
|
345
|
+
@exit_state_stack = @exit_state_stack || []
|
346
|
+
state_to_exit = @exit_state_stack.shift
|
347
|
+
|
348
|
+
if state_to_exit
|
349
|
+
if(state_to_exit.respond_to?(:will_exit_state))
|
350
|
+
|
351
|
+
state_restart = {
|
352
|
+
:statechart => self,
|
353
|
+
:start => state_to_exit
|
354
|
+
}
|
355
|
+
#todo : i'm pretty sure this won't work as written...
|
356
|
+
state_restart[:restart] = Proc.new {
|
357
|
+
#if(debugMode) puts ['RESTART: after async processing on,', self[:start].name, 'is about to fully exit'].join(' ')
|
358
|
+
@statechart.full_exit(state_restart[:start])
|
359
|
+
}
|
360
|
+
delay_for_async = state_to_exit.will_exit_state(state_restart)
|
361
|
+
end
|
362
|
+
full_exit(state_to_exit) unless delay_for_async
|
363
|
+
else
|
364
|
+
@exit_state_stack = nil
|
365
|
+
initiate_enter_state_sequence
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def full_exit(state)
|
370
|
+
return unless state
|
371
|
+
exit_state_handled = false
|
372
|
+
state.exit if(state.respond_to?(:exit))
|
373
|
+
state.did_exit_state if(state.respond_to?(:did_exit_state))
|
374
|
+
#todo: if (DEBUG_MODE) console.log('EXIT: '+state.name);
|
375
|
+
unwind_exit_state_stack
|
376
|
+
end
|
377
|
+
|
378
|
+
def full_enter(state)
|
379
|
+
return unless state
|
380
|
+
enter_state_handled = false
|
381
|
+
#if (DEBUG_MODE) console.log('ENTER: '+state.name);
|
382
|
+
state.enter if state.respond_to?(:enter)
|
383
|
+
state.did_enter_state if state.respond_to?(:did_enter_state)
|
384
|
+
unwind_enter_state_stack()
|
385
|
+
end
|
386
|
+
|
387
|
+
|
388
|
+
def full_exit_from_substates(tree, stop_state)
|
389
|
+
|
390
|
+
return if(!tree || !stop_state)
|
391
|
+
|
392
|
+
all_states = @all_states[tree]
|
393
|
+
curr_states = @current_state
|
394
|
+
@exit_state_stack = @exit_state_stack || []
|
395
|
+
|
396
|
+
stop_state.substates.each do |state|
|
397
|
+
substate_tree = [SUBSTATE_DELIM, tree, stop_state.name, state].join("=>")
|
398
|
+
curr_state = curr_states[substate_tree]
|
399
|
+
|
400
|
+
while(curr_state and curr_state != stop_state)
|
401
|
+
exit_state_handled = false
|
402
|
+
@exit_state_stack.unshift(curr_state)
|
403
|
+
|
404
|
+
#check to see if it has substates
|
405
|
+
full_exit_from_substates(tree, curr_state) if(curr_state.has_concurrent_substates)
|
406
|
+
|
407
|
+
#up to the next parent
|
408
|
+
curr = curr_state.parent_state
|
409
|
+
curr_state = all_states[curr]
|
410
|
+
end
|
411
|
+
|
412
|
+
end
|
413
|
+
|
414
|
+
end
|
415
|
+
|
416
|
+
def initiate_enter_state_sequence
|
417
|
+
enter_states = @enter_states
|
418
|
+
enter_match_index = @enter_state_match_index
|
419
|
+
concurrent_tree = @enter_state_concurrent_tree
|
420
|
+
tree = @enter_state_tree
|
421
|
+
all_states = @all_states[tree]
|
422
|
+
|
423
|
+
#initalize the enter state stack
|
424
|
+
@enter_state_stack = @enter_state_stack || []
|
425
|
+
|
426
|
+
# Finally, from the common parent state, but not including the parent state,
|
427
|
+
# enter the sub states down to the requested state. If the requested state
|
428
|
+
# has an initial sub state, then we must enter it too
|
429
|
+
i = enter_match_index - 1
|
430
|
+
curr_state = enter_states[i]
|
431
|
+
if(curr_state)
|
432
|
+
cascade_enter_substates(curr_state, enter_states,
|
433
|
+
(i-1), concurrent_tree || tree, all_states)
|
434
|
+
end
|
435
|
+
|
436
|
+
#once, we have fully hydrated the Enter State Stack, we must actually async unwind it
|
437
|
+
unwind_enter_state_stack
|
438
|
+
|
439
|
+
#cleanup
|
440
|
+
@enter_states = @enter_state_match_index = @enter_state_concurrent_tree =
|
441
|
+
@enter_state_tree = nil
|
442
|
+
|
443
|
+
end
|
444
|
+
|
445
|
+
def cascade_enter_substates(start, required_states, index, tree, all_states)
|
446
|
+
return unless start
|
447
|
+
name = start.name
|
448
|
+
@enter_state_stack.push(start)
|
449
|
+
@current_state[tree] = start
|
450
|
+
start.local_concurrent_state = tree
|
451
|
+
if(start.has_concurrent_substates)
|
452
|
+
tree = start.global_concurrent_state || DEFAULT_TREE
|
453
|
+
next_tree = [SUBSTATE_DELIM,tree,name].join("=>")
|
454
|
+
start.history = start.history || {}
|
455
|
+
substates = start.substates || []
|
456
|
+
substates.each do |x|
|
457
|
+
next_tree = tree +"=>"+x
|
458
|
+
curr_state = all_states[x]
|
459
|
+
|
460
|
+
# Now, we have to push the item onto the active subtrees for
|
461
|
+
# the base tree for later use of the events.
|
462
|
+
b_tree = curr_state.global_concurrent_state || DEFAULT_TREE
|
463
|
+
a_trees = active_subtrees[b_tree] || []
|
464
|
+
a_trees.unshift(next_tree)
|
465
|
+
@active_subtrees[b_tree] = a_trees
|
466
|
+
index -=1 if(index > -1 && required_states[index] == curr_state)
|
467
|
+
cascade_enter_substates(curr_state, required_states, index, next_tree, all_states)
|
468
|
+
end
|
469
|
+
return
|
470
|
+
else
|
471
|
+
curr_state = required_states[index]
|
472
|
+
if(curr_state and curr_state.is_a?(State))
|
473
|
+
parent_state = all_states[curr_state.parent_state]
|
474
|
+
if(parent_state)
|
475
|
+
if(parent_state.has_concurrent_substates)
|
476
|
+
parent_state.history[tree] = curr_state.name
|
477
|
+
else
|
478
|
+
parent_state.history = current_state.name
|
479
|
+
end
|
480
|
+
end #end parent state
|
481
|
+
|
482
|
+
index -=1 if(index > -1 && required_states[index] == curr_state)
|
483
|
+
cascade_enter_substates(curr_state, required_states, index, tree, all_states)
|
484
|
+
else
|
485
|
+
curr_state = all_states[start.initial_substate]
|
486
|
+
cascade_enter_substates(curr_state, required_states, index, tree, all_states)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
end
|
491
|
+
|
492
|
+
def unwind_enter_state_stack
|
493
|
+
|
494
|
+
@exit_state_stack = @exit_state_stack || []
|
495
|
+
state_to_enter = @enter_state_stack.shift()
|
496
|
+
|
497
|
+
if state_to_enter
|
498
|
+
if state_to_enter.respond_to?(:will_enter_state)
|
499
|
+
|
500
|
+
state_restart = {
|
501
|
+
:statechart => self,
|
502
|
+
:start => state_to_enter
|
503
|
+
}
|
504
|
+
state_restart[:restart] = Proc.new{
|
505
|
+
#if (DEBUG_MODE) console.log(['RESTART: after async processing on,', this._start.name, 'is about to fully enter'].join(' '));
|
506
|
+
@statechart.full_enter(state_restart[:state_to_enter])
|
507
|
+
}
|
508
|
+
delay_for_async = state_to_enter.will_enter_state(state_restart)
|
509
|
+
# if (DEBUG_MODE) {
|
510
|
+
# if (delayForAsync) { console.log('ASYNC: Delayed enter '+stateToEnter.name); }
|
511
|
+
# else { console.warn('ASYNC: Didn\'t return \'true\' willExitState on '+stateToEnter.name+' which is needed if you want async'); }
|
512
|
+
# }
|
513
|
+
end
|
514
|
+
full_enter(state_to_enter) unless delay_for_async
|
515
|
+
|
516
|
+
else
|
517
|
+
@enter_state_stack = nil
|
518
|
+
# Ok, we're done with the current state transition. Make sure to unlock
|
519
|
+
# the goToState and let other pending state transitions
|
520
|
+
@goto_state_locked = false
|
521
|
+
more = flush_pending_state_transitions
|
522
|
+
# Once pending state transitions are flushed then go ahead and start flush
|
523
|
+
# pending actions
|
524
|
+
flush_pending_events if not more and not @in_initial_setup
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
def flush_pending_state_transitions
|
529
|
+
pending = @pending_state_transitions.shift
|
530
|
+
return false unless pending
|
531
|
+
goto_state(pending.requested_state, pending.tree)
|
532
|
+
return true
|
533
|
+
end
|
534
|
+
|
535
|
+
def flush_pending_events
|
536
|
+
pending_event = @pending_events.shift()
|
537
|
+
return if(pending_event == nil)
|
538
|
+
self.send_event(pending_event.evt, pending_event.args)
|
539
|
+
end
|
540
|
+
|
541
|
+
def check_all_current_states(requested_state, tree)
|
542
|
+
current_states = @current_state[tree] || []
|
543
|
+
|
544
|
+
if(current_states == requested_state)
|
545
|
+
return true
|
546
|
+
elsif(current_states.class == String and requested_state == @all_states[tree][current_states])
|
547
|
+
return true
|
548
|
+
elsif(current_states.class == Array and current_states.include?(requested_state))
|
549
|
+
return true
|
550
|
+
else
|
551
|
+
return false
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
# returns the state object for a passed name and tree
|
556
|
+
# was called _parentStateObject in js
|
557
|
+
def state_object(name, tree)
|
558
|
+
if(name && tree && @all_states[tree] && @all_states[tree][name])
|
559
|
+
return @all_states[tree][name]
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
#
|
564
|
+
# returns an array of all the parent states of the passed state
|
565
|
+
#
|
566
|
+
def parent_states(state)
|
567
|
+
ret = []
|
568
|
+
curr = state
|
569
|
+
|
570
|
+
ret.push(curr)
|
571
|
+
curr = state_object(curr.parent_state, curr.global_concurrent_state)
|
572
|
+
|
573
|
+
while(curr)
|
574
|
+
ret.push(curr)
|
575
|
+
curr = state_object(curr.parent_state, curr.global_concurrent_state)
|
576
|
+
end
|
577
|
+
|
578
|
+
return ret
|
579
|
+
end
|
580
|
+
|
581
|
+
# creates an array of all a states parent states
|
582
|
+
# ending with a string of "root" to indcate the
|
583
|
+
# root state
|
584
|
+
def parent_states_with_root(state)
|
585
|
+
ret = parent_states(state)
|
586
|
+
ret.push('root')
|
587
|
+
return ret
|
588
|
+
end
|
589
|
+
end #end class
|
590
|
+
end #end module
|
data/tests/add_state.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require Dir.pwd()+'/tests/test_states'
|
3
|
+
class AddState < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@statechart = Stativus::Statechart.new
|
6
|
+
|
7
|
+
@statechart.add_state(A)
|
8
|
+
@statechart.add_state(B)
|
9
|
+
@statechart.add_state(C)
|
10
|
+
|
11
|
+
@statechart.start("B")
|
12
|
+
@statechart.goto_state("B", Stativus::DEFAULT_TREE)
|
13
|
+
@statechart.goto_state("C", Stativus::DEFAULT_TREE)
|
14
|
+
@default_tree = @statechart.all_states[Stativus::DEFAULT_TREE]
|
15
|
+
end #end setup
|
16
|
+
|
17
|
+
|
18
|
+
def test_states_were_added
|
19
|
+
assert(true, "Sanity check")
|
20
|
+
assert_equal(1, @statechart.all_states.keys.length, "should have one tree")
|
21
|
+
assert_equal(3, @statechart.all_states[Stativus::DEFAULT_TREE].keys.length,
|
22
|
+
"should have 3 states in default tree")
|
23
|
+
|
24
|
+
|
25
|
+
assert_instance_of(A, @default_tree["A"], "should have an A")
|
26
|
+
assert_instance_of(B, @default_tree["B"], "should have an B")
|
27
|
+
assert_instance_of(C, @default_tree["C"], "should have an C")
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_state_has_proper_settings
|
31
|
+
a = @default_tree["A"]
|
32
|
+
b = @default_tree["B"]
|
33
|
+
assert(a.has_concurrent_substates, "a should have concurrent sub states")
|
34
|
+
assert_equal("A", b.parent_state, "a is the parent of B")
|
35
|
+
assert_equal(["B", "C"], a.substates, "a should have 2 substates")
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class First < Stativus::State
|
2
|
+
has_concurrent_substates true
|
3
|
+
|
4
|
+
def test_event
|
5
|
+
self.goto_state('Second')
|
6
|
+
end
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
class FirstFirst < Stativus::State
|
11
|
+
parent_state "First"
|
12
|
+
initial_substate "FirstFirstFirst"
|
13
|
+
|
14
|
+
def test_event
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class FirstSecond < Stativus::State
|
19
|
+
parent_state "First"
|
20
|
+
inital_substate "FirstSecondFirst"
|
21
|
+
|
22
|
+
def test_event
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class FirstFirstFirst < Stativus::State
|
27
|
+
parent_state "FirstFirst"
|
28
|
+
|
29
|
+
def test_event
|
30
|
+
self.goto_state('FirstFirstSecond')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class FirstFirstSecond < Stativus::State
|
35
|
+
parent_state "FirstFirst"
|
36
|
+
|
37
|
+
def test_event
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class FirstSecondSecond < Stativus::State
|
42
|
+
parent_state 'FirstSecond'
|
43
|
+
def test_event
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Second < Stativus::State
|
48
|
+
initial_substate "SecondFirst"
|
49
|
+
def test_event
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class SecondFirst < Stativus::State
|
54
|
+
parent_state "Second"
|
55
|
+
def test_event
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class SecondSecond < Stativus::State
|
60
|
+
parent_state "Second"
|
61
|
+
|
62
|
+
def test_event
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
|
data/tests/events.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
|
4
|
+
$state_transitions = []
|
5
|
+
|
6
|
+
module AllEnterExit
|
7
|
+
def enter
|
8
|
+
#puts "ENT: "+self.name
|
9
|
+
$state_transitions.push "ENT: "+self.name
|
10
|
+
end
|
11
|
+
|
12
|
+
def exit
|
13
|
+
#puts "EXT: "+self.name
|
14
|
+
$state_transitions.push "EXT: "+self.name
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_event
|
18
|
+
$state_transitions.push("EVT: "+self.name+".test_event")
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
class Application < Stativus::State
|
26
|
+
include AllEnterExit
|
27
|
+
initial_substate "SubApplication"
|
28
|
+
end
|
29
|
+
|
30
|
+
class SubApplication < Stativus::State
|
31
|
+
include AllEnterExit
|
32
|
+
parent_state "Application"
|
33
|
+
has_concurrent_substates true
|
34
|
+
end
|
35
|
+
|
36
|
+
class First < Stativus::State
|
37
|
+
include AllEnterExit
|
38
|
+
parent_state "SubApplication"
|
39
|
+
initial_substate "FirstFirst"
|
40
|
+
end
|
41
|
+
|
42
|
+
class FirstFirst < Stativus::State
|
43
|
+
include AllEnterExit
|
44
|
+
parent_state "First"
|
45
|
+
|
46
|
+
def test_event(*args)
|
47
|
+
$state_transitions.push('EVT: '+self.name+'.test_event')
|
48
|
+
self.goto_state("FirstSecond")
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class FirstSecond < Stativus::State
|
54
|
+
include AllEnterExit
|
55
|
+
parent_state "First"
|
56
|
+
end
|
57
|
+
|
58
|
+
class Second < Stativus::State
|
59
|
+
include AllEnterExit
|
60
|
+
parent_state "SubApplication"
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
class Events < Test::Unit::TestCase
|
67
|
+
def setup
|
68
|
+
@statechart = Stativus::Statechart.new
|
69
|
+
|
70
|
+
@statechart.add_state(Application)
|
71
|
+
@statechart.add_state(SubApplication)
|
72
|
+
@statechart.add_state(First)
|
73
|
+
@statechart.add_state(FirstFirst)
|
74
|
+
@statechart.add_state(FirstSecond)
|
75
|
+
@statechart.add_state(Second)
|
76
|
+
|
77
|
+
@statechart.start("Application")
|
78
|
+
|
79
|
+
#state_transitions = []
|
80
|
+
|
81
|
+
end #end setup
|
82
|
+
|
83
|
+
def test_event_propagation
|
84
|
+
expected_events = ['EVT','EVT', 'EXT', 'ENT', 'EVT', 'EVT', 'EVT', 'EVT', 'EVT']
|
85
|
+
$state_transitions = []
|
86
|
+
@statechart.send_event('test_event')
|
87
|
+
#puts $state_transitions
|
88
|
+
assert_equal(4, $state_transitions.length(), "4 transitions after first event")
|
89
|
+
@statechart.send_event('test_event')
|
90
|
+
#puts "second time"
|
91
|
+
#puts $state_transitions
|
92
|
+
assert_equal(9, $state_transitions.length(), "9 transitions after second event")
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class A < Stativus::State
|
2
|
+
has_concurrent_substates true
|
3
|
+
def enter
|
4
|
+
puts "enered A. I have concurrent substates"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class B < Stativus::State
|
9
|
+
parent_state "A"
|
10
|
+
def enter
|
11
|
+
puts "entered B"
|
12
|
+
end
|
13
|
+
|
14
|
+
def exit
|
15
|
+
puts "exited B"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class C < Stativus::State
|
20
|
+
parent_state "A"
|
21
|
+
def enter
|
22
|
+
puts "entered C"
|
23
|
+
end
|
24
|
+
|
25
|
+
def exit
|
26
|
+
puts "exited C"
|
27
|
+
end
|
28
|
+
end
|
data/tests/tests.rb
ADDED
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stativus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Ball
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-05 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: &70289451304540 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70289451304540
|
25
|
+
description: ! "Stativus provides a way to define state in a client facing application.
|
26
|
+
\nIt is a port of the stativus.js library."
|
27
|
+
email: mike.ball3@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files:
|
31
|
+
- README.rdoc
|
32
|
+
files:
|
33
|
+
- lib/stativus.rb
|
34
|
+
- README.rdoc
|
35
|
+
- tests/add_state.rb
|
36
|
+
- tests/complex_states.rb
|
37
|
+
- tests/events.rb
|
38
|
+
- tests/test_states.rb
|
39
|
+
- tests/tests.rb
|
40
|
+
homepage: http://github.com/onkis/stativus-rb
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.8.10
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: a ruby statechart library
|
65
|
+
test_files:
|
66
|
+
- tests/add_state.rb
|
67
|
+
- tests/complex_states.rb
|
68
|
+
- tests/events.rb
|
69
|
+
- tests/test_states.rb
|
70
|
+
- tests/tests.rb
|