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 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