state_machines 0.0.1

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.idea/.name +1 -0
  4. data/.idea/.rakeTasks +7 -0
  5. data/.idea/cssxfire.xml +9 -0
  6. data/.idea/encodings.xml +5 -0
  7. data/.idea/misc.xml +5 -0
  8. data/.idea/modules.xml +12 -0
  9. data/.idea/scopes/scope_settings.xml +5 -0
  10. data/.idea/state_machine2.iml +34 -0
  11. data/.idea/vcs.xml +9 -0
  12. data/.idea/workspace.xml +1156 -0
  13. data/.rspec +3 -0
  14. data/.travis.yml +8 -0
  15. data/Gemfile +4 -0
  16. data/LICENSE.txt +23 -0
  17. data/README.md +29 -0
  18. data/Rakefile +1 -0
  19. data/lib/state_machines/assertions.rb +40 -0
  20. data/lib/state_machines/branch.rb +187 -0
  21. data/lib/state_machines/callback.rb +220 -0
  22. data/lib/state_machines/core.rb +25 -0
  23. data/lib/state_machines/core_ext/class/state_machine.rb +5 -0
  24. data/lib/state_machines/core_ext.rb +2 -0
  25. data/lib/state_machines/error.rb +13 -0
  26. data/lib/state_machines/eval_helpers.rb +87 -0
  27. data/lib/state_machines/event.rb +246 -0
  28. data/lib/state_machines/event_collection.rb +141 -0
  29. data/lib/state_machines/extensions.rb +148 -0
  30. data/lib/state_machines/helper_module.rb +17 -0
  31. data/lib/state_machines/integrations/base.rb +100 -0
  32. data/lib/state_machines/integrations.rb +113 -0
  33. data/lib/state_machines/machine.rb +2234 -0
  34. data/lib/state_machines/machine_collection.rb +84 -0
  35. data/lib/state_machines/macro_methods.rb +520 -0
  36. data/lib/state_machines/matcher.rb +123 -0
  37. data/lib/state_machines/matcher_helpers.rb +54 -0
  38. data/lib/state_machines/node_collection.rb +221 -0
  39. data/lib/state_machines/path.rb +120 -0
  40. data/lib/state_machines/path_collection.rb +90 -0
  41. data/lib/state_machines/state.rb +276 -0
  42. data/lib/state_machines/state_collection.rb +112 -0
  43. data/lib/state_machines/state_context.rb +138 -0
  44. data/lib/state_machines/transition.rb +470 -0
  45. data/lib/state_machines/transition_collection.rb +245 -0
  46. data/lib/state_machines/version.rb +3 -0
  47. data/lib/state_machines/yard.rb +8 -0
  48. data/lib/state_machines.rb +3 -0
  49. data/spec/errors/default_spec.rb +14 -0
  50. data/spec/errors/with_message_spec.rb +39 -0
  51. data/spec/helpers/helper_spec.rb +14 -0
  52. data/spec/internal/app/models/auto_shop.rb +31 -0
  53. data/spec/internal/app/models/car.rb +19 -0
  54. data/spec/internal/app/models/model_base.rb +6 -0
  55. data/spec/internal/app/models/motorcycle.rb +9 -0
  56. data/spec/internal/app/models/traffic_light.rb +47 -0
  57. data/spec/internal/app/models/vehicle.rb +123 -0
  58. data/spec/machine_spec.rb +3167 -0
  59. data/spec/matcher_helpers_spec.rb +39 -0
  60. data/spec/matcher_spec.rb +157 -0
  61. data/spec/models/auto_shop_spec.rb +41 -0
  62. data/spec/models/car_spec.rb +90 -0
  63. data/spec/models/motorcycle_spec.rb +44 -0
  64. data/spec/models/traffic_light_spec.rb +56 -0
  65. data/spec/models/vehicle_spec.rb +580 -0
  66. data/spec/node_collection_spec.rb +371 -0
  67. data/spec/path_collection_spec.rb +271 -0
  68. data/spec/path_spec.rb +488 -0
  69. data/spec/spec_helper.rb +6 -0
  70. data/spec/state_collection_spec.rb +352 -0
  71. data/spec/state_context_spec.rb +442 -0
  72. data/spec/state_machine_spec.rb +29 -0
  73. data/spec/state_spec.rb +970 -0
  74. data/spec/support/migration_helpers.rb +50 -0
  75. data/spec/support/models.rb +6 -0
  76. data/spec/transition_collection_spec.rb +2199 -0
  77. data/spec/transition_spec.rb +1558 -0
  78. data/state_machines.gemspec +23 -0
  79. metadata +194 -0
@@ -0,0 +1,2234 @@
1
+ module StateMachines
2
+ # Represents a state machine for a particular attribute. State machines
3
+ # consist of states, events and a set of transitions that define how the
4
+ # state changes after a particular event is fired.
5
+ #
6
+ # A state machine will not know all of the possible states for an object
7
+ # unless they are referenced *somewhere* in the state machine definition.
8
+ # As a result, any unused states should be defined with the +other_states+
9
+ # or +state+ helper.
10
+ #
11
+ # == Actions
12
+ #
13
+ # When an action is configured for a state machine, it is invoked when an
14
+ # object transitions via an event. The success of the event becomes
15
+ # dependent on the success of the action. If the action is successful, then
16
+ # the transitioned state remains persisted. However, if the action fails
17
+ # (by returning false), the transitioned state will be rolled back.
18
+ #
19
+ # For example,
20
+ #
21
+ # class Vehicle
22
+ # attr_accessor :fail, :saving_state
23
+ #
24
+ # state_machine :initial => :parked, :action => :save do
25
+ # event :ignite do
26
+ # transition :parked => :idling
27
+ # end
28
+ #
29
+ # event :park do
30
+ # transition :idling => :parked
31
+ # end
32
+ # end
33
+ #
34
+ # def save
35
+ # @saving_state = state
36
+ # fail != true
37
+ # end
38
+ # end
39
+ #
40
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
41
+ # vehicle.save # => true
42
+ # vehicle.saving_state # => "parked" # The state was "parked" was save was called
43
+ #
44
+ # # Successful event
45
+ # vehicle.ignite # => true
46
+ # vehicle.saving_state # => "idling" # The state was "idling" when save was called
47
+ # vehicle.state # => "idling"
48
+ #
49
+ # # Failed event
50
+ # vehicle.fail = true
51
+ # vehicle.park # => false
52
+ # vehicle.saving_state # => "parked"
53
+ # vehicle.state # => "idling"
54
+ #
55
+ # As shown, even though the state is set prior to calling the +save+ action
56
+ # on the object, it will be rolled back to the original state if the action
57
+ # fails. *Note* that this will also be the case if an exception is raised
58
+ # while calling the action.
59
+ #
60
+ # === Indirect transitions
61
+ #
62
+ # In addition to the action being run as the _result_ of an event, the action
63
+ # can also be used to run events itself. For example, using the above as an
64
+ # example:
65
+ #
66
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
67
+ #
68
+ # vehicle.state_event = 'ignite'
69
+ # vehicle.save # => true
70
+ # vehicle.state # => "idling"
71
+ # vehicle.state_event # => nil
72
+ #
73
+ # As can be seen, the +save+ action automatically invokes the event stored in
74
+ # the +state_event+ attribute (<tt>:ignite</tt> in this case).
75
+ #
76
+ # One important note about using this technique for running transitions is
77
+ # that if the class in which the state machine is defined *also* defines the
78
+ # action being invoked (and not a superclass), then it must manually run the
79
+ # StateMachine hook that checks for event attributes.
80
+ #
81
+ # For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel,
82
+ # the default action (+save+) is already defined in a base class. As a result,
83
+ # when a state machine is defined in a model / resource, StateMachine can
84
+ # automatically hook into the +save+ action.
85
+ #
86
+ # On the other hand, the Vehicle class from above defined its own +save+
87
+ # method (and there is no +save+ method in its superclass). As a result, it
88
+ # must be modified like so:
89
+ #
90
+ # def save
91
+ # self.class.state_machines.transitions(self, :save).perform do
92
+ # @saving_state = state
93
+ # fail != true
94
+ # end
95
+ # end
96
+ #
97
+ # This will add in the functionality for firing the event stored in the
98
+ # +state_event+ attribute.
99
+ #
100
+ # == Callbacks
101
+ #
102
+ # Callbacks are supported for hooking before and after every possible
103
+ # transition in the machine. Each callback is invoked in the order in which
104
+ # it was defined. See StateMachines::Machine#before_transition and
105
+ # StateMachines::Machine#after_transition for documentation on how to define
106
+ # new callbacks.
107
+ #
108
+ # *Note* that callbacks only get executed within the context of an event. As
109
+ # a result, if a class has an initial state when it's created, any callbacks
110
+ # that would normally get executed when the object enters that state will
111
+ # *not* get triggered.
112
+ #
113
+ # For example,
114
+ #
115
+ # class Vehicle
116
+ # state_machine :initial => :parked do
117
+ # after_transition all => :parked do
118
+ # raise ArgumentError
119
+ # end
120
+ # ...
121
+ # end
122
+ # end
123
+ #
124
+ # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
125
+ # vehicle.save # => true (no exception raised)
126
+ #
127
+ # If you need callbacks to get triggered when an object is created, this
128
+ # should be done by one of the following techniques:
129
+ # * Use a <tt>before :create</tt> or equivalent hook:
130
+ #
131
+ # class Vehicle
132
+ # before :create, :track_initial_transition
133
+ #
134
+ # state_machine do
135
+ # ...
136
+ # end
137
+ # end
138
+ #
139
+ # * Set an initial state and use the correct event to create the
140
+ # object with the proper state, resulting in callbacks being triggered and
141
+ # the object getting persisted (note that the <tt>:pending</tt> state is
142
+ # actually stored as nil):
143
+ #
144
+ # class Vehicle
145
+ # state_machine :initial => :pending
146
+ # after_transition :pending => :parked, :do => :track_initial_transition
147
+ #
148
+ # event :park do
149
+ # transition :pending => :parked
150
+ # end
151
+ #
152
+ # state :pending, :value => nil
153
+ # end
154
+ # end
155
+ #
156
+ # vehicle = Vehicle.new
157
+ # vehicle.park
158
+ #
159
+ # * Use a default event attribute that will automatically trigger when the
160
+ # configured action gets run (note that the <tt>:pending</tt> state is
161
+ # actually stored as nil):
162
+ #
163
+ # class Vehicle < ActiveRecord::Base
164
+ # state_machine :initial => :pending
165
+ # after_transition :pending => :parked, :do => :track_initial_transition
166
+ #
167
+ # event :park do
168
+ # transition :pending => :parked
169
+ # end
170
+ #
171
+ # state :pending, :value => nil
172
+ # end
173
+ #
174
+ # def initialize(*)
175
+ # super
176
+ # self.state_event = 'park'
177
+ # end
178
+ # end
179
+ #
180
+ # vehicle = Vehicle.new
181
+ # vehicle.save
182
+ #
183
+ # === Canceling callbacks
184
+ #
185
+ # Callbacks can be canceled by throwing :halt at any point during the
186
+ # callback. For example,
187
+ #
188
+ # ...
189
+ # throw :halt
190
+ # ...
191
+ #
192
+ # If a +before+ callback halts the chain, the associated transition and all
193
+ # later callbacks are canceled. If an +after+ callback halts the chain,
194
+ # the later callbacks are canceled, but the transition is still successful.
195
+ #
196
+ # These same rules apply to +around+ callbacks with the exception that any
197
+ # +around+ callback that doesn't yield will essentially result in :halt being
198
+ # thrown. Any code executed after the yield will behave in the same way as
199
+ # +after+ callbacks.
200
+ #
201
+ # *Note* that if a +before+ callback fails and the bang version of an event
202
+ # was invoked, an exception will be raised instead of returning false. For
203
+ # example,
204
+ #
205
+ # class Vehicle
206
+ # state_machine :initial => :parked do
207
+ # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
208
+ # ...
209
+ # end
210
+ # end
211
+ #
212
+ # vehicle = Vehicle.new
213
+ # vehicle.park # => false
214
+ # vehicle.park! # => StateMachines::InvalidTransition: Cannot transition state via :park from "idling"
215
+ #
216
+ # == Observers
217
+ #
218
+ # Observers, in the sense of external classes and *not* Ruby's Observable
219
+ # mechanism, can hook into state machines as well. Such observers use the
220
+ # same callback api that's used internally.
221
+ #
222
+ # Below are examples of defining observers for the following state machine:
223
+ #
224
+ # class Vehicle
225
+ # state_machine do
226
+ # event :park do
227
+ # transition :idling => :parked
228
+ # end
229
+ # ...
230
+ # end
231
+ # ...
232
+ # end
233
+ #
234
+ # Event/Transition behaviors:
235
+ #
236
+ # class VehicleObserver
237
+ # def self.before_park(vehicle, transition)
238
+ # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
239
+ # end
240
+ #
241
+ # def self.after_park(vehicle, transition, result)
242
+ # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
243
+ # end
244
+ #
245
+ # def self.before_transition(vehicle, transition)
246
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
247
+ # end
248
+ #
249
+ # def self.after_transition(vehicle, transition)
250
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
251
+ # end
252
+ #
253
+ # def self.around_transition(vehicle, transition)
254
+ # logger.info Benchmark.measure { yield }
255
+ # end
256
+ # end
257
+ #
258
+ # Vehicle.state_machine do
259
+ # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
260
+ # before_transition VehicleObserver.method(:before_transition)
261
+ #
262
+ # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
263
+ # after_transition VehicleObserver.method(:after_transition)
264
+ #
265
+ # around_transition VehicleObserver.method(:around_transition)
266
+ # end
267
+ #
268
+ # One common callback is to record transitions for all models in the system
269
+ # for auditing/debugging purposes. Below is an example of an observer that
270
+ # can easily automate this process for all models:
271
+ #
272
+ # class StateMachineObserver
273
+ # def self.before_transition(object, transition)
274
+ # Audit.log_transition(object.attributes)
275
+ # end
276
+ # end
277
+ #
278
+ # [Vehicle, Switch, Project].each do |klass|
279
+ # klass.state_machines.each do |attribute, machine|
280
+ # machine.before_transition StateMachineObserver.method(:before_transition)
281
+ # end
282
+ # end
283
+ #
284
+ # Additional observer-like behavior may be exposed by the various integrations
285
+ # available. See below for more information on integrations.
286
+ #
287
+ # == Overriding instance / class methods
288
+ #
289
+ # Hooking in behavior to the generated instance / class methods from the
290
+ # state machine, events, and states is very simple because of the way these
291
+ # methods are generated on the class. Using the class's ancestors, the
292
+ # original generated method can be referred to via +super+. For example,
293
+ #
294
+ # class Vehicle
295
+ # state_machine do
296
+ # event :park do
297
+ # ...
298
+ # end
299
+ # end
300
+ #
301
+ # def park(*args)
302
+ # logger.info "..."
303
+ # super
304
+ # end
305
+ # end
306
+ #
307
+ # In the above example, the +park+ instance method that's generated on the
308
+ # Vehicle class (by the associated event) is overridden with custom behavior.
309
+ # Once this behavior is complete, the original method from the state machine
310
+ # is invoked by simply calling +super+.
311
+ #
312
+ # The same technique can be used for +state+, +state_name+, and all other
313
+ # instance *and* class methods on the Vehicle class.
314
+ #
315
+ # == Method conflicts
316
+ #
317
+ # By default state_machine does not redefine methods that exist on
318
+ # superclasses (*including* Object) or any modules (*including* Kernel) that
319
+ # were included before it was defined. This is in order to ensure that
320
+ # existing behavior on the class is not broken by the inclusion of
321
+ # state_machine.
322
+ #
323
+ # If a conflicting method is detected, state_machine will generate a warning.
324
+ # For example, consider the following class:
325
+ #
326
+ # class Vehicle
327
+ # state_machine do
328
+ # event :open do
329
+ # ...
330
+ # end
331
+ # end
332
+ # end
333
+ #
334
+ # In the above class, an event named "open" is defined for its state machine.
335
+ # However, "open" is already defined as an instance method in Ruby's Kernel
336
+ # module that gets included in every Object. As a result, state_machine will
337
+ # generate the following warning:
338
+ #
339
+ # Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.
340
+ #
341
+ # Even though you may not be using Kernel's implementation of the "open"
342
+ # instance method, state_machine isn't aware of this and, as a result, stays
343
+ # safe and just skips redefining the method.
344
+ #
345
+ # As with almost all helpers methods defined by state_machine in your class,
346
+ # there are generic methods available for working around this method conflict.
347
+ # In the example above, you can invoke the "open" event like so:
348
+ #
349
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
350
+ # vehicle.fire_events(:open) # => true
351
+ #
352
+ # # This will not work
353
+ # vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
354
+ #
355
+ # If you want to take on the risk of overriding existing methods and just
356
+ # ignore method conflicts altogether, you can do so by setting the following
357
+ # configuration:
358
+ #
359
+ # StateMachines::Machine.ignore_method_conflicts = true
360
+ #
361
+ # This will allow you to define events like "open" as described above and
362
+ # still generate the "open" instance helper method. For example:
363
+ #
364
+ # StateMachines::Machine.ignore_method_conflicts = true
365
+ #
366
+ # class Vehicle
367
+ # state_machine do
368
+ # event :open do
369
+ # ...
370
+ # end
371
+ # end
372
+ #
373
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
374
+ # vehicle.open # => true
375
+ #
376
+ # By default, state_machine helps prevent you from making mistakes and
377
+ # accidentally overriding methods that you didn't intend to. Once you
378
+ # understand this and what the consequences are, setting the
379
+ # +ignore_method_conflicts+ option is a perfectly reasonable workaround.
380
+ #
381
+ # == Integrations
382
+ #
383
+ # By default, state machines are library-agnostic, meaning that they work
384
+ # on any Ruby class and have no external dependencies. However, there are
385
+ # certain libraries which expose additional behavior that can be taken
386
+ # advantage of by state machines.
387
+ #
388
+ # This library is built to work out of the box with a few popular Ruby
389
+ # libraries that allow for additional behavior to provide a cleaner and
390
+ # smoother experience. This is especially the case for objects backed by a
391
+ # database that may allow for transactions, persistent storage,
392
+ # search/filters, callbacks, etc.
393
+ #
394
+ # When a state machine is defined for classes using any of the above libraries,
395
+ # it will try to automatically determine the integration to use (Agnostic,
396
+ # ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel)
397
+ # based on the class definition. To see how each integration affects the
398
+ # machine's behavior, refer to all constants defined under the
399
+ # StateMachines::Integrations namespace.
400
+ class Machine
401
+
402
+ include EvalHelpers
403
+ include MatcherHelpers
404
+
405
+ class << self
406
+ # Attempts to find or create a state machine for the given class. For
407
+ # example,
408
+ #
409
+ # StateMachines::Machine.find_or_create(Vehicle)
410
+ # StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
411
+ # StateMachines::Machine.find_or_create(Vehicle, :status)
412
+ # StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
413
+ #
414
+ # If a machine of the given name already exists in one of the class's
415
+ # superclasses, then a copy of that machine will be created and stored
416
+ # in the new owner class (the original will remain unchanged).
417
+ def find_or_create(owner_class, *args, &block)
418
+ options = args.last.is_a?(Hash) ? args.pop : {}
419
+ name = args.first || :state
420
+
421
+ # Find an existing machine
422
+ if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
423
+ # Only create a new copy if changes are being made to the machine in
424
+ # a subclass
425
+ if machine.owner_class != owner_class && (options.any? || block_given?)
426
+ machine = machine.clone
427
+ machine.initial_state = options[:initial] if options.include?(:initial)
428
+ machine.owner_class = owner_class
429
+ end
430
+
431
+ # Evaluate DSL
432
+ machine.instance_eval(&block) if block_given?
433
+ else
434
+ # No existing machine: create a new one
435
+ machine = new(owner_class, name, options, &block)
436
+ end
437
+
438
+ machine
439
+ end
440
+
441
+
442
+ def draw(class_names, options = {})
443
+ fail NotImplementedError
444
+ end
445
+ end
446
+
447
+ # Default messages to use for validation errors in ORM integrations
448
+ class << self;
449
+ attr_accessor :default_messages;
450
+ end
451
+ @default_messages = {
452
+ :invalid => 'is invalid',
453
+ :invalid_event => 'cannot transition when %s',
454
+ :invalid_transition => 'cannot transition via "%1$s"'
455
+ }
456
+
457
+ # Whether to ignore any conflicts that are detected for helper methods that
458
+ # get generated for a machine's owner class. Default is false.
459
+ class << self;
460
+ attr_accessor :ignore_method_conflicts;
461
+ end
462
+ @ignore_method_conflicts = false
463
+
464
+ # The class that the machine is defined in
465
+ attr_reader :owner_class
466
+
467
+ # The name of the machine, used for scoping methods generated for the
468
+ # machine as a whole (not states or events)
469
+ attr_reader :name
470
+
471
+ # The events that trigger transitions. These are sorted, by default, in
472
+ # the order in which they were defined.
473
+ attr_reader :events
474
+
475
+ # A list of all of the states known to this state machine. This will pull
476
+ # states from the following sources:
477
+ # * Initial state
478
+ # * State behaviors
479
+ # * Event transitions (:to, :from, and :except_from options)
480
+ # * Transition callbacks (:to, :from, :except_to, and :except_from options)
481
+ # * Unreferenced states (using +other_states+ helper)
482
+ #
483
+ # These are sorted, by default, in the order in which they were referenced.
484
+ attr_reader :states
485
+
486
+ # The callbacks to invoke before/after a transition is performed
487
+ #
488
+ # Maps :before => callbacks and :after => callbacks
489
+ attr_reader :callbacks
490
+
491
+ # The action to invoke when an object transitions
492
+ attr_reader :action
493
+
494
+ # An identifier that forces all methods (including state predicates and
495
+ # event methods) to be generated with the value prefixed or suffixed,
496
+ # depending on the context.
497
+ attr_reader :namespace
498
+
499
+ # Whether the machine will use transactions when firing events
500
+ attr_reader :use_transactions
501
+
502
+ # Creates a new state machine for the given attribute
503
+ def initialize(owner_class, *args, &block)
504
+ options = args.last.is_a?(Hash) ? args.pop : {}
505
+ options.assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
506
+
507
+ # Find an integration that matches this machine's owner class
508
+ if options.include?(:integration)
509
+ @integration = options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
510
+ else
511
+ @integration = StateMachines::Integrations.match(owner_class)
512
+ end
513
+
514
+ if @integration
515
+ extend @integration
516
+ options = (@integration.defaults || {}).merge(options)
517
+ end
518
+
519
+ # Add machine-wide defaults
520
+ options = {:use_transactions => true, :initialize => true}.merge(options)
521
+
522
+ # Set machine configuration
523
+ @name = args.first || :state
524
+ @attribute = options[:attribute] || @name
525
+ @events = EventCollection.new(self)
526
+ @states = StateCollection.new(self)
527
+ @callbacks = {:before => [], :after => [], :failure => []}
528
+ @namespace = options[:namespace]
529
+ @messages = options[:messages] || {}
530
+ @action = options[:action]
531
+ @use_transactions = options[:use_transactions]
532
+ @initialize_state = options[:initialize]
533
+ @action_hook_defined = false
534
+ self.owner_class = owner_class
535
+
536
+ # Merge with sibling machine configurations
537
+ add_sibling_machine_configs
538
+
539
+ # Define class integration
540
+ define_helpers
541
+ define_scopes(options[:plural])
542
+ after_initialize
543
+
544
+ # Evaluate DSL
545
+ instance_eval(&block) if block_given?
546
+ self.initial_state = options[:initial] unless sibling_machines.any?
547
+ end
548
+
549
+ # Creates a copy of this machine in addition to copies of each associated
550
+ # event/states/callback, so that the modifications to those collections do
551
+ # not affect the original machine.
552
+ def initialize_copy(orig) #:nodoc:
553
+ super
554
+
555
+ @events = @events.dup
556
+ @events.machine = self
557
+ @states = @states.dup
558
+ @states.machine = self
559
+ @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
560
+ end
561
+
562
+ # Sets the class which is the owner of this state machine. Any methods
563
+ # generated by states, events, or other parts of the machine will be defined
564
+ # on the given owner class.
565
+ def owner_class=(klass)
566
+ @owner_class = klass
567
+
568
+ # Create modules for extending the class with state/event-specific methods
569
+ @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
570
+ owner_class.class_eval do
571
+ extend helper_modules[:class]
572
+ include helper_modules[:instance]
573
+ end
574
+
575
+ # Add class-/instance-level methods to the owner class for state initialization
576
+ unless owner_class < StateMachines::InstanceMethods
577
+ owner_class.class_eval do
578
+ extend StateMachines::ClassMethods
579
+ include StateMachines::InstanceMethods
580
+ end
581
+
582
+ define_state_initializer if @initialize_state
583
+ end
584
+
585
+ # Record this machine as matched to the name in the current owner class.
586
+ # This will override any machines mapped to the same name in any superclasses.
587
+ owner_class.state_machines[name] = self
588
+ end
589
+
590
+ # Sets the initial state of the machine. This can be either the static name
591
+ # of a state or a lambda block which determines the initial state at
592
+ # creation time.
593
+ def initial_state=(new_initial_state)
594
+ @initial_state = new_initial_state
595
+ add_states([@initial_state]) unless dynamic_initial_state?
596
+
597
+ # Update all states to reflect the new initial state
598
+ states.each { |state| state.initial = (state.name == @initial_state) }
599
+
600
+ # Output a warning if there are conflicting initial states for the machine's
601
+ # attribute
602
+ initial_state = states.detect { |state| state.initial }
603
+ if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
604
+ warn(
605
+ "Both #{owner_class.name} and its #{name.inspect} machine have defined "\
606
+ "a different default for \"#{attribute}\". Use only one or the other for "\
607
+ "defining defaults to avoid unexpected behaviors."
608
+ )
609
+ end
610
+ end
611
+
612
+ # Gets the initial state of the machine for the given object. If a dynamic
613
+ # initial state was configured for this machine, then the object will be
614
+ # passed into the lambda block to help determine the actual state.
615
+ #
616
+ # == Examples
617
+ #
618
+ # With a static initial state:
619
+ #
620
+ # class Vehicle
621
+ # state_machine :initial => :parked do
622
+ # ...
623
+ # end
624
+ # end
625
+ #
626
+ # vehicle = Vehicle.new
627
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
628
+ #
629
+ # With a dynamic initial state:
630
+ #
631
+ # class Vehicle
632
+ # attr_accessor :force_idle
633
+ #
634
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
635
+ # ...
636
+ # end
637
+ # end
638
+ #
639
+ # vehicle = Vehicle.new
640
+ #
641
+ # vehicle.force_idle = true
642
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=false>
643
+ #
644
+ # vehicle.force_idle = false
645
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
646
+ def initial_state(object)
647
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
648
+ end
649
+
650
+ # Whether a dynamic initial state is being used in the machine
651
+ def dynamic_initial_state?
652
+ instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
653
+ end
654
+
655
+ # Initializes the state on the given object. Initial values are only set if
656
+ # the machine's attribute hasn't been previously initialized.
657
+ #
658
+ # Configuration options:
659
+ # * <tt>:force</tt> - Whether to initialize the state regardless of its
660
+ # current value
661
+ # * <tt>:to</tt> - A hash to set the initial value in instead of writing
662
+ # directly to the object
663
+ def initialize_state(object, options = {})
664
+ state = initial_state(object)
665
+ if state && (options[:force] || initialize_state?(object))
666
+ value = state.value
667
+
668
+ if hash = options[:to]
669
+ hash[attribute.to_s] = value
670
+ else
671
+ write(object, :state, value)
672
+ end
673
+ end
674
+ end
675
+
676
+ # Gets the actual name of the attribute on the machine's owner class that
677
+ # stores data with the given name.
678
+ def attribute(name = :state)
679
+ name == :state ? @attribute : :"#{self.name}_#{name}"
680
+ end
681
+
682
+ # Defines a new helper method in an instance or class scope with the given
683
+ # name. If the method is already defined in the scope, then this will not
684
+ # override it.
685
+ #
686
+ # If passing in a block, there are two side effects to be aware of
687
+ # 1. The method cannot be chained, meaning that the block cannot call +super+
688
+ # 2. If the method is already defined in an ancestor, then it will not get
689
+ # overridden and a warning will be output.
690
+ #
691
+ # Example:
692
+ #
693
+ # # Instance helper
694
+ # machine.define_helper(:instance, :state_name) do |machine, object|
695
+ # machine.states.match(object).name
696
+ # end
697
+ #
698
+ # # Class helper
699
+ # machine.define_helper(:class, :state_machine_name) do |machine, klass|
700
+ # "State"
701
+ # end
702
+ #
703
+ # You can also define helpers using string evaluation like so:
704
+ #
705
+ # # Instance helper
706
+ # machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
707
+ # def state_name
708
+ # self.class.state_machine(:state).states.match(self).name
709
+ # end
710
+ # end_eval
711
+ #
712
+ # # Class helper
713
+ # machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
714
+ # def state_machine_name
715
+ # "State"
716
+ # end
717
+ # end_eval
718
+ def define_helper(scope, method, *args, &block)
719
+ helper_module = @helper_modules.fetch(scope)
720
+
721
+ if block_given?
722
+ if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
723
+ ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
724
+ warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
725
+ else
726
+ name = self.name
727
+ helper_module.class_eval do
728
+ define_method(method) do |*block_args|
729
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args)
730
+ end
731
+ end
732
+ end
733
+ else
734
+ helper_module.class_eval(method, *args)
735
+ end
736
+ end
737
+
738
+ # Customizes the definition of one or more states in the machine.
739
+ #
740
+ # Configuration options:
741
+ # * <tt>:value</tt> - The actual value to store when an object transitions
742
+ # to the state. Default is the name (stringified).
743
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
744
+ # then setting this to true will cache the evaluated result
745
+ # * <tt>:if</tt> - Determines whether an object's value matches the state
746
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
747
+ # By default, the configured value is matched.
748
+ # * <tt>:human_name</tt> - The human-readable version of this state's name.
749
+ # By default, this is either defined by the integration or stringifies the
750
+ # name and converts underscores to spaces.
751
+ #
752
+ # == Customizing the stored value
753
+ #
754
+ # Whenever a state is automatically discovered in the state machine, its
755
+ # default value is assumed to be the stringified version of the name. For
756
+ # example,
757
+ #
758
+ # class Vehicle
759
+ # state_machine :initial => :parked do
760
+ # event :ignite do
761
+ # transition :parked => :idling
762
+ # end
763
+ # end
764
+ # end
765
+ #
766
+ # In the above state machine, there are two states automatically discovered:
767
+ # :parked and :idling. These states, by default, will store their stringified
768
+ # equivalents when an object moves into that state (e.g. "parked" / "idling").
769
+ #
770
+ # For legacy systems or when tying state machines into existing frameworks,
771
+ # it's oftentimes necessary to need to store a different value for a state
772
+ # than the default. In order to continue taking advantage of an expressive
773
+ # state machine and helper methods, every defined state can be re-configured
774
+ # with a custom stored value. For example,
775
+ #
776
+ # class Vehicle
777
+ # state_machine :initial => :parked do
778
+ # event :ignite do
779
+ # transition :parked => :idling
780
+ # end
781
+ #
782
+ # state :idling, :value => 'IDLING'
783
+ # state :parked, :value => 'PARKED
784
+ # end
785
+ # end
786
+ #
787
+ # This is also useful if being used in association with a database and,
788
+ # instead of storing the state name in a column, you want to store the
789
+ # state's foreign key:
790
+ #
791
+ # class VehicleState < ActiveRecord::Base
792
+ # end
793
+ #
794
+ # class Vehicle < ActiveRecord::Base
795
+ # state_machine :attribute => :state_id, :initial => :parked do
796
+ # event :ignite do
797
+ # transition :parked => :idling
798
+ # end
799
+ #
800
+ # states.each do |state|
801
+ # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
802
+ # end
803
+ # end
804
+ # end
805
+ #
806
+ # In the above example, each known state is configured to store it's
807
+ # associated database id in the +state_id+ attribute. Also, notice that a
808
+ # lambda block is used to define the state's value. This is required in
809
+ # situations (like testing) where the model is loaded without any existing
810
+ # data (i.e. no VehicleState records available).
811
+ #
812
+ # One caveat to the above example is to keep performance in mind. To avoid
813
+ # constant db hits for looking up the VehicleState ids, the value is cached
814
+ # by specifying the <tt>:cache</tt> option. Alternatively, a custom
815
+ # caching strategy can be used like so:
816
+ #
817
+ # class VehicleState < ActiveRecord::Base
818
+ # cattr_accessor :cache_store
819
+ # self.cache_store = ActiveSupport::Cache::MemoryStore.new
820
+ #
821
+ # def self.find_by_name(name)
822
+ # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
823
+ # end
824
+ # end
825
+ #
826
+ # === Dynamic values
827
+ #
828
+ # In addition to customizing states with other value types, lambda blocks
829
+ # can also be specified to allow for a state's value to be determined
830
+ # dynamically at runtime. For example,
831
+ #
832
+ # class Vehicle
833
+ # state_machine :purchased_at, :initial => :available do
834
+ # event :purchase do
835
+ # transition all => :purchased
836
+ # end
837
+ #
838
+ # event :restock do
839
+ # transition all => :available
840
+ # end
841
+ #
842
+ # state :available, :value => nil
843
+ # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
844
+ # end
845
+ # end
846
+ #
847
+ # In the above definition, the <tt>:purchased</tt> state is customized with
848
+ # both a dynamic value *and* a value matcher.
849
+ #
850
+ # When an object transitions to the purchased state, the value's lambda
851
+ # block will be called. This will get the current time and store it in the
852
+ # object's +purchased_at+ attribute.
853
+ #
854
+ # *Note* that the custom matcher is very important here. Since there's no
855
+ # way for the state machine to figure out an object's state when it's set to
856
+ # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
857
+ # were not configured for the state, then an ArgumentError exception would
858
+ # be raised at runtime, indicating that the state machine could not figure
859
+ # out what the current state of the object was.
860
+ #
861
+ # == Behaviors
862
+ #
863
+ # Behaviors define a series of methods to mixin with objects when the current
864
+ # state matches the given one(s). This allows instance methods to behave
865
+ # a specific way depending on what the value of the object's state is.
866
+ #
867
+ # For example,
868
+ #
869
+ # class Vehicle
870
+ # attr_accessor :driver
871
+ # attr_accessor :passenger
872
+ #
873
+ # state_machine :initial => :parked do
874
+ # event :ignite do
875
+ # transition :parked => :idling
876
+ # end
877
+ #
878
+ # state :parked do
879
+ # def speed
880
+ # 0
881
+ # end
882
+ #
883
+ # def rotate_driver
884
+ # driver = self.driver
885
+ # self.driver = passenger
886
+ # self.passenger = driver
887
+ # true
888
+ # end
889
+ # end
890
+ #
891
+ # state :idling, :first_gear do
892
+ # def speed
893
+ # 20
894
+ # end
895
+ #
896
+ # def rotate_driver
897
+ # self.state = 'parked'
898
+ # rotate_driver
899
+ # end
900
+ # end
901
+ #
902
+ # other_states :backing_up
903
+ # end
904
+ # end
905
+ #
906
+ # In the above example, there are two dynamic behaviors defined for the
907
+ # class:
908
+ # * +speed+
909
+ # * +rotate_driver+
910
+ #
911
+ # Each of these behaviors are instance methods on the Vehicle class. However,
912
+ # which method actually gets invoked is based on the current state of the
913
+ # object. Using the above class as the example:
914
+ #
915
+ # vehicle = Vehicle.new
916
+ # vehicle.driver = 'John'
917
+ # vehicle.passenger = 'Jane'
918
+ #
919
+ # # Behaviors in the "parked" state
920
+ # vehicle.state # => "parked"
921
+ # vehicle.speed # => 0
922
+ # vehicle.rotate_driver # => true
923
+ # vehicle.driver # => "Jane"
924
+ # vehicle.passenger # => "John"
925
+ #
926
+ # vehicle.ignite # => true
927
+ #
928
+ # # Behaviors in the "idling" state
929
+ # vehicle.state # => "idling"
930
+ # vehicle.speed # => 20
931
+ # vehicle.rotate_driver # => true
932
+ # vehicle.driver # => "John"
933
+ # vehicle.passenger # => "Jane"
934
+ #
935
+ # As can be seen, both the +speed+ and +rotate_driver+ instance method
936
+ # implementations changed how they behave based on what the current state
937
+ # of the vehicle was.
938
+ #
939
+ # === Invalid behaviors
940
+ #
941
+ # If a specific behavior has not been defined for a state, then a
942
+ # NoMethodError exception will be raised, indicating that that method would
943
+ # not normally exist for an object with that state.
944
+ #
945
+ # Using the example from before:
946
+ #
947
+ # vehicle = Vehicle.new
948
+ # vehicle.state = 'backing_up'
949
+ # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
950
+ #
951
+ # === Using matchers
952
+ #
953
+ # The +all+ / +any+ matchers can be used to easily define behaviors for a
954
+ # group of states. Note, however, that you cannot use these matchers to
955
+ # set configurations for states. Behaviors using these matchers can be
956
+ # defined at any point in the state machine and will always get applied to
957
+ # the proper states.
958
+ #
959
+ # For example:
960
+ #
961
+ # state_machine :initial => :parked do
962
+ # ...
963
+ #
964
+ # state all - [:parked, :idling, :stalled] do
965
+ # validates_presence_of :speed
966
+ #
967
+ # def speed
968
+ # gear * 10
969
+ # end
970
+ # end
971
+ # end
972
+ #
973
+ # == State-aware class methods
974
+ #
975
+ # In addition to defining scopes for instance methods that are state-aware,
976
+ # the same can be done for certain types of class methods.
977
+ #
978
+ # Some libraries have support for class-level methods that only run certain
979
+ # behaviors based on a conditions hash passed in. For example:
980
+ #
981
+ # class Vehicle < ActiveRecord::Base
982
+ # state_machine do
983
+ # ...
984
+ # state :first_gear, :second_gear, :third_gear do
985
+ # validates_presence_of :speed
986
+ # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
987
+ # end
988
+ # end
989
+ # end
990
+ #
991
+ # In the above ActiveRecord model, two validations have been defined which
992
+ # will *only* run when the Vehicle object is in one of the three states:
993
+ # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
994
+ # conditions can continue to be used.
995
+ #
996
+ # This functionality is not library-specific and can work for any class-level
997
+ # method that is defined like so:
998
+ #
999
+ # def validates_presence_of(attribute, options = {})
1000
+ # ...
1001
+ # end
1002
+ #
1003
+ # The minimum requirement is that the last argument in the method be an
1004
+ # options hash which contains at least <tt>:if</tt> condition support.
1005
+ def state(*names, &block)
1006
+ options = names.last.is_a?(Hash) ? names.pop : {}
1007
+ options.assert_valid_keys(:value, :cache, :if, :human_name)
1008
+
1009
+ # Store the context so that it can be used for / matched against any state
1010
+ # that gets added
1011
+ @states.context(names, &block) if block_given?
1012
+
1013
+ if names.first.is_a?(Matcher)
1014
+ # Add any states referenced in the matcher. When matchers are used,
1015
+ # states are not allowed to be configured.
1016
+ raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
1017
+ states = add_states(names.first.values)
1018
+ else
1019
+ states = add_states(names)
1020
+
1021
+ # Update the configuration for the state(s)
1022
+ states.each do |state|
1023
+ if options.include?(:value)
1024
+ state.value = options[:value]
1025
+ self.states.update(state)
1026
+ end
1027
+
1028
+ state.human_name = options[:human_name] if options.include?(:human_name)
1029
+ state.cache = options[:cache] if options.include?(:cache)
1030
+ state.matcher = options[:if] if options.include?(:if)
1031
+ end
1032
+ end
1033
+
1034
+ states.length == 1 ? states.first : states
1035
+ end
1036
+
1037
+ alias_method :other_states, :state
1038
+
1039
+ # Gets the current value stored in the given object's attribute.
1040
+ #
1041
+ # For example,
1042
+ #
1043
+ # class Vehicle
1044
+ # state_machine :initial => :parked do
1045
+ # ...
1046
+ # end
1047
+ # end
1048
+ #
1049
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1050
+ # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
1051
+ # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
1052
+ def read(object, attribute, ivar = false)
1053
+ attribute = self.attribute(attribute)
1054
+ if ivar
1055
+ object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1056
+ else
1057
+ object.send(attribute)
1058
+ end
1059
+ end
1060
+
1061
+ # Sets a new value in the given object's attribute.
1062
+ #
1063
+ # For example,
1064
+ #
1065
+ # class Vehicle
1066
+ # state_machine :initial => :parked do
1067
+ # ...
1068
+ # end
1069
+ # end
1070
+ #
1071
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1072
+ # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
1073
+ # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
1074
+ # vehicle.state # => "idling"
1075
+ # vehicle.event # => "park"
1076
+ def write(object, attribute, value, ivar = false)
1077
+ attribute = self.attribute(attribute)
1078
+ ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1079
+ end
1080
+
1081
+ # Defines one or more events for the machine and the transitions that can
1082
+ # be performed when those events are run.
1083
+ #
1084
+ # This method is also aliased as +on+ for improved compatibility with
1085
+ # using a domain-specific language.
1086
+ #
1087
+ # Configuration options:
1088
+ # * <tt>:human_name</tt> - The human-readable version of this event's name.
1089
+ # By default, this is either defined by the integration or stringifies the
1090
+ # name and converts underscores to spaces.
1091
+ #
1092
+ # == Instance methods
1093
+ #
1094
+ # The following instance methods are generated when a new event is defined
1095
+ # (the "park" event is used as an example):
1096
+ # * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
1097
+ # transitioning from the current state to the next valid state. If the
1098
+ # last argument is a boolean, it will control whether the machine's action
1099
+ # gets run.
1100
+ # * <tt>park!(..., run_action = true)</tt> - Fires the "park" event,
1101
+ # transitioning from the current state to the next valid state. If the
1102
+ # transition fails, then a StateMachines::InvalidTransition error will be
1103
+ # raised. If the last argument is a boolean, it will control whether the
1104
+ # machine's action gets run.
1105
+ # * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event
1106
+ # can be fired given the current state of the object. This will *not* run
1107
+ # validations or callbacks in ORM integrations. It will only determine if
1108
+ # the state machine defines a valid transition for the event. To check
1109
+ # whether an event can fire *and* passes validations, use event attributes
1110
+ # (e.g. state_event) as described in the "Events" documentation of each
1111
+ # ORM integration.
1112
+ # * <tt>park_transition(requirements = {})</tt> - Gets the next transition
1113
+ # that would be performed if the "park" event were to be fired now on the
1114
+ # object or nil if no transitions can be performed. Like <tt>can_park?</tt>
1115
+ # this will also *not* run validations or callbacks. It will only
1116
+ # determine if the state machine defines a valid transition for the event.
1117
+ #
1118
+ # With a namespace of "car", the above names map to the following methods:
1119
+ # * <tt>can_park_car?</tt>
1120
+ # * <tt>park_car_transition</tt>
1121
+ # * <tt>park_car</tt>
1122
+ # * <tt>park_car!</tt>
1123
+ #
1124
+ # The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
1125
+ # optional set of requirements for determining what transitions are available
1126
+ # for the current object. These requirements include:
1127
+ # * <tt>:from</tt> - One or more states to transition from. If none are
1128
+ # specified, then this will be the object's current state.
1129
+ # * <tt>:to</tt> - One or more states to transition to. If none are
1130
+ # specified, then this will match any to state.
1131
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
1132
+ # conditionals defined for each one. Default is true.
1133
+ #
1134
+ # == Defining transitions
1135
+ #
1136
+ # +event+ requires a block which allows you to define the possible
1137
+ # transitions that can happen as a result of that event. For example,
1138
+ #
1139
+ # event :park, :stop do
1140
+ # transition :idling => :parked
1141
+ # end
1142
+ #
1143
+ # event :first_gear do
1144
+ # transition :parked => :first_gear, :if => :seatbelt_on?
1145
+ # transition :parked => same # Allow to loopback if seatbelt is off
1146
+ # end
1147
+ #
1148
+ # See StateMachines::Event#transition for more information on
1149
+ # the possible options that can be passed in.
1150
+ #
1151
+ # *Note* that this block is executed within the context of the actual event
1152
+ # object. As a result, you will not be able to reference any class methods
1153
+ # on the model without referencing the class itself. For example,
1154
+ #
1155
+ # class Vehicle
1156
+ # def self.safe_states
1157
+ # [:parked, :idling, :stalled]
1158
+ # end
1159
+ #
1160
+ # state_machine do
1161
+ # event :park do
1162
+ # transition Vehicle.safe_states => :parked
1163
+ # end
1164
+ # end
1165
+ # end
1166
+ #
1167
+ # == Overriding the event method
1168
+ #
1169
+ # By default, this will define an instance method (with the same name as the
1170
+ # event) that will fire the next possible transition for that. Although the
1171
+ # +before_transition+, +after_transition+, and +around_transition+ hooks
1172
+ # allow you to define behavior that gets executed as a result of the event's
1173
+ # transition, you can also override the event method in order to have a
1174
+ # little more fine-grained control.
1175
+ #
1176
+ # For example:
1177
+ #
1178
+ # class Vehicle
1179
+ # state_machine do
1180
+ # event :park do
1181
+ # ...
1182
+ # end
1183
+ # end
1184
+ #
1185
+ # def park(*)
1186
+ # take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
1187
+ # if result = super # Runs the transition and all before/after/around hooks
1188
+ # applaud # Executes after the transition (and after_transition hooks)
1189
+ # end
1190
+ # result
1191
+ # end
1192
+ # end
1193
+ #
1194
+ # There are a few important things to note here. First, the method
1195
+ # signature is defined with an unlimited argument list in order to allow
1196
+ # callers to continue passing arguments that are expected by state_machine.
1197
+ # For example, it will still allow calls to +park+ with a single parameter
1198
+ # for skipping the configured action.
1199
+ #
1200
+ # Second, the overridden event method must call +super+ in order to run the
1201
+ # logic for running the next possible transition. In order to remain
1202
+ # consistent with other events, the result of +super+ is returned.
1203
+ #
1204
+ # Third, any behavior defined in this method will *not* get executed if
1205
+ # you're taking advantage of attribute-based event transitions. For example:
1206
+ #
1207
+ # vehicle = Vehicle.new
1208
+ # vehicle.state_event = 'park'
1209
+ # vehicle.save
1210
+ #
1211
+ # In this case, the +park+ event will run the before/after/around transition
1212
+ # hooks and transition the state, but the behavior defined in the overriden
1213
+ # +park+ method will *not* be executed.
1214
+ #
1215
+ # == Defining additional arguments
1216
+ #
1217
+ # Additional arguments can be passed into events and accessed by transition
1218
+ # hooks like so:
1219
+ #
1220
+ # class Vehicle
1221
+ # state_machine do
1222
+ # after_transition :on => :park do |vehicle, transition|
1223
+ # kind = *transition.args # :parallel
1224
+ # ...
1225
+ # end
1226
+ # after_transition :on => :park, :do => :take_deep_breath
1227
+ #
1228
+ # event :park do
1229
+ # ...
1230
+ # end
1231
+ #
1232
+ # def take_deep_breath(transition)
1233
+ # kind = *transition.args # :parallel
1234
+ # ...
1235
+ # end
1236
+ # end
1237
+ # end
1238
+ #
1239
+ # vehicle = Vehicle.new
1240
+ # vehicle.park(:parallel)
1241
+ #
1242
+ # *Remember* that if the last argument is a boolean, it will be used as the
1243
+ # +run_action+ parameter to the event action. Using the +park+ action
1244
+ # example from above, you can might call it like so:
1245
+ #
1246
+ # vehicle.park # => Uses default args and runs machine action
1247
+ # vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
1248
+ # vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
1249
+ #
1250
+ # If you decide to override the +park+ event method *and* define additional
1251
+ # arguments, you can do so as shown below:
1252
+ #
1253
+ # class Vehicle
1254
+ # state_machine do
1255
+ # event :park do
1256
+ # ...
1257
+ # end
1258
+ # end
1259
+ #
1260
+ # def park(kind = :parallel, *args)
1261
+ # take_deep_breath if kind == :parallel
1262
+ # super
1263
+ # end
1264
+ # end
1265
+ #
1266
+ # Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
1267
+ # the entire arguments list to be accessed by transition callbacks through
1268
+ # StateMachines::Transition#args.
1269
+ #
1270
+ # === Using matchers
1271
+ #
1272
+ # The +all+ / +any+ matchers can be used to easily execute blocks for a
1273
+ # group of events. Note, however, that you cannot use these matchers to
1274
+ # set configurations for events. Blocks using these matchers can be
1275
+ # defined at any point in the state machine and will always get applied to
1276
+ # the proper events.
1277
+ #
1278
+ # For example:
1279
+ #
1280
+ # state_machine :initial => :parked do
1281
+ # ...
1282
+ #
1283
+ # event all - [:crash] do
1284
+ # transition :stalled => :parked
1285
+ # end
1286
+ # end
1287
+ #
1288
+ # == Example
1289
+ #
1290
+ # class Vehicle
1291
+ # state_machine do
1292
+ # # The park, stop, and halt events will all share the given transitions
1293
+ # event :park, :stop, :halt do
1294
+ # transition [:idling, :backing_up] => :parked
1295
+ # end
1296
+ #
1297
+ # event :stop do
1298
+ # transition :first_gear => :idling
1299
+ # end
1300
+ #
1301
+ # event :ignite do
1302
+ # transition :parked => :idling
1303
+ # transition :idling => same # Allow ignite while still idling
1304
+ # end
1305
+ # end
1306
+ # end
1307
+ def event(*names, &block)
1308
+ options = names.last.is_a?(Hash) ? names.pop : {}
1309
+ options.assert_valid_keys(:human_name)
1310
+
1311
+ # Store the context so that it can be used for / matched against any event
1312
+ # that gets added
1313
+ @events.context(names, &block) if block_given?
1314
+
1315
+ if names.first.is_a?(Matcher)
1316
+ # Add any events referenced in the matcher. When matchers are used,
1317
+ # events are not allowed to be configured.
1318
+ raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
1319
+ events = add_events(names.first.values)
1320
+ else
1321
+ events = add_events(names)
1322
+
1323
+ # Update the configuration for the event(s)
1324
+ events.each do |event|
1325
+ event.human_name = options[:human_name] if options.include?(:human_name)
1326
+
1327
+ # Add any states that may have been referenced within the event
1328
+ add_states(event.known_states)
1329
+ end
1330
+ end
1331
+
1332
+ events.length == 1 ? events.first : events
1333
+ end
1334
+
1335
+ alias_method :on, :event
1336
+
1337
+ # Creates a new transition that determines what to change the current state
1338
+ # to when an event fires.
1339
+ #
1340
+ # == Defining transitions
1341
+ #
1342
+ # The options for a new transition uses the Hash syntax to map beginning
1343
+ # states to ending states. For example,
1344
+ #
1345
+ # transition :parked => :idling, :idling => :first_gear, :on => :ignite
1346
+ #
1347
+ # In this case, when the +ignite+ event is fired, this transition will cause
1348
+ # the state to be +idling+ if it's current state is +parked+ or +first_gear+
1349
+ # if it's current state is +idling+.
1350
+ #
1351
+ # To help define these implicit transitions, a set of helpers are available
1352
+ # for slightly more complex matching:
1353
+ # * <tt>all</tt> - Matches every state in the machine
1354
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
1355
+ # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
1356
+ # * <tt>same</tt> - Matches the same state being transitioned from
1357
+ #
1358
+ # See StateMachines::MatcherHelpers for more information.
1359
+ #
1360
+ # Examples:
1361
+ #
1362
+ # transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
1363
+ # transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
1364
+ # transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
1365
+ # transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
1366
+ # transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
1367
+ # transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
1368
+ #
1369
+ # transition :parked => same, :on => :park # Loops :parked back to :parked
1370
+ # transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
1371
+ # transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
1372
+ #
1373
+ # # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
1374
+ # transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
1375
+ #
1376
+ # == Verbose transitions
1377
+ #
1378
+ # Transitions can also be defined use an explicit set of configuration
1379
+ # options:
1380
+ # * <tt>:from</tt> - A state or array of states that can be transitioned from.
1381
+ # If not specified, then the transition can occur for *any* state.
1382
+ # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
1383
+ # then the transition will simply loop back (i.e. the state will not change).
1384
+ # * <tt>:except_from</tt> - A state or array of states that *cannot* be
1385
+ # transitioned from.
1386
+ #
1387
+ # These options must be used when defining transitions within the context
1388
+ # of a state.
1389
+ #
1390
+ # Examples:
1391
+ #
1392
+ # transition :to => nil, :on => :park
1393
+ # transition :to => :idling, :on => :ignite
1394
+ # transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
1395
+ # transition :from => nil, :to => :idling, :on => :ignite
1396
+ # transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
1397
+ #
1398
+ # == Conditions
1399
+ #
1400
+ # In addition to the state requirements for each transition, a condition
1401
+ # can also be defined to help determine whether that transition is
1402
+ # available. These options will work on both the normal and verbose syntax.
1403
+ #
1404
+ # Configuration options:
1405
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
1406
+ # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
1407
+ # The condition should return or evaluate to true or false.
1408
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1409
+ # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
1410
+ # The condition should return or evaluate to true or false.
1411
+ #
1412
+ # Examples:
1413
+ #
1414
+ # transition :parked => :idling, :on => :ignite, :if => :moving?
1415
+ # transition :parked => :idling, :on => :ignite, :unless => :stopped?
1416
+ # transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
1417
+ #
1418
+ # transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
1419
+ # transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
1420
+ #
1421
+ # == Order of operations
1422
+ #
1423
+ # Transitions are evaluated in the order in which they're defined. As a
1424
+ # result, if more than one transition applies to a given object, then the
1425
+ # first transition that matches will be performed.
1426
+ def transition(options)
1427
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
1428
+
1429
+ branches = []
1430
+ options = options.dup
1431
+ event(*Array(options.delete(:on))) { branches << transition(options) }
1432
+
1433
+ branches.length == 1 ? branches.first : branches
1434
+ end
1435
+
1436
+ # Creates a callback that will be invoked *before* a transition is
1437
+ # performed so long as the given requirements match the transition.
1438
+ #
1439
+ # == The callback
1440
+ #
1441
+ # Callbacks must be defined as either an argument, in the :do option, or
1442
+ # as a block. For example,
1443
+ #
1444
+ # class Vehicle
1445
+ # state_machine do
1446
+ # before_transition :set_alarm
1447
+ # before_transition :set_alarm, all => :parked
1448
+ # before_transition all => :parked, :do => :set_alarm
1449
+ # before_transition all => :parked do |vehicle, transition|
1450
+ # vehicle.set_alarm
1451
+ # end
1452
+ # ...
1453
+ # end
1454
+ # end
1455
+ #
1456
+ # Notice that the first three callbacks are the same in terms of how the
1457
+ # methods to invoke are defined. However, using the <tt>:do</tt> can
1458
+ # provide for a more fluid DSL.
1459
+ #
1460
+ # In addition, multiple callbacks can be defined like so:
1461
+ #
1462
+ # class Vehicle
1463
+ # state_machine do
1464
+ # before_transition :set_alarm, :lock_doors, all => :parked
1465
+ # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
1466
+ # before_transition :set_alarm do |vehicle, transition|
1467
+ # vehicle.lock_doors
1468
+ # end
1469
+ # end
1470
+ # end
1471
+ #
1472
+ # Notice that the different ways of configuring methods can be mixed.
1473
+ #
1474
+ # == State requirements
1475
+ #
1476
+ # Callbacks can require that the machine be transitioning from and to
1477
+ # specific states. These requirements use a Hash syntax to map beginning
1478
+ # states to ending states. For example,
1479
+ #
1480
+ # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
1481
+ #
1482
+ # In this case, the +set_alarm+ callback will only be called if the machine
1483
+ # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
1484
+ #
1485
+ # To help define state requirements, a set of helpers are available for
1486
+ # slightly more complex matching:
1487
+ # * <tt>all</tt> - Matches every state/event in the machine
1488
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
1489
+ # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
1490
+ # * <tt>same</tt> - Matches the same state being transitioned from
1491
+ #
1492
+ # See StateMachines::MatcherHelpers for more information.
1493
+ #
1494
+ # Examples:
1495
+ #
1496
+ # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
1497
+ # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
1498
+ # before_transition all => :parked, :do => ... # Matches all states to parked
1499
+ # before_transition any => same, :do => ... # Matches every loopback
1500
+ #
1501
+ # == Event requirements
1502
+ #
1503
+ # In addition to state requirements, an event requirement can be defined so
1504
+ # that the callback is only invoked on specific events using the +on+
1505
+ # option. This can also use the same matcher helpers as the state
1506
+ # requirements.
1507
+ #
1508
+ # Examples:
1509
+ #
1510
+ # before_transition :on => :ignite, :do => ... # Matches only on ignite
1511
+ # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1512
+ # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1513
+ #
1514
+ # == Verbose Requirements
1515
+ #
1516
+ # Requirements can also be defined using verbose options rather than the
1517
+ # implicit Hash syntax and helper methods described above.
1518
+ #
1519
+ # Configuration options:
1520
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
1521
+ # are specified, then all states will match.
1522
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
1523
+ # specified, then all states will match.
1524
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
1525
+ # are specified, then all events will match.
1526
+ # * <tt>:except_from</tt> - One or more states *not* being transitioned from
1527
+ # * <tt>:except_to</tt> - One more states *not* being transitioned to
1528
+ # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
1529
+ #
1530
+ # Examples:
1531
+ #
1532
+ # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
1533
+ # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
1534
+ #
1535
+ # == Conditions
1536
+ #
1537
+ # In addition to the state/event requirements, a condition can also be
1538
+ # defined to help determine whether the callback should be invoked.
1539
+ #
1540
+ # Configuration options:
1541
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
1542
+ # callback should occur (e.g. :if => :allow_callbacks, or
1543
+ # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
1544
+ # should return or evaluate to a true or false value.
1545
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1546
+ # callback should not occur (e.g. :unless => :skip_callbacks, or
1547
+ # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
1548
+ # string should return or evaluate to a true or false value.
1549
+ #
1550
+ # Examples:
1551
+ #
1552
+ # before_transition :parked => :idling, :if => :moving?, :do => ...
1553
+ # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1554
+ #
1555
+ # == Accessing the transition
1556
+ #
1557
+ # In addition to passing the object being transitioned, the actual
1558
+ # transition describing the context (e.g. event, from, to) can be accessed
1559
+ # as well. This additional argument is only passed if the callback allows
1560
+ # for it.
1561
+ #
1562
+ # For example,
1563
+ #
1564
+ # class Vehicle
1565
+ # # Only specifies one parameter (the object being transitioned)
1566
+ # before_transition all => :parked do |vehicle|
1567
+ # vehicle.set_alarm
1568
+ # end
1569
+ #
1570
+ # # Specifies 2 parameters (object being transitioned and actual transition)
1571
+ # before_transition all => :parked do |vehicle, transition|
1572
+ # vehicle.set_alarm(transition)
1573
+ # end
1574
+ # end
1575
+ #
1576
+ # *Note* that the object in the callback will only be passed in as an
1577
+ # argument if callbacks are configured to *not* be bound to the object
1578
+ # involved. This is the default and may change on a per-integration basis.
1579
+ #
1580
+ # See StateMachines::Transition for more information about the
1581
+ # attributes available on the transition.
1582
+ #
1583
+ # == Usage with delegates
1584
+ #
1585
+ # As noted above, state_machine uses the callback method's argument list
1586
+ # arity to determine whether to include the transition in the method call.
1587
+ # If you're using delegates, such as those defined in ActiveSupport or
1588
+ # Forwardable, the actual arity of the delegated method gets masked. This
1589
+ # means that callbacks which reference delegates will always get passed the
1590
+ # transition as an argument. For example:
1591
+ #
1592
+ # class Vehicle
1593
+ # extend Forwardable
1594
+ # delegate :refresh => :dashboard
1595
+ #
1596
+ # state_machine do
1597
+ # before_transition :refresh
1598
+ # ...
1599
+ # end
1600
+ #
1601
+ # def dashboard
1602
+ # @dashboard ||= Dashboard.new
1603
+ # end
1604
+ # end
1605
+ #
1606
+ # class Dashboard
1607
+ # def refresh(transition)
1608
+ # # ...
1609
+ # end
1610
+ # end
1611
+ #
1612
+ # In the above example, <tt>Dashboard#refresh</tt> *must* defined a
1613
+ # +transition+ argument. Otherwise, an +ArgumentError+ exception will get
1614
+ # raised. The only way around this is to avoid the use of delegates and
1615
+ # manually define the delegate method so that the correct arity is used.
1616
+ #
1617
+ # == Examples
1618
+ #
1619
+ # Below is an example of a class with one state machine and various types
1620
+ # of +before+ transitions defined for it:
1621
+ #
1622
+ # class Vehicle
1623
+ # state_machine do
1624
+ # # Before all transitions
1625
+ # before_transition :update_dashboard
1626
+ #
1627
+ # # Before specific transition:
1628
+ # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
1629
+ #
1630
+ # # With conditional callback:
1631
+ # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
1632
+ #
1633
+ # # Using helpers:
1634
+ # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
1635
+ # ...
1636
+ # end
1637
+ # end
1638
+ #
1639
+ # As can be seen, any number of transitions can be created using various
1640
+ # combinations of configuration options.
1641
+ def before_transition(*args, &block)
1642
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1643
+ options[:do] = args if args.any?
1644
+ add_callback(:before, options, &block)
1645
+ end
1646
+
1647
+ # Creates a callback that will be invoked *after* a transition is
1648
+ # performed so long as the given requirements match the transition.
1649
+ #
1650
+ # See +before_transition+ for a description of the possible configurations
1651
+ # for defining callbacks.
1652
+ def after_transition(*args, &block)
1653
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1654
+ options[:do] = args if args.any?
1655
+ add_callback(:after, options, &block)
1656
+ end
1657
+
1658
+ # Creates a callback that will be invoked *around* a transition so long as
1659
+ # the given requirements match the transition.
1660
+ #
1661
+ # == The callback
1662
+ #
1663
+ # Around callbacks wrap transitions, executing code both before and after.
1664
+ # These callbacks are defined in the exact same manner as before / after
1665
+ # callbacks with the exception that the transition must be yielded to in
1666
+ # order to finish running it.
1667
+ #
1668
+ # If defining +around+ callbacks using blocks, you must yield within the
1669
+ # transition by directly calling the block (since yielding is not allowed
1670
+ # within blocks).
1671
+ #
1672
+ # For example,
1673
+ #
1674
+ # class Vehicle
1675
+ # state_machine do
1676
+ # around_transition do |block|
1677
+ # Benchmark.measure { block.call }
1678
+ # end
1679
+ #
1680
+ # around_transition do |vehicle, block|
1681
+ # logger.info "vehicle was #{state}..."
1682
+ # block.call
1683
+ # logger.info "...and is now #{state}"
1684
+ # end
1685
+ #
1686
+ # around_transition do |vehicle, transition, block|
1687
+ # logger.info "before #{transition.event}: #{vehicle.state}"
1688
+ # block.call
1689
+ # logger.info "after #{transition.event}: #{vehicle.state}"
1690
+ # end
1691
+ # end
1692
+ # end
1693
+ #
1694
+ # Notice that referencing the block is similar to doing so within an
1695
+ # actual method definition in that it is always the last argument.
1696
+ #
1697
+ # On the other hand, if you're defining +around+ callbacks using method
1698
+ # references, you can yield like normal:
1699
+ #
1700
+ # class Vehicle
1701
+ # state_machine do
1702
+ # around_transition :benchmark
1703
+ # ...
1704
+ # end
1705
+ #
1706
+ # def benchmark
1707
+ # Benchmark.measure { yield }
1708
+ # end
1709
+ # end
1710
+ #
1711
+ # See +before_transition+ for a description of the possible configurations
1712
+ # for defining callbacks.
1713
+ def around_transition(*args, &block)
1714
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1715
+ options[:do] = args if args.any?
1716
+ add_callback(:around, options, &block)
1717
+ end
1718
+
1719
+ # Creates a callback that will be invoked *after* a transition failures to
1720
+ # be performed so long as the given requirements match the transition.
1721
+ #
1722
+ # See +before_transition+ for a description of the possible configurations
1723
+ # for defining callbacks. *Note* however that you cannot define the state
1724
+ # requirements in these callbacks. You may only define event requirements.
1725
+ #
1726
+ # = The callback
1727
+ #
1728
+ # Failure callbacks get invoked whenever an event fails to execute. This
1729
+ # can happen when no transition is available, a +before+ callback halts
1730
+ # execution, or the action associated with this machine fails to succeed.
1731
+ # In any of these cases, any failure callback that matches the attempted
1732
+ # transition will be run.
1733
+ #
1734
+ # For example,
1735
+ #
1736
+ # class Vehicle
1737
+ # state_machine do
1738
+ # after_failure do |vehicle, transition|
1739
+ # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
1740
+ # end
1741
+ #
1742
+ # after_failure :on => :ignite, :do => :log_ignition_failure
1743
+ #
1744
+ # ...
1745
+ # end
1746
+ # end
1747
+ def after_failure(*args, &block)
1748
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1749
+ options[:do] = args if args.any?
1750
+ options.assert_valid_keys(:on, :do, :if, :unless)
1751
+
1752
+ add_callback(:failure, options, &block)
1753
+ end
1754
+
1755
+ # Generates a list of the possible transition sequences that can be run on
1756
+ # the given object. These paths can reveal all of the possible states and
1757
+ # events that can be encountered in the object's state machine based on the
1758
+ # object's current state.
1759
+ #
1760
+ # Configuration options:
1761
+ # * +from+ - The initial state to start all paths from. By default, this
1762
+ # is the object's current state.
1763
+ # * +to+ - The target state to end all paths on. By default, paths will
1764
+ # end when they loop back to the first transition on the path.
1765
+ # * +deep+ - Whether to allow the target state to be crossed more than once
1766
+ # in a path. By default, paths will immediately stop when the target
1767
+ # state (if specified) is reached. If this is enabled, then paths can
1768
+ # continue even after reaching the target state; they will stop when
1769
+ # reaching the target state a second time.
1770
+ #
1771
+ # *Note* that the object is never modified when the list of paths is
1772
+ # generated.
1773
+ #
1774
+ # == Examples
1775
+ #
1776
+ # class Vehicle
1777
+ # state_machine :initial => :parked do
1778
+ # event :ignite do
1779
+ # transition :parked => :idling
1780
+ # end
1781
+ #
1782
+ # event :shift_up do
1783
+ # transition :idling => :first_gear, :first_gear => :second_gear
1784
+ # end
1785
+ #
1786
+ # event :shift_down do
1787
+ # transition :second_gear => :first_gear, :first_gear => :idling
1788
+ # end
1789
+ # end
1790
+ # end
1791
+ #
1792
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
1793
+ # vehicle.state # => "parked"
1794
+ #
1795
+ # vehicle.state_paths
1796
+ # # => [
1797
+ # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1798
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1799
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
1800
+ # # #<StateMachines::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
1801
+ # # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
1802
+ # #
1803
+ # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1804
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1805
+ # # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
1806
+ # # ]
1807
+ #
1808
+ # vehicle.state_paths(:from => :parked, :to => :second_gear)
1809
+ # # => [
1810
+ # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1811
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1812
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
1813
+ # # ]
1814
+ #
1815
+ # In addition to getting the possible paths that can be accessed, you can
1816
+ # also get summary information about the states / events that can be
1817
+ # accessed at some point along one of the paths. For example:
1818
+ #
1819
+ # # Get the list of states that can be accessed from the current state
1820
+ # vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
1821
+ #
1822
+ # # Get the list of events that can be accessed from the current state
1823
+ # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
1824
+ def paths_for(object, requirements = {})
1825
+ PathCollection.new(object, self, requirements)
1826
+ end
1827
+
1828
+ # Marks the given object as invalid with the given message.
1829
+ #
1830
+ # By default, this is a no-op.
1831
+ def invalidate(object, attribute, message, values = [])
1832
+ end
1833
+
1834
+ # Gets a description of the errors for the given object. This is used to
1835
+ # provide more detailed information when an InvalidTransition exception is
1836
+ # raised.
1837
+ def errors_for(object)
1838
+ ''
1839
+ end
1840
+
1841
+ # Resets any errors previously added when invalidating the given object.
1842
+ #
1843
+ # By default, this is a no-op.
1844
+ def reset(object)
1845
+ end
1846
+
1847
+ # Generates the message to use when invalidating the given object after
1848
+ # failing to transition on a specific event
1849
+ def generate_message(name, values = [])
1850
+ message = (@messages[name] || self.class.default_messages[name])
1851
+
1852
+ # Check whether there are actually any values to interpolate to avoid
1853
+ # any warnings
1854
+ if message.scan(/%./).any? { |match| match != '%%' }
1855
+ message % values.map { |value| value.last }
1856
+ else
1857
+ message
1858
+ end
1859
+ end
1860
+
1861
+ # Runs a transaction, rolling back any changes if the yielded block fails.
1862
+ #
1863
+ # This is only applicable to integrations that involve databases. By
1864
+ # default, this will not run any transactions since the changes aren't
1865
+ # taking place within the context of a database.
1866
+ def within_transaction(object)
1867
+ if use_transactions
1868
+ transaction(object) { yield }
1869
+ else
1870
+ yield
1871
+ end
1872
+ end
1873
+
1874
+
1875
+ def draw(graph_options = {})
1876
+ fail NotImplementedError
1877
+ end
1878
+
1879
+ # Determines whether an action hook was defined for firing attribute-based
1880
+ # event transitions when the configured action gets called.
1881
+ def action_hook?(self_only = false)
1882
+ @action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) }
1883
+ end
1884
+
1885
+ protected
1886
+ # Runs additional initialization hooks. By default, this is a no-op.
1887
+ def after_initialize
1888
+ end
1889
+
1890
+ # Looks up other machines that have been defined in the owner class and
1891
+ # are targeting the same attribute as this machine. When accessing
1892
+ # sibling machines, they will be automatically copied for the current
1893
+ # class if they haven't been already. This ensures that any configuration
1894
+ # changes made to the sibling machines only affect this class and not any
1895
+ # base class that may have originally defined the machine.
1896
+ def sibling_machines
1897
+ owner_class.state_machines.inject([]) do |machines, (name, machine)|
1898
+ if machine.attribute == attribute && machine != self
1899
+ machines << (owner_class.state_machine(name) {})
1900
+ end
1901
+ machines
1902
+ end
1903
+ end
1904
+
1905
+ # Determines if the machine's attribute needs to be initialized. This
1906
+ # will only be true if the machine's attribute is blank.
1907
+ def initialize_state?(object)
1908
+ value = read(object, :state)
1909
+ (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
1910
+ end
1911
+
1912
+ # Adds helper methods for interacting with the state machine, including
1913
+ # for states, events, and transitions
1914
+ def define_helpers
1915
+ define_state_accessor
1916
+ define_state_predicate
1917
+ define_event_helpers
1918
+ define_path_helpers
1919
+ define_action_helpers if define_action_helpers?
1920
+ define_name_helpers
1921
+ end
1922
+
1923
+ # Defines the initial values for state machine attributes. Static values
1924
+ # are set prior to the original initialize method and dynamic values are
1925
+ # set *after* the initialize method in case it is dependent on it.
1926
+ def define_state_initializer
1927
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1928
+ def initialize(*)
1929
+ self.class.state_machines.initialize_states(self) { super }
1930
+ end
1931
+ end_eval
1932
+ end
1933
+
1934
+ # Adds reader/writer methods for accessing the state attribute
1935
+ def define_state_accessor
1936
+ attribute = self.attribute
1937
+
1938
+ @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
1939
+ @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
1940
+ end
1941
+
1942
+ # Adds predicate method to the owner class for determining the name of the
1943
+ # current state
1944
+ def define_state_predicate
1945
+ call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
1946
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1947
+ def #{name}?(*args)
1948
+ args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
1949
+ end
1950
+ end_eval
1951
+ end
1952
+
1953
+ # Adds helper methods for getting information about this state machine's
1954
+ # events
1955
+ def define_event_helpers
1956
+ # Gets the events that are allowed to fire on the current object
1957
+ define_helper(:instance, attribute(:events)) do |machine, object, *args|
1958
+ machine.events.valid_for(object, *args).map { |event| event.name }
1959
+ end
1960
+
1961
+ # Gets the next possible transitions that can be run on the current
1962
+ # object
1963
+ define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
1964
+ machine.events.transitions_for(object, *args)
1965
+ end
1966
+
1967
+ # Fire an arbitrary event for this machine
1968
+ define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
1969
+ machine.events.fetch(event).fire(object, *args)
1970
+ end
1971
+
1972
+ # Add helpers for tracking the event / transition to invoke when the
1973
+ # action is called
1974
+ if action
1975
+ event_attribute = attribute(:event)
1976
+ define_helper(:instance, event_attribute) do |machine, object|
1977
+ # Interpret non-blank events as present
1978
+ event = machine.read(object, :event, true)
1979
+ event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
1980
+ end
1981
+
1982
+ # A roundabout way of writing the attribute is used here so that
1983
+ # integrations can hook into this modification
1984
+ define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
1985
+ machine.write(object, :event, value, true)
1986
+ end
1987
+
1988
+ event_transition_attribute = attribute(:event_transition)
1989
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1990
+ protected; attr_accessor #{event_transition_attribute.inspect}
1991
+ end_eval
1992
+ end
1993
+ end
1994
+
1995
+ # Adds helper methods for getting information about this state machine's
1996
+ # available transition paths
1997
+ def define_path_helpers
1998
+ # Gets the paths of transitions available to the current object
1999
+ define_helper(:instance, attribute(:paths)) do |machine, object, *args|
2000
+ machine.paths_for(object, *args)
2001
+ end
2002
+ end
2003
+
2004
+ # Determines whether action helpers should be defined for this machine.
2005
+ # This is only true if there is an action configured and no other machines
2006
+ # have process this same configuration already.
2007
+ def define_action_helpers?
2008
+ action && !owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self }
2009
+ end
2010
+
2011
+ # Adds helper methods for automatically firing events when an action
2012
+ # is invoked
2013
+ def define_action_helpers
2014
+ if action_hook
2015
+ @action_hook_defined = true
2016
+ define_action_hook
2017
+ end
2018
+ end
2019
+
2020
+ # Hooks directly into actions by defining the same method in an included
2021
+ # module. As a result, when the action gets invoked, any state events
2022
+ # defined for the object will get run. Method visibility is preserved.
2023
+ def define_action_hook
2024
+ action_hook = self.action_hook
2025
+ action = self.action
2026
+ private_action_hook = owner_class.private_method_defined?(action_hook)
2027
+
2028
+ # Only define helper if it hasn't
2029
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2030
+ def #{action_hook}(*)
2031
+ self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
2032
+ end
2033
+
2034
+ private #{action_hook.inspect} if #{private_action_hook}
2035
+ end_eval
2036
+ end
2037
+
2038
+ # The method to hook into for triggering transitions when invoked. By
2039
+ # default, this is the action configured for the machine.
2040
+ #
2041
+ # Since the default hook technique relies on module inheritance, the
2042
+ # action must be defined in an ancestor of the owner classs in order for
2043
+ # it to be the action hook.
2044
+ def action_hook
2045
+ action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
2046
+ end
2047
+
2048
+ # Determines whether there's already a helper method defined within the
2049
+ # given scope. This is true only if one of the owner's ancestors defines
2050
+ # the method and is further along in the ancestor chain than this
2051
+ # machine's helper module.
2052
+ def owner_class_ancestor_has_method?(scope, method)
2053
+ superclasses = owner_class.ancestors[1..-1].select { |ancestor| ancestor.is_a?(Class) }
2054
+
2055
+ if scope == :class
2056
+ # Use singleton classes
2057
+ current = (
2058
+ class << owner_class;
2059
+ self;
2060
+ end)
2061
+ superclass = superclasses.first
2062
+ else
2063
+ current = owner_class
2064
+ superclass = owner_class.superclass
2065
+ end
2066
+
2067
+ # Generate the list of modules that *only* occur in the owner class, but
2068
+ # were included *prior* to the helper modules, in addition to the
2069
+ # superclasses
2070
+ ancestors = current.ancestors - superclass.ancestors + superclasses
2071
+ ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
2072
+
2073
+ # Search for for the first ancestor that defined this method
2074
+ ancestors.detect do |ancestor|
2075
+ ancestor = (
2076
+ class << ancestor;
2077
+ self;
2078
+ end) if scope == :class && ancestor.is_a?(Class)
2079
+ ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
2080
+ end
2081
+ end
2082
+
2083
+ # Adds helper methods for accessing naming information about states and
2084
+ # events on the owner class
2085
+ def define_name_helpers
2086
+ # Gets the humanized version of a state
2087
+ define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
2088
+ machine.states.fetch(state).human_name(klass)
2089
+ end
2090
+
2091
+ # Gets the humanized version of an event
2092
+ define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
2093
+ machine.events.fetch(event).human_name(klass)
2094
+ end
2095
+
2096
+ # Gets the state name for the current value
2097
+ define_helper(:instance, attribute(:name)) do |machine, object|
2098
+ machine.states.match!(object).name
2099
+ end
2100
+
2101
+ # Gets the human state name for the current value
2102
+ define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
2103
+ machine.states.match!(object).human_name(object.class)
2104
+ end
2105
+ end
2106
+
2107
+ # Defines the with/without scope helpers for this attribute. Both the
2108
+ # singular and plural versions of the attribute are defined for each
2109
+ # scope helper. A custom plural can be specified if it cannot be
2110
+ # automatically determined by either calling +pluralize+ on the attribute
2111
+ # name or adding an "s" to the end of the name.
2112
+ def define_scopes(custom_plural = nil)
2113
+ plural = custom_plural || pluralize(name)
2114
+
2115
+ [:with, :without].each do |kind|
2116
+ [name, plural].map { |s| s.to_s }.uniq.each do |suffix|
2117
+ method = "#{kind}_#{suffix}"
2118
+
2119
+ if scope = send("create_#{kind}_scope", method)
2120
+ # Converts state names to their corresponding values so that they
2121
+ # can be looked up properly
2122
+ define_helper(:class, method) do |machine, klass, *states|
2123
+ run_scope(scope, machine, klass, states)
2124
+ end
2125
+ end
2126
+ end
2127
+ end
2128
+ end
2129
+
2130
+ # Generates the results for the given scope based on one or more states to
2131
+ # filter by
2132
+ def run_scope(scope, machine, klass, states)
2133
+ values = states.flatten.map { |state| machine.states.fetch(state).value }
2134
+ scope.call(klass, values)
2135
+ end
2136
+
2137
+ # Pluralizes the given word using #pluralize (if available) or simply
2138
+ # adding an "s" to the end of the word
2139
+ def pluralize(word)
2140
+ word = word.to_s
2141
+ if word.respond_to?(:pluralize)
2142
+ word.pluralize
2143
+ else
2144
+ "#{name}s"
2145
+ end
2146
+ end
2147
+
2148
+ # Creates a scope for finding objects *with* a particular value or values
2149
+ # for the attribute.
2150
+ #
2151
+ # By default, this is a no-op.
2152
+ def create_with_scope(name)
2153
+ end
2154
+
2155
+ # Creates a scope for finding objects *without* a particular value or
2156
+ # values for the attribute.
2157
+ #
2158
+ # By default, this is a no-op.
2159
+ def create_without_scope(name)
2160
+ end
2161
+
2162
+ # Always yields
2163
+ def transaction(object)
2164
+ yield
2165
+ end
2166
+
2167
+ # Gets the initial attribute value defined by the owner class (outside of
2168
+ # the machine's definition). By default, this is always nil.
2169
+ def owner_class_attribute_default
2170
+ nil
2171
+ end
2172
+
2173
+ # Checks whether the given state matches the attribute default specified
2174
+ # by the owner class
2175
+ def owner_class_attribute_default_matches?(state)
2176
+ state.matches?(owner_class_attribute_default)
2177
+ end
2178
+
2179
+ # Updates this machine based on the configuration of other machines in the
2180
+ # owner class that share the same target attribute.
2181
+ def add_sibling_machine_configs
2182
+ # Add existing states
2183
+ sibling_machines.each do |machine|
2184
+ machine.states.each { |state| states << state unless states[state.name] }
2185
+ end
2186
+ end
2187
+
2188
+ # Adds a new transition callback of the given type.
2189
+ def add_callback(type, options, &block)
2190
+ callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
2191
+ add_states(callback.known_states)
2192
+ callback
2193
+ end
2194
+
2195
+ # Tracks the given set of states in the list of all known states for
2196
+ # this machine
2197
+ def add_states(new_states)
2198
+ new_states.map do |new_state|
2199
+ # Check for other states that use a different class type for their name.
2200
+ # This typically prevents string / symbol misuse.
2201
+ if new_state && conflict = states.detect { |state| state.name && state.name.class != new_state.class }
2202
+ raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
2203
+ end
2204
+
2205
+ unless state = states[new_state]
2206
+ states << state = State.new(self, new_state)
2207
+
2208
+ # Copy states over to sibling machines
2209
+ sibling_machines.each { |machine| machine.states << state }
2210
+ end
2211
+
2212
+ state
2213
+ end
2214
+ end
2215
+
2216
+ # Tracks the given set of events in the list of all known events for
2217
+ # this machine
2218
+ def add_events(new_events)
2219
+ new_events.map do |new_event|
2220
+ # Check for other states that use a different class type for their name.
2221
+ # This typically prevents string / symbol misuse.
2222
+ if conflict = events.detect { |event| event.name.class != new_event.class }
2223
+ raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
2224
+ end
2225
+
2226
+ unless event = events[new_event]
2227
+ events << event = Event.new(self, new_event)
2228
+ end
2229
+
2230
+ event
2231
+ end
2232
+ end
2233
+ end
2234
+ end