state_machine 0.8.1 → 0.9.0
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 +17 -0
- data/LICENSE +1 -1
- data/README.rdoc +162 -23
- data/Rakefile +3 -18
- data/lib/state_machine.rb +3 -4
- data/lib/state_machine/callback.rb +65 -13
- data/lib/state_machine/eval_helpers.rb +20 -4
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +7 -0
- data/lib/state_machine/integrations.rb +21 -6
- data/lib/state_machine/integrations/active_model.rb +414 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/{active_record → active_model}/observer.rb +7 -7
- data/lib/state_machine/integrations/active_record.rb +65 -129
- data/lib/state_machine/integrations/active_record/locale.rb +4 -11
- data/lib/state_machine/integrations/data_mapper.rb +24 -6
- data/lib/state_machine/integrations/data_mapper/observer.rb +36 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +295 -0
- data/lib/state_machine/integrations/sequel.rb +33 -7
- data/lib/state_machine/machine.rb +121 -23
- data/lib/state_machine/machine_collection.rb +12 -103
- data/lib/state_machine/transition.rb +125 -164
- data/lib/state_machine/transition_collection.rb +244 -0
- data/lib/tasks/state_machine.rb +12 -15
- data/test/functional/state_machine_test.rb +11 -1
- data/test/unit/callback_test.rb +305 -32
- data/test/unit/eval_helpers_test.rb +103 -1
- data/test/unit/event_test.rb +2 -1
- data/test/unit/guard_test.rb +2 -1
- data/test/unit/integrations/active_model_test.rb +909 -0
- data/test/unit/integrations/active_record_test.rb +1542 -1292
- data/test/unit/integrations/data_mapper_test.rb +1369 -1041
- data/test/unit/integrations/mongo_mapper_test.rb +1349 -0
- data/test/unit/integrations/sequel_test.rb +1214 -985
- data/test/unit/integrations_test.rb +8 -0
- data/test/unit/machine_collection_test.rb +140 -513
- data/test/unit/machine_test.rb +212 -10
- data/test/unit/state_test.rb +2 -1
- data/test/unit/transition_collection_test.rb +2098 -0
- data/test/unit/transition_test.rb +704 -552
- metadata +16 -3
@@ -50,15 +50,31 @@ module StateMachine
|
|
50
50
|
# evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
|
51
51
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
|
52
52
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
|
53
|
-
def evaluate_method(object, method, *args)
|
53
|
+
def evaluate_method(object, method, *args, &block)
|
54
54
|
case method
|
55
55
|
when Symbol
|
56
|
-
object.method(method).arity == 0 ? object.send(method) : object.send(method, *args)
|
56
|
+
object.method(method).arity == 0 ? object.send(method, &block) : object.send(method, *args, &block)
|
57
57
|
when Proc, Method
|
58
58
|
args.unshift(object)
|
59
|
-
|
59
|
+
arity = method.arity
|
60
|
+
limit = [0, 1].include?(arity) ? arity : args.length
|
61
|
+
|
62
|
+
# Procs don't support blocks in < Ruby 1.8.6, so it's tacked on as an
|
63
|
+
# argument for consistency across versions of Ruby (even though 1.9
|
64
|
+
# supports yielding within blocks)
|
65
|
+
if block_given? && Proc === method && arity != 0
|
66
|
+
if [1, 2].include?(arity)
|
67
|
+
limit = arity
|
68
|
+
args.insert(limit - 1, block)
|
69
|
+
else
|
70
|
+
limit += 1 unless limit < 0
|
71
|
+
args.push(block)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
method.call(*args[0, limit], &block)
|
60
76
|
when String
|
61
|
-
eval(method, object.instance_eval {binding})
|
77
|
+
eval(method, object.instance_eval {binding}, &block)
|
62
78
|
else
|
63
79
|
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
64
80
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Merb::Plugins.add_rakefiles("#{File.dirname(__FILE__)}/../../tasks/state_machine") if defined?(Merb::Plugins)
|
@@ -32,21 +32,34 @@ module StateMachine
|
|
32
32
|
# class Vehicle
|
33
33
|
# end
|
34
34
|
#
|
35
|
-
# class
|
35
|
+
# class ActiveModelVehicle
|
36
|
+
# include ActiveModel::Dirty
|
37
|
+
# include ActiveModel::Observing
|
38
|
+
# include ActiveModel::Validations
|
36
39
|
# end
|
37
40
|
#
|
38
|
-
# class
|
41
|
+
# class ActiveRecordVehicle < ActiveRecord::Base
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# class DataMapperVehicle
|
39
45
|
# include DataMapper::Resource
|
40
46
|
# end
|
41
47
|
#
|
48
|
+
# class MongoMapperVehicle
|
49
|
+
# include MongoMapper::Document
|
50
|
+
# end
|
51
|
+
#
|
42
52
|
# class SequelVehicle < Sequel::Model
|
43
53
|
# end
|
44
54
|
#
|
45
|
-
# StateMachine::Integrations.match(Vehicle)
|
46
|
-
# StateMachine::Integrations.match(
|
47
|
-
# StateMachine::Integrations.match(
|
48
|
-
# StateMachine::Integrations.match(
|
55
|
+
# StateMachine::Integrations.match(Vehicle) # => nil
|
56
|
+
# StateMachine::Integrations.match(ActiveModelVehicle) # => StateMachine::Integrations::ActiveModel
|
57
|
+
# StateMachine::Integrations.match(ActiveRecordVehicle) # => StateMachine::Integrations::ActiveRecord
|
58
|
+
# StateMachine::Integrations.match(DataMapperVehicle) # => StateMachine::Integrations::DataMapper
|
59
|
+
# StateMachine::Integrations.match(MongoMapperVehicle) # => StateMachine::Integrations::MongoMapper
|
60
|
+
# StateMachine::Integrations.match(SequelVehicle) # => StateMachine::Integrations::Sequel
|
49
61
|
def self.match(klass)
|
62
|
+
constants = self.constants.map {|c| c.to_s}.select {|c| c != 'ActiveModel'}.sort << 'ActiveModel'
|
50
63
|
if integration = constants.find {|name| const_get(name).matches?(klass)}
|
51
64
|
find(integration)
|
52
65
|
end
|
@@ -58,7 +71,9 @@ module StateMachine
|
|
58
71
|
# == Examples
|
59
72
|
#
|
60
73
|
# StateMachine::Integrations.find(:active_record) # => StateMachine::Integrations::ActiveRecord
|
74
|
+
# StateMachine::Integrations.find(:active_model) # => StateMachine::Integrations::ActiveModel
|
61
75
|
# StateMachine::Integrations.find(:data_mapper) # => StateMachine::Integrations::DataMapper
|
76
|
+
# StateMachine::Integrations.find(:mongo_mapper) # => StateMachine::Integrations::MongoMapper
|
62
77
|
# StateMachine::Integrations.find(:sequel) # => StateMachine::Integrations::Sequel
|
63
78
|
# StateMachine::Integrations.find(:invalid) # => NameError: wrong constant name Invalid
|
64
79
|
def self.find(name)
|
@@ -0,0 +1,414 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
# Adds support for integrating state machines with ActiveModel classes.
|
4
|
+
#
|
5
|
+
# == Examples
|
6
|
+
#
|
7
|
+
# If using ActiveModel directly within your class, then any one of the
|
8
|
+
# following features need to be included in order for the integration to be
|
9
|
+
# detected:
|
10
|
+
# * ActiveModel::Dirty
|
11
|
+
# * ActiveModel::Observing
|
12
|
+
# * ActiveModel::Validations
|
13
|
+
#
|
14
|
+
# Below is an example of a simple state machine defined within an
|
15
|
+
# ActiveModel class:
|
16
|
+
#
|
17
|
+
# class Vehicle
|
18
|
+
# include ActiveModel::Dirty
|
19
|
+
# include ActiveModel::Observing
|
20
|
+
# include ActiveModel::Validations
|
21
|
+
#
|
22
|
+
# attr_accessor :state
|
23
|
+
# define_attribute_methods [:state]
|
24
|
+
#
|
25
|
+
# state_machine :initial => :parked do
|
26
|
+
# event :ignite do
|
27
|
+
# transition :parked => :idling
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# The examples in the sections below will use the above class as a
|
33
|
+
# reference.
|
34
|
+
#
|
35
|
+
# == Actions
|
36
|
+
#
|
37
|
+
# By default, no action will be invoked when a state is transitioned. This
|
38
|
+
# means that if you want to save changes when transitioning, you must
|
39
|
+
# define the action yourself like so:
|
40
|
+
#
|
41
|
+
# class Vehicle
|
42
|
+
# include ActiveModel::Validations
|
43
|
+
# attr_accessor :state
|
44
|
+
#
|
45
|
+
# state_machine :action => :save do
|
46
|
+
# ...
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# def save
|
50
|
+
# # Save changes
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# == Validation errors
|
55
|
+
#
|
56
|
+
# In order to hook in validation support for your model, the
|
57
|
+
# ActiveModel::Validations feature must be included. If this is included
|
58
|
+
# and an event fails to successfully fire because there are no matching
|
59
|
+
# transitions for the object, a validation error is added to the object's
|
60
|
+
# state attribute to help in determining why it failed.
|
61
|
+
#
|
62
|
+
# For example,
|
63
|
+
#
|
64
|
+
# vehicle = Vehicle.new
|
65
|
+
# vehicle.ignite # => false
|
66
|
+
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
|
67
|
+
#
|
68
|
+
# == Callbacks
|
69
|
+
#
|
70
|
+
# All before/after transition callbacks defined for ActiveModel models
|
71
|
+
# behave in the same way that other ActiveSupport callbacks behave. The
|
72
|
+
# object involved in the transition is passed in as an argument.
|
73
|
+
#
|
74
|
+
# For example,
|
75
|
+
#
|
76
|
+
# class Vehicle
|
77
|
+
# include ActiveModel::Validations
|
78
|
+
# attr_accessor :state
|
79
|
+
#
|
80
|
+
# state_machine :initial => :parked do
|
81
|
+
# before_transition any => :idling do |vehicle|
|
82
|
+
# vehicle.put_on_seatbelt
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# before_transition do |vehicle, transition|
|
86
|
+
# # log message
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# event :ignite do
|
90
|
+
# transition :parked => :idling
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# def put_on_seatbelt
|
95
|
+
# ...
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# Note, also, that the transition can be accessed by simply defining
|
100
|
+
# additional arguments in the callback block.
|
101
|
+
#
|
102
|
+
# == Observers
|
103
|
+
#
|
104
|
+
# In order to hook in observer support for your application, the
|
105
|
+
# ActiveModel::Observing feature must be included. Because of the way
|
106
|
+
# ActiveModel observers are designed, there is less flexibility around the
|
107
|
+
# specific transitions that can be hooked in. However, a large number of
|
108
|
+
# hooks *are* supported. For example, if a transition for a object's
|
109
|
+
# +state+ attribute changes the state from +parked+ to +idling+ via the
|
110
|
+
# +ignite+ event, the following observer methods are supported:
|
111
|
+
# * before/after_ignite_from_parked_to_idling
|
112
|
+
# * before/after_ignite_from_parked
|
113
|
+
# * before/after_ignite_to_idling
|
114
|
+
# * before/after_ignite
|
115
|
+
# * before/after_transition_state_from_parked_to_idling
|
116
|
+
# * before/after_transition_state_from_parked
|
117
|
+
# * before/after_transition_state_to_idling
|
118
|
+
# * before/after_transition_state
|
119
|
+
# * before/after_transition
|
120
|
+
#
|
121
|
+
# The following class shows an example of some of these hooks:
|
122
|
+
#
|
123
|
+
# class VehicleObserver < ActiveModel::Observer
|
124
|
+
# # Callback for :ignite event *before* the transition is performed
|
125
|
+
# def before_ignite(vehicle, transition)
|
126
|
+
# # log message
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# # Callback for :ignite event *after* the transition has been performed
|
130
|
+
# def after_ignite(vehicle, transition)
|
131
|
+
# # put on seatbelt
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# # Generic transition callback *before* the transition is performed
|
135
|
+
# def after_transition(vehicle, transition)
|
136
|
+
# Audit.log(vehicle, transition)
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# More flexible transition callbacks can be defined directly within the
|
141
|
+
# model as described in StateMachine::Machine#before_transition
|
142
|
+
# and StateMachine::Machine#after_transition.
|
143
|
+
#
|
144
|
+
# To define a single observer for multiple state machines:
|
145
|
+
#
|
146
|
+
# class StateMachineObserver < ActiveModel::Observer
|
147
|
+
# observe Vehicle, Switch, Project
|
148
|
+
#
|
149
|
+
# def after_transition(object, transition)
|
150
|
+
# Audit.log(object, transition)
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# == Dirty Attribute Tracking
|
155
|
+
#
|
156
|
+
# In order to hook in validation support for your model, the
|
157
|
+
# ActiveModel::Validations feature must be included. If this is included
|
158
|
+
# then state attributes will always be properly marked as changed whether
|
159
|
+
# they were a callback or not.
|
160
|
+
#
|
161
|
+
# For example,
|
162
|
+
#
|
163
|
+
# class Vehicle
|
164
|
+
# include ActiveModel::Dirty
|
165
|
+
# attr_accessor :state
|
166
|
+
#
|
167
|
+
# state_machine :initial => :parked do
|
168
|
+
# event :park do
|
169
|
+
# transition :parked => :parked
|
170
|
+
# end
|
171
|
+
# end
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# vehicle = Vehicle.new
|
175
|
+
# vehicle.changed # => []
|
176
|
+
# vehicle.park # => true
|
177
|
+
# vehicle.changed # => ["state"]
|
178
|
+
#
|
179
|
+
# == Creating new integrations
|
180
|
+
#
|
181
|
+
# If you want to integrate state_machine with an ORM that implements parts
|
182
|
+
# or all of the ActiveModel API, the following features must be specified:
|
183
|
+
# * i18n scope (locale)
|
184
|
+
# * Machine defaults
|
185
|
+
#
|
186
|
+
# For example,
|
187
|
+
#
|
188
|
+
# module StateMachine::Integrations::MyORM
|
189
|
+
# include StateMachine::Integrations::ActiveModel
|
190
|
+
#
|
191
|
+
# @defaults = {:action = > :persist}
|
192
|
+
#
|
193
|
+
# def self.matches?(klass)
|
194
|
+
# defined?(::MyORM::Base) && klass <= ::MyORM::Base
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# def self.extended(base)
|
198
|
+
# locale = "#{File.dirname(__FILE__)}/my_orm/locale.rb"
|
199
|
+
# I18n.load_path << locale unless I18n.load_path.include?(locale)
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# protected
|
203
|
+
# def runs_validation_on_action?
|
204
|
+
# action == :persist
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# def i18n_scope
|
208
|
+
# :myorm
|
209
|
+
# end
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# If you wish to implement other features, such as attribute initialization
|
213
|
+
# with protected attributes, named scopes, or database transactions, you
|
214
|
+
# must add these independent of the ActiveModel integration. See the
|
215
|
+
# ActiveRecord implementation for examples of these customizations.
|
216
|
+
module ActiveModel
|
217
|
+
module ClassMethods
|
218
|
+
# The default options to use for state machines using this integration
|
219
|
+
attr_reader :defaults
|
220
|
+
|
221
|
+
# Loads additional files specific to ActiveModel
|
222
|
+
def extended(base) #:nodoc:
|
223
|
+
require 'state_machine/integrations/active_model/observer'
|
224
|
+
|
225
|
+
if Object.const_defined?(:I18n)
|
226
|
+
locale = "#{File.dirname(__FILE__)}/active_model/locale.rb"
|
227
|
+
I18n.load_path << locale unless I18n.load_path.include?(locale)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def self.included(base) #:nodoc:
|
233
|
+
base.class_eval do
|
234
|
+
extend ClassMethods
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
extend ClassMethods
|
239
|
+
|
240
|
+
# Should this integration be used for state machines in the given class?
|
241
|
+
# Classes that include ActiveModel::Dirty, ActiveModel::Observing, or
|
242
|
+
# ActiveModel::Validations will automatically use the ActiveModel
|
243
|
+
# integration.
|
244
|
+
def self.matches?(klass)
|
245
|
+
features = %w(Dirty Observing Validations)
|
246
|
+
defined?(::ActiveModel) && features.any? {|feature| ::ActiveModel.const_defined?(feature) && klass <= ::ActiveModel.const_get(feature)}
|
247
|
+
end
|
248
|
+
|
249
|
+
@defaults = {}
|
250
|
+
|
251
|
+
# Forces the change in state to be recognized regardless of whether the
|
252
|
+
# state value actually changed
|
253
|
+
def write(object, attribute, value)
|
254
|
+
result = super
|
255
|
+
if attribute == :state && supports_dirty_tracking?(object) && !object.send("#{self.attribute}_changed?")
|
256
|
+
object.send("#{self.attribute}_will_change!")
|
257
|
+
end
|
258
|
+
result
|
259
|
+
end
|
260
|
+
|
261
|
+
# Adds a validation error to the given object
|
262
|
+
def invalidate(object, attribute, message, values = [])
|
263
|
+
if supports_validations?
|
264
|
+
attribute = self.attribute(attribute)
|
265
|
+
ancestors = ancestors_for(object.class)
|
266
|
+
|
267
|
+
options = values.inject({}) do |options, (key, value)|
|
268
|
+
# Generate all possible translation keys
|
269
|
+
group = key.to_s.pluralize
|
270
|
+
translations = ancestors.map {|ancestor| :"#{ancestor.model_name.underscore}.#{name}.#{group}.#{value}"}
|
271
|
+
translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.to_s])
|
272
|
+
|
273
|
+
options[key] = I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope, :state_machines])
|
274
|
+
options
|
275
|
+
end
|
276
|
+
|
277
|
+
object.errors.add(attribute, message, options.merge(:default => @messages[message]))
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Resets any errors previously added when invalidating the given object
|
282
|
+
def reset(object)
|
283
|
+
object.errors.clear if supports_validations?
|
284
|
+
end
|
285
|
+
|
286
|
+
protected
|
287
|
+
# Whether observers are supported in the integration. Only true if
|
288
|
+
# ActiveModel::Observer is available.
|
289
|
+
def supports_observers?
|
290
|
+
defined?(::ActiveModel::Observing) && owner_class <= ::ActiveModel::Observing
|
291
|
+
end
|
292
|
+
|
293
|
+
# Whether validations are supported in the integration. Only true if
|
294
|
+
# the ActiveModel feature is enabled on the owner class.
|
295
|
+
def supports_validations?
|
296
|
+
defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
|
297
|
+
end
|
298
|
+
|
299
|
+
# Do validations run when the action configured this machine is
|
300
|
+
# invoked? This is used to determine whether to fire off attribute-based
|
301
|
+
# event transitions when the action is run.
|
302
|
+
def runs_validations_on_action?
|
303
|
+
false
|
304
|
+
end
|
305
|
+
|
306
|
+
# Whether change (dirty) tracking is supported in the integration.
|
307
|
+
# Only true if the ActiveModel feature is enabled on the owner class.
|
308
|
+
def supports_dirty_tracking?(object)
|
309
|
+
defined?(::ActiveModel::Dirty) && owner_class <= ::ActiveModel::Dirty && object.respond_to?("#{self.attribute}_changed?")
|
310
|
+
end
|
311
|
+
|
312
|
+
# Determines the base scope to use when looking up translations
|
313
|
+
def i18n_scope
|
314
|
+
owner_class.i18n_scope
|
315
|
+
end
|
316
|
+
|
317
|
+
# Build a list of ancestors for the given class to use when
|
318
|
+
# determining which localization key to use for a particular string.
|
319
|
+
def ancestors_for(klass)
|
320
|
+
klass.lookup_ancestors
|
321
|
+
end
|
322
|
+
|
323
|
+
# Gets the terminator to use for callbacks
|
324
|
+
def callback_terminator
|
325
|
+
@terminator ||= lambda {|result| result == false}
|
326
|
+
end
|
327
|
+
|
328
|
+
# Adds the default callbacks for notifying ActiveModel observers
|
329
|
+
# before/after a transition has been performed.
|
330
|
+
def after_initialize
|
331
|
+
if supports_observers?
|
332
|
+
callbacks[:before] << Callback.new(:before) {|object, transition| notify(:before, object, transition)}
|
333
|
+
callbacks[:after] << Callback.new(:after) {|object, transition| notify(:after, object, transition)}
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Skips defining reader/writer methods since this is done automatically
|
338
|
+
def define_state_accessor
|
339
|
+
name = self.name
|
340
|
+
|
341
|
+
owner_class.validates_each(attribute) do |object, attr, value|
|
342
|
+
machine = object.class.state_machine(name)
|
343
|
+
machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
|
344
|
+
end if supports_validations?
|
345
|
+
end
|
346
|
+
|
347
|
+
# Adds hooks into validation for automatically firing events
|
348
|
+
def define_action_helpers(*args)
|
349
|
+
super
|
350
|
+
|
351
|
+
action = self.action
|
352
|
+
@instance_helper_module.class_eval do
|
353
|
+
define_method(:valid?) do |*args|
|
354
|
+
self.class.state_machines.transitions(self, action, :after => false).perform { super(*args) }
|
355
|
+
end
|
356
|
+
end if runs_validations_on_action?
|
357
|
+
end
|
358
|
+
|
359
|
+
# Creates a new callback in the callback chain, always inserting it
|
360
|
+
# before the default Observer callbacks that were created after
|
361
|
+
# initialization.
|
362
|
+
def add_callback(type, options, &block)
|
363
|
+
options[:terminator] = callback_terminator
|
364
|
+
|
365
|
+
if supports_observers?
|
366
|
+
@callbacks[type == :around ? :before : type].insert(-2, callback = Callback.new(type, options, &block))
|
367
|
+
add_states(callback.known_states)
|
368
|
+
callback
|
369
|
+
else
|
370
|
+
super
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
private
|
375
|
+
# Notifies observers on the given object that a callback occurred
|
376
|
+
# involving the given transition. This will attempt to call the
|
377
|
+
# following methods on observers:
|
378
|
+
# * #{type}_#{qualified_event}_from_#{from}_to_#{to}
|
379
|
+
# * #{type}_#{qualified_event}_from_#{from}
|
380
|
+
# * #{type}_#{qualified_event}_to_#{to}
|
381
|
+
# * #{type}_#{qualified_event}
|
382
|
+
# * #{type}_transition_#{machine_name}_from_#{from}_to_#{to}
|
383
|
+
# * #{type}_transition_#{machine_name}_from_#{from}
|
384
|
+
# * #{type}_transition_#{machine_name}_to_#{to}
|
385
|
+
# * #{type}_transition_#{machine_name}
|
386
|
+
# * #{type}_transition
|
387
|
+
#
|
388
|
+
# This will always return true regardless of the results of the
|
389
|
+
# callbacks.
|
390
|
+
def notify(type, object, transition)
|
391
|
+
name = self.name
|
392
|
+
event = transition.qualified_event
|
393
|
+
from = transition.from_name
|
394
|
+
to = transition.to_name
|
395
|
+
|
396
|
+
# Machine-specific updates
|
397
|
+
["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
|
398
|
+
["_from_#{from}", nil].each do |from_segment|
|
399
|
+
["_to_#{to}", nil].each do |to_segment|
|
400
|
+
object.class.changed
|
401
|
+
object.class.notify_observers([event_segment, from_segment, to_segment].join, object, transition)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# Generic updates
|
407
|
+
object.class.changed
|
408
|
+
object.class.notify_observers("#{type}_transition", object, transition)
|
409
|
+
|
410
|
+
true
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|