state_machine 0.10.1 → 0.10.2
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 +9 -0
- data/Rakefile +1 -1
- data/lib/state_machine/event.rb +4 -4
- data/lib/state_machine/integrations/active_model.rb +4 -32
- data/lib/state_machine/integrations/active_model/versions.rb +5 -4
- data/lib/state_machine/integrations/active_record.rb +15 -30
- data/lib/state_machine/integrations/active_record/versions.rb +5 -2
- data/lib/state_machine/integrations/data_mapper.rb +10 -14
- data/lib/state_machine/integrations/mongo_mapper.rb +5 -16
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +20 -12
- data/lib/state_machine/integrations/mongoid.rb +71 -14
- data/lib/state_machine/integrations/sequel.rb +45 -52
- data/lib/state_machine/integrations/sequel/versions.rb +2 -10
- data/lib/state_machine/machine.rb +105 -78
- data/lib/state_machine/machine_collection.rb +18 -3
- data/lib/state_machine/state.rb +1 -1
- data/test/unit/integrations/active_model_test.rb +21 -9
- data/test/unit/integrations/active_record_test.rb +20 -13
- data/test/unit/integrations/data_mapper_test.rb +7 -2
- data/test/unit/integrations/mongo_mapper_test.rb +5 -1
- data/test/unit/integrations/mongoid_test.rb +25 -8
- data/test/unit/integrations/sequel_test.rb +10 -6
- data/test/unit/machine_collection_test.rb +40 -0
- data/test/unit/machine_test.rb +91 -154
- metadata +4 -4
data/CHANGELOG.rdoc
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
== master
|
2
2
|
|
3
|
+
== 0.10.2 / 2011-03-31
|
4
|
+
|
5
|
+
* Use more integrated state initialization hooks for ActiveRecord, Mongoid, and Sequel
|
6
|
+
* Remove mass-assignment filtering usage in all ORM integrations
|
7
|
+
* Only support official Mongoid 2.0.0 release and up (no more RC support)
|
8
|
+
* Fix attributes getting initialized more than once if different state machines use the same attribute
|
9
|
+
* Only initialize states if state is blank and blank is not a valid state
|
10
|
+
* Fix instance / class helpers failing when used with certain libraries (such as Thin)
|
11
|
+
|
3
12
|
== 0.10.1 / 2011-03-22
|
4
13
|
|
5
14
|
* Fix classes with multiple state machines failing to initialize in ActiveRecord / Mongoid / Sequel integrations
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ require 'rake/gempackagetask'
|
|
6
6
|
|
7
7
|
spec = Gem::Specification.new do |s|
|
8
8
|
s.name = 'state_machine'
|
9
|
-
s.version = '0.10.
|
9
|
+
s.version = '0.10.2'
|
10
10
|
s.platform = Gem::Platform::RUBY
|
11
11
|
s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
|
12
12
|
s.description = s.summary
|
data/lib/state_machine/event.rb
CHANGED
@@ -280,23 +280,23 @@ module StateMachine
|
|
280
280
|
# the current event
|
281
281
|
def add_actions
|
282
282
|
# Checks whether the event can be fired on the current object
|
283
|
-
machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object,
|
283
|
+
machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object, *args|
|
284
284
|
machine.event(name).can_fire?(object, *args)
|
285
285
|
end
|
286
286
|
|
287
287
|
# Gets the next transition that would be performed if the event were
|
288
288
|
# fired now
|
289
|
-
machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object,
|
289
|
+
machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object, *args|
|
290
290
|
machine.event(name).transition_for(object, *args)
|
291
291
|
end
|
292
292
|
|
293
293
|
# Fires the event
|
294
|
-
machine.define_helper(:instance, qualified_name) do |machine, object,
|
294
|
+
machine.define_helper(:instance, qualified_name) do |machine, object, *args|
|
295
295
|
machine.event(name).fire(object, *args)
|
296
296
|
end
|
297
297
|
|
298
298
|
# Fires the event, raising an exception if it fails
|
299
|
-
machine.define_helper(:instance, "#{qualified_name}!") do |machine, object,
|
299
|
+
machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, *args|
|
300
300
|
object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition.new(object, machine, name))
|
301
301
|
end
|
302
302
|
end
|
@@ -8,7 +8,6 @@ module StateMachine
|
|
8
8
|
# following features need to be included in order for the integration to be
|
9
9
|
# detected:
|
10
10
|
# * ActiveModel::Dirty
|
11
|
-
# * ActiveModel::MassAssignmentSecurity
|
12
11
|
# * ActiveModel::Observing
|
13
12
|
# * ActiveModel::Validations
|
14
13
|
#
|
@@ -17,7 +16,6 @@ module StateMachine
|
|
17
16
|
#
|
18
17
|
# class Vehicle
|
19
18
|
# include ActiveModel::Dirty
|
20
|
-
# include ActiveModel::MassAssignmentSecurity
|
21
19
|
# include ActiveModel::Observing
|
22
20
|
# include ActiveModel::Validations
|
23
21
|
#
|
@@ -270,11 +268,11 @@ module StateMachine
|
|
270
268
|
@defaults = {}
|
271
269
|
|
272
270
|
# Should this integration be used for state machines in the given class?
|
273
|
-
# Classes that include ActiveModel::Dirty,
|
274
|
-
# ActiveModel::
|
275
|
-
#
|
271
|
+
# Classes that include ActiveModel::Dirty, ActiveModel::Observing, or
|
272
|
+
# ActiveModel::Validations will automatically use the ActiveModel
|
273
|
+
# integration.
|
276
274
|
def self.matches?(klass)
|
277
|
-
features = %w(Dirty
|
275
|
+
features = %w(Dirty Observing Validations)
|
278
276
|
defined?(::ActiveModel) && features.any? {|feature| ::ActiveModel.const_defined?(feature) && klass <= ::ActiveModel.const_get(feature)}
|
279
277
|
end
|
280
278
|
|
@@ -340,13 +338,6 @@ module StateMachine
|
|
340
338
|
defined?(::ActiveModel::Dirty) && owner_class <= ::ActiveModel::Dirty && object.respond_to?("#{self.attribute}_changed?")
|
341
339
|
end
|
342
340
|
|
343
|
-
# Whether the protection of attributes via mass-assignment is supported
|
344
|
-
# in this integration. Only true if the ActiveModel feature is enabled
|
345
|
-
# on the owner class.
|
346
|
-
def supports_mass_assignment_security?
|
347
|
-
defined?(::ActiveModel::MassAssignmentSecurity) && owner_class <= ::ActiveModel::MassAssignmentSecurity
|
348
|
-
end
|
349
|
-
|
350
341
|
# Gets the terminator to use for callbacks
|
351
342
|
def callback_terminator
|
352
343
|
@terminator ||= lambda {|result| result == false}
|
@@ -357,25 +348,6 @@ module StateMachine
|
|
357
348
|
klass.i18n_scope
|
358
349
|
end
|
359
350
|
|
360
|
-
# Only allows state initialization on new records that aren't being
|
361
|
-
# created with a set of attributes that includes this machine's
|
362
|
-
# attribute.
|
363
|
-
def initialize_state?(object, options)
|
364
|
-
if supports_mass_assignment_security?
|
365
|
-
attributes = (options[:attributes] || {}).dup.stringify_keys!
|
366
|
-
ignore = filter_attributes(object, attributes).keys
|
367
|
-
!ignore.map {|attribute| attribute.to_sym}.include?(attribute)
|
368
|
-
else
|
369
|
-
super
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
# Filters attributes that cannot be assigned through the initialization
|
374
|
-
# of the object
|
375
|
-
def filter_attributes(object, attributes)
|
376
|
-
object.send(:sanitize_for_mass_assignment, attributes)
|
377
|
-
end
|
378
|
-
|
379
351
|
# The default options to use when generating messages for validation
|
380
352
|
# errors
|
381
353
|
def default_error_message_options(object, attribute, message)
|
@@ -7,10 +7,11 @@ module StateMachine
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def define_validation_hook
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
11
|
+
def valid?(*)
|
12
|
+
self.class.state_machines.transitions(self, #{action.inspect}, :after => false).perform { super }
|
13
|
+
end
|
14
|
+
end_eval
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
@@ -356,46 +356,31 @@ module StateMachine
|
|
356
356
|
end
|
357
357
|
end
|
358
358
|
|
359
|
-
# Loads extensions to ActiveRecord's Observers
|
360
|
-
def load_observer_extensions
|
361
|
-
super
|
362
|
-
::ActiveRecord::Observer.class_eval do
|
363
|
-
include StateMachine::Integrations::ActiveModel::Observer
|
364
|
-
end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
|
365
|
-
end
|
366
|
-
|
367
359
|
# Only runs validations on the action if using <tt>:save</tt>
|
368
360
|
def runs_validations_on_action?
|
369
361
|
action == :save
|
370
362
|
end
|
371
363
|
|
372
|
-
# Only allows state initialization on new records that aren't being
|
373
|
-
# created with a set of attributes that includes this machine's
|
374
|
-
# attribute.
|
375
|
-
def initialize_state?(object, options)
|
376
|
-
super if object.new_record?
|
377
|
-
end
|
378
|
-
|
379
364
|
# Defines an initialization hook into the owner class for setting the
|
380
365
|
# initial state of the machine *before* any attributes are set on the
|
381
366
|
# object
|
382
367
|
def define_state_initializer
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
# to the attributes being set
|
391
|
-
define_helper(:instance, :attributes=) do |machine, object, _super, new_attributes, *|
|
392
|
-
if !object.instance_variable_defined?('@initialized_state_machines')
|
393
|
-
object.class.state_machines.initialize_states(object, :attributes => new_attributes) { _super.call }
|
394
|
-
object.instance_variable_set('@initialized_state_machines', true)
|
395
|
-
else
|
396
|
-
_super.call
|
368
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
369
|
+
# Initializes dynamic states
|
370
|
+
def initialize(*)
|
371
|
+
super do |*args|
|
372
|
+
self.class.state_machines.initialize_states(self, :static => false)
|
373
|
+
yield(*args) if block_given?
|
374
|
+
end
|
397
375
|
end
|
398
|
-
|
376
|
+
|
377
|
+
# Initializes static states
|
378
|
+
def attributes_from_column_definition(*)
|
379
|
+
result = super
|
380
|
+
self.class.state_machines.initialize_states(self, :dynamic => false, :to => result)
|
381
|
+
result
|
382
|
+
end
|
383
|
+
end_eval
|
399
384
|
end
|
400
385
|
|
401
386
|
# Uses around callbacks to run state events if using the :save hook
|
@@ -69,8 +69,11 @@ module StateMachine
|
|
69
69
|
action == :save ? :create_or_update : super
|
70
70
|
end
|
71
71
|
|
72
|
-
def
|
73
|
-
|
72
|
+
def load_observer_extensions
|
73
|
+
super
|
74
|
+
::ActiveRecord::Observer.class_eval do
|
75
|
+
include StateMachine::Integrations::ActiveModel::Observer
|
76
|
+
end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
|
74
77
|
end
|
75
78
|
end
|
76
79
|
|
@@ -306,21 +306,15 @@ module StateMachine
|
|
306
306
|
::DataMapper::Inflector.pluralize(word.to_s)
|
307
307
|
end
|
308
308
|
|
309
|
-
# Only allows state initialization on new records that aren't being
|
310
|
-
# created with a set of attributes that includes this machine's
|
311
|
-
# attribute.
|
312
|
-
def initialize_state?(object, options)
|
313
|
-
ignore = (options[:attributes] || {}).keys
|
314
|
-
!ignore.map {|attribute| attribute.to_sym}.include?(attribute)
|
315
|
-
end
|
316
|
-
|
317
309
|
# Defines an initialization hook into the owner class for setting the
|
318
310
|
# initial state of the machine *before* any attributes are set on the
|
319
311
|
# object
|
320
312
|
def define_state_initializer
|
321
|
-
define_helper
|
322
|
-
|
323
|
-
|
313
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
314
|
+
def initialize(*args)
|
315
|
+
self.class.state_machines.initialize_states(self) { super }
|
316
|
+
end
|
317
|
+
end_eval
|
324
318
|
end
|
325
319
|
|
326
320
|
# Skips defining reader/writer methods since this is done automatically
|
@@ -341,9 +335,11 @@ module StateMachine
|
|
341
335
|
super
|
342
336
|
|
343
337
|
if action == :save && supports_validations?
|
344
|
-
define_helper
|
345
|
-
|
346
|
-
|
338
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
339
|
+
def valid?(*)
|
340
|
+
self.class.state_machines.transitions(self, :save, :after => false).perform { super }
|
341
|
+
end
|
342
|
+
end_eval
|
347
343
|
end
|
348
344
|
end
|
349
345
|
|
@@ -212,26 +212,15 @@ module StateMachine
|
|
212
212
|
action == :save
|
213
213
|
end
|
214
214
|
|
215
|
-
# MongoMapper uses its own implementation of mass-assignment security
|
216
|
-
# instead of ActiveModel's, but still has a similar enough API that it
|
217
|
-
# can get enabled
|
218
|
-
def supports_mass_assignment_security?
|
219
|
-
true
|
220
|
-
end
|
221
|
-
|
222
|
-
# Filters attributes that cannot be assigned through the initialization
|
223
|
-
# of the object
|
224
|
-
def filter_attributes(object, attributes)
|
225
|
-
object.send(:filter_protected_attrs, attributes)
|
226
|
-
end
|
227
|
-
|
228
215
|
# Defines an initialization hook into the owner class for setting the
|
229
216
|
# initial state of the machine *before* any attributes are set on the
|
230
217
|
# object
|
231
218
|
def define_state_initializer
|
232
|
-
define_helper
|
233
|
-
|
234
|
-
|
219
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
220
|
+
def initialize(*args)
|
221
|
+
self.class.state_machines.initialize_states(self) { super }
|
222
|
+
end
|
223
|
+
end_eval
|
235
224
|
end
|
236
225
|
|
237
226
|
# Skips defining reader/writer methods since this is done automatically
|
@@ -6,14 +6,18 @@ module StateMachine
|
|
6
6
|
!defined?(::MongoMapper::Plugins)
|
7
7
|
end
|
8
8
|
|
9
|
-
def initialize_state?(object, options)
|
10
|
-
attributes = options[:attributes] || {}
|
11
|
-
super unless attributes.stringify_keys.key?('_id')
|
12
|
-
end
|
13
|
-
|
14
9
|
def filter_attributes(object, attributes)
|
15
10
|
attributes
|
16
11
|
end
|
12
|
+
|
13
|
+
def define_state_initializer
|
14
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
15
|
+
def initialize(*args)
|
16
|
+
attrs, * = args
|
17
|
+
attrs && attrs.stringify_keys.key?('_id') ? super : self.class.state_machines.initialize_states(self) { super }
|
18
|
+
end
|
19
|
+
end_eval
|
20
|
+
end
|
17
21
|
end
|
18
22
|
|
19
23
|
version '0.5.x - 0.7.x' do
|
@@ -72,23 +76,27 @@ module StateMachine
|
|
72
76
|
end
|
73
77
|
end
|
74
78
|
|
75
|
-
version '0.
|
79
|
+
version '0.7.x - 0.8.3' do
|
76
80
|
def self.active?
|
77
|
-
|
81
|
+
# Only 0.8.x and up has a Version string available, so Plugins is used
|
82
|
+
# to detect when 0.7.x is active
|
83
|
+
defined?(::MongoMapper::Plugins) && (!defined?(::MongoMapper::Version) || ::MongoMapper::Version <= '0.8.3')
|
78
84
|
end
|
79
85
|
|
80
86
|
def define_state_initializer
|
81
|
-
define_helper
|
82
|
-
|
83
|
-
|
84
|
-
|
87
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
88
|
+
def initialize(*args)
|
89
|
+
attrs, from_db = args
|
90
|
+
from_db ? super : self.class.state_machines.initialize_states(self) { super }
|
91
|
+
end
|
92
|
+
end_eval
|
85
93
|
end
|
86
94
|
end
|
87
95
|
|
88
96
|
# Assumes MongoMapper 0.10+ uses ActiveModel 3.1+
|
89
97
|
version '0.9.x' do
|
90
98
|
def self.active?
|
91
|
-
|
99
|
+
defined?(::MongoMapper::Version) && ::MongoMapper::Version =~ /^0\.9\./
|
92
100
|
end
|
93
101
|
|
94
102
|
def define_action_hook
|
@@ -179,6 +179,62 @@ module StateMachine
|
|
179
179
|
#
|
180
180
|
# Note, also, that the transition can be accessed by simply defining
|
181
181
|
# additional arguments in the callback block.
|
182
|
+
#
|
183
|
+
# == Observers
|
184
|
+
#
|
185
|
+
# In addition to support for Mongoid-like hooks, there is additional support
|
186
|
+
# for Mongoid observers. Because of the way Mongoid observers are designed,
|
187
|
+
# there is less flexibility around the specific transitions that can be
|
188
|
+
# hooked in. However, a large number of hooks *are* supported. For
|
189
|
+
# example, if a transition for a record's +state+ attribute changes the
|
190
|
+
# state from +parked+ to +idling+ via the +ignite+ event, the following
|
191
|
+
# observer methods are supported:
|
192
|
+
# * before/after/after_failure_to-_ignite_from_parked_to_idling
|
193
|
+
# * before/after/after_failure_to-_ignite_from_parked
|
194
|
+
# * before/after/after_failure_to-_ignite_to_idling
|
195
|
+
# * before/after/after_failure_to-_ignite
|
196
|
+
# * before/after/after_failure_to-_transition_state_from_parked_to_idling
|
197
|
+
# * before/after/after_failure_to-_transition_state_from_parked
|
198
|
+
# * before/after/after_failure_to-_transition_state_to_idling
|
199
|
+
# * before/after/after_failure_to-_transition_state
|
200
|
+
# * before/after/after_failure_to-_transition
|
201
|
+
#
|
202
|
+
# The following class shows an example of some of these hooks:
|
203
|
+
#
|
204
|
+
# class VehicleObserver < Mongoid::Observer
|
205
|
+
# def before_save(vehicle)
|
206
|
+
# # log message
|
207
|
+
# end
|
208
|
+
#
|
209
|
+
# # Callback for :ignite event *before* the transition is performed
|
210
|
+
# def before_ignite(vehicle, transition)
|
211
|
+
# # log message
|
212
|
+
# end
|
213
|
+
#
|
214
|
+
# # Callback for :ignite event *after* the transition has been performed
|
215
|
+
# def after_ignite(vehicle, transition)
|
216
|
+
# # put on seatbelt
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# # Generic transition callback *before* the transition is performed
|
220
|
+
# def after_transition(vehicle, transition)
|
221
|
+
# Audit.log(vehicle, transition)
|
222
|
+
# end
|
223
|
+
# end
|
224
|
+
#
|
225
|
+
# More flexible transition callbacks can be defined directly within the
|
226
|
+
# model as described in StateMachine::Machine#before_transition
|
227
|
+
# and StateMachine::Machine#after_transition.
|
228
|
+
#
|
229
|
+
# To define a single observer for multiple state machines:
|
230
|
+
#
|
231
|
+
# class StateMachineObserver < Mongoid::Observer
|
232
|
+
# observe Vehicle, Switch, Project
|
233
|
+
#
|
234
|
+
# def after_transition(record, transition)
|
235
|
+
# Audit.log(record, transition)
|
236
|
+
# end
|
237
|
+
# end
|
182
238
|
module Mongoid
|
183
239
|
include Base
|
184
240
|
include ActiveModel
|
@@ -230,25 +286,26 @@ module StateMachine
|
|
230
286
|
action == :save
|
231
287
|
end
|
232
288
|
|
233
|
-
# Only allows state initialization on new records that aren't being
|
234
|
-
# created with a set of attributes that includes this machine's
|
235
|
-
# attribute.
|
236
|
-
def initialize_state?(object, options)
|
237
|
-
super if object.new_record?
|
238
|
-
end
|
239
|
-
|
240
289
|
# Defines an initialization hook into the owner class for setting the
|
241
290
|
# initial state of the machine *before* any attributes are set on the
|
242
291
|
# object
|
243
292
|
def define_state_initializer
|
244
|
-
define_helper
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
293
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
294
|
+
# Initializes dynamic states
|
295
|
+
def initialize(*)
|
296
|
+
super do |*args|
|
297
|
+
self.class.state_machines.initialize_states(self, :static => false)
|
298
|
+
yield(*args) if block_given?
|
299
|
+
end
|
250
300
|
end
|
251
|
-
|
301
|
+
|
302
|
+
# Initializes static states
|
303
|
+
def apply_default_attributes(*)
|
304
|
+
result = super
|
305
|
+
self.class.state_machines.initialize_states(self, :dynamic => false, :to => result) if new_record?
|
306
|
+
result
|
307
|
+
end
|
308
|
+
end_eval
|
252
309
|
end
|
253
310
|
|
254
311
|
# Skips defining reader/writer methods since this is done automatically
|