state_machine 0.9.4 → 0.10.0

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.
Files changed (68) hide show
  1. data/CHANGELOG.rdoc +20 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +74 -4
  4. data/Rakefile +3 -3
  5. data/lib/state_machine.rb +51 -24
  6. data/lib/state_machine/{guard.rb → branch.rb} +34 -40
  7. data/lib/state_machine/callback.rb +13 -18
  8. data/lib/state_machine/error.rb +13 -0
  9. data/lib/state_machine/eval_helpers.rb +3 -0
  10. data/lib/state_machine/event.rb +67 -30
  11. data/lib/state_machine/event_collection.rb +20 -3
  12. data/lib/state_machine/extensions.rb +3 -3
  13. data/lib/state_machine/integrations.rb +7 -0
  14. data/lib/state_machine/integrations/active_model.rb +149 -59
  15. data/lib/state_machine/integrations/active_model/versions.rb +30 -0
  16. data/lib/state_machine/integrations/active_record.rb +74 -148
  17. data/lib/state_machine/integrations/active_record/locale.rb +0 -7
  18. data/lib/state_machine/integrations/active_record/versions.rb +149 -0
  19. data/lib/state_machine/integrations/base.rb +64 -0
  20. data/lib/state_machine/integrations/data_mapper.rb +50 -39
  21. data/lib/state_machine/integrations/data_mapper/observer.rb +47 -12
  22. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  23. data/lib/state_machine/integrations/mongo_mapper.rb +37 -64
  24. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  25. data/lib/state_machine/integrations/mongo_mapper/versions.rb +102 -0
  26. data/lib/state_machine/integrations/mongoid.rb +297 -0
  27. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  28. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  29. data/lib/state_machine/integrations/sequel.rb +99 -55
  30. data/lib/state_machine/integrations/sequel/versions.rb +40 -0
  31. data/lib/state_machine/machine.rb +273 -136
  32. data/lib/state_machine/machine_collection.rb +21 -13
  33. data/lib/state_machine/node_collection.rb +6 -1
  34. data/lib/state_machine/path.rb +120 -0
  35. data/lib/state_machine/path_collection.rb +90 -0
  36. data/lib/state_machine/state.rb +28 -9
  37. data/lib/state_machine/state_collection.rb +1 -1
  38. data/lib/state_machine/transition.rb +65 -6
  39. data/lib/state_machine/transition_collection.rb +1 -1
  40. data/test/files/en.yml +8 -0
  41. data/test/functional/state_machine_test.rb +15 -2
  42. data/test/unit/branch_test.rb +890 -0
  43. data/test/unit/callback_test.rb +9 -36
  44. data/test/unit/error_test.rb +43 -0
  45. data/test/unit/event_collection_test.rb +67 -33
  46. data/test/unit/event_test.rb +165 -38
  47. data/test/unit/integrations/active_model_test.rb +103 -3
  48. data/test/unit/integrations/active_record_test.rb +90 -43
  49. data/test/unit/integrations/base_test.rb +87 -0
  50. data/test/unit/integrations/data_mapper_test.rb +105 -44
  51. data/test/unit/integrations/mongo_mapper_test.rb +261 -64
  52. data/test/unit/integrations/mongoid_test.rb +1529 -0
  53. data/test/unit/integrations/sequel_test.rb +33 -49
  54. data/test/unit/integrations_test.rb +4 -0
  55. data/test/unit/invalid_event_test.rb +15 -2
  56. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  57. data/test/unit/invalid_transition_test.rb +72 -2
  58. data/test/unit/machine_collection_test.rb +55 -61
  59. data/test/unit/machine_test.rb +388 -26
  60. data/test/unit/node_collection_test.rb +14 -4
  61. data/test/unit/path_collection_test.rb +266 -0
  62. data/test/unit/path_test.rb +485 -0
  63. data/test/unit/state_collection_test.rb +30 -0
  64. data/test/unit/state_test.rb +82 -35
  65. data/test/unit/transition_collection_test.rb +48 -44
  66. data/test/unit/transition_test.rb +198 -41
  67. metadata +111 -74
  68. data/test/unit/guard_test.rb +0 -909
@@ -0,0 +1,64 @@
1
+ module StateMachine
2
+ module Integrations
3
+ # Provides a set of base helpers for managing individual integrations
4
+ module Base
5
+ # Never matches
6
+ def self.matches?(klass)
7
+ false
8
+ end
9
+
10
+ def self.included(base) #:nodoc:
11
+ base.class_eval do
12
+ extend ClassMethods
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ # The default options to use for state machines using this integration
18
+ attr_reader :defaults
19
+
20
+ # Tracks the various version overrides for an integration
21
+ def versions
22
+ @versions ||= []
23
+ end
24
+
25
+ # Creates a new version override for an integration. When this
26
+ # integration is activated, each version that is marked as active will
27
+ # also extend the integration.
28
+ #
29
+ # == Example
30
+ #
31
+ # module StateMachine
32
+ # module Integrations
33
+ # module ORMLibrary
34
+ # version '0.2.x - 0.3.x' do
35
+ # def self.active?
36
+ # ::ORMLibrary::VERSION >= '0.2.0' && ::ORMLibrary::VERSION < '0.4.0'
37
+ # end
38
+ #
39
+ # def invalidate(object, attribute, message, values = [])
40
+ # # Override here...
41
+ # end
42
+ # end
43
+ # end
44
+ # end
45
+ # end
46
+ #
47
+ # In the above example, a version override is defined for the ORMLibrary
48
+ # integration when the version is between 0.2.x and 0.3.x.
49
+ def version(name, &block)
50
+ versions << mod = Module.new(&block)
51
+ mod
52
+ end
53
+
54
+ # Extends the given object with any version overrides that are currently
55
+ # active
56
+ def extended(base)
57
+ versions.each do |version|
58
+ base.extend(version) if version.active?
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -241,6 +241,10 @@ module StateMachine
241
241
  # support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
242
242
  # for more information.
243
243
  module DataMapper
244
+ include Base
245
+
246
+ require 'state_machine/integrations/data_mapper/versions'
247
+
244
248
  # The default options to use for state machines using this integration
245
249
  class << self; attr_reader :defaults; end
246
250
  @defaults = {:action => :save, :use_transactions => false}
@@ -255,28 +259,17 @@ module StateMachine
255
259
  # Loads additional files specific to DataMapper
256
260
  def self.extended(base) #:nodoc:
257
261
  require 'dm-core/version' unless ::DataMapper.const_defined?('VERSION')
258
- require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
262
+ super
259
263
  end
260
264
 
261
265
  # Forces the change in state to be recognized regardless of whether the
262
266
  # state value actually changed
263
- def write(object, attribute, value)
264
- if attribute == :state
265
- result = super
266
-
267
- # Change original attributes in 0.9.4 - 0.10.2
268
- if ::DataMapper::VERSION =~ /^0\.9\./
269
- object.original_values[self.attribute] = "#{value}-ignored" if object.original_values[self.attribute] == value
270
- elsif ::DataMapper::VERSION =~ /^0\.10\./
271
- property = owner_class.properties[self.attribute]
272
- object.original_attributes[property] = "#{value}-ignored" unless object.original_attributes.include?(property)
273
- else
274
- object.persisted_state = ::DataMapper::Resource::State::Dirty.new(object) if object.persisted_state.is_a?(::DataMapper::Resource::State::Clean)
275
- property = owner_class.properties[self.attribute]
276
- object.persisted_state.original_attributes[property] = value unless object.persisted_state.original_attributes.include?(property)
277
- end
278
- else
279
- result = super
267
+ def write(object, attribute, value, *args)
268
+ result = super
269
+
270
+ if attribute == :state || attribute == :event && value
271
+ value = read(object, :state) if attribute == :event
272
+ mark_dirty(object, value)
280
273
  end
281
274
 
282
275
  result
@@ -293,6 +286,16 @@ module StateMachine
293
286
  end
294
287
 
295
288
  protected
289
+ # Initializes class-level extensions and defaults for this machine
290
+ def after_initialize
291
+ load_observer_extensions
292
+ end
293
+
294
+ # Loads extensions to DataMapper's Observers
295
+ def load_observer_extensions
296
+ require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
297
+ end
298
+
296
299
  # Is validation support currently loaded?
297
300
  def supports_validations?
298
301
  @supports_validations ||= ::DataMapper.const_defined?('Validate')
@@ -300,21 +303,24 @@ module StateMachine
300
303
 
301
304
  # Pluralizes the name using the built-in inflector
302
305
  def pluralize(word)
303
- defined?(Extlib::Inflection) ? Extlib::Inflection.pluralize(word.to_s) : super
306
+ ::DataMapper::Inflector.pluralize(word.to_s)
307
+ end
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)
304
315
  end
305
316
 
306
317
  # Defines an initialization hook into the owner class for setting the
307
318
  # initial state of the machine *before* any attributes are set on the
308
319
  # object
309
320
  def define_state_initializer
310
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
311
- def initialize(attributes = {}, *args)
312
- ignore = attributes ? attributes.keys : []
313
- initialize_state_machines(:dynamic => false, :ignore => ignore)
314
- super
315
- initialize_state_machines(:dynamic => true, :ignore => ignore)
316
- end
317
- end_eval
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
318
324
  end
319
325
 
320
326
  # Skips defining reader/writer methods since this is done automatically
@@ -332,23 +338,20 @@ module StateMachine
332
338
 
333
339
  # Adds hooks into validation for automatically firing events
334
340
  def define_action_helpers
335
- # 0.9.4 - 0.9.6 fails to run after callbacks when validations are
336
- # enabled because of the way dm-validations integrates
337
- return if ::DataMapper::VERSION =~ /^0\.9\.[4-6]/ && supports_validations?
341
+ super
338
342
 
339
- if action == :save
340
- if super(::DataMapper::VERSION =~ /^0\.\d\./ ? :save : :save_self) && supports_validations?
341
- @instance_helper_module.class_eval do
342
- define_method(:valid?) do |*args|
343
- self.class.state_machines.transitions(self, :save, :after => false).perform { super(*args) }
344
- end
345
- end
343
+ 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
346
  end
347
- else
348
- super
349
347
  end
350
348
  end
351
349
 
350
+ # Uses internal save hooks if using the :save action
351
+ def action_hook
352
+ action == :save ? :save_self : super
353
+ end
354
+
352
355
  # Creates a scope for finding records *with* a particular state or
353
356
  # states for the attribute
354
357
  def create_with_scope(name)
@@ -374,6 +377,14 @@ module StateMachine
374
377
  options[:bind_to_object] = true
375
378
  super
376
379
  end
380
+
381
+ # Marks the object's state as dirty so that the record will be saved
382
+ # even if no actual modifications have been made to the data
383
+ def mark_dirty(object, value)
384
+ object.persisted_state = ::DataMapper::Resource::State::Dirty.new(object) if object.persisted_state.is_a?(::DataMapper::Resource::State::Clean)
385
+ property = owner_class.properties[self.attribute]
386
+ object.persisted_state.original_attributes[property] = value unless object.persisted_state.original_attributes.include?(property)
387
+ end
377
388
  end
378
389
  end
379
390
  end
@@ -1,15 +1,15 @@
1
1
  module StateMachine
2
2
  module Integrations #:nodoc:
3
3
  module DataMapper
4
- # Adds support for creating before/after transition callbacks within a
5
- # DataMapper observer. These callbacks behave very similar to
6
- # before/after hooks during save/update/destroy/etc., but with the
7
- # following modifications:
8
- # * Each callback can define a set of transition conditions (i.e. guards)
9
- # that must be met in order for the callback to get invoked.
4
+ # Adds support for creating before/after/around/failure transition
5
+ # callbacks within a DataMapper observer. These callbacks behave very
6
+ # similar to hooks during save/update/destroy/etc., but with the following
7
+ # modifications:
8
+ # * Each callback can define a set of transition requirements that must be
9
+ # met in order for the callback to get invoked.
10
10
  # * An additional transition parameter is available that provides
11
- # contextual information about the event (see StateMachine::Transition
12
- # for more information)
11
+ # contextual information about the event (see StateMachine::Transition
12
+ # for more information)
13
13
  #
14
14
  # To define a single observer for multiple state machines:
15
15
  #
@@ -91,7 +91,7 @@ module StateMachine
91
91
  # Vehicle instance being transition). This means that +self+ refers
92
92
  # to the vehicle record within each callback block.
93
93
  def before_transition(*args, &block)
94
- add_transition_callback(:before, *args, &block)
94
+ add_transition_callback(:before_transition, *args, &block)
95
95
  end
96
96
 
97
97
  # Creates a callback that will be invoked *after* a transition is
@@ -101,7 +101,7 @@ module StateMachine
101
101
  # See +before_transition+ for a description of the possible configurations
102
102
  # for defining callbacks.
103
103
  def after_transition(*args, &block)
104
- add_transition_callback(:after, *args, &block)
104
+ add_transition_callback(:after_transition, *args, &block)
105
105
  end
106
106
 
107
107
  # Creates a callback that will be invoked *around* a transition so long
@@ -137,7 +137,42 @@ module StateMachine
137
137
  # See +before_transition+ for a description of the possible configurations
138
138
  # for defining callbacks.
139
139
  def around_transition(*args, &block)
140
- add_transition_callback(:around, *args, &block)
140
+ add_transition_callback(:around_transition, *args, &block)
141
+ end
142
+
143
+ # Creates a callback that will be invoked *after* a transition failures to
144
+ # be performed so long as the given requirements match the transition.
145
+ #
146
+ # == Example
147
+ #
148
+ # class Vehicle
149
+ # include DataMapper::Resource
150
+ #
151
+ # property :id, Serial
152
+ # property :state, :String
153
+ #
154
+ # state_machine :initial => :parked do
155
+ # event :ignite do
156
+ # transition :parked => :idling
157
+ # end
158
+ # end
159
+ # end
160
+ #
161
+ # class VehicleObserver
162
+ # after_transition_failure do |transition|
163
+ # # log failure
164
+ # end
165
+ #
166
+ # after_transition_failure :on => :ignite do
167
+ # # log failure
168
+ # end
169
+ # end
170
+ #
171
+ # See +before_transition+ for a description of the possible configurations
172
+ # for defining callbacks. *Note* however that you cannot define the state
173
+ # requirements in these callbacks. You may only define event requirements.
174
+ def after_transition_failure(*args, &block)
175
+ add_transition_callback(:after_failure, *args, &block)
141
176
  end
142
177
 
143
178
  private
@@ -162,7 +197,7 @@ module StateMachine
162
197
  klass.state_machines.values
163
198
  end
164
199
 
165
- state_machines.each {|machine| machine.send("#{type}_transition", *args, &block)}
200
+ state_machines.each {|machine| machine.send(type, *args, &block)}
166
201
  end if observing
167
202
  end
168
203
  end
@@ -0,0 +1,62 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module DataMapper
4
+ version '0.9.x' do
5
+ def self.active?
6
+ ::DataMapper::VERSION =~ /^0\.9\./
7
+ end
8
+
9
+ def action_hook
10
+ action
11
+ end
12
+
13
+ def mark_dirty(object, value)
14
+ object.original_values[self.attribute] = "#{value}-ignored" if object.original_values[self.attribute] == value
15
+ end
16
+ end
17
+
18
+ version '0.9.x - 0.10.x' do
19
+ def self.active?
20
+ ::DataMapper::VERSION =~ /^0\.\d\./ || ::DataMapper::VERSION =~ /^0\.10\./
21
+ end
22
+
23
+ def pluralize(word)
24
+ ::Extlib::Inflection.pluralize(word.to_s)
25
+ end
26
+ end
27
+
28
+ version '1.0.0' do
29
+ def self.active?
30
+ ::DataMapper::VERSION == '1.0.0'
31
+ end
32
+
33
+ def pluralize(word)
34
+ (defined?(::ActiveSupport::Inflector) ? ::ActiveSupport::Inflector : ::Extlib::Inflection).pluralize(word.to_s)
35
+ end
36
+ end
37
+
38
+ version '0.9.4 - 0.9.6' do
39
+ def self.active?
40
+ ::DataMapper::VERSION =~ /^0\.9\.[4-6]/
41
+ end
42
+
43
+ # 0.9.4 - 0.9.6 fails to run after callbacks when validations are
44
+ # enabled because of the way dm-validations integrates
45
+ def define_action_helpers?
46
+ super if action != :save || !supports_validations?
47
+ end
48
+ end
49
+
50
+ version '0.10.x' do
51
+ def self.active?
52
+ ::DataMapper::VERSION =~ /^0\.10\./
53
+ end
54
+
55
+ def mark_dirty(object, value)
56
+ property = owner_class.properties[self.attribute]
57
+ object.original_attributes[property] = "#{value}-ignored" unless object.original_attributes.include?(property)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,5 @@
1
+ require 'state_machine/integrations/active_model'
2
+
1
3
  module StateMachine
2
4
  module Integrations #:nodoc:
3
5
  # Adds support for integrating state machines with MongoMapper models.
@@ -184,8 +186,11 @@ module StateMachine
184
186
  # Note, also, that the transition can be accessed by simply defining
185
187
  # additional arguments in the callback block.
186
188
  module MongoMapper
189
+ include Base
187
190
  include ActiveModel
188
191
 
192
+ require 'state_machine/integrations/mongo_mapper/versions'
193
+
189
194
  # The default options to use for state machines using this integration
190
195
  @defaults = {:action => :save}
191
196
 
@@ -196,20 +201,10 @@ module StateMachine
196
201
  defined?(::MongoMapper::Document) && klass <= ::MongoMapper::Document
197
202
  end
198
203
 
199
- # Adds a validation error to the given object (no i18n support)
200
- def invalidate(object, attribute, message, values = [])
201
- object.errors.add(self.attribute(attribute), generate_message(message, values))
202
- end
203
-
204
204
  protected
205
- # Does not support observers
206
- def supports_observers?
207
- false
208
- end
209
-
210
- # Always adds validation support
211
- def supports_validations?
212
- true
205
+ # The name of this integration
206
+ def integration
207
+ :mongo_mapper
213
208
  end
214
209
 
215
210
  # Only runs validations on the action if using <tt>:save</tt>
@@ -217,71 +212,46 @@ module StateMachine
217
212
  action == :save
218
213
  end
219
214
 
220
- # Always adds dirty tracking support
221
- def supports_dirty_tracking?(object)
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?
222
219
  true
223
220
  end
224
221
 
225
- # Don't allow callback terminators
226
- def callback_terminator
227
- end
228
-
229
- # Don't allow translations
230
- def translate(klass, key, value)
231
- value.to_s.humanize.downcase
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)
232
226
  end
233
227
 
234
228
  # Defines an initialization hook into the owner class for setting the
235
229
  # initial state of the machine *before* any attributes are set on the
236
230
  # object
237
231
  def define_state_initializer
238
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
239
- def initialize(attrs = {}, *args)
240
- from_database = args.first
241
-
242
- if !from_database && (!attrs || !attrs.stringify_keys.key?('_id'))
243
- filtered = respond_to?(:filter_protected_attrs) ? filter_protected_attrs(attrs) : attrs
244
- ignore = filtered ? filtered.keys : []
245
-
246
- initialize_state_machines(:dynamic => false, :ignore => ignore)
247
- super
248
- initialize_state_machines(:dynamic => true, :ignore => ignore)
249
- else
250
- super
251
- end
252
- end
253
- end_eval
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
254
235
  end
255
236
 
256
237
  # Skips defining reader/writer methods since this is done automatically
257
238
  def define_state_accessor
258
239
  owner_class.key(attribute, String) unless owner_class.keys.include?(attribute)
259
-
260
- name = self.name
261
- owner_class.validates_each(attribute, :logic => lambda {|*|
262
- machine = self.class.state_machine(name)
263
- machine.invalidate(self, :state, :invalid) unless machine.states.match(self)
264
- })
240
+ super
265
241
  end
266
242
 
267
- # Adds support for defining the attribute predicate, while providing
268
- # compatibility with the default predicate which determines whether
269
- # *anything* is set for the attribute's value
270
- def define_state_predicate
271
- name = self.name
272
-
273
- # Still use class_eval here instance of define_instance_method since
274
- # we need to be able to call +super+
275
- @instance_helper_module.class_eval do
276
- define_method("#{name}?") do |*args|
277
- args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
278
- end
243
+ # Uses around callbacks to run state events if using the :save hook
244
+ def define_action_hook
245
+ if action_hook == :save
246
+ owner_class.set_callback(:save, :around, self, :prepend => true)
247
+ else
248
+ super
279
249
  end
280
250
  end
281
251
 
282
- # Adds hooks into validation for automatically firing events
283
- def define_action_helpers
284
- super(action == :save ? :create_or_update : action)
252
+ # Runs state events around the machine's :save action
253
+ def around_save(object)
254
+ object.class.state_machines.transitions(object, action).perform { yield }
285
255
  end
286
256
 
287
257
  # Creates a scope for finding records *with* a particular state or
@@ -298,11 +268,14 @@ module StateMachine
298
268
 
299
269
  # Defines a new scope with the given name
300
270
  def define_scope(name, scope)
301
- if defined?(::MongoMapper::Version) && ::MongoMapper::Version >= '0.8.0'
302
- lambda {|model, values| model.query.merge(model.query(scope.call(values)))}
303
- else
304
- lambda {|model, values| model.all(scope.call(values))}
305
- end
271
+ lambda {|model, values| model.query.merge(model.query(scope.call(values)))}
272
+ end
273
+
274
+ # ActiveModel's use of method_missing / respond_to for attribute methods
275
+ # breaks both ancestor lookups and defined?(super). Need to special-case
276
+ # the existence of query attribute methods.
277
+ def owner_class_ancestor_has_method?(method)
278
+ method == "#{name}?" || super
306
279
  end
307
280
  end
308
281
  end