state_machine 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG.rdoc +17 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +162 -23
  4. data/Rakefile +3 -18
  5. data/lib/state_machine.rb +3 -4
  6. data/lib/state_machine/callback.rb +65 -13
  7. data/lib/state_machine/eval_helpers.rb +20 -4
  8. data/lib/state_machine/initializers.rb +4 -0
  9. data/lib/state_machine/initializers/merb.rb +1 -0
  10. data/lib/state_machine/initializers/rails.rb +7 -0
  11. data/lib/state_machine/integrations.rb +21 -6
  12. data/lib/state_machine/integrations/active_model.rb +414 -0
  13. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  14. data/lib/state_machine/integrations/{active_record → active_model}/observer.rb +7 -7
  15. data/lib/state_machine/integrations/active_record.rb +65 -129
  16. data/lib/state_machine/integrations/active_record/locale.rb +4 -11
  17. data/lib/state_machine/integrations/data_mapper.rb +24 -6
  18. data/lib/state_machine/integrations/data_mapper/observer.rb +36 -0
  19. data/lib/state_machine/integrations/mongo_mapper.rb +295 -0
  20. data/lib/state_machine/integrations/sequel.rb +33 -7
  21. data/lib/state_machine/machine.rb +121 -23
  22. data/lib/state_machine/machine_collection.rb +12 -103
  23. data/lib/state_machine/transition.rb +125 -164
  24. data/lib/state_machine/transition_collection.rb +244 -0
  25. data/lib/tasks/state_machine.rb +12 -15
  26. data/test/functional/state_machine_test.rb +11 -1
  27. data/test/unit/callback_test.rb +305 -32
  28. data/test/unit/eval_helpers_test.rb +103 -1
  29. data/test/unit/event_test.rb +2 -1
  30. data/test/unit/guard_test.rb +2 -1
  31. data/test/unit/integrations/active_model_test.rb +909 -0
  32. data/test/unit/integrations/active_record_test.rb +1542 -1292
  33. data/test/unit/integrations/data_mapper_test.rb +1369 -1041
  34. data/test/unit/integrations/mongo_mapper_test.rb +1349 -0
  35. data/test/unit/integrations/sequel_test.rb +1214 -985
  36. data/test/unit/integrations_test.rb +8 -0
  37. data/test/unit/machine_collection_test.rb +140 -513
  38. data/test/unit/machine_test.rb +212 -10
  39. data/test/unit/state_test.rb +2 -1
  40. data/test/unit/transition_collection_test.rb +2098 -0
  41. data/test/unit/transition_test.rb +704 -552
  42. 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
- self.class.perform_within_transaction([self], :action => run_action)
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 machine's +before+ callbacks for this transition. Only
219
- # callbacks that are configured to match the event, from state, and to
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
- # == Example
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
- # class Vehicle
228
- # state_machine do
229
- # before_transition :on => :ignite, :do => lambda {|vehicle| ...}
230
- # end
231
- # end
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
- catch(:halt) do
240
- unless @before_run
241
- callback(:before)
242
- @before_run = true
243
- end
244
-
245
- result = true
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
- result
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
- protected
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