pluginaweek-state_machine 0.7.6

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