state_machine 0.6.3 → 0.7.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 (56) hide show
  1. data/CHANGELOG.rdoc +31 -1
  2. data/README.rdoc +33 -21
  3. data/Rakefile +2 -2
  4. data/examples/merb-rest/controller.rb +51 -0
  5. data/examples/merb-rest/model.rb +28 -0
  6. data/examples/merb-rest/view_edit.html.erb +24 -0
  7. data/examples/merb-rest/view_index.html.erb +23 -0
  8. data/examples/merb-rest/view_new.html.erb +13 -0
  9. data/examples/merb-rest/view_show.html.erb +17 -0
  10. data/examples/rails-rest/controller.rb +43 -0
  11. data/examples/rails-rest/migration.rb +11 -0
  12. data/examples/rails-rest/model.rb +23 -0
  13. data/examples/rails-rest/view_edit.html.erb +25 -0
  14. data/examples/rails-rest/view_index.html.erb +23 -0
  15. data/examples/rails-rest/view_new.html.erb +14 -0
  16. data/examples/rails-rest/view_show.html.erb +17 -0
  17. data/lib/state_machine/assertions.rb +2 -2
  18. data/lib/state_machine/callback.rb +14 -8
  19. data/lib/state_machine/condition_proxy.rb +3 -3
  20. data/lib/state_machine/event.rb +19 -21
  21. data/lib/state_machine/event_collection.rb +114 -0
  22. data/lib/state_machine/extensions.rb +127 -11
  23. data/lib/state_machine/guard.rb +1 -1
  24. data/lib/state_machine/integrations/active_record/locale.rb +2 -1
  25. data/lib/state_machine/integrations/active_record.rb +117 -39
  26. data/lib/state_machine/integrations/data_mapper/observer.rb +20 -64
  27. data/lib/state_machine/integrations/data_mapper.rb +71 -26
  28. data/lib/state_machine/integrations/sequel.rb +69 -21
  29. data/lib/state_machine/machine.rb +267 -139
  30. data/lib/state_machine/machine_collection.rb +145 -0
  31. data/lib/state_machine/matcher.rb +2 -2
  32. data/lib/state_machine/node_collection.rb +9 -4
  33. data/lib/state_machine/state.rb +22 -32
  34. data/lib/state_machine/state_collection.rb +66 -17
  35. data/lib/state_machine/transition.rb +259 -28
  36. data/lib/state_machine.rb +121 -56
  37. data/tasks/state_machine.rake +1 -0
  38. data/tasks/state_machine.rb +26 -0
  39. data/test/active_record.log +116877 -0
  40. data/test/functional/state_machine_test.rb +118 -12
  41. data/test/sequel.log +28542 -0
  42. data/test/unit/callback_test.rb +46 -1
  43. data/test/unit/condition_proxy_test.rb +55 -28
  44. data/test/unit/event_collection_test.rb +228 -0
  45. data/test/unit/event_test.rb +51 -46
  46. data/test/unit/integrations/active_record_test.rb +128 -70
  47. data/test/unit/integrations/data_mapper_test.rb +150 -58
  48. data/test/unit/integrations/sequel_test.rb +63 -6
  49. data/test/unit/invalid_event_test.rb +7 -0
  50. data/test/unit/machine_collection_test.rb +678 -0
  51. data/test/unit/machine_test.rb +198 -91
  52. data/test/unit/node_collection_test.rb +33 -30
  53. data/test/unit/state_collection_test.rb +112 -5
  54. data/test/unit/state_test.rb +23 -3
  55. data/test/unit/transition_test.rb +750 -89
  56. metadata +28 -3
@@ -20,7 +20,7 @@ module StateMachine
20
20
  # The requirement for verifying the event being guarded
21
21
  attr_reader :event_requirement
22
22
 
23
- # One or more requrirements for verifying the states being guarded. All
23
+ # One or more requirements for verifying the states being guarded. All
24
24
  # requirements contain a mapping of {:from => matcher, :to => matcher}.
25
25
  attr_reader :state_requirements
26
26
 
@@ -2,7 +2,8 @@
2
2
  :activerecord => {
3
3
  :errors => {
4
4
  :messages => {
5
- :invalid_transition => StateMachine::Machine.default_invalid_message % ['{{event}}', '{{value}}']
5
+ :invalid_event => StateMachine::Machine.default_messages[:invalid_event] % ['{{state}}'],
6
+ :invalid_transition => StateMachine::Machine.default_messages[:invalid_transition] % ['{{event}}']
6
7
  }
7
8
  }
8
9
  }
@@ -33,6 +33,37 @@ module StateMachine
33
33
  # vehicle.ignite # => true
34
34
  # vehicle.reload # => #<Vehicle id: 1, name: "Ford Explorer", state: "idling">
35
35
  #
36
+ # == Events
37
+ #
38
+ # As described in StateMachine::InstanceMethods#state_machine, event
39
+ # attributes are created for every machine that allow transitions to be
40
+ # performed automatically when the object's action (in this case, :save)
41
+ # is called.
42
+ #
43
+ # In ActiveRecord, these automated events are run in the following order:
44
+ # * before validation - Run before callbacks and persist new states, then validate
45
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
46
+ # * after save - Run after callbacks
47
+ #
48
+ # For example,
49
+ #
50
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
51
+ # vehicle.state_event # => nil
52
+ # vehicle.state_event = 'invalid'
53
+ # vehicle.valid? # => false
54
+ # vehicle.errors.full_messages # => ["State event is invalid"]
55
+ #
56
+ # vehicle.state_event = 'ignite'
57
+ # vehicle.valid? # => true
58
+ # vehicle.save # => true
59
+ # vehicle.state # => "idling"
60
+ # vehicle.state_event # => nil
61
+ #
62
+ # Note that this can also be done on a mass-assignment basis:
63
+ #
64
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
65
+ # vehicle.state # => "idling"
66
+ #
36
67
  # == Transactions
37
68
  #
38
69
  # In order to ensure that any changes made during transition callbacks
@@ -60,6 +91,14 @@ module StateMachine
60
91
  # rolled back. If an after callback halts the chain, the previous result
61
92
  # still applies and the transaction is *not* rolled back.
62
93
  #
94
+ # To turn off transactions:
95
+ #
96
+ # class Vehicle < ActiveRecord::Base
97
+ # state_machine :initial => :parked, :use_transactions => false do
98
+ # ...
99
+ # end
100
+ # end
101
+ #
63
102
  # == Validation errors
64
103
  #
65
104
  # If an event fails to successfully fire because there are no matching
@@ -71,7 +110,7 @@ module StateMachine
71
110
  #
72
111
  # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling">
73
112
  # vehicle.ignite # => false
74
- # vehicle.errors.full_messages # => ["State cannot be transitioned via :ignite from :idling"]
113
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
75
114
  #
76
115
  # If an event fails to fire because of a validation error on the record and
77
116
  # *not* because a matching transition was not available, no error messages
@@ -138,9 +177,21 @@ module StateMachine
138
177
  # In addition to support for ActiveRecord-like hooks, there is additional
139
178
  # support for ActiveRecord observers. Because of the way ActiveRecord
140
179
  # observers are designed, there is less flexibility around the specific
141
- # transitions that can be hooked in. As a result, observers can only
142
- # hook into before/after callbacks for events and generic transitions
143
- # like so:
180
+ # transitions that can be hooked in. However, a large number of hooks
181
+ # *are* supported. For example, if a transition for a record's +state+
182
+ # attribute changes the state from +parked+ to +idling+ via the +ignite+
183
+ # event, the following observer methods are supported:
184
+ # * before/after_ignite_from_parked_to_idling
185
+ # * before/after_ignite_from_parked
186
+ # * before/after_ignite_to_idling
187
+ # * before/after_ignite
188
+ # * before/after_transition_state_from_parked_to_idling
189
+ # * before/after_transition_state_from_parked
190
+ # * before/after_transition_state_to_idling
191
+ # * before/after_transition_state
192
+ # * before/after_transition
193
+ #
194
+ # The following class shows an example of some of these hooks:
144
195
  #
145
196
  # class VehicleObserver < ActiveRecord::Observer
146
197
  # def before_save(vehicle)
@@ -177,6 +228,10 @@ module StateMachine
177
228
  # end
178
229
  # end
179
230
  module ActiveRecord
231
+ # The default options to use for state machines using this integration
232
+ class << self; attr_reader :defaults; end
233
+ @defaults = {:action => :save}
234
+
180
235
  # Should this integration be used for state machines in the given class?
181
236
  # Classes that inherit from ActiveRecord::Base will automatically use
182
237
  # the ActiveRecord integration.
@@ -190,61 +245,57 @@ module StateMachine
190
245
  I18n.load_path << "#{File.dirname(__FILE__)}/active_record/locale.rb" if Object.const_defined?(:I18n)
191
246
  end
192
247
 
193
- # Adds a validation error to the given object after failing to fire a
194
- # specific event
195
- def invalidate(object, event)
248
+ # Adds a validation error to the given object
249
+ def invalidate(object, attribute, message, values = [])
196
250
  if Object.const_defined?(:I18n)
197
- object.errors.add(attribute, :invalid_transition,
198
- :event => event.name,
199
- :value => state_for(object).name,
200
- :default => @invalid_message
201
- )
251
+ options = values.inject({}) {|options, (key, value)| options[key] = value; options}
252
+ object.errors.add(attribute, message, options.merge(
253
+ :default => @messages[message]
254
+ ))
202
255
  else
203
- object.errors.add(attribute, invalid_message(object, event))
256
+ object.errors.add(attribute, generate_message(message, values))
204
257
  end
205
258
  end
206
259
 
207
- # Resets an errors previously added when invalidating the given object
260
+ # Resets any errors previously added when invalidating the given object
208
261
  def reset(object)
209
262
  object.errors.clear
210
263
  end
211
264
 
212
- # Runs a new database transaction, rolling back any changes by raising
213
- # an ActiveRecord::Rollback exception if the yielded block fails
214
- # (i.e. returns false).
215
- def within_transaction(object)
216
- object.class.transaction {raise ::ActiveRecord::Rollback unless yield}
217
- end
218
-
219
265
  protected
220
266
  # Adds the default callbacks for notifying ActiveRecord observers
221
267
  # before/after a transition has been performed.
222
268
  def after_initialize
223
- # Observer callbacks never halt the chain; result is ignored
224
269
  callbacks[:before] << Callback.new {|object, transition| notify(:before, object, transition)}
225
- callbacks[:after] << Callback.new {|object, transition, result| notify(:after, object, transition)}
226
- end
227
-
228
- # Sets the default action for all ActiveRecord state machines to +save+
229
- def default_action
230
- :save
270
+ callbacks[:after] << Callback.new {|object, transition| notify(:after, object, transition)}
231
271
  end
232
272
 
233
273
  # Skips defining reader/writer methods since this is done automatically
234
- def define_attribute_accessor
274
+ def define_state_accessor
235
275
  end
236
276
 
237
277
  # Adds support for defining the attribute predicate, while providing
238
278
  # compatibility with the default predicate which determines whether
239
279
  # *anything* is set for the attribute's value
240
- def define_attribute_predicate
280
+ def define_state_predicate
241
281
  attribute = self.attribute
242
282
 
243
283
  # Still use class_eval here instance of define_instance_method since
244
- # we need to directly override the method defined in the model
245
- owner_class.class_eval do
284
+ # we need to be able to call +super+
285
+ @instance_helper_module.class_eval do
246
286
  define_method("#{attribute}?") do |*args|
247
- args.empty? ? super(*args) : self.class.state_machines[attribute].state?(self, *args)
287
+ args.empty? ? super(*args) : self.class.state_machine(attribute).states.matches?(self, *args)
288
+ end
289
+ end
290
+ end
291
+
292
+ # Adds hooks into validation for automatically firing events
293
+ def define_action_helpers
294
+ if super && action == :save
295
+ @instance_helper_module.class_eval do
296
+ define_method(:valid?) do |*args|
297
+ self.class.state_machines.fire_attribute_events(self, :save, false) { super(*args) }
298
+ end
248
299
  end
249
300
  end
250
301
  end
@@ -263,6 +314,13 @@ module StateMachine
263
314
  define_scope(name, lambda {|values| {:conditions => ["#{attribute} NOT IN (?)", values]}})
264
315
  end
265
316
 
317
+ # Runs a new database transaction, rolling back any changes by raising
318
+ # an ActiveRecord::Rollback exception if the yielded block fails
319
+ # (i.e. returns false).
320
+ def transaction(object)
321
+ object.class.transaction {raise ::ActiveRecord::Rollback unless yield}
322
+ end
323
+
266
324
  # Creates a new callback in the callback chain, always inserting it
267
325
  # before the default Observer callbacks that were created after
268
326
  # initialization.
@@ -288,7 +346,7 @@ module StateMachine
288
346
  # Created the scope and then override it with state translation
289
347
  owner_class.named_scope(name)
290
348
  owner_class.scopes[name] = lambda do |klass, *states|
291
- machine_states = klass.state_machines[attribute].states
349
+ machine_states = klass.state_machine(attribute).states
292
350
  values = states.flatten.map {|state| machine_states.fetch(state).value}
293
351
 
294
352
  ::ActiveRecord::NamedScope::Scope.new(klass, scope.call(values))
@@ -300,18 +358,38 @@ module StateMachine
300
358
  # Notifies observers on the given object that a callback occurred
301
359
  # involving the given transition. This will attempt to call the
302
360
  # following methods on observers:
303
- # * #{type}_#{event}
361
+ # * #{type}_#{qualified_event}_from_#{from}_to_#{to}
362
+ # * #{type}_#{qualified_event}_from_#{from}
363
+ # * #{type}_#{qualified_event}_to_#{to}
364
+ # * #{type}_#{qualified_event}
365
+ # * #{type}_transition_#{attribute}_from_#{from}_to_#{to}
366
+ # * #{type}_transition_#{attribute}_from_#{from}
367
+ # * #{type}_transition_#{attribute}_to_#{to}
368
+ # * #{type}_transition_#{attribute}
304
369
  # * #{type}_transition
305
370
  #
306
371
  # This will always return true regardless of the results of the
307
372
  # callbacks.
308
373
  def notify(type, object, transition)
309
- qualified_event = namespace ? "#{transition.event}_#{namespace}" : transition.event
310
- ["#{type}_#{qualified_event}", "#{type}_transition"].each do |method|
311
- object.class.changed
312
- object.class.notify_observers(method, object, transition)
374
+ attribute = transition.attribute
375
+ event = transition.qualified_event
376
+ from = transition.from_name
377
+ to = transition.to_name
378
+
379
+ # Machine-specific updates
380
+ ["#{type}_#{event}", "#{type}_transition_#{attribute}"].each do |event_segment|
381
+ ["_from_#{from}", nil].each do |from_segment|
382
+ ["_to_#{to}", nil].each do |to_segment|
383
+ object.class.changed
384
+ object.class.notify_observers([event_segment, from_segment, to_segment].join, object, transition)
385
+ end
386
+ end
313
387
  end
314
388
 
389
+ # Generic updates
390
+ object.class.changed
391
+ object.class.notify_observers("#{type}_transition", object, transition)
392
+
315
393
  true
316
394
  end
317
395
  end
@@ -2,7 +2,7 @@ module StateMachine
2
2
  module Integrations #:nodoc:
3
3
  module DataMapper
4
4
  # Adds support for creating before/after transition callbacks within a
5
- # DataMapper observer. These callbacks behave very similarly to
5
+ # DataMapper observer. These callbacks behave very similar to
6
6
  # before/after hooks during save/update/destroy/etc., but with the
7
7
  # following modifications:
8
8
  # * Each callback can define a set of transition conditions (i.e. guards)
@@ -18,8 +18,8 @@ module StateMachine
18
18
  #
19
19
  # observe Vehicle, Switch, Project
20
20
  #
21
- # after_transition do |transition, saved|
22
- # Audit.log(self, transition) if saved
21
+ # after_transition do |transition|
22
+ # Audit.log(self, transition)
23
23
  # end
24
24
  # end
25
25
  #
@@ -35,7 +35,6 @@ module StateMachine
35
35
  # require 'dm-observer'
36
36
  #
37
37
  # If dm-observer is not available, then this feature will be skipped.
38
- #
39
38
  module Observer
40
39
  include MatcherHelpers
41
40
 
@@ -96,59 +95,11 @@ module StateMachine
96
95
  end
97
96
 
98
97
  # Creates a callback that will be invoked *after* a transition is
99
- # performed, so long as the given configuration options match the
100
- # transition. Each part of the transition (event, to state, from state)
101
- # must match in order for the callback to get invoked.
102
- #
103
- # See StateMachine::Machine#after_transition for more
104
- # information about the various configuration options available.
105
- #
106
- # == Examples
107
- #
108
- # class Vehicle
109
- # include DataMapper::Resource
110
- #
111
- # property :id, Serial
112
- # property :state, :String
113
- #
114
- # state_machine :initial => :parked do
115
- # event :ignite do
116
- # transition :parked => :idling
117
- # end
118
- # end
119
- # end
120
- #
121
- # class VehicleObserver
122
- # include DataMapper::Observer
123
- #
124
- # observe Vehicle
125
- #
126
- # after :save do |saved|
127
- # # log message
128
- # end
129
- #
130
- # # Target all state machines
131
- # after_transition :parked => :idling, :on => :ignite do
132
- # # put on seatbelt
133
- # end
134
- #
135
- # # Target a specific state machine
136
- # after_transition :state, any => :idling do
137
- # # put on seatbelt
138
- # end
139
- #
140
- # # Target all state machines without requirements
141
- # after_transition do |transition, saved|
142
- # if saved
143
- # # log message
144
- # end
145
- # end
146
- # end
98
+ # performed so long as the given configuration options match the
99
+ # transition.
147
100
  #
148
- # *Note* that in each of the above +before_transition+ callbacks, the
149
- # callback is executed within the context of the object (i.e. the
150
- # Vehicle instance being transition). This means that +self+ refers
151
- # to the vehicle record within each callback block.
101
+ # See +before_transition+ for a description of the possible configurations
102
+ # for defining callbacks.
152
103
  def after_transition(*args, &block)
153
104
  add_transition_callback(:after, *args, &block)
154
105
  end
@@ -157,20 +108,25 @@ module StateMachine
157
108
  # Adds the transition callback to a specific machine or all of the
158
109
  # state machines for each observed class.
159
110
  def add_transition_callback(type, *args, &block)
160
- if args.first && !args.first.is_a?(Hash)
161
- # Specific attribute is being targeted
162
- attribute = args.first
163
- transition_args = args[1..-1]
111
+ if args.any? && !args.first.is_a?(Hash)
112
+ # Specific attribute(s) being targeted
113
+ attributes = args
114
+ args = args.last.is_a?(Hash) ? [args.pop] : []
164
115
  else
165
116
  # Target all state machines
166
- attribute = nil
167
- transition_args = args
117
+ attributes = nil
168
118
  end
169
119
 
170
120
  # Add the transition callback to each class being observed
171
121
  observing.each do |klass|
172
- state_machines = attribute ? [klass.state_machines[attribute]] : klass.state_machines.values
173
- state_machines.each {|machine| machine.send("#{type}_transition", *transition_args, &block)}
122
+ state_machines =
123
+ if attributes
124
+ attributes.map {|attribute| klass.state_machines.fetch(attribute)}
125
+ else
126
+ klass.state_machines.values
127
+ end
128
+
129
+ state_machines.each {|machine| machine.send("#{type}_transition", *args, &block)}
174
130
  end if observing
175
131
  end
176
132
  end
@@ -39,11 +39,43 @@ module StateMachine
39
39
  # vehicle.ignite # => true
40
40
  # vehicle.reload # => #<Vehicle id=1 name="Ford Explorer" state="idling">
41
41
  #
42
+ # == Events
43
+ #
44
+ # As described in StateMachine::InstanceMethods#state_machine, event
45
+ # attributes are created for every machine that allow transitions to be
46
+ # performed automatically when the object's action (in this case, :save)
47
+ # is called.
48
+ #
49
+ # In DataMapper, these automated events are run in the following order:
50
+ # * before validation - If validation feature loaded, run before callbacks and persist new states, then validate
51
+ # * before save - If validation feature was skipped/not loaded, run before callbacks and persist new states, then save
52
+ # * after save - Run after callbacks
53
+ #
54
+ # For example,
55
+ #
56
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
57
+ # vehicle.state_event # => nil
58
+ # vehicle.state_event = 'invalid'
59
+ # vehicle.valid? # => false
60
+ # vehicle.errors # => #<DataMapper::Validate::ValidationErrors:0xb7a48b54 @errors={"state_event"=>["is invalid"]}>
61
+ #
62
+ # vehicle.state_event = 'ignite'
63
+ # vehicle.valid? # => true
64
+ # vehicle.save # => true
65
+ # vehicle.state # => "idling"
66
+ # vehicle.state_event # => nil
67
+ #
68
+ # Note that this can also be done on a mass-assignment basis:
69
+ #
70
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id=1 name=nil state="idling">
71
+ # vehicle.state # => "idling"
72
+ #
42
73
  # == Transactions
43
74
  #
44
- # In order to ensure that any changes made during transition callbacks
45
- # are rolled back during a failed attempt, every transition is wrapped
46
- # within a transaction.
75
+ # By default, the use of transactions during an event transition is
76
+ # turned off to be consistent with DataMapper. This means that if
77
+ # changes are made to the database during a before callback, but the the
78
+ # transition fails to complete, those changes will *not* be rolled back.
47
79
  #
48
80
  # For example,
49
81
  #
@@ -63,12 +95,15 @@ module StateMachine
63
95
  #
64
96
  # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
65
97
  # vehicle.ignite # => false
66
- # Message.all.count # => 0
98
+ # Message.all.count # => 1
67
99
  #
68
- # *Note* that only before callbacks that halt the callback chain and
69
- # failed attempts to save the record will result in the transaction being
70
- # rolled back. If an after callback halts the chain, the previous result
71
- # still applies and the transaction is *not* rolled back.
100
+ # To turn on transactions:
101
+ #
102
+ # class Vehicle < ActiveRecord::Base
103
+ # state_machine :initial => :parked, :use_transactions => true do
104
+ # ...
105
+ # end
106
+ # end
72
107
  #
73
108
  # == Validation errors
74
109
  #
@@ -81,7 +116,7 @@ module StateMachine
81
116
  #
82
117
  # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
83
118
  # vehicle.ignite # => false
84
- # vehicle.errors.full_messages # => ["cannot be transitioned via :ignite from :idling"]
119
+ # vehicle.errors.full_messages # => ["cannot transition via \"ignite\""]
85
120
  #
86
121
  # If an event fails to fire because of a validation error on the record and
87
122
  # *not* because a matching transition was not available, no error messages
@@ -164,6 +199,10 @@ module StateMachine
164
199
  # support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
165
200
  # for more information.
166
201
  module DataMapper
202
+ # The default options to use for state machines using this integration
203
+ class << self; attr_reader :defaults; end
204
+ @defaults = {:action => :save, :use_transactions => false}
205
+
167
206
  # Should this integration be used for state machines in the given class?
168
207
  # Classes that include DataMapper::Resource will automatically use the
169
208
  # DataMapper integration.
@@ -176,31 +215,31 @@ module StateMachine
176
215
  require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
177
216
  end
178
217
 
179
- # Adds a validation error to the given object after failing to fire a
180
- # specific event
181
- def invalidate(object, event)
182
- object.errors.add(attribute, invalid_message(object, event)) if object.respond_to?(:errors)
218
+ # Adds a validation error to the given object
219
+ def invalidate(object, attribute, message, values = [])
220
+ object.errors.add(attribute, generate_message(message, values)) if object.respond_to?(:errors)
183
221
  end
184
222
 
185
- # Resets an errors previously added when invalidating the given object
223
+ # Resets any errors previously added when invalidating the given object
186
224
  def reset(object)
187
- object.errors.clear
188
- end
189
-
190
- # Runs a new database transaction, rolling back any changes if the
191
- # yielded block fails (i.e. returns false).
192
- def within_transaction(object)
193
- object.class.transaction {|t| t.rollback unless yield}
225
+ object.errors.clear if object.respond_to?(:errors)
194
226
  end
195
227
 
196
228
  protected
197
- # Sets the default action for all DataMapper state machines to +save+
198
- def default_action
199
- :save
229
+ # Skips defining reader/writer methods since this is done automatically
230
+ def define_state_accessor
231
+ owner_class.property(attribute, String) unless owner_class.properties.has_property?(attribute)
200
232
  end
201
233
 
202
- # Skips defining reader/writer methods since this is done automatically
203
- def define_attribute_accessor
234
+ # Adds hooks into validation for automatically firing events
235
+ def define_action_helpers
236
+ if super && action == :save
237
+ @instance_helper_module.class_eval do
238
+ define_method(:valid?) do |*args|
239
+ self.class.state_machines.fire_attribute_events(self, :save, false) { super(*args) }
240
+ end
241
+ end
242
+ end
204
243
  end
205
244
 
206
245
  # Creates a scope for finding records *with* a particular state or
@@ -217,6 +256,12 @@ module StateMachine
217
256
  lambda {|resource, values| resource.all(attribute.to_sym.not => values)}
218
257
  end
219
258
 
259
+ # Runs a new database transaction, rolling back any changes if the
260
+ # yielded block fails (i.e. returns false).
261
+ def transaction(object)
262
+ object.class.transaction {|t| t.rollback unless yield}
263
+ end
264
+
220
265
  # Creates a new callback in the callback chain, always ensuring that
221
266
  # it's configured to bind to the object as this is the convention for
222
267
  # DataMapper/Extlib callbacks