state_machine 0.9.4 → 0.10.0

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