pluginaweek-state_machine 0.7.6

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