state-fu 0.11.1

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.
Files changed (85) hide show
  1. data/LICENSE +40 -0
  2. data/README.textile +293 -0
  3. data/Rakefile +114 -0
  4. data/lib/binding.rb +292 -0
  5. data/lib/event.rb +192 -0
  6. data/lib/executioner.rb +120 -0
  7. data/lib/hooks.rb +39 -0
  8. data/lib/interface.rb +132 -0
  9. data/lib/lathe.rb +538 -0
  10. data/lib/machine.rb +184 -0
  11. data/lib/method_factory.rb +243 -0
  12. data/lib/persistence.rb +116 -0
  13. data/lib/persistence/active_record.rb +34 -0
  14. data/lib/persistence/attribute.rb +47 -0
  15. data/lib/persistence/base.rb +100 -0
  16. data/lib/persistence/relaxdb.rb +23 -0
  17. data/lib/persistence/session.rb +7 -0
  18. data/lib/sprocket.rb +58 -0
  19. data/lib/state-fu.rb +56 -0
  20. data/lib/state.rb +48 -0
  21. data/lib/support/active_support_lite/array.rb +9 -0
  22. data/lib/support/active_support_lite/array/access.rb +60 -0
  23. data/lib/support/active_support_lite/array/conversions.rb +202 -0
  24. data/lib/support/active_support_lite/array/extract_options.rb +21 -0
  25. data/lib/support/active_support_lite/array/grouping.rb +109 -0
  26. data/lib/support/active_support_lite/array/random_access.rb +13 -0
  27. data/lib/support/active_support_lite/array/wrapper.rb +25 -0
  28. data/lib/support/active_support_lite/blank.rb +67 -0
  29. data/lib/support/active_support_lite/cattr_reader.rb +57 -0
  30. data/lib/support/active_support_lite/keys.rb +57 -0
  31. data/lib/support/active_support_lite/misc.rb +59 -0
  32. data/lib/support/active_support_lite/module.rb +1 -0
  33. data/lib/support/active_support_lite/module/delegation.rb +130 -0
  34. data/lib/support/active_support_lite/object.rb +9 -0
  35. data/lib/support/active_support_lite/string.rb +38 -0
  36. data/lib/support/active_support_lite/symbol.rb +16 -0
  37. data/lib/support/applicable.rb +41 -0
  38. data/lib/support/arrays.rb +197 -0
  39. data/lib/support/core_ext.rb +90 -0
  40. data/lib/support/exceptions.rb +106 -0
  41. data/lib/support/has_options.rb +16 -0
  42. data/lib/support/logger.rb +165 -0
  43. data/lib/support/methodical.rb +17 -0
  44. data/lib/support/no_stdout.rb +55 -0
  45. data/lib/support/plotter.rb +62 -0
  46. data/lib/support/vizier.rb +300 -0
  47. data/lib/tasks/spec_last.rake +55 -0
  48. data/lib/tasks/state_fu.rake +57 -0
  49. data/lib/transition.rb +338 -0
  50. data/lib/transition_query.rb +224 -0
  51. data/spec/custom_formatter.rb +49 -0
  52. data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
  53. data/spec/features/method_missing_only_once_spec.rb +28 -0
  54. data/spec/features/not_requirements_spec.rb +118 -0
  55. data/spec/features/plotter_spec.rb +97 -0
  56. data/spec/features/shared_log_spec.rb +7 -0
  57. data/spec/features/singleton_machine_spec.rb +39 -0
  58. data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
  59. data/spec/features/transition_boolean_comparison_spec.rb +101 -0
  60. data/spec/helper.rb +13 -0
  61. data/spec/integration/active_record_persistence_spec.rb +202 -0
  62. data/spec/integration/binding_extension_spec.rb +41 -0
  63. data/spec/integration/class_accessor_spec.rb +117 -0
  64. data/spec/integration/event_definition_spec.rb +74 -0
  65. data/spec/integration/example_01_document_spec.rb +133 -0
  66. data/spec/integration/example_02_string_spec.rb +88 -0
  67. data/spec/integration/instance_accessor_spec.rb +97 -0
  68. data/spec/integration/lathe_extension_spec.rb +67 -0
  69. data/spec/integration/machine_duplication_spec.rb +101 -0
  70. data/spec/integration/relaxdb_persistence_spec.rb +97 -0
  71. data/spec/integration/requirement_reflection_spec.rb +270 -0
  72. data/spec/integration/state_definition_spec.rb +163 -0
  73. data/spec/integration/transition_spec.rb +1033 -0
  74. data/spec/spec.opts +9 -0
  75. data/spec/spec_helper.rb +132 -0
  76. data/spec/state_fu_spec.rb +948 -0
  77. data/spec/units/binding_spec.rb +192 -0
  78. data/spec/units/event_spec.rb +214 -0
  79. data/spec/units/exceptions_spec.rb +82 -0
  80. data/spec/units/lathe_spec.rb +570 -0
  81. data/spec/units/machine_spec.rb +229 -0
  82. data/spec/units/method_factory_spec.rb +366 -0
  83. data/spec/units/sprocket_spec.rb +69 -0
  84. data/spec/units/state_spec.rb +59 -0
  85. metadata +171 -0
data/lib/lathe.rb ADDED
@@ -0,0 +1,538 @@
1
+ module StateFu
2
+ # A Lathe parses and a Machine definition and returns a freshly turned
3
+ # Machine.
4
+ #
5
+ # It provides the means to define the arrangement of StateFu objects
6
+ # ( eg States and Events) which comprise a workflow, process,
7
+ # lifecycle, circuit, syntax, etc.
8
+ class Lathe
9
+
10
+ # @state_or_event can be either nil (the main Lathe for a Machine)
11
+ # or contain a State or Event (a child lathe for a nested block)
12
+
13
+ attr_reader :machine, :state_or_event, :options
14
+
15
+ # you don't need to call this directly.
16
+ def initialize( machine, state_or_event = nil, options={}, &block )
17
+ @machine = machine
18
+ @state_or_event = state_or_event
19
+ @options = options.symbolize_keys!
20
+
21
+ # extend ourself with any previously defined tools
22
+ machine.tools.inject_into( self )
23
+
24
+ if state_or_event
25
+ state_or_event.apply!( options )
26
+ end
27
+ if block_given?
28
+ if block.arity == 1
29
+ if state_or_event
30
+ yield state_or_event
31
+ else
32
+ raise ArgumentError
33
+ end
34
+ else
35
+ instance_eval( &block )
36
+ end
37
+ end
38
+ end
39
+
40
+ #
41
+ # utility methods
42
+ #
43
+
44
+ # a 'child' lathe is created by apply_to, to deal with nested
45
+ # blocks for states / events ( which are state_or_events )
46
+ def nested?
47
+ !!state_or_event
48
+ end
49
+ alias_method :child?, :nested?
50
+
51
+ # is this the toplevel lathe for a machine?
52
+ def master?
53
+ !nested?
54
+ end
55
+
56
+ # get the top level Lathe for the machine
57
+ def master_lathe
58
+ machine.lathe
59
+ end
60
+
61
+ alias_method :context, :state_or_event
62
+
63
+ def context_state
64
+ state_or_event if state_or_event.is_a?(State)
65
+ end
66
+
67
+ def context_event
68
+ state_or_event if state_or_event.is_a?(Event)
69
+ end
70
+
71
+
72
+ #
73
+ # methods for extending the DSL
74
+ #
75
+
76
+ # helpers are mixed into all binding / transition contexts
77
+ def helper( *modules )
78
+ machine.helper *modules
79
+ end
80
+
81
+ # helpers are mixed into all binding / transition contexts
82
+ def tool( *modules, &block )
83
+ machine.tool *modules
84
+ if block_given?
85
+ tool = Module.new
86
+ tool.module_eval &block
87
+ machine.tools << tool
88
+ end
89
+ # inject them into self for immediate use
90
+ modules.flatten.extend( ToolArray ).inject_into( self )
91
+ end
92
+ alias_method :extend_dsl, :tool
93
+
94
+ #
95
+ # event definition methods
96
+ #
97
+
98
+ # Defines an event. Any options supplied will be added to the event,
99
+ # except :from and :to which are used to define the origin / target
100
+ # states. Successive invocations will _update_ (not replace) previously
101
+ # defined events; origin / target states and options are always
102
+ # accumulated, not clobbered.
103
+ #
104
+ # Several different styles of definition are available. Consult the
105
+ # specs / features for examples.
106
+
107
+ def event( name, options={}, &block )
108
+ options.symbolize_keys!
109
+ valid_in_context( State, nil )
110
+ if nested? && state_or_event.is_a?(State) # in state block
111
+ targets = options.delete(:to) || options.delete(:transitions_to)
112
+ evt = define_event( name, options, &block )
113
+ evt.from state_or_event unless state_or_event.nil?
114
+ evt.to( targets ) unless targets.nil?
115
+ evt
116
+ else # in master lathe
117
+ origins = options.delete( :from )
118
+ targets = options.delete( :to ) || options.delete(:transitions_to)
119
+ evt = define_event( name, options, &block )
120
+ evt.from origins unless origins.nil?
121
+ evt.to targets unless targets.nil?
122
+ evt
123
+ end
124
+ end
125
+
126
+ # compatibility methods for activemodel state machine ##############
127
+ def transitions(options={})
128
+ valid_in_context(Event)
129
+ options.symbolize_keys!
130
+
131
+ target = options[:to]
132
+ origins = options[:from]
133
+ hook = options[:on_transition]
134
+ evt = state_or_event
135
+
136
+ if hook
137
+ evt.lathe() { triggers hook }
138
+ end
139
+ #
140
+ # TODO do some type checking
141
+ #
142
+ if origins && target
143
+ evt.add_to_sequence origins, target
144
+ end
145
+ evt
146
+ end
147
+
148
+
149
+ def state_event name, options={}, &block
150
+ valid_in_context State
151
+ options.symbolize_keys!
152
+ state = state_or_event
153
+ targets = options.delete(:to) || options.delete(:transitions_to)
154
+ evt = define_state_or_event( Event, state.own_events, name, options, &block)
155
+ evt.from state
156
+ evt.to(targets) unless targets.nil?
157
+ evt
158
+ end
159
+
160
+ def event name, options={}, &block
161
+ options.symbolize_keys!
162
+ valid_in_context State, nil
163
+ if nested? && state_or_event.is_a?(State) # in state block
164
+ targets = options.delete(:to) || options.delete(:transitions_to)
165
+ evt = define_event name, options, &block
166
+ evt.from state_or_event unless state_or_event.nil?
167
+ evt.to targets unless targets.nil?
168
+ evt
169
+ else # in master lathe
170
+ origins = options.delete(:from)|| options.delete(:transitions_from)
171
+ targets = options.delete(:to) || options.delete(:transitions_to)
172
+ evt = define_event name, options, &block
173
+ evt.from origins unless origins.nil?
174
+ evt.to targets unless targets.nil?
175
+ evt
176
+ end
177
+ end
178
+
179
+
180
+ #####################################
181
+
182
+ # define an event or state requirement.
183
+ # options:
184
+ # :on => :entry|:exit|array (state only) - check requirement on state entry, exit or both?
185
+ # default = :entry
186
+ # :message => string|proc|proc_name_symbol - message to be returned on requirement failure.
187
+ # if a proc or symbol (named proc identifier), evaluated at runtime; a proc should
188
+ # take one argument, which is a StateFu::Transition.
189
+ # :msg => alias for :message, for the morbidly terse
190
+
191
+ def requires( *args, &block )
192
+ valid_in_context Event, State
193
+ options = args.extract_options!.symbolize_keys!
194
+ options.assert_valid_keys :on, :message, :msg
195
+ names = args
196
+ if block_given? && args.length > 1
197
+ raise ArgumentError.new("cannot supply a block for multiple requirements")
198
+ end
199
+ on = nil
200
+ names.each do |name|
201
+ raise ArgumentError.new(name.inspect) unless name.is_a?(Symbol)
202
+ case state_or_event
203
+ when State
204
+ on ||= [(options.delete(:on) || [:entry])].flatten
205
+ state_or_event.entry_requirements << name if on.include?( :entry )
206
+ state_or_event.exit_requirements << name if on.include?( :exit )
207
+ when Event
208
+ state_or_event.requirements << name
209
+ end
210
+ if block_given?
211
+ machine.named_procs[name] = block
212
+ end
213
+ if msg = options.delete(:message) || options.delete(:msg)
214
+ raise ArgumentError, msg.inspect unless [String, Symbol, Proc].include?(msg.class)
215
+ machine.requirement_messages[name] = msg
216
+ end
217
+ end
218
+ end
219
+ alias_method :guard, :requires
220
+ alias_method :must, :requires
221
+ alias_method :must_be, :requires
222
+ alias_method :needs, :requires
223
+
224
+ # create an event from *and* to the current state.
225
+ # Creates a loop, useful (only) for hooking behaviours onto.
226
+ def cycle name=nil, options={}, &block
227
+ _state = nil
228
+ if name.is_a?(Hash) && options.empty?
229
+ options = name
230
+ name = nil
231
+ end
232
+ if _state = options.delete(:state)
233
+ valid_unless_nested("when :state is supplied")
234
+ else
235
+ _state = state_or_event
236
+ valid_in_context( State, "unless :state is supplied" )
237
+ end
238
+
239
+ name ||= options.delete :on
240
+ name ||= "cycle_#{_state.to_sym}"
241
+ evt = define_event( name, options, &block )
242
+ evt.from _state
243
+ evt.to _state
244
+ evt
245
+ end
246
+
247
+ #
248
+ # state definition
249
+ #
250
+
251
+ # define the initial_state (otherwise defaults to the first state mentioned)
252
+ def initial_state *args, &block
253
+ valid_unless_nested()
254
+ machine.initial_state= state( *args, &block)
255
+ end
256
+
257
+ # define a state; given a block, apply the block to a Lathe for the state
258
+ def state name, options={}, &block
259
+ valid_unless_nested()
260
+ define_state( name, options, &block )
261
+ end
262
+
263
+ # define a named proc
264
+ def define method_name, &block
265
+ machine.named_procs[method_name] = block
266
+ end
267
+ alias_method :named_proc, :define
268
+
269
+ #
270
+ # Event definition
271
+ #
272
+
273
+ # set the origin state(s) of an event (or, given a hash of symbols / arrays
274
+ # of symbols, set both the origins and targets)
275
+ # from :my_origin
276
+ # from [:column_a, :column_b]
277
+ # from :eden => :armageddon
278
+ # from [:beginning, :prelogue] => [:ende, :prologue]
279
+ def from *args, &block
280
+ valid_in_context Event
281
+ state_or_event.from( *args, &block )
282
+ end
283
+
284
+ # set the target state(s) of an event
285
+ # to :destination
286
+ # to [:end, :finale, :intermission]
287
+ def to *args, &block
288
+ valid_in_context Event
289
+ state_or_event.to( *args, &block )
290
+ end
291
+
292
+ #
293
+ # define chained events and states succinctly
294
+ # usage: chain 'state1 -event1-> state2 -event2-> state3'
295
+ def chain string
296
+ rx_word = /([a-zA-Z0-9_]+)/
297
+ rx_state = /^#{rx_word}$/
298
+ rx_event = /^(?:-|>)#{rx_word}-?>$/
299
+ previous = nil
300
+ string.split.each do |chunk|
301
+ case chunk
302
+ when rx_state
303
+ current = state $1
304
+ if previous.is_a? Event
305
+ previous.to current
306
+ end
307
+ when rx_event
308
+ current = event $1
309
+ if previous.is_a? State
310
+ current.from previous
311
+ end
312
+ else
313
+ raise ArgumentError, "'#{chunk}' is not a valid token"
314
+ end
315
+ previous = current
316
+ end
317
+ end
318
+
319
+ # chain_states :a => [:b,:c], :c => :d, :c => :d
320
+ # chain_states :a,:b,:c,:d, :a => :c
321
+ def connect_states *array
322
+ array.flatten!
323
+ hash = array.extract_options!.symbolize_keys!
324
+ array.inject(nil) do |origin, target|
325
+ state target
326
+ if origin
327
+ event "#{origin.to_sym}_to_#{target.to_sym}", :from => origin, :to => target
328
+ end
329
+ origin = target
330
+ end
331
+ hash.each do |origin, target|
332
+ event "#{origin.to_sym}_to_#{target.to_sym}", :from => origin, :to => target
333
+ end
334
+ end
335
+ alias_method :connect, :connect_states
336
+
337
+ #
338
+ # Define a series of states at once, or return and iterate over all states yet defined
339
+ #
340
+ # states :a, :b, :c, :colour => "purple"
341
+ # states(:ALL) do
342
+ #
343
+ # end
344
+ def states *args, &block
345
+ valid_unless_nested()
346
+ each_state_or_event 'state', *args, &block
347
+ end
348
+ alias_method :all_states, :states
349
+ alias_method :each_state, :states
350
+
351
+ #
352
+ # Define a series of events at once, or return and iterate over all events yet defined
353
+ #
354
+ def events *args, &block
355
+ valid_in_context nil, State
356
+ each_state_or_event 'event', *args, &block
357
+ end
358
+ alias_method :all_events, :events
359
+ alias_method :each_event, :events
360
+
361
+ # Bunch of silly little methods for defining events
362
+ #:nodoc
363
+
364
+ def before *a, &b; valid_in_context Event; define_hook :before, *a, &b; end
365
+ def on_exit *a, &b; valid_in_context State; define_hook :exit, *a, &b; end
366
+ def execute *a, &b; valid_in_context Event; define_hook :execute, *a, &b; end
367
+ def on_entry *a, &b; valid_in_context State; define_hook :entry, *a, &b; end
368
+ def after *a, &b; valid_in_context Event; define_hook :after, *a, &b; end
369
+ def accepted *a, &b; valid_in_context State; define_hook :accepted, *a, &b; end
370
+
371
+ def before_all *a, &b; valid_in_context nil; define_hook :before_all, *a, &b; end
372
+ def after_all *a, &b; valid_in_context nil; define_hook :after_all, *a, &b; end
373
+
374
+ alias_method :after_everything, :after_all
375
+ alias_method :before_everything, :before_all
376
+
377
+ def after_all *a
378
+ end
379
+
380
+ def will *a, &b
381
+ valid_in_context State, Event
382
+ case state_or_event
383
+ when State
384
+ define_hook :entry, *a, &b
385
+ when Event
386
+ define_hook :execute, *a, &b
387
+ end
388
+ end
389
+ alias_method :fire, :will
390
+ alias_method :fires , :will
391
+ alias_method :firing, :will
392
+ alias_method :cause, :will
393
+ alias_method :causes, :will
394
+ alias_method :triggers, :will
395
+ alias_method :trigger, :will
396
+ alias_method :trigger, :will
397
+
398
+ alias_method :on_change, :accepted
399
+ #
400
+ #
401
+ #
402
+
403
+ private
404
+
405
+ # require that the current state_or_event be of a given type
406
+ def valid_in_context *valid_types
407
+ if valid_types.last.is_a?(String)
408
+ msg = valid_types.pop << " "
409
+ else
410
+ msg = ""
411
+ end
412
+ unless valid_types.include?( state_or_event.class ) || valid_types.include?(nil) && state_or_event.nil?
413
+ v = valid_types.dup.map do |t|
414
+ {
415
+ nil => "if not nested inside a block",
416
+ State => "inside a state definition block",
417
+ Event => "inside an event definition block"
418
+ }[t]
419
+ end
420
+ msg << "this command is only valid " << v.join(',')
421
+ raise ArgumentError, msg
422
+ end
423
+ end
424
+
425
+ # ensure this is not a child lathe
426
+ def valid_unless_nested(msg = nil)
427
+ valid_in_context( nil, msg )
428
+ end
429
+
430
+ # instantiate a child Lathe and apply the given block
431
+ def apply_to state_or_event, options, &block
432
+ StateFu::Lathe.new( machine, state_or_event, options, &block )
433
+ state_or_event
434
+ end
435
+
436
+ # abstract method for defining states / events
437
+ def define_state_or_event klass, collection, name, options={}, &block
438
+ name = name.to_sym
439
+ req = nil
440
+ msg = nil
441
+ options.symbolize_keys!
442
+
443
+ # allow requirements and messages to be added as options
444
+ if k = [:requires, :guard, :must, :must_be, :needs].detect {|k| options.has_key?(k) }
445
+ # Logger.debug("removing option #{k} - will use as requirement ..")
446
+ req = options.delete(k)
447
+ msg = options.delete(:message) || options.delete(:msg)
448
+ raise ArgumentError unless msg.nil? || req.is_a?(Symbol)
449
+ raise ArgumentError unless ([req, msg].map(&:class) - [String, Symbol, Proc, NilClass]).empty?
450
+ end
451
+ # TODO? allow hooks to be defined as options
452
+
453
+ unless state_or_event = collection[name]
454
+ state_or_event = klass.new machine, name, options
455
+ collection << state_or_event
456
+ end
457
+
458
+ apply_to state_or_event, options, &block
459
+
460
+ if req # install requirements
461
+ state_or_event.requirements << req
462
+ machine.requirement_messages[req] = msg if msg
463
+ end
464
+
465
+ state_or_event
466
+ end
467
+
468
+ #:nodoc
469
+ def define_state name, options={}, &block
470
+ collection = machine.states
471
+ define_state_or_event State, collection, name, options, &block
472
+ end
473
+
474
+ #:nodoc
475
+ def define_event name, options={}, &block
476
+ collection = machine.events
477
+ define_state_or_event Event, collection, name, options, &block
478
+ end
479
+
480
+ #:nodoc
481
+ def define_hook slot, *method_names, &block
482
+ raise "wtf" unless machine.is_a?(Machine)
483
+ hooks = (slot.to_s =~ /_all/ ? machine.hooks : state_or_event.hooks)
484
+ unless hooks.has_key? slot
485
+ raise ArgumentError, "invalid hook type #{slot.inspect} for #{state_or_event.class}"
486
+ end
487
+ if block_given?
488
+ method_name = method_names.first
489
+ # unless (-1..1).include?( block.arity )
490
+ # raise ArgumentError, "unexpected block arity: #{block.arity}"
491
+ # end
492
+ case method_name
493
+ when Symbol
494
+ machine.named_procs[method_name] = block
495
+ hook = method_name
496
+ when nil
497
+ hook = block
498
+ # allow only one anonymous hook per slot in the interests of
499
+ # sanity - replace any pre-existing ones
500
+ hooks[slot].delete_if { |h| Proc === h }
501
+ else
502
+ raise ArgumentError.new method_name.inspect
503
+ end
504
+ hooks[slot] << hook
505
+ else
506
+ method_names.each do |method_name|
507
+ if method_name.is_a? Symbol # no block
508
+ hook = method_name
509
+ # prevent duplicates
510
+ hooks[slot].delete_if { |h| hook == h }
511
+ hooks[slot] << hook
512
+ else
513
+ raise ArgumentError, "#{method_name.class} is not a symbol"
514
+ end
515
+ end
516
+ end
517
+
518
+ end
519
+
520
+ #:nodoc
521
+ def each_state_or_event type, *args, &block
522
+ options = args.extract_options!.symbolize_keys!
523
+ if args.empty? || args == [:ALL]
524
+ args = machine.send("#{type}s").except options.delete(:except)
525
+ end
526
+ mod = case type.to_s
527
+ when 'state'
528
+ StateArray
529
+ when 'event'
530
+ EventArray
531
+ end
532
+ args.map do |name|
533
+ self.send type, name, options.dup, &block
534
+ end.extend(mod)
535
+ end
536
+
537
+ end
538
+ end