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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +12 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +502 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +1246 -0
- data/Rakefile +20 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +14 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +13 -0
- data/examples/car.rb +21 -0
- data/examples/doc/AutoShop.html +2856 -0
- data/examples/doc/AutoShop_state.png +0 -0
- data/examples/doc/Car.html +919 -0
- data/examples/doc/Car_state.png +0 -0
- data/examples/doc/TrafficLight.html +2230 -0
- data/examples/doc/TrafficLight_state.png +0 -0
- data/examples/doc/Vehicle.html +7921 -0
- data/examples/doc/Vehicle_state.png +0 -0
- data/examples/doc/_index.html +136 -0
- data/examples/doc/class_list.html +47 -0
- data/examples/doc/css/common.css +1 -0
- data/examples/doc/css/full_list.css +55 -0
- data/examples/doc/css/style.css +322 -0
- data/examples/doc/file_list.html +46 -0
- data/examples/doc/frames.html +13 -0
- data/examples/doc/index.html +136 -0
- data/examples/doc/js/app.js +205 -0
- data/examples/doc/js/full_list.js +173 -0
- data/examples/doc/js/jquery.js +16 -0
- data/examples/doc/method_list.html +734 -0
- data/examples/doc/top-level-namespace.html +105 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +7 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view__form.html.erb +34 -0
- data/examples/rails-rest/view_edit.html.erb +6 -0
- data/examples/rails-rest/view_index.html.erb +25 -0
- data/examples/rails-rest/view_new.html.erb +5 -0
- data/examples/rails-rest/view_show.html.erb +19 -0
- data/examples/traffic_light.rb +9 -0
- data/examples/vehicle.rb +33 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/branch.rb +225 -0
- data/lib/state_machine/callback.rb +236 -0
- data/lib/state_machine/core.rb +7 -0
- data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
- data/lib/state_machine/core_ext.rb +2 -0
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +87 -0
- data/lib/state_machine/event.rb +257 -0
- data/lib/state_machine/event_collection.rb +141 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/graph.rb +92 -0
- data/lib/state_machine/helper_module.rb +17 -0
- data/lib/state_machine/initializers/rails.rb +25 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +33 -0
- data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
- data/lib/state_machine/integrations/active_model/versions.rb +31 -0
- data/lib/state_machine/integrations/active_model.rb +585 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record/versions.rb +123 -0
- data/lib/state_machine/integrations/active_record.rb +525 -0
- data/lib/state_machine/integrations/base.rb +100 -0
- data/lib/state_machine/integrations.rb +121 -0
- data/lib/state_machine/machine.rb +2287 -0
- data/lib/state_machine/machine_collection.rb +74 -0
- data/lib/state_machine/macro_methods.rb +522 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +222 -0
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +297 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/state_context.rb +138 -0
- data/lib/state_machine/transition.rb +470 -0
- data/lib/state_machine/transition_collection.rb +245 -0
- data/lib/state_machine/version.rb +3 -0
- data/lib/state_machine/yard/handlers/base.rb +32 -0
- data/lib/state_machine/yard/handlers/event.rb +25 -0
- data/lib/state_machine/yard/handlers/machine.rb +344 -0
- data/lib/state_machine/yard/handlers/state.rb +25 -0
- data/lib/state_machine/yard/handlers/transition.rb +47 -0
- data/lib/state_machine/yard/handlers.rb +12 -0
- data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
- data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
- data/lib/state_machine/yard/templates.rb +3 -0
- data/lib/state_machine/yard.rb +8 -0
- data/lib/state_machine.rb +8 -0
- data/lib/yard-state_machine.rb +2 -0
- data/state_machine.gemspec +22 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +15 -0
- data/test/functional/state_machine_test.rb +1066 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/branch_test.rb +969 -0
- data/test/unit/callback_test.rb +704 -0
- data/test/unit/error_test.rb +43 -0
- data/test/unit/eval_helpers_test.rb +270 -0
- data/test/unit/event_collection_test.rb +398 -0
- data/test/unit/event_test.rb +1196 -0
- data/test/unit/graph_test.rb +98 -0
- data/test/unit/helper_module_test.rb +17 -0
- data/test/unit/integrations/active_model_test.rb +1245 -0
- data/test/unit/integrations/active_record_test.rb +2551 -0
- data/test/unit/integrations/base_test.rb +104 -0
- data/test/unit/integrations_test.rb +71 -0
- data/test/unit/invalid_event_test.rb +20 -0
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +115 -0
- data/test/unit/machine_collection_test.rb +603 -0
- data/test/unit/machine_test.rb +3395 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +362 -0
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +352 -0
- data/test/unit/state_context_test.rb +441 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +1101 -0
- data/test/unit/transition_collection_test.rb +2168 -0
- data/test/unit/transition_test.rb +1558 -0
- 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
|