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