spree-state_machine 2.0.0.beta1

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 (140) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +12 -0
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +502 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +20 -0
  8. data/README.md +1246 -0
  9. data/Rakefile +20 -0
  10. data/examples/AutoShop_state.png +0 -0
  11. data/examples/Car_state.png +0 -0
  12. data/examples/Gemfile +5 -0
  13. data/examples/Gemfile.lock +14 -0
  14. data/examples/TrafficLight_state.png +0 -0
  15. data/examples/Vehicle_state.png +0 -0
  16. data/examples/auto_shop.rb +13 -0
  17. data/examples/car.rb +21 -0
  18. data/examples/doc/AutoShop.html +2856 -0
  19. data/examples/doc/AutoShop_state.png +0 -0
  20. data/examples/doc/Car.html +919 -0
  21. data/examples/doc/Car_state.png +0 -0
  22. data/examples/doc/TrafficLight.html +2230 -0
  23. data/examples/doc/TrafficLight_state.png +0 -0
  24. data/examples/doc/Vehicle.html +7921 -0
  25. data/examples/doc/Vehicle_state.png +0 -0
  26. data/examples/doc/_index.html +136 -0
  27. data/examples/doc/class_list.html +47 -0
  28. data/examples/doc/css/common.css +1 -0
  29. data/examples/doc/css/full_list.css +55 -0
  30. data/examples/doc/css/style.css +322 -0
  31. data/examples/doc/file_list.html +46 -0
  32. data/examples/doc/frames.html +13 -0
  33. data/examples/doc/index.html +136 -0
  34. data/examples/doc/js/app.js +205 -0
  35. data/examples/doc/js/full_list.js +173 -0
  36. data/examples/doc/js/jquery.js +16 -0
  37. data/examples/doc/method_list.html +734 -0
  38. data/examples/doc/top-level-namespace.html +105 -0
  39. data/examples/merb-rest/controller.rb +51 -0
  40. data/examples/merb-rest/model.rb +28 -0
  41. data/examples/merb-rest/view_edit.html.erb +24 -0
  42. data/examples/merb-rest/view_index.html.erb +23 -0
  43. data/examples/merb-rest/view_new.html.erb +13 -0
  44. data/examples/merb-rest/view_show.html.erb +17 -0
  45. data/examples/rails-rest/controller.rb +43 -0
  46. data/examples/rails-rest/migration.rb +7 -0
  47. data/examples/rails-rest/model.rb +23 -0
  48. data/examples/rails-rest/view__form.html.erb +34 -0
  49. data/examples/rails-rest/view_edit.html.erb +6 -0
  50. data/examples/rails-rest/view_index.html.erb +25 -0
  51. data/examples/rails-rest/view_new.html.erb +5 -0
  52. data/examples/rails-rest/view_show.html.erb +19 -0
  53. data/examples/traffic_light.rb +9 -0
  54. data/examples/vehicle.rb +33 -0
  55. data/lib/state_machine/assertions.rb +36 -0
  56. data/lib/state_machine/branch.rb +225 -0
  57. data/lib/state_machine/callback.rb +236 -0
  58. data/lib/state_machine/core.rb +7 -0
  59. data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
  60. data/lib/state_machine/core_ext.rb +2 -0
  61. data/lib/state_machine/error.rb +13 -0
  62. data/lib/state_machine/eval_helpers.rb +87 -0
  63. data/lib/state_machine/event.rb +257 -0
  64. data/lib/state_machine/event_collection.rb +141 -0
  65. data/lib/state_machine/extensions.rb +149 -0
  66. data/lib/state_machine/graph.rb +92 -0
  67. data/lib/state_machine/helper_module.rb +17 -0
  68. data/lib/state_machine/initializers/rails.rb +25 -0
  69. data/lib/state_machine/initializers.rb +4 -0
  70. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  71. data/lib/state_machine/integrations/active_model/observer.rb +33 -0
  72. data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
  73. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  74. data/lib/state_machine/integrations/active_model.rb +585 -0
  75. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  76. data/lib/state_machine/integrations/active_record/versions.rb +123 -0
  77. data/lib/state_machine/integrations/active_record.rb +525 -0
  78. data/lib/state_machine/integrations/base.rb +100 -0
  79. data/lib/state_machine/integrations.rb +121 -0
  80. data/lib/state_machine/machine.rb +2287 -0
  81. data/lib/state_machine/machine_collection.rb +74 -0
  82. data/lib/state_machine/macro_methods.rb +522 -0
  83. data/lib/state_machine/matcher.rb +123 -0
  84. data/lib/state_machine/matcher_helpers.rb +54 -0
  85. data/lib/state_machine/node_collection.rb +222 -0
  86. data/lib/state_machine/path.rb +120 -0
  87. data/lib/state_machine/path_collection.rb +90 -0
  88. data/lib/state_machine/state.rb +297 -0
  89. data/lib/state_machine/state_collection.rb +112 -0
  90. data/lib/state_machine/state_context.rb +138 -0
  91. data/lib/state_machine/transition.rb +470 -0
  92. data/lib/state_machine/transition_collection.rb +245 -0
  93. data/lib/state_machine/version.rb +3 -0
  94. data/lib/state_machine/yard/handlers/base.rb +32 -0
  95. data/lib/state_machine/yard/handlers/event.rb +25 -0
  96. data/lib/state_machine/yard/handlers/machine.rb +344 -0
  97. data/lib/state_machine/yard/handlers/state.rb +25 -0
  98. data/lib/state_machine/yard/handlers/transition.rb +47 -0
  99. data/lib/state_machine/yard/handlers.rb +12 -0
  100. data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
  101. data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
  102. data/lib/state_machine/yard/templates.rb +3 -0
  103. data/lib/state_machine/yard.rb +8 -0
  104. data/lib/state_machine.rb +8 -0
  105. data/lib/yard-state_machine.rb +2 -0
  106. data/state_machine.gemspec +22 -0
  107. data/test/files/en.yml +17 -0
  108. data/test/files/switch.rb +15 -0
  109. data/test/functional/state_machine_test.rb +1066 -0
  110. data/test/test_helper.rb +7 -0
  111. data/test/unit/assertions_test.rb +40 -0
  112. data/test/unit/branch_test.rb +969 -0
  113. data/test/unit/callback_test.rb +704 -0
  114. data/test/unit/error_test.rb +43 -0
  115. data/test/unit/eval_helpers_test.rb +270 -0
  116. data/test/unit/event_collection_test.rb +398 -0
  117. data/test/unit/event_test.rb +1196 -0
  118. data/test/unit/graph_test.rb +98 -0
  119. data/test/unit/helper_module_test.rb +17 -0
  120. data/test/unit/integrations/active_model_test.rb +1245 -0
  121. data/test/unit/integrations/active_record_test.rb +2551 -0
  122. data/test/unit/integrations/base_test.rb +104 -0
  123. data/test/unit/integrations_test.rb +71 -0
  124. data/test/unit/invalid_event_test.rb +20 -0
  125. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  126. data/test/unit/invalid_transition_test.rb +115 -0
  127. data/test/unit/machine_collection_test.rb +603 -0
  128. data/test/unit/machine_test.rb +3395 -0
  129. data/test/unit/matcher_helpers_test.rb +37 -0
  130. data/test/unit/matcher_test.rb +155 -0
  131. data/test/unit/node_collection_test.rb +362 -0
  132. data/test/unit/path_collection_test.rb +266 -0
  133. data/test/unit/path_test.rb +485 -0
  134. data/test/unit/state_collection_test.rb +352 -0
  135. data/test/unit/state_context_test.rb +441 -0
  136. data/test/unit/state_machine_test.rb +31 -0
  137. data/test/unit/state_test.rb +1101 -0
  138. data/test/unit/transition_collection_test.rb +2168 -0
  139. data/test/unit/transition_test.rb +1558 -0
  140. metadata +264 -0
@@ -0,0 +1,470 @@
1
+ require 'state_machine/transition_collection'
2
+ require 'state_machine/error'
3
+
4
+ module StateMachine
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 = StateMachine.new(Vehicle)
171
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
172
+ # StateMachine::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 = StateMachine.new(Vehicle)
190
+ # transition = StateMachine::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 = StateMachine::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 = StateMachine::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 = StateMachine::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 = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
334
+ # transition # => #<StateMachine::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 = StateMachine.new(Vehicle)
464
+ # transition = StateMachine::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 StateMachine
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