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