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.
Files changed (120) hide show
  1. data/.gitignore +11 -0
  2. data/.travis.yml +16 -0
  3. data/.yardopts +5 -0
  4. data/Appraisals +260 -0
  5. data/CHANGELOG.rdoc +15 -0
  6. data/Gemfile +3 -0
  7. data/README.rdoc +156 -29
  8. data/Rakefile +31 -57
  9. data/gemfiles/active_model-3.0.0.gemfile +7 -0
  10. data/gemfiles/active_model-3.0.0.gemfile.lock +32 -0
  11. data/gemfiles/active_model-3.0.5.gemfile +7 -0
  12. data/gemfiles/active_model-3.0.5.gemfile.lock +32 -0
  13. data/gemfiles/active_record-2.0.0.gemfile +8 -0
  14. data/gemfiles/active_record-2.0.0.gemfile.lock +30 -0
  15. data/gemfiles/active_record-2.0.5.gemfile +8 -0
  16. data/gemfiles/active_record-2.0.5.gemfile.lock +30 -0
  17. data/gemfiles/active_record-2.1.0.gemfile +8 -0
  18. data/gemfiles/active_record-2.1.0.gemfile.lock +30 -0
  19. data/gemfiles/active_record-2.1.2.gemfile +8 -0
  20. data/gemfiles/active_record-2.1.2.gemfile.lock +30 -0
  21. data/gemfiles/active_record-2.2.3.gemfile +8 -0
  22. data/gemfiles/active_record-2.2.3.gemfile.lock +30 -0
  23. data/gemfiles/active_record-2.3.12.gemfile +8 -0
  24. data/gemfiles/active_record-2.3.12.gemfile.lock +30 -0
  25. data/gemfiles/active_record-3.0.0.gemfile +8 -0
  26. data/gemfiles/active_record-3.0.0.gemfile.lock +44 -0
  27. data/gemfiles/active_record-3.0.5.gemfile +8 -0
  28. data/gemfiles/active_record-3.0.5.gemfile.lock +43 -0
  29. data/gemfiles/data_mapper-0.10.2.gemfile +12 -0
  30. data/gemfiles/data_mapper-0.10.2.gemfile.lock +45 -0
  31. data/gemfiles/data_mapper-0.9.11.gemfile +12 -0
  32. data/gemfiles/data_mapper-0.9.11.gemfile.lock +47 -0
  33. data/gemfiles/data_mapper-0.9.4.gemfile +12 -0
  34. data/gemfiles/data_mapper-0.9.4.gemfile.lock +61 -0
  35. data/gemfiles/data_mapper-0.9.7.gemfile +12 -0
  36. data/gemfiles/data_mapper-0.9.7.gemfile.lock +57 -0
  37. data/gemfiles/data_mapper-1.0.0.gemfile +12 -0
  38. data/gemfiles/data_mapper-1.0.0.gemfile.lock +53 -0
  39. data/gemfiles/data_mapper-1.0.1.gemfile +12 -0
  40. data/gemfiles/data_mapper-1.0.1.gemfile.lock +53 -0
  41. data/gemfiles/data_mapper-1.0.2.gemfile +12 -0
  42. data/gemfiles/data_mapper-1.0.2.gemfile.lock +53 -0
  43. data/gemfiles/data_mapper-1.1.0.gemfile +12 -0
  44. data/gemfiles/data_mapper-1.1.0.gemfile.lock +51 -0
  45. data/gemfiles/default.gemfile +7 -0
  46. data/gemfiles/default.gemfile.lock +24 -0
  47. data/gemfiles/mongo_mapper-0.5.5.gemfile +8 -0
  48. data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +33 -0
  49. data/gemfiles/mongo_mapper-0.5.8.gemfile +8 -0
  50. data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +33 -0
  51. data/gemfiles/mongo_mapper-0.6.0.gemfile +8 -0
  52. data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +33 -0
  53. data/gemfiles/mongo_mapper-0.6.10.gemfile +8 -0
  54. data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +33 -0
  55. data/gemfiles/mongo_mapper-0.7.0.gemfile +8 -0
  56. data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +33 -0
  57. data/gemfiles/mongo_mapper-0.7.5.gemfile +8 -0
  58. data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +36 -0
  59. data/gemfiles/mongo_mapper-0.8.0.gemfile +10 -0
  60. data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +40 -0
  61. data/gemfiles/mongo_mapper-0.8.3.gemfile +10 -0
  62. data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +40 -0
  63. data/gemfiles/mongo_mapper-0.8.4.gemfile +8 -0
  64. data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +38 -0
  65. data/gemfiles/mongo_mapper-0.8.6.gemfile +8 -0
  66. data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +38 -0
  67. data/gemfiles/mongo_mapper-0.9.0.gemfile +7 -0
  68. data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +41 -0
  69. data/gemfiles/mongoid-2.0.0.gemfile +7 -0
  70. data/gemfiles/mongoid-2.0.0.gemfile.lock +42 -0
  71. data/gemfiles/mongoid-2.1.4.gemfile +7 -0
  72. data/gemfiles/mongoid-2.1.4.gemfile.lock +40 -0
  73. data/gemfiles/sequel-2.11.0.gemfile +8 -0
  74. data/gemfiles/sequel-2.11.0.gemfile.lock +28 -0
  75. data/gemfiles/sequel-2.12.0.gemfile +8 -0
  76. data/gemfiles/sequel-2.12.0.gemfile.lock +28 -0
  77. data/gemfiles/sequel-2.8.0.gemfile +8 -0
  78. data/gemfiles/sequel-2.8.0.gemfile.lock +28 -0
  79. data/gemfiles/sequel-3.0.0.gemfile +8 -0
  80. data/gemfiles/sequel-3.0.0.gemfile.lock +28 -0
  81. data/gemfiles/sequel-3.13.0.gemfile +8 -0
  82. data/gemfiles/sequel-3.13.0.gemfile.lock +28 -0
  83. data/gemfiles/sequel-3.14.0.gemfile +8 -0
  84. data/gemfiles/sequel-3.14.0.gemfile.lock +28 -0
  85. data/gemfiles/sequel-3.23.0.gemfile +8 -0
  86. data/gemfiles/sequel-3.23.0.gemfile.lock +28 -0
  87. data/gemfiles/sequel-3.24.0.gemfile +8 -0
  88. data/gemfiles/sequel-3.24.0.gemfile.lock +28 -0
  89. data/lib/state_machine/event.rb +13 -90
  90. data/lib/state_machine/helper_module.rb +17 -0
  91. data/lib/state_machine/integrations/active_model.rb +35 -0
  92. data/lib/state_machine/integrations/active_record.rb +41 -2
  93. data/lib/state_machine/integrations/data_mapper.rb +17 -2
  94. data/lib/state_machine/integrations/mongo_mapper.rb +34 -7
  95. data/lib/state_machine/integrations/mongoid.rb +34 -26
  96. data/lib/state_machine/integrations/mongoid/versions.rb +29 -3
  97. data/lib/state_machine/integrations/sequel.rb +22 -72
  98. data/lib/state_machine/integrations/sequel/versions.rb +87 -6
  99. data/lib/state_machine/machine.rb +279 -19
  100. data/lib/state_machine/state.rb +2 -2
  101. data/lib/state_machine/state_context.rb +133 -0
  102. data/lib/state_machine/version.rb +3 -0
  103. data/state_machine.gemspec +22 -0
  104. data/test/test_helper.rb +1 -3
  105. data/test/unit/branch_test.rb +1 -3
  106. data/test/unit/event_collection_test.rb +3 -3
  107. data/test/unit/event_test.rb +1 -3
  108. data/test/unit/helper_module_test.rb +17 -0
  109. data/test/unit/integrations/active_model_test.rb +0 -4
  110. data/test/unit/integrations/active_record_test.rb +50 -9
  111. data/test/unit/integrations/data_mapper_test.rb +267 -253
  112. data/test/unit/integrations/mongo_mapper_test.rb +47 -15
  113. data/test/unit/integrations/mongoid_test.rb +50 -8
  114. data/test/unit/integrations/sequel_test.rb +10 -6
  115. data/test/unit/machine_test.rb +206 -25
  116. data/test/unit/state_context_test.rb +421 -0
  117. data/test/unit/state_test.rb +20 -3
  118. metadata +303 -128
  119. data/lib/state_machine/condition_proxy.rb +0 -94
  120. 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 owner_class.state_machines.any? {|name, machine| machine.attribute == attribute && machine != self}
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 => Module.new, :class => Module.new}
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
- # == Defining additional arguments
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
- # Additional arguments on event actions can be defined like so:
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(kind = :parallel, *args)
1041
- # take_deep_breath if kind == :parallel
1042
- # super
1043
- # end
1044
- #
1045
- # def take_deep_breath
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
- # Note that +super+ is called instead of <tt>super(*args)</tt>. This allows
1051
- # the entire arguments list to be accessed by transition callbacks through
1052
- # StateMachine::Transition#args like so:
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
- # after_transition :on => :park do |vehicle, transition|
1055
- # kind = *transition.args
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]) + 1..-1].reverse
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
@@ -1,5 +1,5 @@
1
1
  require 'state_machine/assertions'
2
- require 'state_machine/condition_proxy'
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 = ConditionProxy.new(owner_class, lambda {|object| object.class.state_machine(machine_name).states.matches?(object, name)})
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