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