state_machine 0.6.3 → 0.7.0

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