state_machine 1.0.1 → 1.0.2
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/.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
|