state_machine 1.0.2 → 1.0.3

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 (102) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +0 -2
  3. data/.yardopts +3 -2
  4. data/Appraisals +48 -0
  5. data/{CHANGELOG.rdoc → CHANGELOG.md} +63 -46
  6. data/README.md +1029 -0
  7. data/gemfiles/active_model-3.0.0.gemfile.lock +1 -3
  8. data/gemfiles/active_model-3.0.5.gemfile.lock +1 -3
  9. data/gemfiles/active_model-3.1.1.gemfile +7 -0
  10. data/gemfiles/active_model-3.1.1.gemfile.lock +32 -0
  11. data/gemfiles/active_record-2.0.0.gemfile.lock +1 -3
  12. data/gemfiles/active_record-2.0.5.gemfile.lock +1 -3
  13. data/gemfiles/active_record-2.1.0.gemfile.lock +1 -3
  14. data/gemfiles/active_record-2.1.2.gemfile.lock +1 -3
  15. data/gemfiles/active_record-2.2.3.gemfile.lock +1 -3
  16. data/gemfiles/active_record-2.3.12.gemfile.lock +1 -3
  17. data/gemfiles/active_record-3.0.0.gemfile.lock +1 -3
  18. data/gemfiles/active_record-3.0.5.gemfile.lock +1 -3
  19. data/gemfiles/active_record-3.1.1.gemfile +8 -0
  20. data/gemfiles/active_record-3.1.1.gemfile.lock +43 -0
  21. data/gemfiles/data_mapper-0.10.2.gemfile.lock +1 -3
  22. data/gemfiles/data_mapper-0.9.11.gemfile.lock +1 -3
  23. data/gemfiles/data_mapper-0.9.4.gemfile.lock +1 -3
  24. data/gemfiles/data_mapper-0.9.7.gemfile.lock +1 -3
  25. data/gemfiles/data_mapper-1.0.0.gemfile.lock +1 -3
  26. data/gemfiles/data_mapper-1.0.1.gemfile.lock +1 -3
  27. data/gemfiles/data_mapper-1.0.2.gemfile.lock +1 -3
  28. data/gemfiles/data_mapper-1.1.0.gemfile.lock +1 -3
  29. data/gemfiles/data_mapper-1.2.0.gemfile +12 -0
  30. data/gemfiles/data_mapper-1.2.0.gemfile.lock +49 -0
  31. data/gemfiles/default.gemfile.lock +1 -3
  32. data/gemfiles/graphviz-0.9.0.gemfile +7 -0
  33. data/gemfiles/graphviz-0.9.0.gemfile.lock +24 -0
  34. data/gemfiles/graphviz-0.9.21.gemfile +7 -0
  35. data/gemfiles/graphviz-0.9.21.gemfile.lock +24 -0
  36. data/gemfiles/graphviz-1.0.0.gemfile +7 -0
  37. data/gemfiles/graphviz-1.0.0.gemfile.lock +24 -0
  38. data/gemfiles/mongo_mapper-0.10.0.gemfile +7 -0
  39. data/gemfiles/mongo_mapper-0.10.0.gemfile.lock +41 -0
  40. data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +1 -3
  41. data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +1 -3
  42. data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +1 -3
  43. data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +1 -3
  44. data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +1 -3
  45. data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +1 -3
  46. data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +1 -3
  47. data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +1 -3
  48. data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +1 -3
  49. data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +1 -3
  50. data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +1 -3
  51. data/gemfiles/mongoid-2.0.0.gemfile.lock +1 -3
  52. data/gemfiles/mongoid-2.1.4.gemfile.lock +1 -3
  53. data/gemfiles/mongoid-2.2.4.gemfile +7 -0
  54. data/gemfiles/mongoid-2.2.4.gemfile.lock +40 -0
  55. data/gemfiles/mongoid-2.3.3.gemfile +7 -0
  56. data/gemfiles/mongoid-2.3.3.gemfile.lock +40 -0
  57. data/gemfiles/sequel-2.11.0.gemfile.lock +1 -3
  58. data/gemfiles/sequel-2.12.0.gemfile.lock +1 -3
  59. data/gemfiles/sequel-2.8.0.gemfile.lock +1 -3
  60. data/gemfiles/sequel-3.0.0.gemfile.lock +1 -3
  61. data/gemfiles/sequel-3.13.0.gemfile.lock +1 -3
  62. data/gemfiles/sequel-3.14.0.gemfile.lock +1 -3
  63. data/gemfiles/sequel-3.23.0.gemfile.lock +1 -3
  64. data/gemfiles/sequel-3.24.0.gemfile.lock +1 -3
  65. data/gemfiles/sequel-3.29.0.gemfile +8 -0
  66. data/gemfiles/sequel-3.29.0.gemfile.lock +26 -0
  67. data/lib/state_machine.rb +45 -0
  68. data/lib/state_machine/event.rb +18 -3
  69. data/lib/state_machine/event_collection.rb +1 -1
  70. data/lib/state_machine/integrations/active_model.rb +59 -16
  71. data/lib/state_machine/integrations/active_model/observer.rb +3 -15
  72. data/lib/state_machine/integrations/active_record.rb +46 -9
  73. data/lib/state_machine/integrations/data_mapper.rb +42 -2
  74. data/lib/state_machine/integrations/data_mapper/versions.rb +22 -10
  75. data/lib/state_machine/integrations/mongo_mapper.rb +55 -0
  76. data/lib/state_machine/integrations/mongo_mapper/versions.rb +3 -3
  77. data/lib/state_machine/integrations/mongoid.rb +57 -12
  78. data/lib/state_machine/integrations/mongoid/versions.rb +22 -4
  79. data/lib/state_machine/integrations/sequel.rb +45 -0
  80. data/lib/state_machine/integrations/sequel/versions.rb +3 -0
  81. data/lib/state_machine/machine.rb +148 -34
  82. data/lib/state_machine/node_collection.rb +36 -3
  83. data/lib/state_machine/state.rb +6 -3
  84. data/lib/state_machine/state_collection.rb +1 -1
  85. data/lib/state_machine/version.rb +1 -1
  86. data/lib/tasks/state_machine.rb +11 -9
  87. data/state_machine.gemspec +2 -3
  88. data/test/functional/state_machine_test.rb +54 -1
  89. data/test/unit/event_collection_test.rb +4 -0
  90. data/test/unit/event_test.rb +34 -1
  91. data/test/unit/integrations/active_model_test.rb +80 -0
  92. data/test/unit/integrations/active_record_test.rb +105 -2
  93. data/test/unit/integrations/data_mapper_test.rb +27 -25
  94. data/test/unit/integrations/mongo_mapper_test.rb +80 -25
  95. data/test/unit/integrations/mongoid_test.rb +61 -6
  96. data/test/unit/integrations/sequel_test.rb +8 -2
  97. data/test/unit/machine_test.rb +87 -9
  98. data/test/unit/node_collection_test.rb +129 -12
  99. data/test/unit/state_collection_test.rb +4 -0
  100. data/test/unit/state_test.rb +2 -2
  101. metadata +30 -24
  102. data/README.rdoc +0 -844
@@ -187,6 +187,11 @@ module StateMachine
187
187
  # Because of the way named scopes work in MongoMapper, they *cannot* be
188
188
  # chained.
189
189
  #
190
+ # Note that states can also be referenced by the string version of their
191
+ # name:
192
+ #
193
+ # Vehicle.with_state('parked')
194
+ #
190
195
  # == Callbacks
191
196
  #
192
197
  # All before/after transition callbacks defined for MongoMapper models
@@ -219,6 +224,56 @@ module StateMachine
219
224
  #
220
225
  # Note, also, that the transition can be accessed by simply defining
221
226
  # additional arguments in the callback block.
227
+ #
228
+ # == Internationalization
229
+ #
230
+ # Any error message that is generated from performing invalid transitions
231
+ # can be localized. The following default translations are used:
232
+ #
233
+ # en:
234
+ # mongo_mapper:
235
+ # errors:
236
+ # messages:
237
+ # invalid: "is invalid"
238
+ # # %{value} = attribute value, %{state} = Human state name
239
+ # invalid_event: "cannot transition when %{state}"
240
+ # # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
241
+ # invalid_transition: "cannot transition via %{event}"
242
+ #
243
+ # You can override these for a specific model like so:
244
+ #
245
+ # en:
246
+ # mongo_mapper:
247
+ # errors:
248
+ # models:
249
+ # user:
250
+ # invalid: "is not valid"
251
+ #
252
+ # In addition to the above, you can also provide translations for the
253
+ # various states / events in each state machine. Using the Vehicle example,
254
+ # state translations will be looked for using the following keys, where
255
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
256
+ # * <tt>mongo_mapper.state_machines.#{model_name}.#{machine_name}.states.#{state_name}</tt>
257
+ # * <tt>mongo_mapper.state_machines.#{model_name}.states.#{state_name}</tt>
258
+ # * <tt>mongo_mapper.state_machines.#{machine_name}.states.#{state_name}</tt>
259
+ # * <tt>mongo_mapper.state_machines.states.#{state_name}</tt>
260
+ #
261
+ # Event translations will be looked for using the following keys, where
262
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
263
+ # * <tt>mongo_mapper.state_machines.#{model_name}.#{machine_name}.events.#{event_name}</tt>
264
+ # * <tt>mongo_mapper.state_machines.#{model_name}.events.#{event_name}</tt>
265
+ # * <tt>mongo_mapper.state_machines.#{machine_name}.events.#{event_name}</tt>
266
+ # * <tt>mongo_mapper.state_machines.events.#{event_name}</tt>
267
+ #
268
+ # An example translation configuration might look like so:
269
+ #
270
+ # es:
271
+ # mongo_mapper:
272
+ # state_machines:
273
+ # states:
274
+ # parked: 'estacionado'
275
+ # events:
276
+ # park: 'estacionarse'
222
277
  module MongoMapper
223
278
  include Base
224
279
  include ActiveModel
@@ -22,7 +22,7 @@ module StateMachine
22
22
 
23
23
  version '0.5.x - 0.7.x' do
24
24
  def self.active?
25
- !defined?(::MongoMapper::Version) || ::MongoMapper::Version < '0.8.0'
25
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version =~ /^0\.[5-7]\./
26
26
  end
27
27
 
28
28
  def define_scope(name, scope)
@@ -32,7 +32,7 @@ module StateMachine
32
32
 
33
33
  version '0.5.x - 0.8.x' do
34
34
  def self.active?
35
- !defined?(::MongoMapper::Version) || ::MongoMapper::Version < '0.9.0'
35
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version =~ /^0\.[5-8]\./
36
36
  end
37
37
 
38
38
  def invalidate(object, attribute, message, values = [])
@@ -80,7 +80,7 @@ module StateMachine
80
80
  def self.active?
81
81
  # Only 0.8.x and up has a Version string available, so Plugins is used
82
82
  # to detect when 0.7.x is active
83
- defined?(::MongoMapper::Plugins) && (!defined?(::MongoMapper::Version) || ::MongoMapper::Version <= '0.8.3')
83
+ defined?(::MongoMapper::Plugins) && (!defined?(::MongoMapper::Version) || ::MongoMapper::Version =~ /^0\.(7|8\.[0-3])\./)
84
84
  end
85
85
 
86
86
  def define_state_initializer
@@ -181,6 +181,11 @@ module StateMachine
181
181
  # Because of the way named scopes work in Mongoid, they *cannot* be
182
182
  # chained.
183
183
  #
184
+ # Note that states can also be referenced by the string version of their
185
+ # name:
186
+ #
187
+ # Vehicle.with_state('parked')
188
+ #
184
189
  # == Callbacks
185
190
  #
186
191
  # All before/after transition callbacks defined for Mongoid models
@@ -269,6 +274,56 @@ module StateMachine
269
274
  # Audit.log(record, transition)
270
275
  # end
271
276
  # end
277
+ #
278
+ # == Internationalization
279
+ #
280
+ # Any error message that is generated from performing invalid transitions
281
+ # can be localized. The following default translations are used:
282
+ #
283
+ # en:
284
+ # mongoid:
285
+ # errors:
286
+ # messages:
287
+ # invalid: "is invalid"
288
+ # # %{value} = attribute value, %{state} = Human state name
289
+ # invalid_event: "cannot transition when %{state}"
290
+ # # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
291
+ # invalid_transition: "cannot transition via %{event}"
292
+ #
293
+ # You can override these for a specific model like so:
294
+ #
295
+ # en:
296
+ # mongoid:
297
+ # errors:
298
+ # models:
299
+ # user:
300
+ # invalid: "is not valid"
301
+ #
302
+ # In addition to the above, you can also provide translations for the
303
+ # various states / events in each state machine. Using the Vehicle example,
304
+ # state translations will be looked for using the following keys, where
305
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
306
+ # * <tt>mongoid.state_machines.#{model_name}.#{machine_name}.states.#{state_name}</tt>
307
+ # * <tt>mongoid.state_machines.#{model_name}.states.#{state_name}</tt>
308
+ # * <tt>mongoid.state_machines.#{machine_name}.states.#{state_name}</tt>
309
+ # * <tt>mongoid.state_machines.states.#{state_name}</tt>
310
+ #
311
+ # Event translations will be looked for using the following keys, where
312
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
313
+ # * <tt>mongoid.state_machines.#{model_name}.#{machine_name}.events.#{event_name}</tt>
314
+ # * <tt>mongoid.state_machines.#{model_name}.events.#{event_name}</tt>
315
+ # * <tt>mongoid.state_machines.#{machine_name}.events.#{event_name}</tt>
316
+ # * <tt>mongoid.state_machines.events.#{event_name}</tt>
317
+ #
318
+ # An example translation configuration might look like so:
319
+ #
320
+ # es:
321
+ # mongoid:
322
+ # state_machines:
323
+ # states:
324
+ # parked: 'estacionado'
325
+ # events:
326
+ # park: 'estacionarse'
272
327
  module Mongoid
273
328
  include Base
274
329
  include ActiveModel
@@ -307,19 +362,9 @@ module StateMachine
307
362
  # object
308
363
  def define_state_initializer
309
364
  define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
310
- # Initializes dynamic states
311
365
  def initialize(*)
312
- super do |*args|
313
- self.class.state_machines.initialize_states(self, :static => false)
314
- yield(*args) if block_given?
315
- end
316
- end
317
-
318
- # Initializes static states
319
- def apply_default_attributes(*)
320
- result = super
321
- self.class.state_machines.initialize_states(self, :dynamic => false, :to => result) if new_record?
322
- result
366
+ @attributes = {}
367
+ self.class.state_machines.initialize_states(self) { super }
323
368
  end
324
369
  end_eval
325
370
  end
@@ -1,10 +1,28 @@
1
1
  module StateMachine
2
2
  module Integrations #:nodoc:
3
3
  module Mongoid
4
- # Assumes Mongoid 2.2+ uses ActiveModel 3.1+
5
- version '2.0.x - 2.1.x' do
4
+ version '2.0.x - 2.2.x' do
6
5
  def self.active?
7
- ::Mongoid::VERSION >= '2.0.0' && ::Mongoid::VERSION < '2.2.0'
6
+ ::Mongoid::VERSION =~ /^2\.[0-2]\./
7
+ end
8
+
9
+ def define_state_initializer
10
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
11
+ # Initializes dynamic states
12
+ def initialize(*)
13
+ super do |*args|
14
+ self.class.state_machines.initialize_states(self, :static => false)
15
+ yield(*args) if block_given?
16
+ end
17
+ end
18
+
19
+ # Initializes static states
20
+ def apply_default_attributes(*)
21
+ result = super
22
+ self.class.state_machines.initialize_states(self, :dynamic => false, :to => result) if new_record?
23
+ result
24
+ end
25
+ end_eval
8
26
  end
9
27
 
10
28
  def define_action_hook
@@ -16,7 +34,7 @@ module StateMachine
16
34
 
17
35
  version '2.0.x' do
18
36
  def self.active?
19
- ::Mongoid::VERSION >= '2.0.0' && ::Mongoid::VERSION < '2.1.0'
37
+ ::Mongoid::VERSION =~ /^2\.0\./
20
38
  end
21
39
 
22
40
  # Forces the change in state to be recognized regardless of whether the
@@ -193,6 +193,11 @@ module StateMachine
193
193
  #
194
194
  # Vehicle.with_state(:parked).order(:id.desc)
195
195
  #
196
+ # Note that states can also be referenced by the string version of their
197
+ # name:
198
+ #
199
+ # Vehicle.with_state('parked')
200
+ #
196
201
  # == Callbacks
197
202
  #
198
203
  # All before/after transition callbacks defined for Sequel resources
@@ -224,6 +229,34 @@ module StateMachine
224
229
  #
225
230
  # Note, also, that the transition can be accessed by simply defining
226
231
  # additional arguments in the callback block.
232
+ #
233
+ # === Failure callbacks
234
+ #
235
+ # +after_failure+ callbacks allow you to execute behaviors when a transition
236
+ # is allowed, but fails to save. This could be useful for something like
237
+ # auditing transition attempts. Since callbacks run within transactions in
238
+ # Sequel, a save failure will cause any records that get created in
239
+ # your callback to roll back. You can work around this issue like so:
240
+ #
241
+ # DB = Sequel.connect('mysql://localhost/app')
242
+ # DB_LOGS = Sequel.connect('mysql://localhost/app')
243
+ #
244
+ # class TransitionLog < Sequel::Model(DB_LOGS[:transition_logs])
245
+ # end
246
+ #
247
+ # class Vehicle < Sequel::Model(DB[:vehicles])
248
+ # state_machine do
249
+ # after_failure do |transition|
250
+ # TransitionLog.create(:vehicle => vehicle, :transition => transition)
251
+ # end
252
+ #
253
+ # ...
254
+ # end
255
+ # end
256
+ #
257
+ # The +TransitionLog+ model uses a second connection to the database that
258
+ # allows new records to be saved without being affected by rollbacks in the
259
+ # +Vehicle+ model's transaction.
227
260
  module Sequel
228
261
  include Base
229
262
 
@@ -276,6 +309,18 @@ module StateMachine
276
309
  end
277
310
 
278
311
  protected
312
+ # Initializes class-level extensions for this machine
313
+ def define_helpers
314
+ load_plugins
315
+ super
316
+ end
317
+
318
+ # Loads all of the Sequel plugins necessary to run
319
+ def load_plugins
320
+ owner_class.plugin(:validation_class_methods)
321
+ owner_class.plugin(:hook_class_methods)
322
+ end
323
+
279
324
  # Loads the built-in inflector
280
325
  def load_inflector
281
326
  require 'sequel/extensions/inflector'
@@ -73,6 +73,9 @@ module StateMachine
73
73
  !defined?(::Sequel::MAJOR) || ::Sequel::MAJOR == 2 && ::Sequel::MINOR <= 11
74
74
  end
75
75
 
76
+ def load_plugins
77
+ end
78
+
76
79
  def load_inflector
77
80
  end
78
81
 
@@ -139,11 +139,60 @@ module StateMachine
139
139
  # vehicle.save # => true (no exception raised)
140
140
  #
141
141
  # If you need callbacks to get triggered when an object is created, this
142
- # should be done by either:
143
- # * Use a <tt>before :save</tt> or equivalent hook, or
144
- # * Set an initial state of nil and use the correct event to create the
142
+ # should be done by one of the following techniques:
143
+ # * Use a <tt>before :create</tt> or equivalent hook:
144
+ #
145
+ # class Vehicle
146
+ # before :create, :track_initial_transition
147
+ #
148
+ # state_machine do
149
+ # ...
150
+ # end
151
+ # end
152
+ #
153
+ # * Set an initial state and use the correct event to create the
145
154
  # object with the proper state, resulting in callbacks being triggered and
146
- # the object getting persisted
155
+ # the object getting persisted (note that the <tt>:pending</tt> state is
156
+ # actually stored as nil):
157
+ #
158
+ # class Vehicle
159
+ # state_machine :initial => :pending
160
+ # after_transition :pending => :parked, :do => :track_initial_transition
161
+ #
162
+ # event :park do
163
+ # transition :pending => :parked
164
+ # end
165
+ #
166
+ # state :pending, :value => nil
167
+ # end
168
+ # end
169
+ #
170
+ # vehicle = Vehicle.new
171
+ # vehicle.park
172
+ #
173
+ # * Use a default event attribute that will automatically trigger when the
174
+ # configured action gets run (note that the <tt>:pending</tt> state is
175
+ # actually stored as nil):
176
+ #
177
+ # class Vehicle < ActiveRecord::Base
178
+ # state_machine :initial => :pending
179
+ # after_transition :pending => :parked, :do => :track_initial_transition
180
+ #
181
+ # event :park do
182
+ # transition :pending => :parked
183
+ # end
184
+ #
185
+ # state :pending, :value => nil
186
+ # end
187
+ #
188
+ # def initialize(*)
189
+ # super
190
+ # self.state_event = 'park'
191
+ # end
192
+ # end
193
+ #
194
+ # vehicle = Vehicle.new
195
+ # vehicle.save
147
196
  #
148
197
  # === Canceling callbacks
149
198
  #
@@ -301,7 +350,7 @@ module StateMachine
301
350
  # module that gets included in every Object. As a result, state_machine will
302
351
  # generate the following warning:
303
352
  #
304
- # Instance method "open" is already defined in Object, use generic helper instead.
353
+ # Instance method "open" is already defined in Object, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true.
305
354
  #
306
355
  # Even though you may not be using Kernel's implementation of the "open"
307
356
  # instance method, state_machine isn't aware of this and, as a result, stays
@@ -696,7 +745,7 @@ module StateMachine
696
745
  if block_given?
697
746
  if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
698
747
  ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
699
- warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead."
748
+ warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true."
700
749
  else
701
750
  name = self.name
702
751
  helper_module.class_eval do
@@ -923,6 +972,28 @@ module StateMachine
923
972
  # vehicle.state = 'backing_up'
924
973
  # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
925
974
  #
975
+ # === Using matchers
976
+ #
977
+ # The +all+ / +any+ matchers can be used to easily define behaviors for a
978
+ # group of states. Note, however, that you cannot use these matchers to
979
+ # set configurations for states. Behaviors using these matchers can be
980
+ # defined at any point in the state machine and will always get applied to
981
+ # the proper states.
982
+ #
983
+ # For example:
984
+ #
985
+ # state_machine :initial => :parked do
986
+ # ...
987
+ #
988
+ # state all - [:parked, :idling, :stalled] do
989
+ # validates_presence_of :speed
990
+ #
991
+ # def speed
992
+ # gear * 10
993
+ # end
994
+ # end
995
+ # end
996
+ #
926
997
  # == State-aware class methods
927
998
  #
928
999
  # In addition to defining scopes for instance methods that are state-aware,
@@ -959,17 +1030,29 @@ module StateMachine
959
1030
  options = names.last.is_a?(Hash) ? names.pop : {}
960
1031
  assert_valid_keys(options, :value, :cache, :if, :human_name)
961
1032
 
962
- states = add_states(names)
963
- states.each do |state|
964
- if options.include?(:value)
965
- state.value = options[:value]
966
- self.states.update(state)
967
- end
1033
+ # Store the context so that it can be used for / matched against any state
1034
+ # that gets added
1035
+ @states.context(names, &block) if block_given?
1036
+
1037
+ if names.first.is_a?(Matcher)
1038
+ # Add any states referenced in the matcher. When matchers are used,
1039
+ # states are not allowed to be configured.
1040
+ raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
1041
+ states = add_states(names.first.values)
1042
+ else
1043
+ states = add_states(names)
968
1044
 
969
- state.human_name = options[:human_name] if options.include?(:human_name)
970
- state.cache = options[:cache] if options.include?(:cache)
971
- state.matcher = options[:if] if options.include?(:if)
972
- state.context(&block) if block_given?
1045
+ # Update the configuration for the state(s)
1046
+ states.each do |state|
1047
+ if options.include?(:value)
1048
+ state.value = options[:value]
1049
+ self.states.update(state)
1050
+ end
1051
+
1052
+ state.human_name = options[:human_name] if options.include?(:human_name)
1053
+ state.cache = options[:cache] if options.include?(:cache)
1054
+ state.matcher = options[:if] if options.include?(:if)
1055
+ end
973
1056
  end
974
1057
 
975
1058
  states.length == 1 ? states.first : states
@@ -1038,14 +1121,18 @@ module StateMachine
1038
1121
  # transition fails, then a StateMachine::InvalidTransition error will be
1039
1122
  # raised. If the last argument is a boolean, it will control whether the
1040
1123
  # machine's action gets run.
1041
- # * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event can be fired given
1042
- # the current state of the object. This will *not* run validations in
1043
- # ORM integrations. To check whether an event can fire *and* passes
1044
- # validations, use event attributes (e.g. state_event) as described in the
1045
- # "Events" documentation of each ORM integration.
1046
- # * <tt>park_transition(requirements = {})</tt> - Gets the next transition that would be
1047
- # performed if the "park" event were to be fired now on the object or nil
1048
- # if no transitions can be performed.
1124
+ # * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event
1125
+ # can be fired given the current state of the object. This will *not* run
1126
+ # validations or callbacks in ORM integrations. It will only determine if
1127
+ # the state machine defines a valid transition for the event. To check
1128
+ # whether an event can fire *and* passes validations, use event attributes
1129
+ # (e.g. state_event) as described in the "Events" documentation of each
1130
+ # ORM integration.
1131
+ # * <tt>park_transition(requirements = {})</tt> - Gets the next transition
1132
+ # that would be performed if the "park" event were to be fired now on the
1133
+ # object or nil if no transitions can be performed. Like <tt>can_park?</tt>
1134
+ # this will also *not* run validations or callbacks. It will only
1135
+ # determine if the state machine defines a valid transition for the event.
1049
1136
  #
1050
1137
  # With a namespace of "car", the above names map to the following methods:
1051
1138
  # * <tt>can_park_car?</tt>
@@ -1199,6 +1286,24 @@ module StateMachine
1199
1286
  # the entire arguments list to be accessed by transition callbacks through
1200
1287
  # StateMachine::Transition#args.
1201
1288
  #
1289
+ # === Using matchers
1290
+ #
1291
+ # The +all+ / +any+ matchers can be used to easily execute blocks for a
1292
+ # group of events. Note, however, that you cannot use these matchers to
1293
+ # set configurations for events. Blocks using these matchers can be
1294
+ # defined at any point in the state machine and will always get applied to
1295
+ # the proper events.
1296
+ #
1297
+ # For example:
1298
+ #
1299
+ # state_machine :initial => :parked do
1300
+ # ...
1301
+ #
1302
+ # event all - [:crash] do
1303
+ # transition :stalled => :parked
1304
+ # end
1305
+ # end
1306
+ #
1202
1307
  # == Example
1203
1308
  #
1204
1309
  # class Vehicle
@@ -1222,16 +1327,25 @@ module StateMachine
1222
1327
  options = names.last.is_a?(Hash) ? names.pop : {}
1223
1328
  assert_valid_keys(options, :human_name)
1224
1329
 
1225
- events = add_events(names)
1226
- events.each do |event|
1227
- event.human_name = options[:human_name] if options.include?(:human_name)
1330
+ # Store the context so that it can be used for / matched against any event
1331
+ # that gets added
1332
+ @events.context(names, &block) if block_given?
1333
+
1334
+ if names.first.is_a?(Matcher)
1335
+ # Add any events referenced in the matcher. When matchers are used,
1336
+ # events are not allowed to be configured.
1337
+ raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
1338
+ events = add_events(names.first.values)
1339
+ else
1340
+ events = add_events(names)
1228
1341
 
1229
- if block_given?
1230
- event.instance_eval(&block)
1342
+ # Update the configuration for the event(s)
1343
+ events.each do |event|
1344
+ event.human_name = options[:human_name] if options.include?(:human_name)
1345
+
1346
+ # Add any states that may have been referenced within the event
1231
1347
  add_states(event.known_states)
1232
1348
  end
1233
-
1234
- event
1235
1349
  end
1236
1350
 
1237
1351
  events.length == 1 ? events.first : events
@@ -1777,7 +1891,7 @@ module StateMachine
1777
1891
  graphvizVersion = Constants::RGV_VERSION.split('.')
1778
1892
  file = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
1779
1893
 
1780
- if graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
1894
+ if graphvizVersion[0] == '0' && graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
1781
1895
  outputOptions = {:output => options[:format], :file => file}
1782
1896
  else
1783
1897
  outputOptions = {options[:format] => file}
@@ -1785,8 +1899,8 @@ module StateMachine
1785
1899
 
1786
1900
  graph.output(outputOptions)
1787
1901
  graph
1788
- rescue LoadError
1789
- $stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` >= v0.9.0 and try again.'
1902
+ rescue LoadError => ex
1903
+ $stderr.puts "Cannot draw the machine (#{ex.message}). `gem install ruby-graphviz` >= v0.9.0 and try again."
1790
1904
  false
1791
1905
  end
1792
1906
  end