hsume2-state_machine 1.0.1

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