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.
- data/LICENSE +40 -0
- data/README.textile +293 -0
- data/Rakefile +114 -0
- data/lib/binding.rb +292 -0
- data/lib/event.rb +192 -0
- data/lib/executioner.rb +120 -0
- data/lib/hooks.rb +39 -0
- data/lib/interface.rb +132 -0
- data/lib/lathe.rb +538 -0
- data/lib/machine.rb +184 -0
- data/lib/method_factory.rb +243 -0
- data/lib/persistence.rb +116 -0
- data/lib/persistence/active_record.rb +34 -0
- data/lib/persistence/attribute.rb +47 -0
- data/lib/persistence/base.rb +100 -0
- data/lib/persistence/relaxdb.rb +23 -0
- data/lib/persistence/session.rb +7 -0
- data/lib/sprocket.rb +58 -0
- data/lib/state-fu.rb +56 -0
- data/lib/state.rb +48 -0
- data/lib/support/active_support_lite/array.rb +9 -0
- data/lib/support/active_support_lite/array/access.rb +60 -0
- data/lib/support/active_support_lite/array/conversions.rb +202 -0
- data/lib/support/active_support_lite/array/extract_options.rb +21 -0
- data/lib/support/active_support_lite/array/grouping.rb +109 -0
- data/lib/support/active_support_lite/array/random_access.rb +13 -0
- data/lib/support/active_support_lite/array/wrapper.rb +25 -0
- data/lib/support/active_support_lite/blank.rb +67 -0
- data/lib/support/active_support_lite/cattr_reader.rb +57 -0
- data/lib/support/active_support_lite/keys.rb +57 -0
- data/lib/support/active_support_lite/misc.rb +59 -0
- data/lib/support/active_support_lite/module.rb +1 -0
- data/lib/support/active_support_lite/module/delegation.rb +130 -0
- data/lib/support/active_support_lite/object.rb +9 -0
- data/lib/support/active_support_lite/string.rb +38 -0
- data/lib/support/active_support_lite/symbol.rb +16 -0
- data/lib/support/applicable.rb +41 -0
- data/lib/support/arrays.rb +197 -0
- data/lib/support/core_ext.rb +90 -0
- data/lib/support/exceptions.rb +106 -0
- data/lib/support/has_options.rb +16 -0
- data/lib/support/logger.rb +165 -0
- data/lib/support/methodical.rb +17 -0
- data/lib/support/no_stdout.rb +55 -0
- data/lib/support/plotter.rb +62 -0
- data/lib/support/vizier.rb +300 -0
- data/lib/tasks/spec_last.rake +55 -0
- data/lib/tasks/state_fu.rake +57 -0
- data/lib/transition.rb +338 -0
- data/lib/transition_query.rb +224 -0
- data/spec/custom_formatter.rb +49 -0
- data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
- data/spec/features/method_missing_only_once_spec.rb +28 -0
- data/spec/features/not_requirements_spec.rb +118 -0
- data/spec/features/plotter_spec.rb +97 -0
- data/spec/features/shared_log_spec.rb +7 -0
- data/spec/features/singleton_machine_spec.rb +39 -0
- data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
- data/spec/features/transition_boolean_comparison_spec.rb +101 -0
- data/spec/helper.rb +13 -0
- data/spec/integration/active_record_persistence_spec.rb +202 -0
- data/spec/integration/binding_extension_spec.rb +41 -0
- data/spec/integration/class_accessor_spec.rb +117 -0
- data/spec/integration/event_definition_spec.rb +74 -0
- data/spec/integration/example_01_document_spec.rb +133 -0
- data/spec/integration/example_02_string_spec.rb +88 -0
- data/spec/integration/instance_accessor_spec.rb +97 -0
- data/spec/integration/lathe_extension_spec.rb +67 -0
- data/spec/integration/machine_duplication_spec.rb +101 -0
- data/spec/integration/relaxdb_persistence_spec.rb +97 -0
- data/spec/integration/requirement_reflection_spec.rb +270 -0
- data/spec/integration/state_definition_spec.rb +163 -0
- data/spec/integration/transition_spec.rb +1033 -0
- data/spec/spec.opts +9 -0
- data/spec/spec_helper.rb +132 -0
- data/spec/state_fu_spec.rb +948 -0
- data/spec/units/binding_spec.rb +192 -0
- data/spec/units/event_spec.rb +214 -0
- data/spec/units/exceptions_spec.rb +82 -0
- data/spec/units/lathe_spec.rb +570 -0
- data/spec/units/machine_spec.rb +229 -0
- data/spec/units/method_factory_spec.rb +366 -0
- data/spec/units/sprocket_spec.rb +69 -0
- data/spec/units/state_spec.rb +59 -0
- 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
|