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 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.1'
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
@@ -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, _super, *args|
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, _super, *args|
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, _super, *args|
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, _super, *args|
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, ActiveModel::MassAssignmentSecurity,
274
- # ActiveModel::Observing, or ActiveModel::Validations will automatically
275
- # use the ActiveModel integration.
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 MassAssignmentSecurity Observing Validations)
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
- action = self.action
11
- define_helper(:instance, :valid?) do |machine, object, _super, *|
12
- object.class.state_machines.transitions(object, action, :after => false).perform { _super.call }
13
- end
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
- # Ensure that the attributes setter gets used to force initialization
384
- # of the state machines
385
- define_helper(:instance, :initialize) do |machine, object, _super, *args|
386
- _super.call(args.shift || {}, *args)
387
- end
388
-
389
- # Hooks in to attribute initialization to set the states *prior*
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
- end
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 filter_attributes(object, attributes)
73
- object.send(:remove_attributes_protected_from_mass_assignment, attributes)
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(:instance, :initialize) do |machine, object, _super, *args|
322
- object.class.state_machines.initialize_states(object, :attributes => args.first) { _super.call }
323
- end
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(:instance, :valid?) do |machine, object, _super, *|
345
- object.class.state_machines.transitions(object, :save, :after => false).perform { _super.call }
346
- end
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(:instance, :initialize) do |machine, object, _super, *args|
233
- object.class.state_machines.initialize_states(object, :attributes => args.first) { _super.call }
234
- end
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.5.x - 0.8.3' do
79
+ version '0.7.x - 0.8.3' do
76
80
  def self.active?
77
- !defined?(::MongoMapper::Version) || ::MongoMapper::Version <= '0.8.3'
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(:instance, :initialize) do |machine, object, _super, *args|
82
- attrs, from_db = args
83
- from_db ? _super.call : object.class.state_machines.initialize_states(object, :attributes => attrs) { _super.call }
84
- end
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
- !defined?(::MongoMapper::Version) || ::MongoMapper::Version =~ /^0\.9\./
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(:instance, :process) do |machine, object, _super, *args|
245
- if !object.instance_variable_defined?('@initialized_state_machines')
246
- object.class.state_machines.initialize_states(object, :attributes => args.first) { _super.call }
247
- object.instance_variable_set('@initialized_state_machines', true)
248
- else
249
- _super.call
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
- end
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