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.
- data/CHANGELOG.rdoc +273 -0
- data/LICENSE +20 -0
- data/README.rdoc +466 -0
- data/Rakefile +98 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +429 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +189 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +251 -0
- data/lib/state_machine/event_collection.rb +113 -0
- data/lib/state_machine/extensions.rb +158 -0
- data/lib/state_machine/guard.rb +219 -0
- data/lib/state_machine/integrations.rb +68 -0
- data/lib/state_machine/integrations/active_record.rb +444 -0
- data/lib/state_machine/integrations/active_record/locale.rb +10 -0
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +325 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
- data/lib/state_machine/integrations/sequel.rb +292 -0
- data/lib/state_machine/machine.rb +1431 -0
- data/lib/state_machine/machine_collection.rb +146 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +249 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +367 -0
- data/tasks/state_machine.rake +1 -0
- data/tasks/state_machine.rb +30 -0
- data/test/classes/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +941 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +455 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +129 -0
- data/test/unit/event_collection_test.rb +293 -0
- data/test/unit/event_test.rb +605 -0
- data/test/unit/guard_test.rb +862 -0
- data/test/unit/integrations/active_record_test.rb +1001 -0
- data/test/unit/integrations/data_mapper_test.rb +694 -0
- data/test/unit/integrations/sequel_test.rb +486 -0
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +710 -0
- data/test/unit/machine_test.rb +1910 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +795 -0
- data/test/unit/transition_test.rb +1113 -0
- 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
|