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.
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