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