enum_state_machine 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -12
  3. data/.ruby-version +1 -1
  4. data/.ruby-version.orig +5 -0
  5. data/Gemfile +0 -1
  6. data/Rakefile +0 -18
  7. data/enum_state_machine.gemspec +35 -0
  8. data/enum_state_machine.gemspec.orig +43 -0
  9. data/lib/enum_state_machine/assertions.rb +36 -0
  10. data/lib/enum_state_machine/branch.rb +225 -0
  11. data/lib/enum_state_machine/callback.rb +232 -0
  12. data/lib/enum_state_machine/core.rb +12 -0
  13. data/lib/enum_state_machine/core_ext/class/state_machine.rb +5 -0
  14. data/lib/enum_state_machine/core_ext.rb +2 -0
  15. data/lib/enum_state_machine/error.rb +13 -0
  16. data/lib/enum_state_machine/eval_helpers.rb +87 -0
  17. data/lib/enum_state_machine/event.rb +257 -0
  18. data/lib/enum_state_machine/event_collection.rb +141 -0
  19. data/lib/enum_state_machine/extensions.rb +149 -0
  20. data/lib/enum_state_machine/graph.rb +93 -0
  21. data/lib/enum_state_machine/helper_module.rb +17 -0
  22. data/lib/enum_state_machine/initializers/rails.rb +22 -0
  23. data/lib/enum_state_machine/initializers.rb +4 -0
  24. data/lib/enum_state_machine/integrations/active_model/locale.rb +11 -0
  25. data/lib/enum_state_machine/integrations/active_model/observer.rb +33 -0
  26. data/lib/enum_state_machine/integrations/active_model/observer_update.rb +42 -0
  27. data/lib/enum_state_machine/integrations/active_model/versions.rb +31 -0
  28. data/lib/enum_state_machine/integrations/active_model.rb +585 -0
  29. data/lib/enum_state_machine/integrations/active_record/locale.rb +20 -0
  30. data/lib/enum_state_machine/integrations/active_record/versions.rb +123 -0
  31. data/lib/enum_state_machine/integrations/active_record.rb +548 -0
  32. data/lib/enum_state_machine/integrations/base.rb +100 -0
  33. data/lib/enum_state_machine/integrations.rb +97 -0
  34. data/lib/enum_state_machine/machine.rb +2292 -0
  35. data/lib/enum_state_machine/machine_collection.rb +86 -0
  36. data/lib/enum_state_machine/macro_methods.rb +518 -0
  37. data/lib/enum_state_machine/matcher.rb +123 -0
  38. data/lib/enum_state_machine/matcher_helpers.rb +54 -0
  39. data/lib/enum_state_machine/node_collection.rb +222 -0
  40. data/lib/enum_state_machine/path.rb +120 -0
  41. data/lib/enum_state_machine/path_collection.rb +90 -0
  42. data/lib/enum_state_machine/state.rb +297 -0
  43. data/lib/enum_state_machine/state_collection.rb +112 -0
  44. data/lib/enum_state_machine/state_context.rb +138 -0
  45. data/lib/enum_state_machine/state_enum.rb +23 -0
  46. data/lib/enum_state_machine/transition.rb +470 -0
  47. data/lib/enum_state_machine/transition_collection.rb +245 -0
  48. data/lib/enum_state_machine/version.rb +3 -0
  49. data/lib/enum_state_machine/yard/handlers/base.rb +32 -0
  50. data/lib/enum_state_machine/yard/handlers/event.rb +25 -0
  51. data/lib/enum_state_machine/yard/handlers/machine.rb +344 -0
  52. data/lib/enum_state_machine/yard/handlers/state.rb +25 -0
  53. data/lib/enum_state_machine/yard/handlers/transition.rb +47 -0
  54. data/lib/enum_state_machine/yard/handlers.rb +12 -0
  55. data/lib/enum_state_machine/yard/templates/default/class/html/setup.rb +30 -0
  56. data/lib/enum_state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
  57. data/lib/enum_state_machine/yard/templates.rb +3 -0
  58. data/lib/enum_state_machine/yard.rb +8 -0
  59. data/lib/enum_state_machine.rb +9 -0
  60. data/lib/tasks/enum_state_machine.rake +1 -0
  61. data/lib/tasks/enum_state_machine.rb +24 -0
  62. data/lib/yard-enum_state_machine.rb +2 -0
  63. data/test/functional/state_machine_test.rb +1066 -0
  64. data/test/unit/graph_test.rb +9 -5
  65. data/test/unit/integrations/active_model_test.rb +1245 -0
  66. data/test/unit/integrations/active_record_test.rb +2551 -0
  67. data/test/unit/integrations/base_test.rb +104 -0
  68. data/test/unit/integrations_test.rb +71 -0
  69. data/test/unit/invalid_event_test.rb +20 -0
  70. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  71. data/test/unit/invalid_transition_test.rb +115 -0
  72. data/test/unit/machine_collection_test.rb +603 -0
  73. data/test/unit/machine_test.rb +3395 -0
  74. data/test/unit/state_machine_test.rb +31 -0
  75. metadata +212 -44
  76. data/Appraisals +0 -28
  77. data/gemfiles/active_model_4.0.4.gemfile +0 -9
  78. data/gemfiles/active_model_4.0.4.gemfile.lock +0 -51
  79. data/gemfiles/active_record_4.0.4.gemfile +0 -11
  80. data/gemfiles/active_record_4.0.4.gemfile.lock +0 -61
  81. data/gemfiles/default.gemfile +0 -7
  82. data/gemfiles/default.gemfile.lock +0 -27
  83. data/gemfiles/graphviz_1.0.9.gemfile +0 -7
  84. data/gemfiles/graphviz_1.0.9.gemfile.lock +0 -30
@@ -0,0 +1,470 @@
1
+ require 'enum_state_machine/transition_collection'
2
+ require 'enum_state_machine/error'
3
+
4
+ module EnumStateMachine
5
+ # An invalid transition was attempted
6
+ class InvalidTransition < Error
7
+ # The machine attempting to be transitioned
8
+ attr_reader :machine
9
+
10
+ # The current state value for the machine
11
+ attr_reader :from
12
+
13
+ def initialize(object, machine, event) #:nodoc:
14
+ @machine = machine
15
+ @from_state = machine.states.match!(object)
16
+ @from = machine.read(object, :state)
17
+ @event = machine.events.fetch(event)
18
+ errors = machine.errors_for(object)
19
+
20
+ message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}"
21
+ message << " (Reason(s): #{errors})" unless errors.empty?
22
+ super(object, message)
23
+ end
24
+
25
+ # The event that triggered the failed transition
26
+ def event
27
+ @event.name
28
+ end
29
+
30
+ # The fully-qualified name of the event that triggered the failed transition
31
+ def qualified_event
32
+ @event.qualified_name
33
+ end
34
+
35
+ # The name for the current state
36
+ def from_name
37
+ @from_state.name
38
+ end
39
+
40
+ # The fully-qualified name for the current state
41
+ def qualified_from_name
42
+ @from_state.qualified_name
43
+ end
44
+ end
45
+
46
+ # A set of transition failed to run in parallel
47
+ class InvalidParallelTransition < Error
48
+ # The set of events that failed the transition(s)
49
+ attr_reader :events
50
+
51
+ def initialize(object, events) #:nodoc:
52
+ @events = events
53
+
54
+ super(object, "Cannot run events in parallel: #{events * ', '}")
55
+ end
56
+ end
57
+
58
+ # A transition represents a state change for a specific attribute.
59
+ #
60
+ # Transitions consist of:
61
+ # * An event
62
+ # * A starting state
63
+ # * An ending state
64
+ class Transition
65
+ # The object being transitioned
66
+ attr_reader :object
67
+
68
+ # The state machine for which this transition is defined
69
+ attr_reader :machine
70
+
71
+ # The original state value *before* the transition
72
+ attr_reader :from
73
+
74
+ # The new state value *after* the transition
75
+ attr_reader :to
76
+
77
+ # The arguments passed in to the event that triggered the transition
78
+ # (does not include the +run_action+ boolean argument if specified)
79
+ attr_accessor :args
80
+
81
+ # The result of invoking the action associated with the machine
82
+ attr_reader :result
83
+
84
+ # Whether the transition is only existing temporarily for the object
85
+ attr_writer :transient
86
+
87
+ # Determines whether the curreny ruby implementation supports pausing and
88
+ # resuming transitions
89
+ def self.pause_supported?
90
+ !defined?(RUBY_ENGINE) || %w(ruby maglev).include?(RUBY_ENGINE)
91
+ end
92
+
93
+ # Creates a new, specific transition
94
+ def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
95
+ @object = object
96
+ @machine = machine
97
+ @args = []
98
+ @transient = false
99
+ @resume_block = nil
100
+
101
+ @event = machine.events.fetch(event)
102
+ @from_state = machine.states.fetch(from_name)
103
+ @from = read_state ? machine.read(object, :state) : @from_state.value
104
+ @to_state = machine.states.fetch(to_name)
105
+ @to = @to_state.value
106
+
107
+ reset
108
+ end
109
+
110
+ # The attribute which this transition's machine is defined for
111
+ def attribute
112
+ machine.attribute
113
+ end
114
+
115
+ # The action that will be run when this transition is performed
116
+ def action
117
+ machine.action
118
+ end
119
+
120
+ # The event that triggered the transition
121
+ def event
122
+ @event.name
123
+ end
124
+
125
+ # The fully-qualified name of the event that triggered the transition
126
+ def qualified_event
127
+ @event.qualified_name
128
+ end
129
+
130
+ # The human-readable name of the event that triggered the transition
131
+ def human_event
132
+ @event.human_name(@object.class)
133
+ end
134
+
135
+ # The state name *before* the transition
136
+ def from_name
137
+ @from_state.name
138
+ end
139
+
140
+ # The fully-qualified state name *before* the transition
141
+ def qualified_from_name
142
+ @from_state.qualified_name
143
+ end
144
+
145
+ # The human-readable state name *before* the transition
146
+ def human_from_name
147
+ @from_state.human_name(@object.class)
148
+ end
149
+
150
+ # The new state name *after* the transition
151
+ def to_name
152
+ @to_state.name
153
+ end
154
+
155
+ # The new fully-qualified state name *after* the transition
156
+ def qualified_to_name
157
+ @to_state.qualified_name
158
+ end
159
+
160
+ # The new human-readable state name *after* the transition
161
+ def human_to_name
162
+ @to_state.human_name(@object.class)
163
+ end
164
+
165
+ # Does this transition represent a loopback (i.e. the from and to state
166
+ # are the same)
167
+ #
168
+ # == Example
169
+ #
170
+ # machine = EnumStateMachine.new(Vehicle)
171
+ # EnumStateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
172
+ # EnumStateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
173
+ def loopback?
174
+ from_name == to_name
175
+ end
176
+
177
+ # Is this transition existing for a short period only? If this is set, it
178
+ # indicates that the transition (or the event backing it) should not be
179
+ # written to the object if it fails.
180
+ def transient?
181
+ @transient
182
+ end
183
+
184
+ # A hash of all the core attributes defined for this transition with their
185
+ # names as keys and values of the attributes as values.
186
+ #
187
+ # == Example
188
+ #
189
+ # machine = EnumStateMachine.new(Vehicle)
190
+ # transition = EnumStateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
191
+ # transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
192
+ def attributes
193
+ @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
194
+ end
195
+
196
+ # Runs the actual transition and any before/after callbacks associated
197
+ # with the transition. The action associated with the transition/machine
198
+ # can be skipped by passing in +false+.
199
+ #
200
+ # == Examples
201
+ #
202
+ # class Vehicle
203
+ # state_machine :action => :save do
204
+ # ...
205
+ # end
206
+ # end
207
+ #
208
+ # vehicle = Vehicle.new
209
+ # transition = EnumStateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
210
+ # transition.perform # => Runs the +save+ action after setting the state attribute
211
+ # transition.perform(false) # => Only sets the state attribute
212
+ # transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
213
+ # transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
214
+ def perform(*args)
215
+ run_action = [true, false].include?(args.last) ? args.pop : true
216
+ self.args = args
217
+
218
+ # Run the transition
219
+ !!TransitionCollection.new([self], :actions => run_action).perform
220
+ end
221
+
222
+ # Runs a block within a transaction for the object being transitioned.
223
+ # By default, transactions are a no-op unless otherwise defined by the
224
+ # machine's integration.
225
+ def within_transaction
226
+ machine.within_transaction(object) do
227
+ yield
228
+ end
229
+ end
230
+
231
+ # Runs the before / after callbacks for this transition. If a block is
232
+ # provided, then it will be executed between the before and after callbacks.
233
+ #
234
+ # Configuration options:
235
+ # * +before+ - Whether to run before callbacks.
236
+ # * +after+ - Whether to run after callbacks. If false, then any around
237
+ # callbacks will be paused until called again with +after+ enabled.
238
+ # Default is true.
239
+ #
240
+ # This will return true if all before callbacks gets executed. After
241
+ # callbacks will not have an effect on the result.
242
+ def run_callbacks(options = {}, &block)
243
+ options = {:before => true, :after => true}.merge(options)
244
+ @success = false
245
+
246
+ halted = pausable { before(options[:after], &block) } if options[:before]
247
+
248
+ # After callbacks are only run if:
249
+ # * An around callback didn't halt after yielding
250
+ # * They're enabled or the run didn't succeed
251
+ after if !(@before_run && halted) && (options[:after] || !@success)
252
+
253
+ @before_run
254
+ end
255
+
256
+ # Transitions the current value of the state to that specified by the
257
+ # transition. Once the state is persisted, it cannot be persisted again
258
+ # until this transition is reset.
259
+ #
260
+ # == Example
261
+ #
262
+ # class Vehicle
263
+ # state_machine do
264
+ # event :ignite do
265
+ # transition :parked => :idling
266
+ # end
267
+ # end
268
+ # end
269
+ #
270
+ # vehicle = Vehicle.new
271
+ # transition = EnumStateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
272
+ # transition.persist
273
+ #
274
+ # vehicle.state # => 'idling'
275
+ def persist
276
+ unless @persisted
277
+ machine.write(object, :state, to)
278
+ @persisted = true
279
+ end
280
+ end
281
+
282
+ # Rolls back changes made to the object's state via this transition. This
283
+ # will revert the state back to the +from+ value.
284
+ #
285
+ # == Example
286
+ #
287
+ # class Vehicle
288
+ # state_machine :initial => :parked do
289
+ # event :ignite do
290
+ # transition :parked => :idling
291
+ # end
292
+ # end
293
+ # end
294
+ #
295
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
296
+ # transition = EnumStateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
297
+ #
298
+ # # Persist the new state
299
+ # vehicle.state # => "parked"
300
+ # transition.persist
301
+ # vehicle.state # => "idling"
302
+ #
303
+ # # Roll back to the original state
304
+ # transition.rollback
305
+ # vehicle.state # => "parked"
306
+ def rollback
307
+ reset
308
+ machine.write(object, :state, from)
309
+ end
310
+
311
+ # Resets any tracking of which callbacks have already been run and whether
312
+ # the state has already been persisted
313
+ def reset
314
+ @before_run = @persisted = @after_run = false
315
+ @paused_block = nil
316
+ end
317
+
318
+ # Determines equality of transitions by testing whether the object, states,
319
+ # and event involved in the transition are equal
320
+ def ==(other)
321
+ other.instance_of?(self.class) &&
322
+ other.object == object &&
323
+ other.machine == machine &&
324
+ other.from_name == from_name &&
325
+ other.to_name == to_name &&
326
+ other.event == event
327
+ end
328
+
329
+ # Generates a nicely formatted description of this transitions's contents.
330
+ #
331
+ # For example,
332
+ #
333
+ # transition = EnumStateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
334
+ # transition # => #<EnumStateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
335
+ def inspect
336
+ "#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
337
+ end
338
+
339
+ private
340
+ # Runs a block that may get paused. If the block doesn't pause, then
341
+ # execution will continue as normal. If the block gets paused, then it
342
+ # will take care of switching the execution context when it's resumed.
343
+ #
344
+ # This will return true if the given block halts for a reason other than
345
+ # getting paused.
346
+ def pausable
347
+ begin
348
+ halted = !catch(:halt) { yield; true }
349
+ rescue Exception => error
350
+ raise unless @resume_block
351
+ end
352
+
353
+ if @resume_block
354
+ @resume_block.call(halted, error)
355
+ else
356
+ halted
357
+ end
358
+ end
359
+
360
+ # Pauses the current callback execution. This should only occur within
361
+ # around callbacks when the remainder of the callback will be executed at
362
+ # a later point in time.
363
+ def pause
364
+ raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' unless self.class.pause_supported?
365
+
366
+ unless @resume_block
367
+ require 'continuation' unless defined?(callcc)
368
+ callcc do |block|
369
+ @paused_block = block
370
+ throw :halt, true
371
+ end
372
+ end
373
+ end
374
+
375
+ # Resumes the execution of a previously paused callback execution. Once
376
+ # the paused callbacks complete, the current execution will continue.
377
+ def resume
378
+ if @paused_block
379
+ halted, error = callcc do |block|
380
+ @resume_block = block
381
+ @paused_block.call
382
+ end
383
+
384
+ @resume_block = @paused_block = nil
385
+
386
+ raise error if error
387
+ !halted
388
+ else
389
+ true
390
+ end
391
+ end
392
+
393
+ # Runs the machine's +before+ callbacks for this transition. Only
394
+ # callbacks that are configured to match the event, from state, and to
395
+ # state will be invoked.
396
+ #
397
+ # Once the callbacks are run, they cannot be run again until this transition
398
+ # is reset.
399
+ def before(complete = true, index = 0, &block)
400
+ unless @before_run
401
+ while callback = machine.callbacks[:before][index]
402
+ index += 1
403
+
404
+ if callback.type == :around
405
+ # Around callback: need to handle recursively. Execution only gets
406
+ # paused if:
407
+ # * The block fails and the callback doesn't run on failures OR
408
+ # * The block succeeds, but after callbacks are disabled (in which
409
+ # case a continuation is stored for later execution)
410
+ return if catch(:cancel) do
411
+ callback.call(object, context, self) do
412
+ before(complete, index, &block)
413
+
414
+ pause if @success && !complete
415
+ throw :cancel, true unless @success
416
+ end
417
+ end
418
+ else
419
+ # Normal before callback
420
+ callback.call(object, context, self)
421
+ end
422
+ end
423
+
424
+ @before_run = true
425
+ end
426
+
427
+ action = {:success => true}.merge(block_given? ? yield : {})
428
+ @result, @success = action[:result], action[:success]
429
+ end
430
+
431
+ # Runs the machine's +after+ callbacks for this transition. Only
432
+ # callbacks that are configured to match the event, from state, and to
433
+ # state will be invoked.
434
+ #
435
+ # Once the callbacks are run, they cannot be run again until this transition
436
+ # is reset.
437
+ #
438
+ # == Halting
439
+ #
440
+ # If any callback throws a <tt>:halt</tt> exception, it will be caught
441
+ # and the callback chain will be automatically stopped. However, this
442
+ # exception will not bubble up to the caller since +after+ callbacks
443
+ # should never halt the execution of a +perform+.
444
+ def after
445
+ unless @after_run
446
+ # First resume previously paused callbacks
447
+ if resume
448
+ catch(:halt) do
449
+ type = @success ? :after : :failure
450
+ machine.callbacks[type].each {|callback| callback.call(object, context, self)}
451
+ end
452
+ end
453
+
454
+ @after_run = true
455
+ end
456
+ end
457
+
458
+ # Gets a hash of the context defining this unique transition (including
459
+ # event, from state, and to state).
460
+ #
461
+ # == Example
462
+ #
463
+ # machine = EnumStateMachine.new(Vehicle)
464
+ # transition = EnumStateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
465
+ # transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
466
+ def context
467
+ @context ||= {:on => event, :from => from_name, :to => to_name}
468
+ end
469
+ end
470
+ end
@@ -0,0 +1,245 @@
1
+ module EnumStateMachine
2
+ # Represents a collection of transitions in a state machine
3
+ class TransitionCollection < Array
4
+ include Assertions
5
+
6
+ # Whether to skip running the action for each transition's machine
7
+ attr_reader :skip_actions
8
+
9
+ # Whether to skip running the after callbacks
10
+ attr_reader :skip_after
11
+
12
+ # Whether transitions should wrapped around a transaction block
13
+ attr_reader :use_transaction
14
+
15
+ # Creates a new collection of transitions that can be run in parallel. Each
16
+ # transition *must* be for a different attribute.
17
+ #
18
+ # Configuration options:
19
+ # * <tt>:actions</tt> - Whether to run the action configured for each transition
20
+ # * <tt>:after</tt> - Whether to run after callbacks
21
+ # * <tt>:transaction</tt> - Whether to wrap transitions within a transaction
22
+ def initialize(transitions = [], options = {})
23
+ super(transitions)
24
+
25
+ # Determine the validity of the transitions as a whole
26
+ @valid = all?
27
+ reject! {|transition| !transition}
28
+
29
+ attributes = map {|transition| transition.attribute}.uniq
30
+ raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length
31
+
32
+ assert_valid_keys(options, :actions, :after, :transaction)
33
+ options = {:actions => true, :after => true, :transaction => true}.merge(options)
34
+ @skip_actions = !options[:actions]
35
+ @skip_after = !options[:after]
36
+ @use_transaction = options[:transaction]
37
+ end
38
+
39
+ # Runs each of the collection's transitions in parallel.
40
+ #
41
+ # All transitions will run through the following steps:
42
+ # 1. Before callbacks
43
+ # 2. Persist state
44
+ # 3. Invoke action
45
+ # 4. After callbacks (if configured)
46
+ # 5. Rollback (if action is unsuccessful)
47
+ #
48
+ # If a block is passed to this method, that block will be called instead
49
+ # of invoking each transition's action.
50
+ def perform(&block)
51
+ reset
52
+
53
+ if valid?
54
+ if use_event_attributes? && !block_given?
55
+ each do |transition|
56
+ transition.transient = true
57
+ transition.machine.write(object, :event_transition, transition)
58
+ end
59
+
60
+ run_actions
61
+ else
62
+ within_transaction do
63
+ catch(:halt) { run_callbacks(&block) }
64
+ rollback unless success?
65
+ end
66
+ end
67
+ end
68
+
69
+ if actions.length == 1 && results.include?(actions.first)
70
+ results[actions.first]
71
+ else
72
+ success?
73
+ end
74
+ end
75
+
76
+ protected
77
+ attr_reader :results #:nodoc:
78
+
79
+ private
80
+ # Is this a valid set of transitions? If the collection was creating with
81
+ # any +false+ values for transitions, then the the collection will be
82
+ # marked as invalid.
83
+ def valid?
84
+ @valid
85
+ end
86
+
87
+ # Did each transition perform successfully? This will only be true if the
88
+ # following requirements are met:
89
+ # * No +before+ callbacks halt
90
+ # * All actions run successfully (always true if skipping actions)
91
+ def success?
92
+ @success
93
+ end
94
+
95
+ # Gets the object being transitioned
96
+ def object
97
+ first.object
98
+ end
99
+
100
+ # Gets the list of actions to run. If configured to skip actions, then
101
+ # this will return an empty collection.
102
+ def actions
103
+ empty? ? [nil] : map {|transition| transition.action}.uniq
104
+ end
105
+
106
+ # Determines whether an event attribute be used to trigger the transitions
107
+ # in this collection or whether the transitions be run directly *outside*
108
+ # of the action.
109
+ def use_event_attributes?
110
+ !skip_actions && !skip_after && actions.all? && actions.length == 1 && first.machine.action_hook?
111
+ end
112
+
113
+ # Resets any information tracked from previous attempts to perform the
114
+ # collection
115
+ def reset
116
+ @results = {}
117
+ @success = false
118
+ end
119
+
120
+ # Runs each transition's callbacks recursively. Once all before callbacks
121
+ # have been executed, the transitions will then be persisted and the
122
+ # configured actions will be run.
123
+ #
124
+ # If any transition fails to run its callbacks, :halt will be thrown.
125
+ def run_callbacks(index = 0, &block)
126
+ if transition = self[index]
127
+ throw :halt unless transition.run_callbacks(:after => !skip_after) do
128
+ run_callbacks(index + 1, &block)
129
+ {:result => results[transition.action], :success => success?}
130
+ end
131
+ else
132
+ persist
133
+ run_actions(&block)
134
+ end
135
+ end
136
+
137
+ # Transitions the current value of the object's states to those specified by
138
+ # each transition
139
+ def persist
140
+ each {|transition| transition.persist}
141
+ end
142
+
143
+ # Runs the actions for each transition. If a block is given method, then it
144
+ # will be called instead of invoking each transition's action.
145
+ #
146
+ # The results of the actions will be used to determine #success?.
147
+ def run_actions
148
+ catch_exceptions do
149
+ @success = if block_given?
150
+ result = yield
151
+ actions.each {|action| results[action] = result}
152
+ !!result
153
+ else
154
+ actions.compact.each {|action| !skip_actions && results[action] = object.send(action)}
155
+ results.values.all?
156
+ end
157
+ end
158
+ end
159
+
160
+ # Rolls back changes made to the object's states via each transition
161
+ def rollback
162
+ each {|transition| transition.rollback}
163
+ end
164
+
165
+ # Wraps the given block with a rescue handler so that any exceptions that
166
+ # occur will automatically result in the transition rolling back any changes
167
+ # that were made to the object involved.
168
+ def catch_exceptions
169
+ begin
170
+ yield
171
+ rescue Exception
172
+ rollback
173
+ raise
174
+ end
175
+ end
176
+
177
+ # Runs a block within a transaction for the object being transitioned. If
178
+ # transactions are disabled, then this is a no-op.
179
+ def within_transaction
180
+ if use_transaction && !empty?
181
+ first.within_transaction do
182
+ yield
183
+ success?
184
+ end
185
+ else
186
+ yield
187
+ end
188
+ end
189
+ end
190
+
191
+ # Represents a collection of transitions that were generated from attribute-
192
+ # based events
193
+ class AttributeTransitionCollection < TransitionCollection
194
+ def initialize(transitions = [], options = {}) #:nodoc:
195
+ super(transitions, {:transaction => false, :actions => false}.merge(options))
196
+ end
197
+
198
+ private
199
+ # Hooks into running transition callbacks so that event / event transition
200
+ # attributes can be properly updated
201
+ def run_callbacks(index = 0)
202
+ if index == 0
203
+ # Clears any traces of the event attribute to prevent it from being
204
+ # evaluated multiple times if actions are nested
205
+ each do |transition|
206
+ transition.machine.write(object, :event, nil)
207
+ transition.machine.write(object, :event_transition, nil)
208
+ end
209
+
210
+ # Rollback only if exceptions occur during before callbacks
211
+ begin
212
+ super
213
+ rescue Exception
214
+ rollback unless @before_run
215
+ raise
216
+ end
217
+
218
+ # Persists transitions on the object if partial transition was successful.
219
+ # This allows us to reference them later to complete the transition with
220
+ # after callbacks.
221
+ each {|transition| transition.machine.write(object, :event_transition, transition)} if skip_after && success?
222
+ else
223
+ super
224
+ end
225
+ end
226
+
227
+ # Tracks that before callbacks have now completed
228
+ def persist
229
+ @before_run = true
230
+ super
231
+ end
232
+
233
+ # Resets callback tracking
234
+ def reset
235
+ super
236
+ @before_run = false
237
+ end
238
+
239
+ # Resets the event attribute so it can be re-evaluated if attempted again
240
+ def rollback
241
+ super
242
+ each {|transition| transition.machine.write(object, :event, transition.event) unless transition.transient?}
243
+ end
244
+ end
245
+ end