davidlee-state-fu 0.3.1 → 0.10.0

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