state_machine 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +11 -0
- data/.travis.yml +16 -0
- data/.yardopts +5 -0
- data/Appraisals +260 -0
- data/CHANGELOG.rdoc +15 -0
- data/Gemfile +3 -0
- data/README.rdoc +156 -29
- data/Rakefile +31 -57
- data/gemfiles/active_model-3.0.0.gemfile +7 -0
- data/gemfiles/active_model-3.0.0.gemfile.lock +32 -0
- data/gemfiles/active_model-3.0.5.gemfile +7 -0
- data/gemfiles/active_model-3.0.5.gemfile.lock +32 -0
- data/gemfiles/active_record-2.0.0.gemfile +8 -0
- data/gemfiles/active_record-2.0.0.gemfile.lock +30 -0
- data/gemfiles/active_record-2.0.5.gemfile +8 -0
- data/gemfiles/active_record-2.0.5.gemfile.lock +30 -0
- data/gemfiles/active_record-2.1.0.gemfile +8 -0
- data/gemfiles/active_record-2.1.0.gemfile.lock +30 -0
- data/gemfiles/active_record-2.1.2.gemfile +8 -0
- data/gemfiles/active_record-2.1.2.gemfile.lock +30 -0
- data/gemfiles/active_record-2.2.3.gemfile +8 -0
- data/gemfiles/active_record-2.2.3.gemfile.lock +30 -0
- data/gemfiles/active_record-2.3.12.gemfile +8 -0
- data/gemfiles/active_record-2.3.12.gemfile.lock +30 -0
- data/gemfiles/active_record-3.0.0.gemfile +8 -0
- data/gemfiles/active_record-3.0.0.gemfile.lock +44 -0
- data/gemfiles/active_record-3.0.5.gemfile +8 -0
- data/gemfiles/active_record-3.0.5.gemfile.lock +43 -0
- data/gemfiles/data_mapper-0.10.2.gemfile +12 -0
- data/gemfiles/data_mapper-0.10.2.gemfile.lock +45 -0
- data/gemfiles/data_mapper-0.9.11.gemfile +12 -0
- data/gemfiles/data_mapper-0.9.11.gemfile.lock +47 -0
- data/gemfiles/data_mapper-0.9.4.gemfile +12 -0
- data/gemfiles/data_mapper-0.9.4.gemfile.lock +61 -0
- data/gemfiles/data_mapper-0.9.7.gemfile +12 -0
- data/gemfiles/data_mapper-0.9.7.gemfile.lock +57 -0
- data/gemfiles/data_mapper-1.0.0.gemfile +12 -0
- data/gemfiles/data_mapper-1.0.0.gemfile.lock +53 -0
- data/gemfiles/data_mapper-1.0.1.gemfile +12 -0
- data/gemfiles/data_mapper-1.0.1.gemfile.lock +53 -0
- data/gemfiles/data_mapper-1.0.2.gemfile +12 -0
- data/gemfiles/data_mapper-1.0.2.gemfile.lock +53 -0
- data/gemfiles/data_mapper-1.1.0.gemfile +12 -0
- data/gemfiles/data_mapper-1.1.0.gemfile.lock +51 -0
- data/gemfiles/default.gemfile +7 -0
- data/gemfiles/default.gemfile.lock +24 -0
- data/gemfiles/mongo_mapper-0.5.5.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +33 -0
- data/gemfiles/mongo_mapper-0.5.8.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +33 -0
- data/gemfiles/mongo_mapper-0.6.0.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +33 -0
- data/gemfiles/mongo_mapper-0.6.10.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +33 -0
- data/gemfiles/mongo_mapper-0.7.0.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +33 -0
- data/gemfiles/mongo_mapper-0.7.5.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper-0.8.0.gemfile +10 -0
- data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +40 -0
- data/gemfiles/mongo_mapper-0.8.3.gemfile +10 -0
- data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +40 -0
- data/gemfiles/mongo_mapper-0.8.4.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +38 -0
- data/gemfiles/mongo_mapper-0.8.6.gemfile +8 -0
- data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +38 -0
- data/gemfiles/mongo_mapper-0.9.0.gemfile +7 -0
- data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +41 -0
- data/gemfiles/mongoid-2.0.0.gemfile +7 -0
- data/gemfiles/mongoid-2.0.0.gemfile.lock +42 -0
- data/gemfiles/mongoid-2.1.4.gemfile +7 -0
- data/gemfiles/mongoid-2.1.4.gemfile.lock +40 -0
- data/gemfiles/sequel-2.11.0.gemfile +8 -0
- data/gemfiles/sequel-2.11.0.gemfile.lock +28 -0
- data/gemfiles/sequel-2.12.0.gemfile +8 -0
- data/gemfiles/sequel-2.12.0.gemfile.lock +28 -0
- data/gemfiles/sequel-2.8.0.gemfile +8 -0
- data/gemfiles/sequel-2.8.0.gemfile.lock +28 -0
- data/gemfiles/sequel-3.0.0.gemfile +8 -0
- data/gemfiles/sequel-3.0.0.gemfile.lock +28 -0
- data/gemfiles/sequel-3.13.0.gemfile +8 -0
- data/gemfiles/sequel-3.13.0.gemfile.lock +28 -0
- data/gemfiles/sequel-3.14.0.gemfile +8 -0
- data/gemfiles/sequel-3.14.0.gemfile.lock +28 -0
- data/gemfiles/sequel-3.23.0.gemfile +8 -0
- data/gemfiles/sequel-3.23.0.gemfile.lock +28 -0
- data/gemfiles/sequel-3.24.0.gemfile +8 -0
- data/gemfiles/sequel-3.24.0.gemfile.lock +28 -0
- data/lib/state_machine/event.rb +13 -90
- data/lib/state_machine/helper_module.rb +17 -0
- data/lib/state_machine/integrations/active_model.rb +35 -0
- data/lib/state_machine/integrations/active_record.rb +41 -2
- data/lib/state_machine/integrations/data_mapper.rb +17 -2
- data/lib/state_machine/integrations/mongo_mapper.rb +34 -7
- data/lib/state_machine/integrations/mongoid.rb +34 -26
- data/lib/state_machine/integrations/mongoid/versions.rb +29 -3
- data/lib/state_machine/integrations/sequel.rb +22 -72
- data/lib/state_machine/integrations/sequel/versions.rb +87 -6
- data/lib/state_machine/machine.rb +279 -19
- data/lib/state_machine/state.rb +2 -2
- data/lib/state_machine/state_context.rb +133 -0
- data/lib/state_machine/version.rb +3 -0
- data/state_machine.gemspec +22 -0
- data/test/test_helper.rb +1 -3
- data/test/unit/branch_test.rb +1 -3
- data/test/unit/event_collection_test.rb +3 -3
- data/test/unit/event_test.rb +1 -3
- data/test/unit/helper_module_test.rb +17 -0
- data/test/unit/integrations/active_model_test.rb +0 -4
- data/test/unit/integrations/active_record_test.rb +50 -9
- data/test/unit/integrations/data_mapper_test.rb +267 -253
- data/test/unit/integrations/mongo_mapper_test.rb +47 -15
- data/test/unit/integrations/mongoid_test.rb +50 -8
- data/test/unit/integrations/sequel_test.rb +10 -6
- data/test/unit/machine_test.rb +206 -25
- data/test/unit/state_context_test.rb +421 -0
- data/test/unit/state_test.rb +20 -3
- metadata +303 -128
- data/lib/state_machine/condition_proxy.rb +0 -94
- data/test/unit/condition_proxy_test.rb +0 -328
@@ -2,6 +2,7 @@ require 'state_machine/extensions'
|
|
2
2
|
require 'state_machine/assertions'
|
3
3
|
require 'state_machine/integrations'
|
4
4
|
|
5
|
+
require 'state_machine/helper_module'
|
5
6
|
require 'state_machine/state'
|
6
7
|
require 'state_machine/event'
|
7
8
|
require 'state_machine/callback'
|
@@ -275,6 +276,72 @@ module StateMachine
|
|
275
276
|
#
|
276
277
|
# The same technique can be used for +state+, +state_name+, and all other
|
277
278
|
# instance *and* class methods on the Vehicle class.
|
279
|
+
#
|
280
|
+
# == Method conflicts
|
281
|
+
#
|
282
|
+
# By default state_machine does not redefine methods that exist on
|
283
|
+
# superclasses (*including* Object) or any modules (*including* Kernel) that
|
284
|
+
# were included before it was defined. This is in order to ensure that
|
285
|
+
# existing behavior on the class is not broken by the inclusion of
|
286
|
+
# state_machine.
|
287
|
+
#
|
288
|
+
# If a conflicting method is detected, state_machine will generate a warning.
|
289
|
+
# For example, consider the following class:
|
290
|
+
#
|
291
|
+
# class Vehicle
|
292
|
+
# state_machine do
|
293
|
+
# event :open do
|
294
|
+
# ...
|
295
|
+
# end
|
296
|
+
# end
|
297
|
+
# end
|
298
|
+
#
|
299
|
+
# In the above class, an event named "open" is defined for its state machine.
|
300
|
+
# However, "open" is already defined as an instance method in Ruby's Kernel
|
301
|
+
# module that gets included in every Object. As a result, state_machine will
|
302
|
+
# generate the following warning:
|
303
|
+
#
|
304
|
+
# Instance method "open" is already defined in Object, use generic helper instead.
|
305
|
+
#
|
306
|
+
# Even though you may not be using Kernel's implementation of the "open"
|
307
|
+
# instance method, state_machine isn't aware of this and, as a result, stays
|
308
|
+
# safe and just skips redefining the method.
|
309
|
+
#
|
310
|
+
# As with almost all helpers methods defined by state_machine in your class,
|
311
|
+
# there are generic methods available for working around this method conflict.
|
312
|
+
# In the example above, you can invoke the "open" event like so:
|
313
|
+
#
|
314
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
|
315
|
+
# vehicle.fire_events(:open) # => true
|
316
|
+
#
|
317
|
+
# # This will not work
|
318
|
+
# vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
|
319
|
+
#
|
320
|
+
# If you want to take on the risk of overriding existing methods and just
|
321
|
+
# ignore method conflicts altogether, you can do so by setting the following
|
322
|
+
# configuration:
|
323
|
+
#
|
324
|
+
# StateMachine::Machine.ignore_method_conflicts = true
|
325
|
+
#
|
326
|
+
# This will allow you to define events like "open" as described above and
|
327
|
+
# still generate the "open" instance helper method. For example:
|
328
|
+
#
|
329
|
+
# StateMachine::Machine.ignore_method_conflicts = true
|
330
|
+
#
|
331
|
+
# class Vehicle
|
332
|
+
# state_machine do
|
333
|
+
# event :open do
|
334
|
+
# ...
|
335
|
+
# end
|
336
|
+
# end
|
337
|
+
#
|
338
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
|
339
|
+
# vehicle.open # => true
|
340
|
+
#
|
341
|
+
# By default, state_machine helps prevent you from making mistakes and
|
342
|
+
# accidentally overriding methods that you didn't intend to. Once you
|
343
|
+
# understand this and what the consequences are, setting the
|
344
|
+
# +ignore_method_conflicts+ option is a perfectly reasonable workaround.
|
278
345
|
#
|
279
346
|
# == Integrations
|
280
347
|
#
|
@@ -451,7 +518,10 @@ module StateMachine
|
|
451
518
|
@use_transactions = options[:use_transactions]
|
452
519
|
@initialize_state = options[:initialize]
|
453
520
|
self.owner_class = owner_class
|
454
|
-
self.initial_state = options[:initial] unless
|
521
|
+
self.initial_state = options[:initial] unless sibling_machines.any?
|
522
|
+
|
523
|
+
# Merge with sibling machine configurations
|
524
|
+
add_sibling_machine_configs
|
455
525
|
|
456
526
|
# Define class integration
|
457
527
|
define_helpers
|
@@ -482,7 +552,7 @@ module StateMachine
|
|
482
552
|
@owner_class = klass
|
483
553
|
|
484
554
|
# Create modules for extending the class with state/event-specific methods
|
485
|
-
@helper_modules = helper_modules = {:instance =>
|
555
|
+
@helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
|
486
556
|
owner_class.class_eval do
|
487
557
|
extend helper_modules[:class]
|
488
558
|
include helper_modules[:instance]
|
@@ -1026,9 +1096,16 @@ module StateMachine
|
|
1026
1096
|
# end
|
1027
1097
|
# end
|
1028
1098
|
#
|
1029
|
-
# ==
|
1099
|
+
# == Overriding the event method
|
1100
|
+
#
|
1101
|
+
# By default, this will define an instance method (with the same name as the
|
1102
|
+
# event) that will fire the next possible transition for that. Although the
|
1103
|
+
# +before_transition+, +after_transition+, and +around_transition+ hooks
|
1104
|
+
# allow you to define behavior that gets executed as a result of the event's
|
1105
|
+
# transition, you can also override the event method in order to have a
|
1106
|
+
# little more fine-grained control.
|
1030
1107
|
#
|
1031
|
-
#
|
1108
|
+
# For example:
|
1032
1109
|
#
|
1033
1110
|
# class Vehicle
|
1034
1111
|
# state_machine do
|
@@ -1037,24 +1114,62 @@ module StateMachine
|
|
1037
1114
|
# end
|
1038
1115
|
# end
|
1039
1116
|
#
|
1040
|
-
# def park(
|
1041
|
-
# take_deep_breath if
|
1042
|
-
# super
|
1043
|
-
#
|
1044
|
-
#
|
1045
|
-
#
|
1046
|
-
# sleep 3
|
1117
|
+
# def park(*)
|
1118
|
+
# take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
|
1119
|
+
# if result = super # Runs the transition and all before/after/around hooks
|
1120
|
+
# applaud # Executes after the transition (and after_transition hooks)
|
1121
|
+
# end
|
1122
|
+
# result
|
1047
1123
|
# end
|
1048
1124
|
# end
|
1049
1125
|
#
|
1050
|
-
#
|
1051
|
-
#
|
1052
|
-
#
|
1126
|
+
# There are a few important things to note here. First, the method
|
1127
|
+
# signature is defined with an unlimited argument list in order to allow
|
1128
|
+
# callers to continue passing arguments that are expected by state_machine.
|
1129
|
+
# For example, it will still allow calls to +park+ with a single parameter
|
1130
|
+
# for skipping the configured action.
|
1053
1131
|
#
|
1054
|
-
#
|
1055
|
-
#
|
1056
|
-
#
|
1132
|
+
# Second, the overridden event method must call +super+ in order to run the
|
1133
|
+
# logic for running the next possible transition. In order to remain
|
1134
|
+
# consistent with other events, the result of +super+ is returned.
|
1135
|
+
#
|
1136
|
+
# Third, any behavior defined in this method will *not* get executed if
|
1137
|
+
# you're taking advantage of attribute-based event transitions. For example:
|
1138
|
+
#
|
1139
|
+
# vehicle = Vehicle.new
|
1140
|
+
# vehicle.state_event = 'park'
|
1141
|
+
# vehicle.save
|
1142
|
+
#
|
1143
|
+
# In this case, the +park+ event will run the before/after/around transition
|
1144
|
+
# hooks and transition the state, but the behavior defined in the overriden
|
1145
|
+
# +park+ method will *not* be executed.
|
1146
|
+
#
|
1147
|
+
# == Defining additional arguments
|
1148
|
+
#
|
1149
|
+
# Additional arguments can be passed into events and accessed by transition
|
1150
|
+
# hooks like so:
|
1151
|
+
#
|
1152
|
+
# class Vehicle
|
1153
|
+
# state_machine do
|
1154
|
+
# after_transition :on => :park do |vehicle, transition|
|
1155
|
+
# kind = *transition.args # :parallel
|
1156
|
+
# ...
|
1157
|
+
# end
|
1158
|
+
# after_transition :on => :park, :do => :take_deep_breath
|
1159
|
+
#
|
1160
|
+
# event :park do
|
1161
|
+
# ...
|
1162
|
+
# end
|
1163
|
+
#
|
1164
|
+
# def take_deep_breath(transition)
|
1165
|
+
# kind = *transition.args # :parallel
|
1166
|
+
# ...
|
1167
|
+
# end
|
1168
|
+
# end
|
1057
1169
|
# end
|
1170
|
+
#
|
1171
|
+
# vehicle = Vehicle.new
|
1172
|
+
# vehicle.park(:parallel)
|
1058
1173
|
#
|
1059
1174
|
# *Remember* that if the last argument is a boolean, it will be used as the
|
1060
1175
|
# +run_action+ parameter to the event action. Using the +park+ action
|
@@ -1064,6 +1179,26 @@ module StateMachine
|
|
1064
1179
|
# vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
|
1065
1180
|
# vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
|
1066
1181
|
#
|
1182
|
+
# If you decide to override the +park+ event method *and* define additional
|
1183
|
+
# arguments, you can do so as shown below:
|
1184
|
+
#
|
1185
|
+
# class Vehicle
|
1186
|
+
# state_machine do
|
1187
|
+
# event :park do
|
1188
|
+
# ...
|
1189
|
+
# end
|
1190
|
+
# end
|
1191
|
+
#
|
1192
|
+
# def park(kind = :parallel, *args)
|
1193
|
+
# take_deep_breath if kind == :parallel
|
1194
|
+
# super
|
1195
|
+
# end
|
1196
|
+
# end
|
1197
|
+
#
|
1198
|
+
# Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
|
1199
|
+
# the entire arguments list to be accessed by transition callbacks through
|
1200
|
+
# StateMachine::Transition#args.
|
1201
|
+
#
|
1067
1202
|
# == Example
|
1068
1203
|
#
|
1069
1204
|
# class Vehicle
|
@@ -1103,6 +1238,104 @@ module StateMachine
|
|
1103
1238
|
end
|
1104
1239
|
alias_method :on, :event
|
1105
1240
|
|
1241
|
+
# Creates a new transition that determines what to change the current state
|
1242
|
+
# to when an event fires.
|
1243
|
+
#
|
1244
|
+
# == Defining transitions
|
1245
|
+
#
|
1246
|
+
# The options for a new transition uses the Hash syntax to map beginning
|
1247
|
+
# states to ending states. For example,
|
1248
|
+
#
|
1249
|
+
# transition :parked => :idling, :idling => :first_gear, :on => :ignite
|
1250
|
+
#
|
1251
|
+
# In this case, when the +ignite+ event is fired, this transition will cause
|
1252
|
+
# the state to be +idling+ if it's current state is +parked+ or +first_gear+
|
1253
|
+
# if it's current state is +idling+.
|
1254
|
+
#
|
1255
|
+
# To help define these implicit transitions, a set of helpers are available
|
1256
|
+
# for slightly more complex matching:
|
1257
|
+
# * <tt>all</tt> - Matches every state in the machine
|
1258
|
+
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
|
1259
|
+
# * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
|
1260
|
+
# * <tt>same</tt> - Matches the same state being transitioned from
|
1261
|
+
#
|
1262
|
+
# See StateMachine::MatcherHelpers for more information.
|
1263
|
+
#
|
1264
|
+
# Examples:
|
1265
|
+
#
|
1266
|
+
# transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
|
1267
|
+
# transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
|
1268
|
+
# transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
|
1269
|
+
# transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
|
1270
|
+
# transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
|
1271
|
+
# transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
|
1272
|
+
#
|
1273
|
+
# transition :parked => same, :on => :park # Loops :parked back to :parked
|
1274
|
+
# transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
|
1275
|
+
# transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
|
1276
|
+
#
|
1277
|
+
# # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
|
1278
|
+
# transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
|
1279
|
+
#
|
1280
|
+
# == Verbose transitions
|
1281
|
+
#
|
1282
|
+
# Transitions can also be defined use an explicit set of configuration
|
1283
|
+
# options:
|
1284
|
+
# * <tt>:from</tt> - A state or array of states that can be transitioned from.
|
1285
|
+
# If not specified, then the transition can occur for *any* state.
|
1286
|
+
# * <tt>:to</tt> - The state that's being transitioned to. If not specified,
|
1287
|
+
# then the transition will simply loop back (i.e. the state will not change).
|
1288
|
+
# * <tt>:except_from</tt> - A state or array of states that *cannot* be
|
1289
|
+
# transitioned from.
|
1290
|
+
#
|
1291
|
+
# These options must be used when defining transitions within the context
|
1292
|
+
# of a state.
|
1293
|
+
#
|
1294
|
+
# Examples:
|
1295
|
+
#
|
1296
|
+
# transition :to => nil, :on => :park
|
1297
|
+
# transition :to => :idling, :on => :ignite
|
1298
|
+
# transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
|
1299
|
+
# transition :from => nil, :to => :idling, :on => :ignite
|
1300
|
+
# transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
|
1301
|
+
#
|
1302
|
+
# == Conditions
|
1303
|
+
#
|
1304
|
+
# In addition to the state requirements for each transition, a condition
|
1305
|
+
# can also be defined to help determine whether that transition is
|
1306
|
+
# available. These options will work on both the normal and verbose syntax.
|
1307
|
+
#
|
1308
|
+
# Configuration options:
|
1309
|
+
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
1310
|
+
# transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
|
1311
|
+
# The condition should return or evaluate to true or false.
|
1312
|
+
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
1313
|
+
# transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
|
1314
|
+
# The condition should return or evaluate to true or false.
|
1315
|
+
#
|
1316
|
+
# Examples:
|
1317
|
+
#
|
1318
|
+
# transition :parked => :idling, :on => :ignite, :if => :moving?
|
1319
|
+
# transition :parked => :idling, :on => :ignite, :unless => :stopped?
|
1320
|
+
# transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
|
1321
|
+
#
|
1322
|
+
# transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
|
1323
|
+
# transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
|
1324
|
+
#
|
1325
|
+
# == Order of operations
|
1326
|
+
#
|
1327
|
+
# Transitions are evaluated in the order in which they're defined. As a
|
1328
|
+
# result, if more than one transition applies to a given object, then the
|
1329
|
+
# first transition that matches will be performed.
|
1330
|
+
def transition(options)
|
1331
|
+
raise ArgumentError, 'Must specify :on event' unless options[:on]
|
1332
|
+
|
1333
|
+
branches = []
|
1334
|
+
event(*Array(options.delete(:on))) { branches << transition(options) }
|
1335
|
+
|
1336
|
+
branches.length == 1 ? branches.first : branches
|
1337
|
+
end
|
1338
|
+
|
1106
1339
|
# Creates a callback that will be invoked *before* a transition is
|
1107
1340
|
# performed so long as the given requirements match the transition.
|
1108
1341
|
#
|
@@ -1569,6 +1802,21 @@ module StateMachine
|
|
1569
1802
|
def after_initialize
|
1570
1803
|
end
|
1571
1804
|
|
1805
|
+
# Looks up other machines that have been defined in the owner class and
|
1806
|
+
# are targeting the same attribute as this machine. When accessing
|
1807
|
+
# sibling machines, they will be automatically copied for the current
|
1808
|
+
# class if they haven't been already. This ensures that any configuration
|
1809
|
+
# changes made to the sibling machines only affect this class and not any
|
1810
|
+
# base class that may have originally defined the machine.
|
1811
|
+
def sibling_machines
|
1812
|
+
owner_class.state_machines.inject([]) do |machines, (name, machine)|
|
1813
|
+
if machine.attribute == attribute && machine != self
|
1814
|
+
machines << (owner_class.state_machine(name) {})
|
1815
|
+
end
|
1816
|
+
machines
|
1817
|
+
end
|
1818
|
+
end
|
1819
|
+
|
1572
1820
|
# Determines if the machine's attribute needs to be initialized. This
|
1573
1821
|
# will only be true if the machine's attribute is blank.
|
1574
1822
|
def initialize_state?(object)
|
@@ -1612,7 +1860,7 @@ module StateMachine
|
|
1612
1860
|
call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
|
1613
1861
|
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
1614
1862
|
def #{name}?(*args)
|
1615
|
-
args.empty? && #{call_super} ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
|
1863
|
+
args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
|
1616
1864
|
end
|
1617
1865
|
end_eval
|
1618
1866
|
end
|
@@ -1727,7 +1975,7 @@ module StateMachine
|
|
1727
1975
|
# were included *prior* to the helper modules, in addition to the
|
1728
1976
|
# superclasses
|
1729
1977
|
ancestors = current.ancestors - superclass.ancestors + superclasses
|
1730
|
-
ancestors = ancestors[ancestors.index(@helper_modules[scope])
|
1978
|
+
ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
|
1731
1979
|
|
1732
1980
|
# Search for for the first ancestor that defined this method
|
1733
1981
|
ancestors.detect do |ancestor|
|
@@ -1820,6 +2068,15 @@ module StateMachine
|
|
1820
2068
|
yield
|
1821
2069
|
end
|
1822
2070
|
|
2071
|
+
# Updates this machine based on the configuration of other machines in the
|
2072
|
+
# owner class that share the same target attribute.
|
2073
|
+
def add_sibling_machine_configs
|
2074
|
+
# Add existing states
|
2075
|
+
sibling_machines.each do |machine|
|
2076
|
+
machine.states.each {|state| states << state unless states[state.name]}
|
2077
|
+
end
|
2078
|
+
end
|
2079
|
+
|
1823
2080
|
# Adds a new transition callback of the given type.
|
1824
2081
|
def add_callback(type, options, &block)
|
1825
2082
|
callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
|
@@ -1833,6 +2090,9 @@ module StateMachine
|
|
1833
2090
|
new_states.map do |new_state|
|
1834
2091
|
unless state = states[new_state]
|
1835
2092
|
states << state = State.new(self, new_state)
|
2093
|
+
|
2094
|
+
# Copy states over to sibling machines
|
2095
|
+
sibling_machines.each {|machine| machine.states << state}
|
1836
2096
|
end
|
1837
2097
|
|
1838
2098
|
state
|
data/lib/state_machine/state.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'state_machine/assertions'
|
2
|
-
require 'state_machine/
|
2
|
+
require 'state_machine/state_context'
|
3
3
|
|
4
4
|
module StateMachine
|
5
5
|
# A state defines a value that an attribute can be in after being transitioned
|
@@ -184,7 +184,7 @@ module StateMachine
|
|
184
184
|
name = self.name
|
185
185
|
|
186
186
|
# Evaluate the method definitions
|
187
|
-
context =
|
187
|
+
context = StateContext.new(self)
|
188
188
|
context.class_eval(&block)
|
189
189
|
context.instance_methods.each do |method|
|
190
190
|
methods[method.to_sym] = context.instance_method(method)
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'state_machine/assertions'
|
2
|
+
require 'state_machine/eval_helpers'
|
3
|
+
|
4
|
+
module StateMachine
|
5
|
+
# Represents a module which will get evaluated within the context of a state.
|
6
|
+
#
|
7
|
+
# Class-level methods are proxied to the owner class, injecting a custom
|
8
|
+
# <tt>:if</tt> condition along with method. This assumes that the method has
|
9
|
+
# support for a set of configuration options, including <tt>:if</tt>. This
|
10
|
+
# condition will check that the object's state matches this context's state.
|
11
|
+
#
|
12
|
+
# Instance-level methods are used to define state-driven behavior on the
|
13
|
+
# state's owner class.
|
14
|
+
#
|
15
|
+
# == Examples
|
16
|
+
#
|
17
|
+
# class Vehicle
|
18
|
+
# class << self
|
19
|
+
# attr_accessor :validations
|
20
|
+
#
|
21
|
+
# def validate(options, &block)
|
22
|
+
# validations << options
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# self.validations = []
|
27
|
+
# attr_accessor :state, :simulate
|
28
|
+
#
|
29
|
+
# def moving?
|
30
|
+
# self.class.validations.all? {|validation| validation[:if].call(self)}
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# In the above class, a simple set of validation behaviors have been defined.
|
35
|
+
# Each validation consists of a configuration like so:
|
36
|
+
#
|
37
|
+
# Vehicle.validate :unless => :simulate
|
38
|
+
# Vehicle.validate :if => lambda {|vehicle| ...}
|
39
|
+
#
|
40
|
+
# In order to scope validations to a particular state context, the class-level
|
41
|
+
# +validate+ method can be invoked like so:
|
42
|
+
#
|
43
|
+
# machine = StateMachine::Machine.new(Vehicle)
|
44
|
+
# context = StateMachine::StateContext.new(machine.state(:first_gear))
|
45
|
+
# context.validate(:unless => :simulate)
|
46
|
+
#
|
47
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
|
48
|
+
# vehicle.moving? # => false
|
49
|
+
#
|
50
|
+
# vehicle.state = 'first_gear'
|
51
|
+
# vehicle.moving? # => true
|
52
|
+
#
|
53
|
+
# vehicle.simulate = true
|
54
|
+
# vehicle.moving? # => false
|
55
|
+
class StateContext < Module
|
56
|
+
include Assertions
|
57
|
+
include EvalHelpers
|
58
|
+
|
59
|
+
# The state machine for which this context's state is defined
|
60
|
+
attr_reader :machine
|
61
|
+
|
62
|
+
# The state that must be present in an object for this context to be active
|
63
|
+
attr_reader :state
|
64
|
+
|
65
|
+
# Creates a new context for the given state
|
66
|
+
def initialize(state)
|
67
|
+
@state = state
|
68
|
+
@machine = state.machine
|
69
|
+
|
70
|
+
state_name = state.name
|
71
|
+
machine_name = machine.name
|
72
|
+
@condition = lambda {|object| object.class.state_machine(machine_name).states.matches?(object, state_name)}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates a new transition that determines what to change the current state
|
76
|
+
# to when an event fires from this state.
|
77
|
+
#
|
78
|
+
# Since this transition is being defined within a state context, you do
|
79
|
+
# *not* need to specify the <tt>:from</tt> option for the transition. For
|
80
|
+
# example:
|
81
|
+
#
|
82
|
+
# state_machine do
|
83
|
+
# state :parked do
|
84
|
+
# transition :to => same, :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
|
85
|
+
# transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# See StateMachine::Machine#transition for a description of the possible
|
90
|
+
# configurations for defining transitions.
|
91
|
+
def transition(options)
|
92
|
+
assert_valid_keys(options, :to, :on, :if, :unless)
|
93
|
+
raise ArgumentError, 'Must specify :to state and :on event' unless options[:to] && options[:on]
|
94
|
+
|
95
|
+
machine.transition(options.merge(:from => state.name))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Hooks in condition-merging to methods that don't exist in this module
|
99
|
+
def method_missing(*args, &block)
|
100
|
+
# Get the configuration
|
101
|
+
if args.last.is_a?(Hash)
|
102
|
+
options = args.last
|
103
|
+
else
|
104
|
+
args << options = {}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Get any existing condition that may need to be merged
|
108
|
+
if_condition = options.delete(:if)
|
109
|
+
unless_condition = options.delete(:unless)
|
110
|
+
|
111
|
+
# Provide scope access to configuration in case the block is evaluated
|
112
|
+
# within the object instance
|
113
|
+
proxy = self
|
114
|
+
proxy_condition = @condition
|
115
|
+
|
116
|
+
# Replace the configuration condition with the one configured for this
|
117
|
+
# proxy, merging together any existing conditions
|
118
|
+
options[:if] = lambda do |*args|
|
119
|
+
# Block may be executed within the context of the actual object, so
|
120
|
+
# it'll either be the first argument or the executing context
|
121
|
+
object = args.first || self
|
122
|
+
|
123
|
+
proxy.evaluate_method(object, proxy_condition) &&
|
124
|
+
Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
|
125
|
+
!Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Evaluate the method on the owner class with the condition proxied
|
129
|
+
# through
|
130
|
+
machine.owner_class.send(*args, &block)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|