stativus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -0,0 +1,4 @@
1
+ require './lib/stativus.rb'
2
+ #require './tests/complex_states.rb'
3
+ require './tests/add_state.rb'
4
+ require './tests/events.rb'
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