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
@@ -64,6 +64,9 @@ module StateMachine
|
|
64
64
|
# vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
|
65
65
|
# vehicle.state # => "idling"
|
66
66
|
#
|
67
|
+
# This technique is always used for transitioning states when the +save+
|
68
|
+
# action (which is the default) is configured for the machine.
|
69
|
+
#
|
67
70
|
# === Security implications
|
68
71
|
#
|
69
72
|
# Beware that public event attributes mean that events can be fired
|
@@ -131,6 +134,9 @@ module StateMachine
|
|
131
134
|
# end
|
132
135
|
# end
|
133
136
|
#
|
137
|
+
# If using the +save+ action for the machine, this option will be ignored as
|
138
|
+
# the transaction will be created by Sequel within +save+.
|
139
|
+
#
|
134
140
|
# == Validation errors
|
135
141
|
#
|
136
142
|
# If an event fails to successfully fire because there are no matching
|
@@ -278,14 +284,34 @@ module StateMachine
|
|
278
284
|
end
|
279
285
|
end
|
280
286
|
|
281
|
-
# Adds hooks into validation for automatically firing events
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
+
# Adds hooks into validation for automatically firing events. This is
|
288
|
+
# a bit more complicated than other integrations since Sequel doesn't
|
289
|
+
# provide an easy way to hook around validation / save calls
|
290
|
+
def define_action_helpers
|
291
|
+
if action == :save
|
292
|
+
@instance_helper_module.class_eval do
|
293
|
+
define_method(:valid?) do |*args|
|
294
|
+
yielded = false
|
295
|
+
result = self.class.state_machines.transitions(self, :save, :after => false).perform do
|
296
|
+
yielded = true
|
297
|
+
super(*args)
|
298
|
+
end
|
299
|
+
|
300
|
+
raise_on_save_failure && !yielded && !result ? save_failure(:validation) : result
|
287
301
|
end
|
288
|
-
|
302
|
+
|
303
|
+
define_method(defined?(::Sequel::MAJOR) && (::Sequel::MAJOR >= 3 || ::Sequel::MAJOR == 2 && ::Sequel::MINOR == 12) ? :_save : :save) do |*args|
|
304
|
+
yielded = false
|
305
|
+
result = self.class.state_machines.transitions(self, :save).perform do
|
306
|
+
yielded = true
|
307
|
+
super(*args)
|
308
|
+
end
|
309
|
+
|
310
|
+
yielded || result ? result : save_failure(:save)
|
311
|
+
end
|
312
|
+
end unless owner_class.state_machines.any? {|name, machine| machine.action == :save && machine != self}
|
313
|
+
else
|
314
|
+
super
|
289
315
|
end
|
290
316
|
end
|
291
317
|
|
@@ -90,17 +90,17 @@ module StateMachine
|
|
90
90
|
# action being invoked (and not a superclass), then it must manually run the
|
91
91
|
# StateMachine hook that checks for event attributes.
|
92
92
|
#
|
93
|
-
# For example, in ActiveRecord, DataMapper, and Sequel, the
|
94
|
-
# (+save+) is already defined in a base class. As a result,
|
95
|
-
# machine is defined in a model / resource, StateMachine can
|
96
|
-
# hook into the +save+ action.
|
93
|
+
# For example, in ActiveRecord, DataMapper, MongoMapper, and Sequel, the
|
94
|
+
# default action (+save+) is already defined in a base class. As a result,
|
95
|
+
# when a state machine is defined in a model / resource, StateMachine can
|
96
|
+
# automatically hook into the +save+ action.
|
97
97
|
#
|
98
98
|
# On the other hand, the Vehicle class from above defined its own +save+
|
99
99
|
# method (and there is no +save+ method in its superclass). As a result, it
|
100
100
|
# must be modified like so:
|
101
101
|
#
|
102
102
|
# def save
|
103
|
-
# self.class.state_machines.
|
103
|
+
# self.class.state_machines.transitions(self, :save).perform do
|
104
104
|
# @saving_state = state
|
105
105
|
# fail != true
|
106
106
|
# end
|
@@ -156,6 +156,11 @@ module StateMachine
|
|
156
156
|
# later callbacks are canceled. If an +after+ callback halts the chain,
|
157
157
|
# the later callbacks are canceled, but the transition is still successful.
|
158
158
|
#
|
159
|
+
# These same rules apply to +around+ callbacks with the exception that any
|
160
|
+
# +around+ callback that doesn't yield will essentially result in :halt being
|
161
|
+
# thrown. Any code executed after the yield will behave in the same way as
|
162
|
+
# +after+ callbacks.
|
163
|
+
#
|
159
164
|
# *Note* that if a +before+ callback fails and the bang version of an event
|
160
165
|
# was invoked, an exception will be raised instead of returning false. For
|
161
166
|
# example,
|
@@ -207,6 +212,10 @@ module StateMachine
|
|
207
212
|
# def self.after_transition(vehicle, transition)
|
208
213
|
# logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
|
209
214
|
# end
|
215
|
+
#
|
216
|
+
# def self.around_transition(vehicle, transition)
|
217
|
+
# logger.info Benchmark.measure { yield }
|
218
|
+
# end
|
210
219
|
# end
|
211
220
|
#
|
212
221
|
# Vehicle.state_machine do
|
@@ -215,6 +224,8 @@ module StateMachine
|
|
215
224
|
#
|
216
225
|
# after_transition :on => :park, :do => VehicleObserver.method(:after_park)
|
217
226
|
# after_transition VehicleObserver.method(:after_transition)
|
227
|
+
#
|
228
|
+
# around_transition VehicleObserver.method(:around_transition)
|
218
229
|
# end
|
219
230
|
#
|
220
231
|
# One common callback is to record transitions for all models in the system
|
@@ -279,11 +290,13 @@ module StateMachine
|
|
279
290
|
#
|
280
291
|
# When a state machine is defined for classes using any of the above libraries,
|
281
292
|
# it will try to automatically determine the integration to use (Agnostic,
|
282
|
-
# ActiveRecord, DataMapper, or Sequel) based on the
|
283
|
-
# see how each integration affects the machine's
|
284
|
-
# constants defined under the StateMachine::Integrations
|
293
|
+
# ActiveModel, ActiveRecord, DataMapper, MongoMapper, or Sequel) based on the
|
294
|
+
# class definition. To see how each integration affects the machine's
|
295
|
+
# behavior, refer to all constants defined under the StateMachine::Integrations
|
296
|
+
# namespace.
|
285
297
|
class Machine
|
286
298
|
include Assertions
|
299
|
+
include EvalHelpers
|
287
300
|
include MatcherHelpers
|
288
301
|
|
289
302
|
class << self
|
@@ -489,6 +502,12 @@ module StateMachine
|
|
489
502
|
states.each {|state| state.initial = (state.name == @initial_state)}
|
490
503
|
end
|
491
504
|
|
505
|
+
# Initializes the state on the given object. This will always write to the
|
506
|
+
# attribute regardless of whether a value is already present.
|
507
|
+
def initialize_state(object)
|
508
|
+
write(object, :state, initial_state(object).value)
|
509
|
+
end
|
510
|
+
|
492
511
|
# Gets the actual name of the attribute on the machine's owner class that
|
493
512
|
# stores data with the given name.
|
494
513
|
def attribute(name = :state)
|
@@ -569,7 +588,7 @@ module StateMachine
|
|
569
588
|
# vehicle.force_idle = false
|
570
589
|
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
|
571
590
|
def initial_state(object)
|
572
|
-
states.fetch(dynamic_initial_state? ? @initial_state
|
591
|
+
states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state)
|
573
592
|
end
|
574
593
|
|
575
594
|
# Whether a dynamic initial state is being used in the machine
|
@@ -917,6 +936,7 @@ module StateMachine
|
|
917
936
|
#
|
918
937
|
# event :first_gear do
|
919
938
|
# transition :parked => :first_gear, :if => :seatbelt_on?
|
939
|
+
# transition :parked => same # Allow to loopback if seatbelt is off
|
920
940
|
# end
|
921
941
|
#
|
922
942
|
# See StateMachine::Event#transition for more information on
|
@@ -983,6 +1003,7 @@ module StateMachine
|
|
983
1003
|
#
|
984
1004
|
# event :ignite do
|
985
1005
|
# transition :parked => :idling
|
1006
|
+
# transition :idling => same # Allow ignite while still idling
|
986
1007
|
# end
|
987
1008
|
# end
|
988
1009
|
# end
|
@@ -1084,15 +1105,20 @@ module StateMachine
|
|
1084
1105
|
#
|
1085
1106
|
# == Result requirements
|
1086
1107
|
#
|
1087
|
-
# By default, after_transition callbacks
|
1108
|
+
# By default, +after_transition+ callbacks and code executed after an
|
1109
|
+
# +around_transition+ callback yields will only be run if the transition
|
1088
1110
|
# was performed successfully. A transition is successful if the machine's
|
1089
1111
|
# action is not configured or does not return false when it is invoked.
|
1090
|
-
# In order to include failed attempts when running an after_transition
|
1091
|
-
# callback, the
|
1112
|
+
# In order to include failed attempts when running an +after_transition+ or
|
1113
|
+
# +around_transition+ callback, the <tt>:include_failures</tt> option can be
|
1114
|
+
# specified like so:
|
1092
1115
|
#
|
1093
1116
|
# after_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
|
1094
1117
|
# after_transition :do => ... # Runs only on successful attempts to transition
|
1095
1118
|
#
|
1119
|
+
# around_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
|
1120
|
+
# around_transition :do => ... # Runs only on successful attempts to transition
|
1121
|
+
#
|
1096
1122
|
# == Verbose Requirements
|
1097
1123
|
#
|
1098
1124
|
# Requirements can also be defined using verbose options rather than the
|
@@ -1203,6 +1229,67 @@ module StateMachine
|
|
1203
1229
|
add_callback(:after, options, &block)
|
1204
1230
|
end
|
1205
1231
|
|
1232
|
+
# Creates a callback that will be invoked *around* a transition so long as
|
1233
|
+
# the given requirements match the transition.
|
1234
|
+
#
|
1235
|
+
# == The callback
|
1236
|
+
#
|
1237
|
+
# Around callbacks wrap transitions, executing code both before and after.
|
1238
|
+
# These callbacks are defined in the exact same manner as before / after
|
1239
|
+
# callbacks with the exception that the transition must be yielded to in
|
1240
|
+
# order to finish running it.
|
1241
|
+
#
|
1242
|
+
# If defining +around+ callbacks using blocks, you must yield within the
|
1243
|
+
# transition by directly calling the block (since yielding is not allowed
|
1244
|
+
# within blocks).
|
1245
|
+
#
|
1246
|
+
# For example,
|
1247
|
+
#
|
1248
|
+
# class Vehicle
|
1249
|
+
# state_machine do
|
1250
|
+
# around_transition do |block|
|
1251
|
+
# Benchmark.measure { block.call }
|
1252
|
+
# end
|
1253
|
+
#
|
1254
|
+
# around_transition do |vehicle, block|
|
1255
|
+
# logger.info "vehicle was #{state}..."
|
1256
|
+
# block.call
|
1257
|
+
# logger.info "...and is now #{state}"
|
1258
|
+
# end
|
1259
|
+
#
|
1260
|
+
# around_transition do |vehicle, transition, block|
|
1261
|
+
# logger.info "before #{transition.event}: #{vehicle.state}"
|
1262
|
+
# block.call
|
1263
|
+
# logger.info "after #{transition.event}: #{vehicle.state}"
|
1264
|
+
# end
|
1265
|
+
# end
|
1266
|
+
# end
|
1267
|
+
#
|
1268
|
+
# Notice that referencing the block is similar to doing so within an
|
1269
|
+
# actual method definition in that it is always the last argument.
|
1270
|
+
#
|
1271
|
+
# On the other hand, if you're defining +around+ callbacks using method
|
1272
|
+
# references, you can yield like normal:
|
1273
|
+
#
|
1274
|
+
# class Vehicle
|
1275
|
+
# state_machine do
|
1276
|
+
# around_transition :benchmark
|
1277
|
+
# ...
|
1278
|
+
# end
|
1279
|
+
#
|
1280
|
+
# def benchmark
|
1281
|
+
# Benchmark.measure { yield }
|
1282
|
+
# end
|
1283
|
+
# end
|
1284
|
+
#
|
1285
|
+
# See +before_transition+ for a description of the possible configurations
|
1286
|
+
# for defining callbacks.
|
1287
|
+
def around_transition(*args, &block)
|
1288
|
+
options = (args.last.is_a?(Hash) ? args.pop : {})
|
1289
|
+
options[:do] = args if args.any?
|
1290
|
+
add_callback(:around, options, &block)
|
1291
|
+
end
|
1292
|
+
|
1206
1293
|
# Marks the given object as invalid with the given message.
|
1207
1294
|
#
|
1208
1295
|
# By default, this is a no-op.
|
@@ -1265,6 +1352,7 @@ module StateMachine
|
|
1265
1352
|
begin
|
1266
1353
|
# Load the graphviz library
|
1267
1354
|
require 'rubygems'
|
1355
|
+
gem 'ruby-graphviz', '>=0.9.0'
|
1268
1356
|
require 'graphviz'
|
1269
1357
|
|
1270
1358
|
graph = GraphViz.new('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB')
|
@@ -1284,7 +1372,7 @@ module StateMachine
|
|
1284
1372
|
# Generate the graph
|
1285
1373
|
graphvizVersion = Constants::RGV_VERSION.split('.')
|
1286
1374
|
|
1287
|
-
if graphvizVersion[
|
1375
|
+
if graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
|
1288
1376
|
outputOptions = {
|
1289
1377
|
:output => options[:format],
|
1290
1378
|
:file => File.join(options[:path], "#{options[:name]}.#{options[:format]}")
|
@@ -1298,11 +1386,17 @@ module StateMachine
|
|
1298
1386
|
graph.output(outputOptions)
|
1299
1387
|
graph
|
1300
1388
|
rescue LoadError
|
1301
|
-
$stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
|
1389
|
+
$stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` >= v0.9.0 and try again.'
|
1302
1390
|
false
|
1303
1391
|
end
|
1304
1392
|
end
|
1305
1393
|
|
1394
|
+
# Determines whether a helper method was defined for firing attribute-based
|
1395
|
+
# event transitions when the configuration action gets called.
|
1396
|
+
def action_helper_defined?
|
1397
|
+
@action_helper_defined
|
1398
|
+
end
|
1399
|
+
|
1306
1400
|
protected
|
1307
1401
|
# Runs additional initialization hooks. By default, this is a no-op.
|
1308
1402
|
def after_initialize
|
@@ -1389,23 +1483,27 @@ module StateMachine
|
|
1389
1483
|
# Adds helper methods for automatically firing events when an action
|
1390
1484
|
# is invoked
|
1391
1485
|
def define_action_helpers(action_hook = self.action)
|
1392
|
-
|
1393
|
-
|
1486
|
+
private_action = owner_class.private_method_defined?(action_hook)
|
1487
|
+
action_defined = @action_helper_defined = owner_class.ancestors.any? do |ancestor|
|
1488
|
+
ancestor != owner_class && (ancestor.method_defined?(action_hook) || ancestor.private_method_defined?(action_hook))
|
1489
|
+
end
|
1490
|
+
action_overridden = owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
|
1394
1491
|
|
1395
|
-
|
1396
|
-
|
1492
|
+
# Only define helper if:
|
1493
|
+
# 1. Action was originally defined somewhere other than the owner class
|
1494
|
+
# 2. It hasn't already been overridden by another machine
|
1495
|
+
if action_defined && !action_overridden
|
1496
|
+
action = self.action
|
1397
1497
|
@instance_helper_module.class_eval do
|
1398
|
-
# Override the default action to invoke the before / after hooks
|
1399
1498
|
define_method(action_hook) do |*args|
|
1400
|
-
self.class.state_machines.
|
1499
|
+
self.class.state_machines.transitions(self, action).perform { super(*args) }
|
1401
1500
|
end
|
1402
1501
|
|
1403
|
-
private action_hook if
|
1502
|
+
private action_hook if private_action
|
1404
1503
|
end
|
1405
1504
|
|
1406
1505
|
true
|
1407
1506
|
else
|
1408
|
-
# Action already defined: don't add integration-specific hooks
|
1409
1507
|
false
|
1410
1508
|
end
|
1411
1509
|
end
|
@@ -1466,7 +1564,7 @@ module StateMachine
|
|
1466
1564
|
|
1467
1565
|
# Adds a new transition callback of the given type.
|
1468
1566
|
def add_callback(type, options, &block)
|
1469
|
-
callbacks[type] << callback = Callback.new(options, &block)
|
1567
|
+
callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
|
1470
1568
|
add_states(callback.known_states)
|
1471
1569
|
callback
|
1472
1570
|
end
|
@@ -6,13 +6,13 @@ module StateMachine
|
|
6
6
|
# (which must mean the defaults are being skipped)
|
7
7
|
def initialize_states(object, options = {})
|
8
8
|
if ignore = options[:ignore]
|
9
|
-
ignore.map
|
9
|
+
ignore = ignore.map {|attribute| attribute.to_sym}
|
10
10
|
end
|
11
11
|
|
12
12
|
each_value do |machine|
|
13
13
|
if (!ignore || !ignore.include?(machine.attribute)) && (!options.include?(:dynamic) || machine.dynamic_initial_state? == options[:dynamic])
|
14
14
|
value = machine.read(object, :state)
|
15
|
-
machine.
|
15
|
+
machine.initialize_state(object) if ignore || value.nil? || value.respond_to?(:empty?) && value.empty?
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -26,9 +26,7 @@ module StateMachine
|
|
26
26
|
transitions = events.collect do |event_name|
|
27
27
|
# Find the actual event being run
|
28
28
|
event = nil
|
29
|
-
detect
|
30
|
-
event = machine.events[event_name, :qualified_name]
|
31
|
-
end
|
29
|
+
detect {|name, machine| event = machine.events[event_name, :qualified_name]}
|
32
30
|
|
33
31
|
raise InvalidEvent, "#{event_name.inspect} is an unknown state machine event" unless event
|
34
32
|
|
@@ -44,112 +42,23 @@ module StateMachine
|
|
44
42
|
# Run the events in parallel only if valid transitions were found for
|
45
43
|
# all of them
|
46
44
|
if events.length == transitions.length
|
47
|
-
|
45
|
+
TransitionCollection.new(transitions, :actions => run_action).perform
|
48
46
|
else
|
49
47
|
false
|
50
48
|
end
|
51
49
|
end
|
52
50
|
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
# example, when validating records in ORM integrations).
|
57
|
-
#
|
58
|
-
# The event attributes that will be fired are based on which machines
|
59
|
-
# match the action that is being invoked.
|
60
|
-
#
|
61
|
-
# == Examples
|
62
|
-
#
|
63
|
-
# class Vehicle
|
64
|
-
# include DataMapper::Resource
|
65
|
-
# property :id, Serial
|
66
|
-
#
|
67
|
-
# state_machine :initial => :parked do
|
68
|
-
# event :ignite do
|
69
|
-
# transition :parked => :idling
|
70
|
-
# end
|
71
|
-
# end
|
72
|
-
#
|
73
|
-
# state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
|
74
|
-
# event :disable do
|
75
|
-
# transition all => :off
|
76
|
-
# end
|
77
|
-
# end
|
78
|
-
# end
|
79
|
-
#
|
80
|
-
# With valid events:
|
81
|
-
#
|
82
|
-
# vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
|
83
|
-
# vehicle.state_event = 'ignite'
|
84
|
-
# vehicle.alarm_state_event = 'disable'
|
85
|
-
#
|
86
|
-
# Vehicle.state_machines.fire_event_attributes(vehicle, :save) { true }
|
87
|
-
# vehicle.state # => "idling"
|
88
|
-
# vehicle.state_event # => nil
|
89
|
-
# vehicle.alarm_state # => "off"
|
90
|
-
# vehicle.alarm_state_event # => nil
|
91
|
-
#
|
92
|
-
# With invalid events:
|
93
|
-
#
|
94
|
-
# vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
|
95
|
-
# vehicle.state_event = 'park'
|
96
|
-
# vehicle.alarm_state_event = 'disable'
|
97
|
-
#
|
98
|
-
# Vehicle.state_machines.fire_event_attributes(vehicle, :save) { true }
|
99
|
-
# vehicle.state # => "parked"
|
100
|
-
# vehicle.state_event # => nil
|
101
|
-
# vehicle.alarm_state # => "active"
|
102
|
-
# vehicle.alarm_state_event # => nil
|
103
|
-
# vehicle.errors # => #<DataMapper::Validate::ValidationErrors:0xb7af9abc @errors={"state_event"=>["is invalid"]}>
|
104
|
-
#
|
105
|
-
# With partial firing:
|
51
|
+
# Builds the collection of transitions for all event attributes defined on
|
52
|
+
# the given object. This will only include events whose machine actions
|
53
|
+
# match the one specified.
|
106
54
|
#
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
# vehicle.state # => "idling"
|
112
|
-
# vehicle.state_event # => "ignite"
|
113
|
-
# vehicle.state_event_transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
114
|
-
def fire_event_attributes(object, action, complete = true)
|
115
|
-
# Get the transitions to fire for each applicable machine
|
116
|
-
transitions = map {|name, machine| machine.action == action ? machine.events.attribute_transition_for(object, true) : nil}.compact
|
117
|
-
return yield if transitions.empty?
|
118
|
-
|
119
|
-
# The value generated by the yielded block (the actual action)
|
120
|
-
action_value = nil
|
121
|
-
|
122
|
-
# Make sure all events were valid
|
123
|
-
if result = transitions.all? {|transition| transition != false}
|
124
|
-
# Clear any traces of the event since transitions are available and to
|
125
|
-
# prevent from being evaluated multiple times if actions are nested
|
126
|
-
transitions.each do |transition|
|
127
|
-
transition.machine.write(object, :event, nil)
|
128
|
-
transition.machine.write(object, :event_transition, nil)
|
129
|
-
end
|
130
|
-
|
131
|
-
# Perform the transitions
|
132
|
-
begin
|
133
|
-
result = Transition.perform(transitions, :after => complete) { action_value = yield }
|
134
|
-
rescue Exception
|
135
|
-
# Reset the event attribute so it can be re-evaluated if attempted again
|
136
|
-
transitions.each do |transition|
|
137
|
-
transition.machine.write(object, :event, transition.event)
|
138
|
-
end
|
139
|
-
|
140
|
-
raise
|
141
|
-
end
|
142
|
-
|
143
|
-
transitions.each do |transition|
|
144
|
-
# Revert event if failed (to allow for more attempts)
|
145
|
-
transition.machine.write(object, :event, transition.event) unless result
|
146
|
-
|
147
|
-
# Track transition if partial transition was successful
|
148
|
-
transition.machine.write(object, :event_transition, transition) if !complete && result
|
149
|
-
end
|
55
|
+
# These should only be fired as a result of the action being run.
|
56
|
+
def transitions(object, action, options = {})
|
57
|
+
transitions = map do |name, machine|
|
58
|
+
machine.events.attribute_transition_for(object, true) if machine.action == action
|
150
59
|
end
|
151
60
|
|
152
|
-
|
61
|
+
AttributeTransitionCollection.new(transitions.compact, options)
|
153
62
|
end
|
154
63
|
end
|
155
64
|
end
|