state_machine 0.8.1 → 0.9.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.
- data/CHANGELOG.rdoc +17 -0
- data/LICENSE +1 -1
- data/README.rdoc +162 -23
- data/Rakefile +3 -18
- data/lib/state_machine.rb +3 -4
- data/lib/state_machine/callback.rb +65 -13
- data/lib/state_machine/eval_helpers.rb +20 -4
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +7 -0
- data/lib/state_machine/integrations.rb +21 -6
- data/lib/state_machine/integrations/active_model.rb +414 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/{active_record → active_model}/observer.rb +7 -7
- data/lib/state_machine/integrations/active_record.rb +65 -129
- data/lib/state_machine/integrations/active_record/locale.rb +4 -11
- data/lib/state_machine/integrations/data_mapper.rb +24 -6
- data/lib/state_machine/integrations/data_mapper/observer.rb +36 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +295 -0
- data/lib/state_machine/integrations/sequel.rb +33 -7
- data/lib/state_machine/machine.rb +121 -23
- data/lib/state_machine/machine_collection.rb +12 -103
- data/lib/state_machine/transition.rb +125 -164
- data/lib/state_machine/transition_collection.rb +244 -0
- data/lib/tasks/state_machine.rb +12 -15
- data/test/functional/state_machine_test.rb +11 -1
- data/test/unit/callback_test.rb +305 -32
- data/test/unit/eval_helpers_test.rb +103 -1
- data/test/unit/event_test.rb +2 -1
- data/test/unit/guard_test.rb +2 -1
- data/test/unit/integrations/active_model_test.rb +909 -0
- data/test/unit/integrations/active_record_test.rb +1542 -1292
- data/test/unit/integrations/data_mapper_test.rb +1369 -1041
- data/test/unit/integrations/mongo_mapper_test.rb +1349 -0
- data/test/unit/integrations/sequel_test.rb +1214 -985
- data/test/unit/integrations_test.rb +8 -0
- data/test/unit/machine_collection_test.rb +140 -513
- data/test/unit/machine_test.rb +212 -10
- data/test/unit/state_test.rb +2 -1
- data/test/unit/transition_collection_test.rb +2098 -0
- data/test/unit/transition_test.rb +704 -552
- metadata +16 -3
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'state_machine/transition_collection'
|
2
|
+
|
1
3
|
module StateMachine
|
2
4
|
# An invalid transition was attempted
|
3
5
|
class InvalidTransition < StandardError
|
@@ -10,83 +12,6 @@ module StateMachine
|
|
10
12
|
# * A starting state
|
11
13
|
# * An ending state
|
12
14
|
class Transition
|
13
|
-
class << self
|
14
|
-
# Runs one or more transitions in parallel. All transitions will run
|
15
|
-
# through the following steps:
|
16
|
-
# 1. Before callbacks
|
17
|
-
# 2. Persist state
|
18
|
-
# 3. Invoke action
|
19
|
-
# 4. After callbacks (if configured)
|
20
|
-
# 5. Rollback (if action is unsuccessful)
|
21
|
-
#
|
22
|
-
# Configuration options:
|
23
|
-
# * <tt>:action</tt> - Whether to run the action configured for each transition
|
24
|
-
# * <tt>:after</tt> - Whether to run after callbacks
|
25
|
-
#
|
26
|
-
# If a block is passed to this method, that block will be called instead
|
27
|
-
# of invoking each transition's action.
|
28
|
-
def perform(transitions, options = {})
|
29
|
-
# Validate that the transitions are for separate machines / attributes
|
30
|
-
attributes = transitions.map {|transition| transition.attribute}.uniq
|
31
|
-
raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != transitions.length
|
32
|
-
|
33
|
-
success = false
|
34
|
-
|
35
|
-
# Run before callbacks. If any callback halts, then the entire chain
|
36
|
-
# is halted for every transition.
|
37
|
-
if transitions.all? {|transition| transition.before}
|
38
|
-
# Persist the new state for each attribute
|
39
|
-
transitions.each {|transition| transition.persist}
|
40
|
-
|
41
|
-
# Run the actions associated with each machine
|
42
|
-
begin
|
43
|
-
results = {}
|
44
|
-
success =
|
45
|
-
if block_given?
|
46
|
-
# Block was given: use the result for each transition
|
47
|
-
result = yield
|
48
|
-
transitions.each {|transition| results[transition.action] = result}
|
49
|
-
!!result
|
50
|
-
elsif options[:action] == false
|
51
|
-
# Skip the action
|
52
|
-
true
|
53
|
-
else
|
54
|
-
# Run each transition's action (only once)
|
55
|
-
object = transitions.first.object
|
56
|
-
transitions.all? do |transition|
|
57
|
-
action = transition.action
|
58
|
-
action && !results.include?(action) ? results[action] = object.send(action) : true
|
59
|
-
end
|
60
|
-
end
|
61
|
-
rescue Exception
|
62
|
-
# Action failed: rollback
|
63
|
-
transitions.each {|transition| transition.rollback}
|
64
|
-
raise
|
65
|
-
end
|
66
|
-
|
67
|
-
# Run after callbacks even when the actions failed. The :after option
|
68
|
-
# is ignored if the transitions were unsuccessful.
|
69
|
-
transitions.each {|transition| transition.after(results[transition.action], success)} unless options[:after] == false && success
|
70
|
-
|
71
|
-
# Rollback the transitions if the transaction was unsuccessful
|
72
|
-
transitions.each {|transition| transition.rollback} unless success
|
73
|
-
end
|
74
|
-
|
75
|
-
success
|
76
|
-
end
|
77
|
-
|
78
|
-
# Runs one or more transitions within a transaction. See StateMachine::Transition.perform
|
79
|
-
# for more information.
|
80
|
-
def perform_within_transaction(transitions, options = {})
|
81
|
-
success = false
|
82
|
-
transitions.first.within_transaction do
|
83
|
-
success = perform(transitions, options)
|
84
|
-
end
|
85
|
-
|
86
|
-
success
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
15
|
# The object being transitioned
|
91
16
|
attr_reader :object
|
92
17
|
|
@@ -124,11 +49,15 @@ module StateMachine
|
|
124
49
|
# The result of invoking the action associated with the machine
|
125
50
|
attr_reader :result
|
126
51
|
|
52
|
+
# Whether the transition is only existing temporarily for the object
|
53
|
+
attr_writer :transient
|
54
|
+
|
127
55
|
# Creates a new, specific transition
|
128
56
|
def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
|
129
57
|
@object = object
|
130
58
|
@machine = machine
|
131
59
|
@args = []
|
60
|
+
@transient = false
|
132
61
|
|
133
62
|
# Event information
|
134
63
|
event = machine.events.fetch(event)
|
@@ -146,6 +75,8 @@ module StateMachine
|
|
146
75
|
@to = to_state.value
|
147
76
|
@to_name = to_state.name
|
148
77
|
@qualified_to_name = to_state.qualified_name
|
78
|
+
|
79
|
+
reset
|
149
80
|
end
|
150
81
|
|
151
82
|
# The attribute which this transition's machine is defined for
|
@@ -170,6 +101,13 @@ module StateMachine
|
|
170
101
|
from_name == to_name
|
171
102
|
end
|
172
103
|
|
104
|
+
# Is this transition existing for a short period only? If this is set, it
|
105
|
+
# indicates that the transition (or the event backing it) should not be
|
106
|
+
# written to the object if it fails.
|
107
|
+
def transient?
|
108
|
+
@transient
|
109
|
+
end
|
110
|
+
|
173
111
|
# A hash of all the core attributes defined for this transition with their
|
174
112
|
# names as keys and values of the attributes as values.
|
175
113
|
#
|
@@ -203,7 +141,7 @@ module StateMachine
|
|
203
141
|
self.args = args
|
204
142
|
|
205
143
|
# Run the transition
|
206
|
-
|
144
|
+
!!TransitionCollection.new([self], :actions => run_action).perform
|
207
145
|
end
|
208
146
|
|
209
147
|
# Runs a block within a transaction for the object being transitioned.
|
@@ -215,37 +153,39 @@ module StateMachine
|
|
215
153
|
end
|
216
154
|
end
|
217
155
|
|
218
|
-
# Runs the
|
219
|
-
#
|
220
|
-
# state will be invoked.
|
221
|
-
#
|
222
|
-
# Once the callbacks are run, they cannot be run again until this transition
|
223
|
-
# is reset.
|
156
|
+
# Runs the before / after callbacks for this transition. If a block is
|
157
|
+
# provided, then it will be executed between the before and after callbacks.
|
224
158
|
#
|
225
|
-
#
|
159
|
+
# Configuration options:
|
160
|
+
# * +after+ - Whether to run after callbacks. If false, then any around
|
161
|
+
# callbacks will be paused until called again with +after+ enabled.
|
162
|
+
# Default is true.
|
226
163
|
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
#
|
233
|
-
# vehicle = Vehicle.new
|
234
|
-
# transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
|
235
|
-
# transition.before
|
236
|
-
def before
|
237
|
-
result = false
|
164
|
+
# This will return true if all before callbacks gets executed. After
|
165
|
+
# callbacks will not have an effect on the result.
|
166
|
+
def run_callbacks(options = {}, &block)
|
167
|
+
options = {:after => true}.merge(options)
|
168
|
+
@success = false
|
238
169
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
170
|
+
# Run before callbacks. :halt is caught here so that it rolls up through
|
171
|
+
# any around callbacks.
|
172
|
+
begin
|
173
|
+
halted = !catch(:halt) { before(options[:after], &block); true }
|
174
|
+
rescue Exception => error
|
175
|
+
raise unless @after_block
|
176
|
+
end
|
177
|
+
|
178
|
+
# After callbacks are only run if:
|
179
|
+
# * There isn't an after block already running
|
180
|
+
# * An around callback didn't halt after yielding
|
181
|
+
# * They're enabled or the run didn't succeed
|
182
|
+
if @after_block
|
183
|
+
@after_block.call(halted, error)
|
184
|
+
elsif !(@before_run && halted) && (options[:after] || !@success)
|
185
|
+
after
|
246
186
|
end
|
247
187
|
|
248
|
-
|
188
|
+
@before_run
|
249
189
|
end
|
250
190
|
|
251
191
|
# Transitions the current value of the state to that specified by the
|
@@ -274,51 +214,6 @@ module StateMachine
|
|
274
214
|
end
|
275
215
|
end
|
276
216
|
|
277
|
-
# Runs the machine's +after+ callbacks for this transition. Only
|
278
|
-
# callbacks that are configured to match the event, from state, and to
|
279
|
-
# state will be invoked.
|
280
|
-
#
|
281
|
-
# The result can be used to indicate whether the associated machine action
|
282
|
-
# was executed successfully.
|
283
|
-
#
|
284
|
-
# Once the callbacks are run, they cannot be run again until this transition
|
285
|
-
# is reset.
|
286
|
-
#
|
287
|
-
# == Halting
|
288
|
-
#
|
289
|
-
# If any callback throws a <tt>:halt</tt> exception, it will be caught
|
290
|
-
# and the callback chain will be automatically stopped. However, this
|
291
|
-
# exception will not bubble up to the caller since +after+ callbacks
|
292
|
-
# should never halt the execution of a +perform+.
|
293
|
-
#
|
294
|
-
# == Example
|
295
|
-
#
|
296
|
-
# class Vehicle
|
297
|
-
# state_machine do
|
298
|
-
# after_transition :on => :ignite, :do => lambda {|vehicle| ...}
|
299
|
-
#
|
300
|
-
# event :ignite do
|
301
|
-
# transition :parked => :idling
|
302
|
-
# end
|
303
|
-
# end
|
304
|
-
# end
|
305
|
-
#
|
306
|
-
# vehicle = Vehicle.new
|
307
|
-
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
308
|
-
# transition.after(true)
|
309
|
-
def after(result = nil, success = true)
|
310
|
-
@result = result
|
311
|
-
|
312
|
-
catch(:halt) do
|
313
|
-
unless @after_run
|
314
|
-
callback(:after, :success => success)
|
315
|
-
@after_run = true
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
true
|
320
|
-
end
|
321
|
-
|
322
217
|
# Rolls back changes made to the object's state via this transition. This
|
323
218
|
# will revert the state back to the +from+ value.
|
324
219
|
#
|
@@ -352,6 +247,7 @@ module StateMachine
|
|
352
247
|
# the state has already been persisted
|
353
248
|
def reset
|
354
249
|
@before_run = @persisted = @after_run = false
|
250
|
+
@around_block = nil
|
355
251
|
end
|
356
252
|
|
357
253
|
# Generates a nicely formatted description of this transitions's contents.
|
@@ -364,7 +260,86 @@ module StateMachine
|
|
364
260
|
"#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
|
365
261
|
end
|
366
262
|
|
367
|
-
|
263
|
+
private
|
264
|
+
# Runs the machine's +before+ callbacks for this transition. Only
|
265
|
+
# callbacks that are configured to match the event, from state, and to
|
266
|
+
# state will be invoked.
|
267
|
+
#
|
268
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
269
|
+
# is reset.
|
270
|
+
def before(complete = true, index = 0, &block)
|
271
|
+
unless @before_run
|
272
|
+
while callback = machine.callbacks[:before][index]
|
273
|
+
index += 1
|
274
|
+
|
275
|
+
if callback.type == :around
|
276
|
+
# Around callback: need to handle recursively. Execution only gets
|
277
|
+
# paused if:
|
278
|
+
# * The block fails and the callback doesn't run on failures OR
|
279
|
+
# * The block succeeds, but after callbacks are disabled (in which
|
280
|
+
# case a continuation is stored for later execution)
|
281
|
+
return if catch(:pause) do
|
282
|
+
callback.call(object, context, self) do
|
283
|
+
before(complete, index, &block)
|
284
|
+
|
285
|
+
if @success && !complete && !@around_block && !@after_block
|
286
|
+
require 'continuation' unless defined?(callcc)
|
287
|
+
callcc {|block| @around_block = block}
|
288
|
+
end
|
289
|
+
|
290
|
+
throw :pause, true if @around_block && !@after_block || !callback.matches_success?(@success)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
else
|
294
|
+
# Normal before callback
|
295
|
+
callback.call(object, context, self)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
@before_run = true
|
300
|
+
end
|
301
|
+
|
302
|
+
action = {:success => true}.merge(block_given? ? yield : {})
|
303
|
+
@result, @success = action[:result], action[:success]
|
304
|
+
end
|
305
|
+
|
306
|
+
# Runs the machine's +after+ callbacks for this transition. Only
|
307
|
+
# callbacks that are configured to match the event, from state, and to
|
308
|
+
# state will be invoked.
|
309
|
+
#
|
310
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
311
|
+
# is reset.
|
312
|
+
#
|
313
|
+
# == Halting
|
314
|
+
#
|
315
|
+
# If any callback throws a <tt>:halt</tt> exception, it will be caught
|
316
|
+
# and the callback chain will be automatically stopped. However, this
|
317
|
+
# exception will not bubble up to the caller since +after+ callbacks
|
318
|
+
# should never halt the execution of a +perform+.
|
319
|
+
def after
|
320
|
+
unless @after_run
|
321
|
+
catch(:halt) do
|
322
|
+
# First call any yielded around blocks
|
323
|
+
if @around_block
|
324
|
+
halted, error = callcc do |block|
|
325
|
+
@after_block = block
|
326
|
+
@around_block.call
|
327
|
+
end
|
328
|
+
|
329
|
+
@after_block = @around_block = nil
|
330
|
+
raise error if error
|
331
|
+
throw :halt if halted
|
332
|
+
end
|
333
|
+
|
334
|
+
# Call normal after callbacks in order
|
335
|
+
after_context = context.merge(:success => @success)
|
336
|
+
machine.callbacks[:after].each {|callback| callback.call(object, after_context, self)}
|
337
|
+
end
|
338
|
+
|
339
|
+
@after_run = true
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
368
343
|
# Gets a hash of the context defining this unique transition (including
|
369
344
|
# event, from state, and to state).
|
370
345
|
#
|
@@ -376,19 +351,5 @@ module StateMachine
|
|
376
351
|
def context
|
377
352
|
@context ||= {:on => event, :from => from_name, :to => to_name}
|
378
353
|
end
|
379
|
-
|
380
|
-
# Runs the callbacks of the given type for this transition. This will
|
381
|
-
# only invoke callbacks that exactly match the event, from state, and
|
382
|
-
# to state that describe this transition.
|
383
|
-
#
|
384
|
-
# Additional callback parameters can be specified. By default, this
|
385
|
-
# transition is also passed into callbacks.
|
386
|
-
def callback(type, context = {})
|
387
|
-
context = self.context.merge(context)
|
388
|
-
|
389
|
-
machine.callbacks[type].each do |callback|
|
390
|
-
callback.call(object, context, self)
|
391
|
-
end
|
392
|
-
end
|
393
354
|
end
|
394
355
|
end
|
@@ -0,0 +1,244 @@
|
|
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
|
+
private
|
77
|
+
attr_reader :results #:nodoc:
|
78
|
+
|
79
|
+
# Is this a valid set of transitions? If the collection was creating with
|
80
|
+
# any +false+ values for transitions, then the the collection will be
|
81
|
+
# marked as invalid.
|
82
|
+
def valid?
|
83
|
+
@valid
|
84
|
+
end
|
85
|
+
|
86
|
+
# Did each transition perform successfully? This will only be true if the
|
87
|
+
# following requirements are met:
|
88
|
+
# * No +before+ callbacks halt
|
89
|
+
# * All actions run successfully (always true if skipping actions)
|
90
|
+
def success?
|
91
|
+
@success
|
92
|
+
end
|
93
|
+
|
94
|
+
# Gets the object being transitioned
|
95
|
+
def object
|
96
|
+
first.object
|
97
|
+
end
|
98
|
+
|
99
|
+
# Gets the list of actions to run. If configured to skip actions, then
|
100
|
+
# this will return an empty collection.
|
101
|
+
def actions
|
102
|
+
empty? ? [nil] : map {|transition| transition.action}.uniq
|
103
|
+
end
|
104
|
+
|
105
|
+
# Determines whether an event attribute be used to trigger the transitions
|
106
|
+
# in this collection or whether the transitions be run directly *outside*
|
107
|
+
# of the action.
|
108
|
+
def use_event_attributes?
|
109
|
+
!skip_actions && !skip_after && actions.all? && actions.length == 1 && first.machine.action_helper_defined?
|
110
|
+
end
|
111
|
+
|
112
|
+
# Resets any information tracked from previous attempts to perform the
|
113
|
+
# collection
|
114
|
+
def reset
|
115
|
+
@results = {}
|
116
|
+
@success = false
|
117
|
+
end
|
118
|
+
|
119
|
+
# Runs each transition's callbacks recursively. Once all before callbacks
|
120
|
+
# have been executed, the transitions will then be persisted and the
|
121
|
+
# configured actions will be run.
|
122
|
+
#
|
123
|
+
# If any transition fails to run its callbacks, :halt will be thrown.
|
124
|
+
def run_callbacks(index = 0, &block)
|
125
|
+
if transition = self[index]
|
126
|
+
throw :halt unless transition.run_callbacks(:after => !skip_after) do
|
127
|
+
run_callbacks(index + 1, &block)
|
128
|
+
{:result => results[transition.action], :success => success?}
|
129
|
+
end
|
130
|
+
else
|
131
|
+
persist
|
132
|
+
run_actions(&block)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Transitions the current value of the object's states to those specified by
|
137
|
+
# each transition
|
138
|
+
def persist
|
139
|
+
each {|transition| transition.persist}
|
140
|
+
end
|
141
|
+
|
142
|
+
# Runs the actions for each transition. If a block is given method, then it
|
143
|
+
# will be called instead of invoking each transition's action.
|
144
|
+
#
|
145
|
+
# The results of the actions will be used to determine #success?.
|
146
|
+
def run_actions
|
147
|
+
catch_exceptions do
|
148
|
+
@success = if block_given?
|
149
|
+
result = yield
|
150
|
+
actions.each {|action| results[action] = result}
|
151
|
+
!!result
|
152
|
+
else
|
153
|
+
actions.compact.each {|action| !skip_actions && results[action] = object.send(action)}
|
154
|
+
results.values.all?
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Rolls back changes made to the object's states via each transition
|
160
|
+
def rollback
|
161
|
+
each {|transition| transition.rollback}
|
162
|
+
end
|
163
|
+
|
164
|
+
# Wraps the given block with a rescue handler so that any exceptions that
|
165
|
+
# occur will automatically result in the transition rolling back any changes
|
166
|
+
# that were made to the object involved.
|
167
|
+
def catch_exceptions
|
168
|
+
begin
|
169
|
+
yield
|
170
|
+
rescue Exception
|
171
|
+
rollback
|
172
|
+
raise
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Runs a block within a transaction for the object being transitioned. If
|
177
|
+
# transactions are disabled, then this is a no-op.
|
178
|
+
def within_transaction
|
179
|
+
if use_transaction && !empty?
|
180
|
+
first.within_transaction do
|
181
|
+
yield
|
182
|
+
success?
|
183
|
+
end
|
184
|
+
else
|
185
|
+
yield
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Represents a collection of transitions that were generated from attribute-
|
191
|
+
# based events
|
192
|
+
class AttributeTransitionCollection < TransitionCollection
|
193
|
+
def initialize(transitions = [], options = {}) #:nodoc:
|
194
|
+
super(transitions, {:transaction => false, :actions => false}.merge(options))
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
# Hooks into running transition callbacks so that event / event transition
|
199
|
+
# attributes can be properly updated
|
200
|
+
def run_callbacks(index = 0)
|
201
|
+
if index == 0
|
202
|
+
# Clears any traces of the event attribute to prevent it from being
|
203
|
+
# evaluated multiple times if actions are nested
|
204
|
+
each do |transition|
|
205
|
+
transition.machine.write(object, :event, nil)
|
206
|
+
transition.machine.write(object, :event_transition, nil)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Rollback only if exceptions occur during before callbacks
|
210
|
+
begin
|
211
|
+
super
|
212
|
+
rescue Exception
|
213
|
+
rollback unless @before_run
|
214
|
+
raise
|
215
|
+
end
|
216
|
+
|
217
|
+
# Persists transitions on the object if partial transition was successful.
|
218
|
+
# This allows us to reference them later to complete the transition with
|
219
|
+
# after callbacks.
|
220
|
+
each {|transition| transition.machine.write(object, :event_transition, transition)} if skip_after && success?
|
221
|
+
else
|
222
|
+
super
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Tracks that before callbacks have now completed
|
227
|
+
def persist
|
228
|
+
@before_run = true
|
229
|
+
super
|
230
|
+
end
|
231
|
+
|
232
|
+
# Resets callback tracking
|
233
|
+
def reset
|
234
|
+
super
|
235
|
+
@before_run = false
|
236
|
+
end
|
237
|
+
|
238
|
+
# Resets the event attribute so it can be re-evaluated if attempted again
|
239
|
+
def rollback
|
240
|
+
super
|
241
|
+
each {|transition| transition.machine.write(object, :event, transition.event) unless transition.transient?}
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|