state_machine 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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