verborghs-state_machine 0.9.4

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 (89) hide show
  1. data/CHANGELOG.rdoc +360 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +635 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine/assertions.rb +36 -0
  28. data/lib/state_machine/callback.rb +241 -0
  29. data/lib/state_machine/condition_proxy.rb +106 -0
  30. data/lib/state_machine/eval_helpers.rb +83 -0
  31. data/lib/state_machine/event.rb +267 -0
  32. data/lib/state_machine/event_collection.rb +122 -0
  33. data/lib/state_machine/extensions.rb +149 -0
  34. data/lib/state_machine/guard.rb +230 -0
  35. data/lib/state_machine/initializers/merb.rb +1 -0
  36. data/lib/state_machine/initializers/rails.rb +5 -0
  37. data/lib/state_machine/initializers.rb +4 -0
  38. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  40. data/lib/state_machine/integrations/active_model.rb +445 -0
  41. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  42. data/lib/state_machine/integrations/active_record.rb +522 -0
  43. data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
  44. data/lib/state_machine/integrations/data_mapper.rb +379 -0
  45. data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
  46. data/lib/state_machine/integrations/sequel.rb +356 -0
  47. data/lib/state_machine/integrations.rb +83 -0
  48. data/lib/state_machine/machine.rb +1645 -0
  49. data/lib/state_machine/machine_collection.rb +64 -0
  50. data/lib/state_machine/matcher.rb +123 -0
  51. data/lib/state_machine/matcher_helpers.rb +54 -0
  52. data/lib/state_machine/node_collection.rb +152 -0
  53. data/lib/state_machine/state.rb +260 -0
  54. data/lib/state_machine/state_collection.rb +112 -0
  55. data/lib/state_machine/transition.rb +399 -0
  56. data/lib/state_machine/transition_collection.rb +244 -0
  57. data/lib/state_machine.rb +421 -0
  58. data/lib/tasks/state_machine.rake +1 -0
  59. data/lib/tasks/state_machine.rb +27 -0
  60. data/test/files/en.yml +9 -0
  61. data/test/files/switch.rb +11 -0
  62. data/test/functional/state_machine_test.rb +980 -0
  63. data/test/test_helper.rb +4 -0
  64. data/test/unit/assertions_test.rb +40 -0
  65. data/test/unit/callback_test.rb +728 -0
  66. data/test/unit/condition_proxy_test.rb +328 -0
  67. data/test/unit/eval_helpers_test.rb +222 -0
  68. data/test/unit/event_collection_test.rb +324 -0
  69. data/test/unit/event_test.rb +795 -0
  70. data/test/unit/guard_test.rb +909 -0
  71. data/test/unit/integrations/active_model_test.rb +956 -0
  72. data/test/unit/integrations/active_record_test.rb +1918 -0
  73. data/test/unit/integrations/data_mapper_test.rb +1814 -0
  74. data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
  75. data/test/unit/integrations/sequel_test.rb +1492 -0
  76. data/test/unit/integrations_test.rb +50 -0
  77. data/test/unit/invalid_event_test.rb +7 -0
  78. data/test/unit/invalid_transition_test.rb +7 -0
  79. data/test/unit/machine_collection_test.rb +565 -0
  80. data/test/unit/machine_test.rb +2349 -0
  81. data/test/unit/matcher_helpers_test.rb +37 -0
  82. data/test/unit/matcher_test.rb +155 -0
  83. data/test/unit/node_collection_test.rb +207 -0
  84. data/test/unit/state_collection_test.rb +280 -0
  85. data/test/unit/state_machine_test.rb +31 -0
  86. data/test/unit/state_test.rb +848 -0
  87. data/test/unit/transition_collection_test.rb +2098 -0
  88. data/test/unit/transition_test.rb +1384 -0
  89. metadata +176 -0
@@ -0,0 +1,1645 @@
1
+ require 'state_machine/extensions'
2
+ require 'state_machine/assertions'
3
+ require 'state_machine/integrations'
4
+
5
+ require 'state_machine/state'
6
+ require 'state_machine/event'
7
+ require 'state_machine/callback'
8
+ require 'state_machine/node_collection'
9
+ require 'state_machine/state_collection'
10
+ require 'state_machine/event_collection'
11
+ require 'state_machine/matcher_helpers'
12
+
13
+ module StateMachine
14
+ # Represents a state machine for a particular attribute. State machines
15
+ # consist of states, events and a set of transitions that define how the
16
+ # state changes after a particular event is fired.
17
+ #
18
+ # A state machine will not know all of the possible states for an object
19
+ # unless they are referenced *somewhere* in the state machine definition.
20
+ # As a result, any unused states should be defined with the +other_states+
21
+ # or +state+ helper.
22
+ #
23
+ # == Actions
24
+ #
25
+ # When an action is configured for a state machine, it is invoked when an
26
+ # object transitions via an event. The success of the event becomes
27
+ # dependent on the success of the action. If the action is successful, then
28
+ # the transitioned state remains persisted. However, if the action fails
29
+ # (by returning false), the transitioned state will be rolled back.
30
+ #
31
+ # For example,
32
+ #
33
+ # class Vehicle
34
+ # attr_accessor :fail, :saving_state
35
+ #
36
+ # state_machine :initial => :parked, :action => :save do
37
+ # event :ignite do
38
+ # transition :parked => :idling
39
+ # end
40
+ #
41
+ # event :park do
42
+ # transition :idling => :parked
43
+ # end
44
+ # end
45
+ #
46
+ # def save
47
+ # @saving_state = state
48
+ # fail != true
49
+ # end
50
+ # end
51
+ #
52
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
53
+ # vehicle.save # => true
54
+ # vehicle.saving_state # => "parked" # The state was "parked" was save was called
55
+ #
56
+ # # Successful event
57
+ # vehicle.ignite # => true
58
+ # vehicle.saving_state # => "idling" # The state was "idling" when save was called
59
+ # vehicle.state # => "idling"
60
+ #
61
+ # # Failed event
62
+ # vehicle.fail = true
63
+ # vehicle.park # => false
64
+ # vehicle.saving_state # => "parked"
65
+ # vehicle.state # => "idling"
66
+ #
67
+ # As shown, even though the state is set prior to calling the +save+ action
68
+ # on the object, it will be rolled back to the original state if the action
69
+ # fails. *Note* that this will also be the case if an exception is raised
70
+ # while calling the action.
71
+ #
72
+ # === Indirect transitions
73
+ #
74
+ # In addition to the action being run as the _result_ of an event, the action
75
+ # can also be used to run events itself. For example, using the above as an
76
+ # example:
77
+ #
78
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
79
+ #
80
+ # vehicle.state_event = 'ignite'
81
+ # vehicle.save # => true
82
+ # vehicle.state # => "idling"
83
+ # vehicle.state_event # => nil
84
+ #
85
+ # As can be seen, the +save+ action automatically invokes the event stored in
86
+ # the +state_event+ attribute (<tt>:ignite</tt> in this case).
87
+ #
88
+ # One important note about using this technique for running transitions is
89
+ # that if the class in which the state machine is defined *also* defines the
90
+ # action being invoked (and not a superclass), then it must manually run the
91
+ # StateMachine hook that checks for event attributes.
92
+ #
93
+ # For example, in ActiveRecord, DataMapper, MongoMapper, and Sequel, the
94
+ # default action (+save+) is already defined in a base class. As a result,
95
+ # when a state machine is defined in a model / resource, StateMachine can
96
+ # automatically hook into the +save+ action.
97
+ #
98
+ # On the other hand, the Vehicle class from above defined its own +save+
99
+ # method (and there is no +save+ method in its superclass). As a result, it
100
+ # must be modified like so:
101
+ #
102
+ # def save
103
+ # self.class.state_machines.transitions(self, :save).perform do
104
+ # @saving_state = state
105
+ # fail != true
106
+ # end
107
+ # end
108
+ #
109
+ # This will add in the functionality for firing the event stored in the
110
+ # +state_event+ attribute.
111
+ #
112
+ # == Callbacks
113
+ #
114
+ # Callbacks are supported for hooking before and after every possible
115
+ # transition in the machine. Each callback is invoked in the order in which
116
+ # it was defined. See StateMachine::Machine#before_transition and
117
+ # StateMachine::Machine#after_transition for documentation on how to define
118
+ # new callbacks.
119
+ #
120
+ # *Note* that callbacks only get executed within the context of an event. As
121
+ # a result, if a class has an initial state when it's created, any callbacks
122
+ # that would normally get executed when the object enters that state will
123
+ # *not* get triggered.
124
+ #
125
+ # For example,
126
+ #
127
+ # class Vehicle
128
+ # state_machine :initial => :parked do
129
+ # after_transition all => :parked do
130
+ # raise ArgumentError
131
+ # end
132
+ # ...
133
+ # end
134
+ # end
135
+ #
136
+ # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
137
+ # vehicle.save # => true (no exception raised)
138
+ #
139
+ # If you need callbacks to get triggered when an object is created, this
140
+ # should be done by either:
141
+ # * Use a <tt>before :save</tt> or equivalent hook, or
142
+ # * Set an initial state of nil and use the correct event to create the
143
+ # object with the proper state, resulting in callbacks being triggered and
144
+ # the object getting persisted
145
+ #
146
+ # === Canceling callbacks
147
+ #
148
+ # Callbacks can be canceled by throwing :halt at any point during the
149
+ # callback. For example,
150
+ #
151
+ # ...
152
+ # throw :halt
153
+ # ...
154
+ #
155
+ # If a +before+ callback halts the chain, the associated transition and all
156
+ # later callbacks are canceled. If an +after+ callback halts the chain,
157
+ # the later callbacks are canceled, but the transition is still successful.
158
+ #
159
+ # These same rules apply to +around+ callbacks with the exception that any
160
+ # +around+ callback that doesn't yield will essentially result in :halt being
161
+ # thrown. Any code executed after the yield will behave in the same way as
162
+ # +after+ callbacks.
163
+ #
164
+ # *Note* that if a +before+ callback fails and the bang version of an event
165
+ # was invoked, an exception will be raised instead of returning false. For
166
+ # example,
167
+ #
168
+ # class Vehicle
169
+ # state_machine :initial => :parked do
170
+ # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
171
+ # ...
172
+ # end
173
+ # end
174
+ #
175
+ # vehicle = Vehicle.new
176
+ # vehicle.park # => false
177
+ # vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
178
+ #
179
+ # == Observers
180
+ #
181
+ # Observers, in the sense of external classes and *not* Ruby's Observable
182
+ # mechanism, can hook into state machines as well. Such observers use the
183
+ # same callback api that's used internally.
184
+ #
185
+ # Below are examples of defining observers for the following state machine:
186
+ #
187
+ # class Vehicle
188
+ # state_machine do
189
+ # event :park do
190
+ # transition :idling => :parked
191
+ # end
192
+ # ...
193
+ # end
194
+ # ...
195
+ # end
196
+ #
197
+ # Event/Transition behaviors:
198
+ #
199
+ # class VehicleObserver
200
+ # def self.before_park(vehicle, transition)
201
+ # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
202
+ # end
203
+ #
204
+ # def self.after_park(vehicle, transition, result)
205
+ # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
206
+ # end
207
+ #
208
+ # def self.before_transition(vehicle, transition)
209
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
210
+ # end
211
+ #
212
+ # def self.after_transition(vehicle, transition)
213
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
214
+ # end
215
+ #
216
+ # def self.around_transition(vehicle, transition)
217
+ # logger.info Benchmark.measure { yield }
218
+ # end
219
+ # end
220
+ #
221
+ # Vehicle.state_machine do
222
+ # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
223
+ # before_transition VehicleObserver.method(:before_transition)
224
+ #
225
+ # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
226
+ # after_transition VehicleObserver.method(:after_transition)
227
+ #
228
+ # around_transition VehicleObserver.method(:around_transition)
229
+ # end
230
+ #
231
+ # One common callback is to record transitions for all models in the system
232
+ # for auditing/debugging purposes. Below is an example of an observer that
233
+ # can easily automate this process for all models:
234
+ #
235
+ # class StateMachineObserver
236
+ # def self.before_transition(object, transition)
237
+ # Audit.log_transition(object.attributes)
238
+ # end
239
+ # end
240
+ #
241
+ # [Vehicle, Switch, Project].each do |klass|
242
+ # klass.state_machines.each do |attribute, machine|
243
+ # machine.before_transition StateMachineObserver.method(:before_transition)
244
+ # end
245
+ # end
246
+ #
247
+ # Additional observer-like behavior may be exposed by the various integrations
248
+ # available. See below for more information on integrations.
249
+ #
250
+ # == Overriding instance / class methods
251
+ #
252
+ # Hooking in behavior to the generated instance / class methods from the
253
+ # state machine, events, and states is very simple because of the way these
254
+ # methods are generated on the class. Using the class's ancestors, the
255
+ # original generated method can be referred to via +super+. For example,
256
+ #
257
+ # class Vehicle
258
+ # state_machine do
259
+ # event :park do
260
+ # ...
261
+ # end
262
+ # end
263
+ #
264
+ # def park(*args)
265
+ # logger.info "..."
266
+ # super
267
+ # end
268
+ # end
269
+ #
270
+ # In the above example, the +park+ instance method that's generated on the
271
+ # Vehicle class (by the associated event) is overridden with custom behavior.
272
+ # Once this behavior is complete, the original method from the state machine
273
+ # is invoked by simply calling +super+.
274
+ #
275
+ # The same technique can be used for +state+, +state_name+, and all other
276
+ # instance *and* class methods on the Vehicle class.
277
+ #
278
+ # == Integrations
279
+ #
280
+ # By default, state machines are library-agnostic, meaning that they work
281
+ # on any Ruby class and have no external dependencies. However, there are
282
+ # certain libraries which expose additional behavior that can be taken
283
+ # advantage of by state machines.
284
+ #
285
+ # This library is built to work out of the box with a few popular Ruby
286
+ # libraries that allow for additional behavior to provide a cleaner and
287
+ # smoother experience. This is especially the case for objects backed by a
288
+ # database that may allow for transactions, persistent storage,
289
+ # search/filters, callbacks, etc.
290
+ #
291
+ # When a state machine is defined for classes using any of the above libraries,
292
+ # it will try to automatically determine the integration to use (Agnostic,
293
+ # ActiveModel, ActiveRecord, DataMapper, MongoMapper, or Sequel) based on the
294
+ # class definition. To see how each integration affects the machine's
295
+ # behavior, refer to all constants defined under the StateMachine::Integrations
296
+ # namespace.
297
+ class Machine
298
+ include Assertions
299
+ include EvalHelpers
300
+ include MatcherHelpers
301
+
302
+ class << self
303
+ # Attempts to find or create a state machine for the given class. For
304
+ # example,
305
+ #
306
+ # StateMachine::Machine.find_or_create(Vehicle)
307
+ # StateMachine::Machine.find_or_create(Vehicle, :initial => :parked)
308
+ # StateMachine::Machine.find_or_create(Vehicle, :status)
309
+ # StateMachine::Machine.find_or_create(Vehicle, :status, :initial => :parked)
310
+ #
311
+ # If a machine of the given name already exists in one of the class's
312
+ # superclasses, then a copy of that machine will be created and stored
313
+ # in the new owner class (the original will remain unchanged).
314
+ def find_or_create(owner_class, *args, &block)
315
+ options = args.last.is_a?(Hash) ? args.pop : {}
316
+ name = args.first || :state
317
+
318
+ # Find an existing machine
319
+ if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
320
+ # Only create a new copy if changes are being made to the machine in
321
+ # a subclass
322
+ if machine.owner_class != owner_class && (options.any? || block_given?)
323
+ machine = machine.clone
324
+ machine.initial_state = options[:initial] if options.include?(:initial)
325
+ machine.owner_class = owner_class
326
+ end
327
+
328
+ # Evaluate DSL
329
+ machine.instance_eval(&block) if block_given?
330
+ else
331
+ # No existing machine: create a new one
332
+ machine = new(owner_class, name, options, &block)
333
+ end
334
+
335
+ machine
336
+ end
337
+
338
+ # Draws the state machines defined in the given classes using GraphViz.
339
+ # The given classes must be a comma-delimited string of class names.
340
+ #
341
+ # Configuration options:
342
+ # * <tt>:file</tt> - A comma-delimited string of files to load that
343
+ # contain the state machine definitions to draw
344
+ # * <tt>:path</tt> - The path to write the graph file to
345
+ # * <tt>:format</tt> - The image format to generate the graph in
346
+ # * <tt>:font</tt> - The name of the font to draw state names in
347
+ def draw(class_names, options = {})
348
+ raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
349
+
350
+ # Load any files
351
+ if files = options.delete(:file)
352
+ files.split(',').each {|file| require file}
353
+ end
354
+
355
+ class_names.split(',').each do |class_name|
356
+ # Navigate through the namespace structure to get to the class
357
+ klass = Object
358
+ class_name.split('::').each do |name|
359
+ klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
360
+ end
361
+
362
+ # Draw each of the class's state machines
363
+ klass.state_machines.each_value do |machine|
364
+ machine.draw(options)
365
+ end
366
+ end
367
+ end
368
+ end
369
+
370
+ # Default messages to use for validation errors in ORM integrations
371
+ class << self; attr_accessor :default_messages; end
372
+ @default_messages = {
373
+ :invalid => 'is invalid',
374
+ :invalid_event => 'cannot transition when %s',
375
+ :invalid_transition => 'cannot transition via "%s"'
376
+ }
377
+
378
+ # The class that the machine is defined in
379
+ attr_accessor :owner_class
380
+
381
+ # The name of the machine, used for scoping methods generated for the
382
+ # machine as a whole (not states or events)
383
+ attr_reader :name
384
+
385
+ # The events that trigger transitions. These are sorted, by default, in
386
+ # the order in which they were defined.
387
+ attr_reader :events
388
+
389
+ # A list of all of the states known to this state machine. This will pull
390
+ # states from the following sources:
391
+ # * Initial state
392
+ # * State behaviors
393
+ # * Event transitions (:to, :from, and :except_from options)
394
+ # * Transition callbacks (:to, :from, :except_to, and :except_from options)
395
+ # * Unreferenced states (using +other_states+ helper)
396
+ #
397
+ # These are sorted, by default, in the order in which they were referenced.
398
+ attr_reader :states
399
+
400
+ # The callbacks to invoke before/after a transition is performed
401
+ #
402
+ # Maps :before => callbacks and :after => callbacks
403
+ attr_reader :callbacks
404
+
405
+ # The action to invoke when an object transitions
406
+ attr_reader :action
407
+
408
+ # An identifier that forces all methods (including state predicates and
409
+ # event methods) to be generated with the value prefixed or suffixed,
410
+ # depending on the context.
411
+ attr_reader :namespace
412
+
413
+ # Whether the machine will use transactions when firing events
414
+ attr_reader :use_transactions
415
+
416
+ # Creates a new state machine for the given attribute
417
+ def initialize(owner_class, *args, &block)
418
+ options = args.last.is_a?(Hash) ? args.pop : {}
419
+ assert_valid_keys(options, :attribute, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions)
420
+
421
+ # Find an integration that matches this machine's owner class
422
+ if options.include?(:integration)
423
+ integration = StateMachine::Integrations.find(options[:integration]) if options[:integration]
424
+ else
425
+ integration = StateMachine::Integrations.match(owner_class)
426
+ end
427
+
428
+ if integration
429
+ extend integration
430
+ options = integration.defaults.merge(options) if integration.respond_to?(:defaults)
431
+ end
432
+
433
+ # Add machine-wide defaults
434
+ options = {:use_transactions => true}.merge(options)
435
+
436
+ # Set machine configuration
437
+ @name = args.first || :state
438
+ @attribute = options[:attribute] || @name
439
+ @events = EventCollection.new(self)
440
+ @states = StateCollection.new(self)
441
+ @callbacks = {:before => [], :after => []}
442
+ @namespace = options[:namespace]
443
+ @messages = options[:messages] || {}
444
+ @action = options[:action]
445
+ @use_transactions = options[:use_transactions]
446
+ self.owner_class = owner_class
447
+ self.initial_state = options[:initial]
448
+
449
+ # Define class integration
450
+ define_helpers
451
+ define_scopes(options[:plural])
452
+ after_initialize
453
+
454
+ # Evaluate DSL
455
+ instance_eval(&block) if block_given?
456
+ end
457
+
458
+ # Creates a copy of this machine in addition to copies of each associated
459
+ # event/states/callback, so that the modifications to those collections do
460
+ # not affect the original machine.
461
+ def initialize_copy(orig) #:nodoc:
462
+ super
463
+
464
+ @events = @events.dup
465
+ @events.machine = self
466
+ @states = @states.dup
467
+ @states.machine = self
468
+ @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
469
+ end
470
+
471
+ # Sets the class which is the owner of this state machine. Any methods
472
+ # generated by states, events, or other parts of the machine will be defined
473
+ # on the given owner class.
474
+ def owner_class=(klass)
475
+ @owner_class = klass
476
+
477
+ # Create modules for extending the class with state/event-specific methods
478
+ class_helper_module = @class_helper_module = Module.new
479
+ instance_helper_module = @instance_helper_module = Module.new
480
+ owner_class.class_eval do
481
+ extend class_helper_module
482
+ include instance_helper_module
483
+ end
484
+
485
+ # Add class-/instance-level methods to the owner class for state initialization
486
+ unless owner_class < StateMachine::InstanceMethods
487
+ owner_class.class_eval do
488
+ extend StateMachine::ClassMethods
489
+ include StateMachine::InstanceMethods
490
+ end
491
+
492
+ define_state_initializer
493
+ end
494
+
495
+ # Record this machine as matched to the name in the current owner class.
496
+ # This will override any machines mapped to the same name in any superclasses.
497
+ owner_class.state_machines[name] = self
498
+ end
499
+
500
+ # Sets the initial state of the machine. This can be either the static name
501
+ # of a state or a lambda block which determines the initial state at
502
+ # creation time.
503
+ def initial_state=(new_initial_state)
504
+ @initial_state = new_initial_state
505
+ add_states([@initial_state]) unless dynamic_initial_state?
506
+
507
+ # Update all states to reflect the new initial state
508
+ states.each {|state| state.initial = (state.name == @initial_state)}
509
+ end
510
+
511
+ # Initializes the state on the given object. This will always write to the
512
+ # attribute regardless of whether a value is already present.
513
+ def initialize_state(object)
514
+ write(object, :state, initial_state(object).value)
515
+ end
516
+
517
+ # Gets the actual name of the attribute on the machine's owner class that
518
+ # stores data with the given name.
519
+ def attribute(name = :state)
520
+ name == :state ? @attribute : :"#{self.name}_#{name}"
521
+ end
522
+
523
+ # Defines a new instance method with the given name on the machine's owner
524
+ # class. If the method is already defined in the class, then this will not
525
+ # override it.
526
+ #
527
+ # Example:
528
+ #
529
+ # machine.define_instance_method(:state_name) do |machine, object|
530
+ # machine.states.match(object)
531
+ # end
532
+ def define_instance_method(method, &block)
533
+ name = self.name
534
+
535
+ @instance_helper_module.class_eval do
536
+ define_method(method) do |*args|
537
+ block.call(self.class.state_machine(name), self, *args)
538
+ end
539
+ end
540
+ end
541
+ attr_reader :instance_helper_module
542
+
543
+ # Defines a new class method with the given name on the machine's owner
544
+ # class. If the method is already defined in the class, then this will not
545
+ # override it.
546
+ #
547
+ # Example:
548
+ #
549
+ # machine.define_class_method(:states) do |machine, klass|
550
+ # machine.states.keys
551
+ # end
552
+ def define_class_method(method, &block)
553
+ name = self.name
554
+
555
+ @class_helper_module.class_eval do
556
+ define_method(method) do |*args|
557
+ block.call(self.state_machine(name), self, *args)
558
+ end
559
+ end
560
+ end
561
+
562
+ # Gets the initial state of the machine for the given object. If a dynamic
563
+ # initial state was configured for this machine, then the object will be
564
+ # passed into the lambda block to help determine the actual state.
565
+ #
566
+ # == Examples
567
+ #
568
+ # With a static initial state:
569
+ #
570
+ # class Vehicle
571
+ # state_machine :initial => :parked do
572
+ # ...
573
+ # end
574
+ # end
575
+ #
576
+ # vehicle = Vehicle.new
577
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
578
+ #
579
+ # With a dynamic initial state:
580
+ #
581
+ # class Vehicle
582
+ # attr_accessor :force_idle
583
+ #
584
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
585
+ # ...
586
+ # end
587
+ # end
588
+ #
589
+ # vehicle = Vehicle.new
590
+ #
591
+ # vehicle.force_idle = true
592
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
593
+ #
594
+ # vehicle.force_idle = false
595
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
596
+ def initial_state(object)
597
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state)
598
+ end
599
+
600
+ # Whether a dynamic initial state is being used in the machine
601
+ def dynamic_initial_state?
602
+ @initial_state.is_a?(Proc)
603
+ end
604
+
605
+ # Customizes the definition of one or more states in the machine.
606
+ #
607
+ # Configuration options:
608
+ # * <tt>:value</tt> - The actual value to store when an object transitions
609
+ # to the state. Default is the name (stringified).
610
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
611
+ # then setting this to true will cache the evaluated result
612
+ # * <tt>:if</tt> - Determines whether an object's value matches the state
613
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
614
+ # By default, the configured value is matched.
615
+ # * <tt>:human_name</tt> - The human-readable version of this state's name.
616
+ # By default, this is either defined by the integration or stringifies the
617
+ # name and converts underscores to spaces.
618
+ #
619
+ # == Customizing the stored value
620
+ #
621
+ # Whenever a state is automatically discovered in the state machine, its
622
+ # default value is assumed to be the stringified version of the name. For
623
+ # example,
624
+ #
625
+ # class Vehicle
626
+ # state_machine :initial => :parked do
627
+ # event :ignite do
628
+ # transition :parked => :idling
629
+ # end
630
+ # end
631
+ # end
632
+ #
633
+ # In the above state machine, there are two states automatically discovered:
634
+ # :parked and :idling. These states, by default, will store their stringified
635
+ # equivalents when an object moves into that state (e.g. "parked" / "idling").
636
+ #
637
+ # For legacy systems or when tying state machines into existing frameworks,
638
+ # it's oftentimes necessary to need to store a different value for a state
639
+ # than the default. In order to continue taking advantage of an expressive
640
+ # state machine and helper methods, every defined state can be re-configured
641
+ # with a custom stored value. For example,
642
+ #
643
+ # class Vehicle
644
+ # state_machine :initial => :parked do
645
+ # event :ignite do
646
+ # transition :parked => :idling
647
+ # end
648
+ #
649
+ # state :idling, :value => 'IDLING'
650
+ # state :parked, :value => 'PARKED
651
+ # end
652
+ # end
653
+ #
654
+ # This is also useful if being used in association with a database and,
655
+ # instead of storing the state name in a column, you want to store the
656
+ # state's foreign key:
657
+ #
658
+ # class VehicleState < ActiveRecord::Base
659
+ # end
660
+ #
661
+ # class Vehicle < ActiveRecord::Base
662
+ # state_machine :attribute => :state_id, :initial => :parked do
663
+ # event :ignite do
664
+ # transition :parked => :idling
665
+ # end
666
+ #
667
+ # states.each do |state|
668
+ # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
669
+ # end
670
+ # end
671
+ # end
672
+ #
673
+ # In the above example, each known state is configured to store it's
674
+ # associated database id in the +state_id+ attribute. Also, notice that a
675
+ # lambda block is used to define the state's value. This is required in
676
+ # situations (like testing) where the model is loaded without any existing
677
+ # data (i.e. no VehicleState records available).
678
+ #
679
+ # One caveat to the above example is to keep performance in mind. To avoid
680
+ # constant db hits for looking up the VehicleState ids, the value is cached
681
+ # by specifying the <tt>:cache</tt> option. Alternatively, a custom
682
+ # caching strategy can be used like so:
683
+ #
684
+ # class VehicleState < ActiveRecord::Base
685
+ # cattr_accessor :cache_store
686
+ # self.cache_store = ActiveSupport::Cache::MemoryStore.new
687
+ #
688
+ # def self.find_by_name(name)
689
+ # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
690
+ # end
691
+ # end
692
+ #
693
+ # === Dynamic values
694
+ #
695
+ # In addition to customizing states with other value types, lambda blocks
696
+ # can also be specified to allow for a state's value to be determined
697
+ # dynamically at runtime. For example,
698
+ #
699
+ # class Vehicle
700
+ # state_machine :purchased_at, :initial => :available do
701
+ # event :purchase do
702
+ # transition all => :purchased
703
+ # end
704
+ #
705
+ # event :restock do
706
+ # transition all => :available
707
+ # end
708
+ #
709
+ # state :available, :value => nil
710
+ # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
711
+ # end
712
+ # end
713
+ #
714
+ # In the above definition, the <tt>:purchased</tt> state is customized with
715
+ # both a dynamic value *and* a value matcher.
716
+ #
717
+ # When an object transitions to the purchased state, the value's lambda
718
+ # block will be called. This will get the current time and store it in the
719
+ # object's +purchased_at+ attribute.
720
+ #
721
+ # *Note* that the custom matcher is very important here. Since there's no
722
+ # way for the state machine to figure out an object's state when it's set to
723
+ # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
724
+ # were not configured for the state, then an ArgumentError exception would
725
+ # be raised at runtime, indicating that the state machine could not figure
726
+ # out what the current state of the object was.
727
+ #
728
+ # == Behaviors
729
+ #
730
+ # Behaviors define a series of methods to mixin with objects when the current
731
+ # state matches the given one(s). This allows instance methods to behave
732
+ # a specific way depending on what the value of the object's state is.
733
+ #
734
+ # For example,
735
+ #
736
+ # class Vehicle
737
+ # attr_accessor :driver
738
+ # attr_accessor :passenger
739
+ #
740
+ # state_machine :initial => :parked do
741
+ # event :ignite do
742
+ # transition :parked => :idling
743
+ # end
744
+ #
745
+ # state :parked do
746
+ # def speed
747
+ # 0
748
+ # end
749
+ #
750
+ # def rotate_driver
751
+ # driver = self.driver
752
+ # self.driver = passenger
753
+ # self.passenger = driver
754
+ # true
755
+ # end
756
+ # end
757
+ #
758
+ # state :idling, :first_gear do
759
+ # def speed
760
+ # 20
761
+ # end
762
+ #
763
+ # def rotate_driver
764
+ # self.state = 'parked'
765
+ # rotate_driver
766
+ # end
767
+ # end
768
+ #
769
+ # other_states :backing_up
770
+ # end
771
+ # end
772
+ #
773
+ # In the above example, there are two dynamic behaviors defined for the
774
+ # class:
775
+ # * +speed+
776
+ # * +rotate_driver+
777
+ #
778
+ # Each of these behaviors are instance methods on the Vehicle class. However,
779
+ # which method actually gets invoked is based on the current state of the
780
+ # object. Using the above class as the example:
781
+ #
782
+ # vehicle = Vehicle.new
783
+ # vehicle.driver = 'John'
784
+ # vehicle.passenger = 'Jane'
785
+ #
786
+ # # Behaviors in the "parked" state
787
+ # vehicle.state # => "parked"
788
+ # vehicle.speed # => 0
789
+ # vehicle.rotate_driver # => true
790
+ # vehicle.driver # => "Jane"
791
+ # vehicle.passenger # => "John"
792
+ #
793
+ # vehicle.ignite # => true
794
+ #
795
+ # # Behaviors in the "idling" state
796
+ # vehicle.state # => "idling"
797
+ # vehicle.speed # => 20
798
+ # vehicle.rotate_driver # => true
799
+ # vehicle.driver # => "John"
800
+ # vehicle.passenger # => "Jane"
801
+ #
802
+ # As can be seen, both the +speed+ and +rotate_driver+ instance method
803
+ # implementations changed how they behave based on what the current state
804
+ # of the vehicle was.
805
+ #
806
+ # === Invalid behaviors
807
+ #
808
+ # If a specific behavior has not been defined for a state, then a
809
+ # NoMethodError exception will be raised, indicating that that method would
810
+ # not normally exist for an object with that state.
811
+ #
812
+ # Using the example from before:
813
+ #
814
+ # vehicle = Vehicle.new
815
+ # vehicle.state = 'backing_up'
816
+ # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
817
+ #
818
+ # == State-aware class methods
819
+ #
820
+ # In addition to defining scopes for instance methods that are state-aware,
821
+ # the same can be done for certain types of class methods.
822
+ #
823
+ # Some libraries have support for class-level methods that only run certain
824
+ # behaviors based on a conditions hash passed in. For example:
825
+ #
826
+ # class Vehicle < ActiveRecord::Base
827
+ # state_machine do
828
+ # ...
829
+ # state :first_gear, :second_gear, :third_gear do
830
+ # validates_presence_of :speed
831
+ # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
832
+ # end
833
+ # end
834
+ # end
835
+ #
836
+ # In the above ActiveRecord model, two validations have been defined which
837
+ # will *only* run when the Vehicle object is in one of the three states:
838
+ # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
839
+ # conditions can continue to be used.
840
+ #
841
+ # This functionality is not library-specific and can work for any class-level
842
+ # method that is defined like so:
843
+ #
844
+ # def validates_presence_of(attribute, options = {})
845
+ # ...
846
+ # end
847
+ #
848
+ # The minimum requirement is that the last argument in the method be an
849
+ # options hash which contains at least <tt>:if</tt> condition support.
850
+ def state(*names, &block)
851
+ options = names.last.is_a?(Hash) ? names.pop : {}
852
+ assert_valid_keys(options, :value, :cache, :if, :human_name)
853
+
854
+ states = add_states(names)
855
+ states.each do |state|
856
+ if options.include?(:value)
857
+ state.value = options[:value]
858
+ self.states.update(state)
859
+ end
860
+
861
+ state.human_name = options[:human_name] if options.include?(:human_name)
862
+ state.cache = options[:cache] if options.include?(:cache)
863
+ state.matcher = options[:if] if options.include?(:if)
864
+ state.context(&block) if block_given?
865
+ end
866
+
867
+ states.length == 1 ? states.first : states
868
+ end
869
+ alias_method :other_states, :state
870
+
871
+ # Gets the current value stored in the given object's attribute.
872
+ #
873
+ # For example,
874
+ #
875
+ # class Vehicle
876
+ # state_machine :initial => :parked do
877
+ # ...
878
+ # end
879
+ # end
880
+ #
881
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
882
+ # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
883
+ # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
884
+ def read(object, attribute, ivar = false)
885
+ attribute = self.attribute(attribute)
886
+ ivar ? object.instance_variable_get("@#{attribute}") : object.send(attribute)
887
+ end
888
+
889
+ # Sets a new value in the given object's attribute.
890
+ #
891
+ # For example,
892
+ #
893
+ # class Vehicle
894
+ # state_machine :initial => :parked do
895
+ # ...
896
+ # end
897
+ # end
898
+ #
899
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
900
+ # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
901
+ # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
902
+ # vehicle.state # => "idling"
903
+ # vehicle.event # => "park"
904
+ def write(object, attribute, value)
905
+ object.send("#{self.attribute(attribute)}=", value)
906
+ end
907
+
908
+ # Defines one or more events for the machine and the transitions that can
909
+ # be performed when those events are run.
910
+ #
911
+ # This method is also aliased as +on+ for improved compatibility with
912
+ # using a domain-specific language.
913
+ #
914
+ # Configuration options:
915
+ # * <tt>:human_name</tt> - The human-readable version of this event's name.
916
+ # By default, this is either defined by the integration or stringifies the
917
+ # name and converts underscores to spaces.
918
+ #
919
+ # == Instance methods
920
+ #
921
+ # The following instance methods are generated when a new event is defined
922
+ # (the "park" event is used as an example):
923
+ # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
924
+ # the current state of the object. This will *not* run validations in
925
+ # ORM integrations. To check whether an event can fire *and* passes
926
+ # validations, use event attributes (e.g. state_event) as described in the
927
+ # "Events" documentation of each ORM integration.
928
+ # * <tt>park_transition</tt> - Gets the next transition that would be
929
+ # performed if the "park" event were to be fired now on the object or nil
930
+ # if no transitions can be performed.
931
+ # * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
932
+ # transitioning from the current state to the next valid state. If the
933
+ # last argument is a boolean, it will control whether the machine's action
934
+ # gets run.
935
+ # * <tt>park!(..., run_action = true)</tt> - Fires the "park" event,
936
+ # transitioning from the current state to the next valid state. If the
937
+ # transition fails, then a StateMachine::InvalidTransition error will be
938
+ # raised. If the last argument is a boolean, it will control whether the
939
+ # machine's action gets run.
940
+ #
941
+ # With a namespace of "car", the above names map to the following methods:
942
+ # * <tt>can_park_car?</tt>
943
+ # * <tt>park_car_transition</tt>
944
+ # * <tt>park_car</tt>
945
+ # * <tt>park_car!</tt>
946
+ #
947
+ # == Defining transitions
948
+ #
949
+ # +event+ requires a block which allows you to define the possible
950
+ # transitions that can happen as a result of that event. For example,
951
+ #
952
+ # event :park, :stop do
953
+ # transition :idling => :parked
954
+ # end
955
+ #
956
+ # event :first_gear do
957
+ # transition :parked => :first_gear, :if => :seatbelt_on?
958
+ # transition :parked => same # Allow to loopback if seatbelt is off
959
+ # end
960
+ #
961
+ # See StateMachine::Event#transition for more information on
962
+ # the possible options that can be passed in.
963
+ #
964
+ # *Note* that this block is executed within the context of the actual event
965
+ # object. As a result, you will not be able to reference any class methods
966
+ # on the model without referencing the class itself. For example,
967
+ #
968
+ # class Vehicle
969
+ # def self.safe_states
970
+ # [:parked, :idling, :stalled]
971
+ # end
972
+ #
973
+ # state_machine do
974
+ # event :park do
975
+ # transition Vehicle.safe_states => :parked
976
+ # end
977
+ # end
978
+ # end
979
+ #
980
+ # == Defining additional arguments
981
+ #
982
+ # Additional arguments on event actions can be defined like so:
983
+ #
984
+ # class Vehicle
985
+ # state_machine do
986
+ # event :park do
987
+ # ...
988
+ # end
989
+ # end
990
+ #
991
+ # def park(kind = :parallel, *args)
992
+ # take_deep_breath if kind == :parallel
993
+ # super
994
+ # end
995
+ #
996
+ # def take_deep_breath
997
+ # sleep 3
998
+ # end
999
+ # end
1000
+ #
1001
+ # Note that +super+ is called instead of <tt>super(*args)</tt>. This allows
1002
+ # the entire arguments list to be accessed by transition callbacks through
1003
+ # StateMachine::Transition#args like so:
1004
+ #
1005
+ # after_transition :on => :park do |vehicle, transition|
1006
+ # kind = *transition.args
1007
+ # ...
1008
+ # end
1009
+ #
1010
+ # *Remember* that if the last argument is a boolean, it will be used as the
1011
+ # +run_action+ parameter to the event action. Using the +park+ action
1012
+ # example from above, you can might call it like so:
1013
+ #
1014
+ # vehicle.park # => Uses default args and runs machine action
1015
+ # vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
1016
+ # vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
1017
+ #
1018
+ # == Example
1019
+ #
1020
+ # class Vehicle
1021
+ # state_machine do
1022
+ # # The park, stop, and halt events will all share the given transitions
1023
+ # event :park, :stop, :halt do
1024
+ # transition [:idling, :backing_up] => :parked
1025
+ # end
1026
+ #
1027
+ # event :stop do
1028
+ # transition :first_gear => :idling
1029
+ # end
1030
+ #
1031
+ # event :ignite do
1032
+ # transition :parked => :idling
1033
+ # transition :idling => same # Allow ignite while still idling
1034
+ # end
1035
+ # end
1036
+ # end
1037
+ def event(*names, &block)
1038
+ options = names.last.is_a?(Hash) ? names.pop : {}
1039
+ assert_valid_keys(options, :human_name)
1040
+
1041
+ events = add_events(names)
1042
+ events.each do |event|
1043
+ event.human_name = options[:human_name] if options.include?(:human_name)
1044
+
1045
+ if block_given?
1046
+ event.instance_eval(&block)
1047
+ add_states(event.known_states)
1048
+ end
1049
+
1050
+ event
1051
+ end
1052
+
1053
+ events.length == 1 ? events.first : events
1054
+ end
1055
+ alias_method :on, :event
1056
+
1057
+ # Creates a callback that will be invoked *before* a transition is
1058
+ # performed so long as the given requirements match the transition.
1059
+ #
1060
+ # == The callback
1061
+ #
1062
+ # Callbacks must be defined as either an argument, in the :do option, or
1063
+ # as a block. For example,
1064
+ #
1065
+ # class Vehicle
1066
+ # state_machine do
1067
+ # before_transition :set_alarm
1068
+ # before_transition :set_alarm, all => :parked
1069
+ # before_transition all => :parked, :do => :set_alarm
1070
+ # before_transition all => :parked do |vehicle, transition|
1071
+ # vehicle.set_alarm
1072
+ # end
1073
+ # ...
1074
+ # end
1075
+ # end
1076
+ #
1077
+ # Notice that the first three callbacks are the same in terms of how the
1078
+ # methods to invoke are defined. However, using the <tt>:do</tt> can
1079
+ # provide for a more fluid DSL.
1080
+ #
1081
+ # In addition, multiple callbacks can be defined like so:
1082
+ #
1083
+ # class Vehicle
1084
+ # state_machine do
1085
+ # before_transition :set_alarm, :lock_doors, all => :parked
1086
+ # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
1087
+ # before_transition :set_alarm do |vehicle, transition|
1088
+ # vehicle.lock_doors
1089
+ # end
1090
+ # end
1091
+ # end
1092
+ #
1093
+ # Notice that the different ways of configuring methods can be mixed.
1094
+ #
1095
+ # == State requirements
1096
+ #
1097
+ # Callbacks can require that the machine be transitioning from and to
1098
+ # specific states. These requirements use a Hash syntax to map beginning
1099
+ # states to ending states. For example,
1100
+ #
1101
+ # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
1102
+ #
1103
+ # In this case, the +set_alarm+ callback will only be called if the machine
1104
+ # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
1105
+ #
1106
+ # To help define state requirements, a set of helpers are available for
1107
+ # slightly more complex matching:
1108
+ # * <tt>all</tt> - Matches every state/event in the machine
1109
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
1110
+ # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
1111
+ # * <tt>same</tt> - Matches the same state being transitioned from
1112
+ #
1113
+ # See StateMachine::MatcherHelpers for more information.
1114
+ #
1115
+ # Examples:
1116
+ #
1117
+ # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
1118
+ # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
1119
+ # before_transition all => :parked, :do => ... # Matches all states to parked
1120
+ # before_transition any => same, :do => ... # Matches every loopback
1121
+ #
1122
+ # == Event requirements
1123
+ #
1124
+ # In addition to state requirements, an event requirement can be defined so
1125
+ # that the callback is only invoked on specific events using the +on+
1126
+ # option. This can also use the same matcher helpers as the state
1127
+ # requirements.
1128
+ #
1129
+ # Examples:
1130
+ #
1131
+ # before_transition :on => :ignite, :do => ... # Matches only on ignite
1132
+ # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1133
+ # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1134
+ #
1135
+ # == Result requirements
1136
+ #
1137
+ # By default, +after_transition+ callbacks and code executed after an
1138
+ # +around_transition+ callback yields will only be run if the transition
1139
+ # was performed successfully. A transition is successful if the machine's
1140
+ # action is not configured or does not return false when it is invoked.
1141
+ # In order to include failed attempts when running an +after_transition+ or
1142
+ # +around_transition+ callback, the <tt>:include_failures</tt> option can be
1143
+ # specified like so:
1144
+ #
1145
+ # after_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
1146
+ # after_transition :do => ... # Runs only on successful attempts to transition
1147
+ #
1148
+ # around_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
1149
+ # around_transition :do => ... # Runs only on successful attempts to transition
1150
+ #
1151
+ # == Verbose Requirements
1152
+ #
1153
+ # Requirements can also be defined using verbose options rather than the
1154
+ # implicit Hash syntax and helper methods described above.
1155
+ #
1156
+ # Configuration options:
1157
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
1158
+ # are specified, then all states will match.
1159
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
1160
+ # specified, then all states will match.
1161
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
1162
+ # are specified, then all events will match.
1163
+ # * <tt>:except_from</tt> - One or more states *not* being transitioned from
1164
+ # * <tt>:except_to</tt> - One more states *not* being transitioned to
1165
+ # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
1166
+ #
1167
+ # Examples:
1168
+ #
1169
+ # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
1170
+ # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
1171
+ #
1172
+ # == Conditions
1173
+ #
1174
+ # In addition to the state/event requirements, a condition can also be
1175
+ # defined to help determine whether the callback should be invoked.
1176
+ #
1177
+ # Configuration options:
1178
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
1179
+ # callback should occur (e.g. :if => :allow_callbacks, or
1180
+ # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
1181
+ # should return or evaluate to a true or false value.
1182
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1183
+ # callback should not occur (e.g. :unless => :skip_callbacks, or
1184
+ # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
1185
+ # string should return or evaluate to a true or false value.
1186
+ #
1187
+ # Examples:
1188
+ #
1189
+ # before_transition :parked => :idling, :if => :moving?, :do => ...
1190
+ # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1191
+ #
1192
+ # === Accessing the transition
1193
+ #
1194
+ # In addition to passing the object being transitioned, the actual
1195
+ # transition describing the context (e.g. event, from, to) can be accessed
1196
+ # as well. This additional argument is only passed if the callback allows
1197
+ # for it.
1198
+ #
1199
+ # For example,
1200
+ #
1201
+ # class Vehicle
1202
+ # # Only specifies one parameter (the object being transitioned)
1203
+ # before_transition all => :parked do |vehicle|
1204
+ # vehicle.set_alarm
1205
+ # end
1206
+ #
1207
+ # # Specifies 2 parameters (object being transitioned and actual transition)
1208
+ # before_transition all => :parked do |vehicle, transition|
1209
+ # vehicle.set_alarm(transition)
1210
+ # end
1211
+ # end
1212
+ #
1213
+ # *Note* that the object in the callback will only be passed in as an
1214
+ # argument if callbacks are configured to *not* be bound to the object
1215
+ # involved. This is the default and may change on a per-integration basis.
1216
+ #
1217
+ # See StateMachine::Transition for more information about the
1218
+ # attributes available on the transition.
1219
+ #
1220
+ # == Examples
1221
+ #
1222
+ # Below is an example of a class with one state machine and various types
1223
+ # of +before+ transitions defined for it:
1224
+ #
1225
+ # class Vehicle
1226
+ # state_machine do
1227
+ # # Before all transitions
1228
+ # before_transition :update_dashboard
1229
+ #
1230
+ # # Before specific transition:
1231
+ # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
1232
+ #
1233
+ # # With conditional callback:
1234
+ # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
1235
+ #
1236
+ # # Using helpers:
1237
+ # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
1238
+ # ...
1239
+ # end
1240
+ # end
1241
+ #
1242
+ # As can be seen, any number of transitions can be created using various
1243
+ # combinations of configuration options.
1244
+ def before_transition(*args, &block)
1245
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1246
+ options[:do] = args if args.any?
1247
+ add_callback(:before, options, &block)
1248
+ end
1249
+
1250
+ # Creates a callback that will be invoked *after* a transition is
1251
+ # performed so long as the given requirements match the transition.
1252
+ #
1253
+ # See +before_transition+ for a description of the possible configurations
1254
+ # for defining callbacks.
1255
+ def after_transition(*args, &block)
1256
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1257
+ options[:do] = args if args.any?
1258
+ add_callback(:after, options, &block)
1259
+ end
1260
+
1261
+ # Creates a callback that will be invoked *around* a transition so long as
1262
+ # the given requirements match the transition.
1263
+ #
1264
+ # == The callback
1265
+ #
1266
+ # Around callbacks wrap transitions, executing code both before and after.
1267
+ # These callbacks are defined in the exact same manner as before / after
1268
+ # callbacks with the exception that the transition must be yielded to in
1269
+ # order to finish running it.
1270
+ #
1271
+ # If defining +around+ callbacks using blocks, you must yield within the
1272
+ # transition by directly calling the block (since yielding is not allowed
1273
+ # within blocks).
1274
+ #
1275
+ # For example,
1276
+ #
1277
+ # class Vehicle
1278
+ # state_machine do
1279
+ # around_transition do |block|
1280
+ # Benchmark.measure { block.call }
1281
+ # end
1282
+ #
1283
+ # around_transition do |vehicle, block|
1284
+ # logger.info "vehicle was #{state}..."
1285
+ # block.call
1286
+ # logger.info "...and is now #{state}"
1287
+ # end
1288
+ #
1289
+ # around_transition do |vehicle, transition, block|
1290
+ # logger.info "before #{transition.event}: #{vehicle.state}"
1291
+ # block.call
1292
+ # logger.info "after #{transition.event}: #{vehicle.state}"
1293
+ # end
1294
+ # end
1295
+ # end
1296
+ #
1297
+ # Notice that referencing the block is similar to doing so within an
1298
+ # actual method definition in that it is always the last argument.
1299
+ #
1300
+ # On the other hand, if you're defining +around+ callbacks using method
1301
+ # references, you can yield like normal:
1302
+ #
1303
+ # class Vehicle
1304
+ # state_machine do
1305
+ # around_transition :benchmark
1306
+ # ...
1307
+ # end
1308
+ #
1309
+ # def benchmark
1310
+ # Benchmark.measure { yield }
1311
+ # end
1312
+ # end
1313
+ #
1314
+ # See +before_transition+ for a description of the possible configurations
1315
+ # for defining callbacks.
1316
+ def around_transition(*args, &block)
1317
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1318
+ options[:do] = args if args.any?
1319
+ add_callback(:around, options, &block)
1320
+ end
1321
+
1322
+ # Marks the given object as invalid with the given message.
1323
+ #
1324
+ # By default, this is a no-op.
1325
+ def invalidate(object, attribute, message, values = [])
1326
+ end
1327
+
1328
+ # Resets any errors previously added when invalidating the given object.
1329
+ #
1330
+ # By default, this is a no-op.
1331
+ def reset(object)
1332
+ end
1333
+
1334
+ # Generates the message to use when invalidating the given object after
1335
+ # failing to transition on a specific event
1336
+ def generate_message(name, values = [])
1337
+ (@messages[name] || self.class.default_messages[name]) % values.map {|value| value.last}
1338
+ end
1339
+
1340
+ # Runs a transaction, rolling back any changes if the yielded block fails.
1341
+ #
1342
+ # This is only applicable to integrations that involve databases. By
1343
+ # default, this will not run any transactions since the changes aren't
1344
+ # taking place within the context of a database.
1345
+ def within_transaction(object)
1346
+ if use_transactions
1347
+ transaction(object) { yield }
1348
+ else
1349
+ yield
1350
+ end
1351
+ end
1352
+
1353
+ # Draws a directed graph of the machine for visualizing the various events,
1354
+ # states, and their transitions.
1355
+ #
1356
+ # This requires both the Ruby graphviz gem and the graphviz library be
1357
+ # installed on the system.
1358
+ #
1359
+ # Configuration options:
1360
+ # * <tt>:name</tt> - The name of the file to write to (without the file extension).
1361
+ # Default is "#{owner_class.name}_#{name}"
1362
+ # * <tt>:path</tt> - The path to write the graph file to. Default is the
1363
+ # current directory (".").
1364
+ # * <tt>:format</tt> - The image format to generate the graph in.
1365
+ # Default is "png'.
1366
+ # * <tt>:font</tt> - The name of the font to draw state names in.
1367
+ # Default is "Arial".
1368
+ # * <tt>:orientation</tt> - The direction of the graph ("portrait" or
1369
+ # "landscape"). Default is "portrait".
1370
+ # * <tt>:output</tt> - Whether to generate the output of the graph
1371
+ def draw(options = {})
1372
+ options = {
1373
+ :name => "#{owner_class.name}_#{name}",
1374
+ :path => '.',
1375
+ :format => 'png',
1376
+ :font => 'Arial',
1377
+ :orientation => 'portrait'
1378
+ }.merge(options)
1379
+ assert_valid_keys(options, :name, :path, :format, :font, :orientation)
1380
+
1381
+ begin
1382
+ # Load the graphviz library
1383
+ require 'rubygems'
1384
+ gem 'ruby-graphviz', '>=0.9.0'
1385
+ require 'graphviz'
1386
+
1387
+ graph = GraphViz.new('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB')
1388
+
1389
+ # Add nodes
1390
+ states.by_priority.each do |state|
1391
+ node = state.draw(graph)
1392
+ node.fontname = options[:font]
1393
+ end
1394
+
1395
+ # Add edges
1396
+ events.each do |event|
1397
+ edges = event.draw(graph)
1398
+ edges.each {|edge| edge.fontname = options[:font]}
1399
+ end
1400
+
1401
+ # Generate the graph
1402
+ graphvizVersion = Constants::RGV_VERSION.split('.')
1403
+
1404
+ if graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
1405
+ outputOptions = {
1406
+ :output => options[:format],
1407
+ :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}")
1408
+ }
1409
+ else
1410
+ outputOptions = {
1411
+ options[:format] => File.join(options[:path], "#{options[:name]}.#{options[:format]}")
1412
+ }
1413
+ end
1414
+
1415
+ graph.output(outputOptions)
1416
+ graph
1417
+ rescue LoadError
1418
+ $stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` >= v0.9.0 and try again.'
1419
+ false
1420
+ end
1421
+ end
1422
+
1423
+ # Determines whether a helper method was defined for firing attribute-based
1424
+ # event transitions when the configuration action gets called.
1425
+ def action_helper_defined?
1426
+ @action_helper_defined
1427
+ end
1428
+
1429
+ protected
1430
+ # Runs additional initialization hooks. By default, this is a no-op.
1431
+ def after_initialize
1432
+ end
1433
+
1434
+ # Adds helper methods for interacting with the state machine, including
1435
+ # for states, events, and transitions
1436
+ def define_helpers
1437
+ define_state_accessor
1438
+ define_state_predicate
1439
+ define_event_helpers
1440
+ define_action_helpers if action
1441
+ define_name_helpers
1442
+ end
1443
+
1444
+ # Defines the initial values for state machine attributes. Static values
1445
+ # are set prior to the original initialize method and dynamic values are
1446
+ # set *after* the initialize method in case it is dependent on it.
1447
+ def define_state_initializer
1448
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
1449
+ def initialize(*args)
1450
+ initialize_state_machines(:dynamic => false)
1451
+ super
1452
+ initialize_state_machines(:dynamic => true)
1453
+ end
1454
+ end_eval
1455
+ end
1456
+
1457
+ # Adds reader/writer methods for accessing the state attribute
1458
+ def define_state_accessor
1459
+ attribute = self.attribute
1460
+
1461
+ @instance_helper_module.class_eval do
1462
+ attr_accessor attribute
1463
+ end
1464
+ end
1465
+
1466
+ # Adds predicate method to the owner class for determining the name of the
1467
+ # current state
1468
+ def define_state_predicate
1469
+ define_instance_method("#{name}?") do |machine, object, state|
1470
+ machine.states.matches?(object, state)
1471
+ end
1472
+ end
1473
+
1474
+ # Adds helper methods for getting information about this state machine's
1475
+ # events
1476
+ def define_event_helpers
1477
+ # Gets the events that are allowed to fire on the current object
1478
+ define_instance_method(attribute(:events)) do |machine, object|
1479
+ machine.events.valid_for(object).map {|event| event.name}
1480
+ end
1481
+
1482
+ # Gets the next possible transitions that can be run on the current
1483
+ # object
1484
+ define_instance_method(attribute(:transitions)) do |machine, object, *args|
1485
+ machine.events.transitions_for(object, *args)
1486
+ end
1487
+
1488
+ # Add helpers for interacting with the action
1489
+ if action
1490
+ # Tracks the event / transition to invoke when the action is called
1491
+ event_attribute = attribute(:event)
1492
+ event_transition_attribute = attribute(:event_transition)
1493
+ @instance_helper_module.class_eval do
1494
+ attr_writer event_attribute
1495
+
1496
+ protected
1497
+ attr_accessor event_transition_attribute
1498
+ end
1499
+
1500
+ # Interpret non-blank events as present
1501
+ define_instance_method(attribute(:event)) do |machine, object|
1502
+ event = machine.read(object, :event, true)
1503
+ event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
1504
+ end
1505
+ end
1506
+ end
1507
+
1508
+ # Adds helper methods for automatically firing events when an action
1509
+ # is invoked
1510
+ def define_action_helpers(action_hook = self.action)
1511
+ private_action = owner_class.private_method_defined?(action_hook)
1512
+ action_defined = @action_helper_defined = owner_class.ancestors.any? do |ancestor|
1513
+ ancestor != owner_class && (ancestor.method_defined?(action_hook) || ancestor.private_method_defined?(action_hook))
1514
+ end
1515
+ action_overridden = owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
1516
+
1517
+ # Only define helper if:
1518
+ # 1. Action was originally defined somewhere other than the owner class
1519
+ # 2. It hasn't already been overridden by another machine
1520
+ if action_defined && !action_overridden
1521
+ action = self.action
1522
+ @instance_helper_module.class_eval do
1523
+ define_method(action_hook) do |*args|
1524
+ self.class.state_machines.transitions(self, action).perform { super(*args) }
1525
+ end
1526
+
1527
+ private action_hook if private_action
1528
+ end
1529
+
1530
+ true
1531
+ else
1532
+ false
1533
+ end
1534
+ end
1535
+
1536
+ # Adds helper methods for accessing naming information about states and
1537
+ # events on the owner class
1538
+ def define_name_helpers
1539
+ # Gets the humanized version of a state
1540
+ define_class_method("human_#{attribute(:name)}") do |machine, klass, state|
1541
+ machine.states.fetch(state).human_name(klass)
1542
+ end
1543
+
1544
+ # Gets the humanized version of an event
1545
+ define_class_method("human_#{attribute(:event_name)}") do |machine, klass, event|
1546
+ machine.events.fetch(event).human_name(klass)
1547
+ end
1548
+
1549
+ # Gets the state name for the current value
1550
+ define_instance_method(attribute(:name)) do |machine, object|
1551
+ machine.states.match!(object).name
1552
+ end
1553
+
1554
+ # Gets the human state name for the current value
1555
+ define_instance_method("human_#{attribute(:name)}") do |machine, object|
1556
+ machine.states.match!(object).human_name(object.class)
1557
+ end
1558
+ end
1559
+
1560
+ # Defines the with/without scope helpers for this attribute. Both the
1561
+ # singular and plural versions of the attribute are defined for each
1562
+ # scope helper. A custom plural can be specified if it cannot be
1563
+ # automatically determined by either calling +pluralize+ on the attribute
1564
+ # name or adding an "s" to the end of the name.
1565
+ def define_scopes(custom_plural = nil)
1566
+ plural = custom_plural || pluralize(name)
1567
+
1568
+ [name, plural].uniq.each do |name|
1569
+ [:with, :without].each do |kind|
1570
+ method = "#{kind}_#{name}"
1571
+
1572
+ if scope = send("create_#{kind}_scope", method)
1573
+ # Converts state names to their corresponding values so that they
1574
+ # can be looked up properly
1575
+ define_class_method(method) do |machine, klass, *states|
1576
+ values = states.flatten.map {|state| machine.states.fetch(state).value}
1577
+ scope.call(klass, values)
1578
+ end
1579
+ end
1580
+ end
1581
+ end
1582
+ end
1583
+
1584
+ # Pluralizes the given word using #pluralize (if available) or simply
1585
+ # adding an "s" to the end of the word
1586
+ def pluralize(word)
1587
+ word = word.to_s
1588
+ if word.respond_to?(:pluralize)
1589
+ word.pluralize
1590
+ else
1591
+ "#{name}s"
1592
+ end
1593
+ end
1594
+
1595
+ # Creates a scope for finding objects *with* a particular value or values
1596
+ # for the attribute.
1597
+ #
1598
+ # By default, this is a no-op.
1599
+ def create_with_scope(name)
1600
+ end
1601
+
1602
+ # Creates a scope for finding objects *without* a particular value or
1603
+ # values for the attribute.
1604
+ #
1605
+ # By default, this is a no-op.
1606
+ def create_without_scope(name)
1607
+ end
1608
+
1609
+ # Always yields
1610
+ def transaction(object)
1611
+ yield
1612
+ end
1613
+
1614
+ # Adds a new transition callback of the given type.
1615
+ def add_callback(type, options, &block)
1616
+ callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
1617
+ add_states(callback.known_states)
1618
+ callback
1619
+ end
1620
+
1621
+ # Tracks the given set of states in the list of all known states for
1622
+ # this machine
1623
+ def add_states(new_states)
1624
+ new_states.map do |new_state|
1625
+ unless state = states[new_state]
1626
+ states << state = State.new(self, new_state)
1627
+ end
1628
+
1629
+ state
1630
+ end
1631
+ end
1632
+
1633
+ # Tracks the given set of events in the list of all known events for
1634
+ # this machine
1635
+ def add_events(new_events)
1636
+ new_events.map do |new_event|
1637
+ unless event = events[new_event]
1638
+ events << event = Event.new(self, new_event)
1639
+ end
1640
+
1641
+ event
1642
+ end
1643
+ end
1644
+ end
1645
+ end