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,4 @@
1
+ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
+ translations = eval(IO.read(filename), binding, filename)
3
+ translations[:en][:mongo_mapper] = translations[:en].delete(:activemodel)
4
+ translations
@@ -0,0 +1,102 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module MongoMapper
4
+ version '0.5.x - 0.6.x' do
5
+ def self.active?
6
+ !defined?(::MongoMapper::Plugins)
7
+ end
8
+
9
+ def initialize_state?(object, options)
10
+ attributes = options[:attributes] || {}
11
+ super unless attributes.stringify_keys.key?('_id')
12
+ end
13
+
14
+ def filter_attributes(object, attributes)
15
+ attributes
16
+ end
17
+ end
18
+
19
+ version '0.5.x - 0.7.x' do
20
+ def self.active?
21
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version < '0.8.0'
22
+ end
23
+
24
+ def define_scope(name, scope)
25
+ lambda {|model, values| model.all(scope.call(values))}
26
+ end
27
+ end
28
+
29
+ version '0.5.x - 0.8.x' do
30
+ def self.active?
31
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version < '0.9.0'
32
+ end
33
+
34
+ def invalidate(object, attribute, message, values = [])
35
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
36
+ end
37
+
38
+ def define_state_accessor
39
+ owner_class.key(attribute, String) unless owner_class.keys.include?(attribute)
40
+
41
+ name = self.name
42
+ owner_class.validates_each(attribute, :logic => lambda {|*|
43
+ machine = self.class.state_machine(name)
44
+ machine.invalidate(self, :state, :invalid) unless machine.states.match(self)
45
+ })
46
+ end
47
+
48
+ def action_hook
49
+ action == :save ? :create_or_update : super
50
+ end
51
+
52
+ def load_locale
53
+ end
54
+
55
+ def supports_observers?
56
+ false
57
+ end
58
+
59
+ def supports_validations?
60
+ true
61
+ end
62
+
63
+ def supports_dirty_tracking?(object)
64
+ true
65
+ end
66
+
67
+ def callback_terminator
68
+ end
69
+
70
+ def translate(klass, key, value)
71
+ value.to_s.humanize.downcase
72
+ end
73
+ end
74
+
75
+ version '0.5.x - 0.8.3' do
76
+ def self.active?
77
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version <= '0.8.3'
78
+ end
79
+
80
+ 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
85
+ end
86
+ end
87
+
88
+ # Assumes MongoMapper 0.10+ uses ActiveModel 3.1+
89
+ version '0.9.x' do
90
+ def self.active?
91
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version =~ /^0\.9\./
92
+ end
93
+
94
+ def define_action_hook
95
+ # +around+ callbacks don't have direct access to results until AS 3.1
96
+ owner_class.set_callback(:save, :after, 'value', :prepend => true) if action_hook == :save
97
+ super
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,297 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with Mongoid models.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # Mongoid model:
9
+ #
10
+ # class Vehicle
11
+ # include Mongoid::Document
12
+ #
13
+ # state_machine :initial => :parked do
14
+ # event :ignite do
15
+ # transition :parked => :idling
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # The examples in the sections below will use the above class as a
21
+ # reference.
22
+ #
23
+ # == Actions
24
+ #
25
+ # By default, the action that will be invoked when a state is transitioned
26
+ # is the +save+ action. This will cause the record to save the changes
27
+ # made to the state machine's attribute. *Note* that if any other changes
28
+ # were made to the record prior to transition, then those changes will
29
+ # be saved as well.
30
+ #
31
+ # For example,
32
+ #
33
+ # vehicle = Vehicle.create # => #<Vehicle _id: 4d70e028b876bb54d9000003, name: nil, state: "parked">
34
+ # vehicle.name = 'Ford Explorer'
35
+ # vehicle.ignite # => true
36
+ # vehicle.reload # => #<Vehicle _id: 4d70e028b876bb54d9000003, name: "Ford Explorer", state: "idling">
37
+ #
38
+ # == Events
39
+ #
40
+ # As described in StateMachine::InstanceMethods#state_machine, event
41
+ # attributes are created for every machine that allow transitions to be
42
+ # performed automatically when the object's action (in this case, :save)
43
+ # is called.
44
+ #
45
+ # In Mongoid, these automated events are run in the following order:
46
+ # * before validation - Run before callbacks and persist new states, then validate
47
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
48
+ # * after save - Run after callbacks
49
+ #
50
+ # For example,
51
+ #
52
+ # vehicle = Vehicle.create # => #<Vehicle _id: 4d70e028b876bb54d9000003, name: nil, state: "parked">
53
+ # vehicle.state_event # => nil
54
+ # vehicle.state_event = 'invalid'
55
+ # vehicle.valid? # => false
56
+ # vehicle.errors.full_messages # => ["State event is invalid"]
57
+ #
58
+ # vehicle.state_event = 'ignite'
59
+ # vehicle.valid? # => true
60
+ # vehicle.save # => true
61
+ # vehicle.state # => "idling"
62
+ # vehicle.state_event # => nil
63
+ #
64
+ # Note that this can also be done on a mass-assignment basis:
65
+ #
66
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle _id: 4d70e028b876bb54d9000003, name: nil, state: "idling">
67
+ # vehicle.state # => "idling"
68
+ #
69
+ # This technique is always used for transitioning states when the +save+
70
+ # action (which is the default) is configured for the machine.
71
+ #
72
+ # === Security implications
73
+ #
74
+ # Beware that public event attributes mean that events can be fired
75
+ # whenever mass-assignment is being used. If you want to prevent malicious
76
+ # users from tampering with events through URLs / forms, the attribute
77
+ # should be protected like so:
78
+ #
79
+ # class Vehicle
80
+ # include Mongoid::Document
81
+ #
82
+ # attr_protected :state_event
83
+ # # attr_accessible ... # Alternative technique
84
+ #
85
+ # state_machine do
86
+ # ...
87
+ # end
88
+ # end
89
+ #
90
+ # If you want to only have *some* events be able to fire via mass-assignment,
91
+ # you can build two state machines (one public and one protected) like so:
92
+ #
93
+ # class Vehicle
94
+ # include Mongoid::Document
95
+ #
96
+ # attr_protected :state_event # Prevent access to events in the first machine
97
+ #
98
+ # state_machine do
99
+ # # Define private events here
100
+ # end
101
+ #
102
+ # # Public machine targets the same state as the private machine
103
+ # state_machine :public_state, :attribute => :state do
104
+ # # Define public events here
105
+ # end
106
+ # end
107
+ #
108
+ # == Validation errors
109
+ #
110
+ # If an event fails to successfully fire because there are no matching
111
+ # transitions for the current record, a validation error is added to the
112
+ # record's state attribute to help in determining why it failed and for
113
+ # reporting via the UI.
114
+ #
115
+ # For example,
116
+ #
117
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle _id: 4d70e028b876bb54d9000003, name: nil, state: "idling">
118
+ # vehicle.ignite # => false
119
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
120
+ #
121
+ # If an event fails to fire because of a validation error on the record and
122
+ # *not* because a matching transition was not available, no error messages
123
+ # will be added to the state attribute.
124
+ #
125
+ # == Scopes
126
+ #
127
+ # To assist in filtering models with specific states, a series of basic
128
+ # scopes are defined on the model for finding records with or without a
129
+ # particular set of states.
130
+ #
131
+ # These scopes are essentially the functional equivalent of the following
132
+ # definitions:
133
+ #
134
+ # class Vehicle
135
+ # include Mongoid::Document
136
+ #
137
+ # scope :with_states, lambda {|*states| where(:state => {'$in' => states})}
138
+ # # with_states also aliased to with_state
139
+ #
140
+ # scope :without_states, lambda {|*states| where(:state => {'$nin' => states})}
141
+ # # without_states also aliased to without_state
142
+ # end
143
+ #
144
+ # *Note*, however, that the states are converted to their stored values
145
+ # before being passed into the query.
146
+ #
147
+ # Because of the way named scopes work in Mongoid, they *cannot* be
148
+ # chained.
149
+ #
150
+ # == Callbacks
151
+ #
152
+ # All before/after transition callbacks defined for Mongoid models
153
+ # behave in the same way that other Mongoid callbacks behave. The
154
+ # object involved in the transition is passed in as an argument.
155
+ #
156
+ # For example,
157
+ #
158
+ # class Vehicle
159
+ # include Mongoid::Document
160
+ #
161
+ # state_machine :initial => :parked do
162
+ # before_transition any => :idling do |vehicle|
163
+ # vehicle.put_on_seatbelt
164
+ # end
165
+ #
166
+ # before_transition do |vehicle, transition|
167
+ # # log message
168
+ # end
169
+ #
170
+ # event :ignite do
171
+ # transition :parked => :idling
172
+ # end
173
+ # end
174
+ #
175
+ # def put_on_seatbelt
176
+ # ...
177
+ # end
178
+ # end
179
+ #
180
+ # Note, also, that the transition can be accessed by simply defining
181
+ # additional arguments in the callback block.
182
+ module Mongoid
183
+ include Base
184
+ include ActiveModel
185
+
186
+ require 'state_machine/integrations/mongoid/versions'
187
+
188
+ # The default options to use for state machines using this integration
189
+ @defaults = {:action => :save}
190
+
191
+ # Should this integration be used for state machines in the given class?
192
+ # Classes that include Mongoid::Document will automatically use the
193
+ # Mongoid integration.
194
+ def self.matches?(klass)
195
+ defined?(::Mongoid::Document) && klass <= ::Mongoid::Document
196
+ end
197
+
198
+ def self.extended(base) #:nodoc:
199
+ require 'mongoid/version'
200
+ super
201
+ end
202
+
203
+ # Forces the change in state to be recognized regardless of whether the
204
+ # state value actually changed
205
+ def write(object, attribute, value, *args)
206
+ result = super
207
+
208
+ if (attribute == :state || attribute == :event && value) && !object.send("#{self.attribute}_changed?")
209
+ current = read(object, :state)
210
+ object.changes[self.attribute.to_s] = [attribute == :event ? current : value, current]
211
+ end
212
+
213
+ result
214
+ end
215
+
216
+ protected
217
+ # The name of this integration
218
+ def integration
219
+ :mongoid
220
+ end
221
+
222
+ # Mongoid uses its own implementation of dirty tracking instead of
223
+ # ActiveModel's and doesn't support the #{attribute}_will_change! APIs
224
+ def supports_dirty_tracking?(object)
225
+ false
226
+ end
227
+
228
+ # Only runs validations on the action if using <tt>:save</tt>
229
+ def runs_validations_on_action?
230
+ action == :save
231
+ end
232
+
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
+ if object.new_record? && !object.instance_variable_defined?('@initialized_state_machines')
238
+ object.instance_variable_set('@initialized_state_machines', true)
239
+ super
240
+ end
241
+ end
242
+
243
+ # Defines an initialization hook into the owner class for setting the
244
+ # initial state of the machine *before* any attributes are set on the
245
+ # object
246
+ def define_state_initializer
247
+ define_helper(:instance, :process) do |machine, object, _super, *args|
248
+ object.class.state_machines.initialize_states(object, :attributes => args.first) { _super.call }
249
+ end
250
+ end
251
+
252
+ # Skips defining reader/writer methods since this is done automatically
253
+ def define_state_accessor
254
+ owner_class.field(attribute, :type => String) unless owner_class.fields.include?(attribute)
255
+ super
256
+ end
257
+
258
+ # Uses around callbacks to run state events if using the :save hook
259
+ def define_action_hook
260
+ if action_hook == :save
261
+ owner_class.set_callback(:save, :around, self, :prepend => true)
262
+ else
263
+ super
264
+ end
265
+ end
266
+
267
+ # Runs state events around the machine's :save action
268
+ def around_save(object)
269
+ object.class.state_machines.transitions(object, action).perform { yield }
270
+ end
271
+
272
+ # Creates a scope for finding records *with* a particular state or
273
+ # states for the attribute
274
+ def create_with_scope(name)
275
+ define_scope(name, lambda {|values| {attribute => {'$in' => values}}})
276
+ end
277
+
278
+ # Creates a scope for finding records *without* a particular state or
279
+ # states for the attribute
280
+ def create_without_scope(name)
281
+ define_scope(name, lambda {|values| {attribute => {'$nin' => values}}})
282
+ end
283
+
284
+ # Defines a new scope with the given name
285
+ def define_scope(name, scope)
286
+ lambda {|model, values| model.criteria.where(scope.call(values))}
287
+ end
288
+
289
+ # ActiveModel's use of method_missing / respond_to for attribute methods
290
+ # breaks both ancestor lookups and defined?(super). Need to special-case
291
+ # the existence of query attribute methods.
292
+ def owner_class_ancestor_has_method?(method)
293
+ method == "#{name}?" || super
294
+ end
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,4 @@
1
+ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
+ translations = eval(IO.read(filename), binding, filename)
3
+ translations[:en][:mongoid] = translations[:en].delete(:activemodel)
4
+ translations
@@ -0,0 +1,18 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module Mongoid
4
+ # Assumes Mongoid 2.1+ uses ActiveModel 3.1+
5
+ version '2.0.x' do
6
+ def self.active?
7
+ ::Mongoid::VERSION >= '2.0.0' && ::Mongoid::VERSION < '2.1.0'
8
+ end
9
+
10
+ def define_action_hook
11
+ # +around+ callbacks don't have direct access to results until AS 3.1
12
+ owner_class.set_callback(:save, :after, 'value', :prepend => true) if action_hook == :save
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -216,6 +216,10 @@ module StateMachine
216
216
  # Note, also, that the transition can be accessed by simply defining
217
217
  # additional arguments in the callback block.
218
218
  module Sequel
219
+ include Base
220
+
221
+ require 'state_machine/integrations/sequel/versions'
222
+
219
223
  # The default options to use for state machines using this integration
220
224
  class << self; attr_reader :defaults; end
221
225
  @defaults = {:action => :save}
@@ -227,17 +231,16 @@ module StateMachine
227
231
  defined?(::Sequel::Model) && klass <= ::Sequel::Model
228
232
  end
229
233
 
230
- # Loads additional files specific to Sequel
231
- def self.extended(base) #:nodoc:
232
- require 'sequel/extensions/inflector' if ::Sequel.const_defined?('VERSION') && ::Sequel::VERSION >= '2.12.0'
233
- end
234
-
235
234
  # Forces the change in state to be recognized regardless of whether the
236
235
  # state value actually changed
237
- def write(object, attribute, value)
236
+ def write(object, attribute, value, *args)
238
237
  result = super
238
+
239
239
  column = self.attribute.to_sym
240
- object.changed_columns << column if attribute == :state && owner_class.columns.include?(column) && !object.changed_columns.include?(column)
240
+ if (attribute == :state || attribute == :event && value) && owner_class.columns.include?(column) && !object.changed_columns.include?(column)
241
+ object.changed_columns << column
242
+ end
243
+
241
244
  result
242
245
  end
243
246
 
@@ -251,28 +254,40 @@ module StateMachine
251
254
  object.errors.clear
252
255
  end
253
256
 
257
+ # Pluralizes the name using the built-in inflector
258
+ def pluralize(word)
259
+ load_inflector
260
+ super
261
+ end
262
+
254
263
  protected
264
+ # Loads the built-in inflector
265
+ def load_inflector
266
+ require 'sequel/extensions/inflector'
267
+ end
268
+
269
+ # Only allows state initialization on new records that aren't being
270
+ # created with a set of attributes that includes this machine's
271
+ # attribute.
272
+ def initialize_state?(object, options)
273
+ if object.new? && !object.instance_variable_defined?('@initialized_state_machines')
274
+ object.instance_variable_set('@initialized_state_machines', true)
275
+
276
+ attributes = options[:attributes] || {}
277
+ ignore = object.send(:setter_methods, nil, nil).map {|setter| setter.chop.to_sym} & attributes.keys.map {|key| key.to_sym}
278
+ !ignore.map {|attribute| attribute.to_sym}.include?(attribute)
279
+ end
280
+ end
281
+
255
282
  # Defines an initialization hook into the owner class for setting the
256
283
  # initial state of the machine *before* any attributes are set on the
257
284
  # object
258
285
  def define_state_initializer
259
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
260
- # Hooks in to attribute initialization to set the states *prior*
261
- # to the attributes being set
262
- def set(hash, *args)
263
- if new? && !@initialized_state_machines
264
- @initialized_state_machines = true
265
-
266
- ignore = setter_methods(nil, nil).map {|setter| setter.chop.to_sym} & (hash ? hash.keys.map {|attribute| attribute.to_sym} : [])
267
- initialize_state_machines(:dynamic => false, :ignore => ignore)
268
- result = super
269
- initialize_state_machines(:dynamic => true, :ignore => ignore)
270
- result
271
- else
272
- super
273
- end
274
- end
275
- end_eval
286
+ # Hooks in to attribute initialization to set the states *prior* to
287
+ # the attributes being set
288
+ define_helper(:instance, :set) do |machine, object, _super, *args|
289
+ object.class.state_machines.initialize_states(object, :attributes => args.first) { _super.call }
290
+ end
276
291
  end
277
292
 
278
293
  # Skips defining reader/writer methods since this is done automatically
@@ -286,45 +301,74 @@ module StateMachine
286
301
 
287
302
  # Adds hooks into validation for automatically firing events. This is
288
303
  # a bit more complicated than other integrations since Sequel doesn't
289
- # provide an easy way to hook around validation / save calls
290
- def define_action_helpers
291
- if action == :save
292
- @instance_helper_module.class_eval do
293
- define_method(:valid?) do |*args|
294
- yielded = false
295
- result = self.class.state_machines.transitions(self, :save, :after => false).perform do
296
- yielded = true
297
- super(*args)
298
- end
299
-
300
- if defined?(::Sequel::MAJOR) && (::Sequel::MAJOR > 3 || ::Sequel::MAJOR == 3 && ::Sequel::MINOR > 13)
301
- raise_on_failure?(args.first || {}) && !yielded && !result ? raise_hook_failure(:validation) : result
302
- else
303
- raise_on_save_failure && !yielded && !result ? save_failure(:validation) : result
304
- end
304
+ # provide an easy way to hook around validation calls
305
+ def define_action_helpers
306
+ super
307
+
308
+ if action == :save
309
+ handle_validation_failure = self.handle_validation_failure
310
+ define_helper(:instance, :valid?) do |machine, object, _super, *args|
311
+ yielded = false
312
+ result = object.class.state_machines.transitions(object, :save, :after => false).perform do
313
+ yielded = true
314
+ _super.call
305
315
  end
306
316
 
307
- define_method(defined?(::Sequel::MAJOR) && (::Sequel::MAJOR >= 3 || ::Sequel::MAJOR == 2 && ::Sequel::MINOR == 12) ? :_save : :save) do |*args|
308
- yielded = false
309
- result = self.class.state_machines.transitions(self, :save).perform do
310
- yielded = true
311
- super(*args)
312
- end
313
-
314
- if yielded || result
315
- result
316
- elsif defined?(::Sequel::MAJOR) && (::Sequel::MAJOR > 3 || ::Sequel::MAJOR == 3 && ::Sequel::MINOR > 13)
317
- raise_hook_failure(:save)
318
- else
319
- save_failure(:save)
320
- end
317
+ !yielded && !result ? handle_validation_failure.call(object, args, yielded, result) : result
318
+ end
319
+ end
320
+ end
321
+
322
+ # Uses custom hooks for :save actions in order to preserve failure
323
+ # behavior within Sequel. This is a bit more complicated than other
324
+ # integrations since Sequel doesn't provide an easy way to hook around
325
+ # save calls.
326
+ def define_action_hook
327
+ if action == :save
328
+ action_hook = self.action_hook
329
+ handle_save_failure = self.handle_save_failure
330
+
331
+ define_helper(:instance, action_hook) do |machine, object, _super, *|
332
+ yielded = false
333
+ result = object.class.state_machines.transitions(object, :save).perform do
334
+ yielded = true
335
+ _super.call
336
+ end
337
+
338
+ if yielded || result
339
+ result
340
+ else
341
+ handle_save_failure.call(object)
321
342
  end
322
- end unless owner_class.state_machines.any? {|name, machine| machine.action == :save && machine != self}
343
+ end
323
344
  else
324
345
  super
325
346
  end
326
347
  end
327
348
 
349
+ # Uses internal save hooks if using the :save action
350
+ def action_hook
351
+ action == :save ? :_save : super
352
+ end
353
+
354
+ # Handles whether validation errors should be raised
355
+ def handle_validation_failure
356
+ lambda do |object, args, yielded, result|
357
+ object.instance_eval do
358
+ raise_on_failure?(args.first || {}) ? raise_hook_failure(:validation) : result
359
+ end
360
+ end
361
+ end
362
+
363
+ # Handles how save failures are raised
364
+ def handle_save_failure
365
+ lambda do |object|
366
+ object.instance_eval do
367
+ raise_hook_failure(:save)
368
+ end
369
+ end
370
+ end
371
+
328
372
  # Creates a scope for finding records *with* a particular state or
329
373
  # states for the attribute
330
374
  def create_with_scope(name)