state_machine 0.7.6 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,22 @@
1
1
  == master
2
2
 
3
+ == 0.8.0 / 2009-08-15
4
+
5
+ * Add support for DataMapper 0.10.0
6
+ * Always interpet nil return values from actions as failed attempts
7
+ * Fix loopbacks not causing records to save in ORM integrations if no other fields were changed
8
+ * Fix events not failing with useful errors when an object's state is invalid
9
+ * Use more friendly NoMethodError messages for state-driven behaviors
10
+ * Fix before_transition callbacks getting run twice when using event attributes in ORM integrations
11
+ * Add the ability to query for the availability of specific transitions on an object
12
+ * Allow after_transition callbacks to be explicitly run on failed attempts
13
+ * By default, don't run after_transition callbacks on failed attempts
14
+ * Fix not allowing multiple methods to be specified as arguments in callbacks
15
+ * Fix initial states being set when loading records from the database in Sequel integration
16
+ * Allow static initial states to be set earlier in the initialization of an object
17
+ * Use friendly validation errors for nil states
18
+ * Fix states not being validated properly when using custom names in ActiveRecord / DataMapper integrations
19
+
3
20
  == 0.7.6 / 2009-06-17
4
21
 
5
22
  * Allow multiple state machines on the same class to target the same attribute
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = 'state_machine'
8
- s.version = '0.7.6'
8
+ s.version = '0.8.0'
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
11
11
  s.description = s.summary
data/lib/state_machine.rb CHANGED
@@ -47,7 +47,7 @@ module StateMachine
47
47
  # result, you will not be able to access any class methods unless you refer
48
48
  # to them directly (i.e. specifying the class name).
49
49
  #
50
- # For examples on the types of configured state machines and blocks, see
50
+ # For examples on the types of state machine configurations and blocks, see
51
51
  # the section below.
52
52
  #
53
53
  # == Examples
@@ -102,8 +102,11 @@ module StateMachine
102
102
  # * <tt>state_name</tt> - Gets the name of the state for the current value
103
103
  # * <tt>state_events</tt> - Gets the list of events that can be fired on
104
104
  # the current object's state (uses the *unqualified* event names)
105
- # * <tt>state_transitions</tt> - Gets the list of possible transitions
106
- # that can be made on the current object's state
105
+ # * <tt>state_transitions(requirements = {})</tt> - Gets the list of possible
106
+ # transitions that can be made on the current object's state. Additional
107
+ # requirements, such as the :from / :to state and :on event can be specified
108
+ # to restrict the transitions to select. By default, the current state
109
+ # will be used for the :from state.
107
110
  #
108
111
  # For example,
109
112
  #
@@ -253,7 +256,7 @@ module StateMachine
253
256
  #
254
257
  # class Vehicle
255
258
  # include DataMapper::Resource
256
- # property :id, Integer, :serial => true
259
+ # property :id, Serial
257
260
  #
258
261
  # state_machine :initial => :parked do
259
262
  # event :ignite do
@@ -354,7 +357,7 @@ module StateMachine
354
357
  #
355
358
  # For integrations that support it, a group of default scope filters will
356
359
  # be automatically created for assisting in finding objects that have the
357
- # attribute set to the value for a given set of states.
360
+ # attribute set to one of a given set of states.
358
361
  #
359
362
  # For example,
360
363
  #
@@ -368,9 +371,9 @@ module StateMachine
368
371
  # :with_state, :with_states, :without_state, or :without_states), then a
369
372
  # scope will not be defined for that name.
370
373
  #
371
- # See StateMachine::Machine for more information about using
372
- # integrations and the individual integration docs for information about
373
- # the actual scopes that are generated.
374
+ # See StateMachine::Machine for more information about using integrations
375
+ # and the individual integration docs for information about the actual
376
+ # scopes that are generated.
374
377
  def state_machine(*args, &block)
375
378
  StateMachine::Machine.find_or_create(self, *args, &block)
376
379
  end
@@ -142,7 +142,7 @@ module StateMachine
142
142
  # requirements configured for this callback.
143
143
  #
144
144
  # If a terminator has been configured and it matches the result from the
145
- # evaluated method, then the callback chain should be halted
145
+ # evaluated method, then the callback chain should be halted.
146
146
  def call(object, context = {}, *args)
147
147
  if @guard.matches?(object, context)
148
148
  @methods.each do |method|
@@ -65,8 +65,8 @@ module StateMachine
65
65
  # state to be +idling+ if it's current state is +parked+ or +first_gear+
66
66
  # if it's current state is +idling+.
67
67
  #
68
- # To help defining these implicit transitions, a set of helpers are available
69
- # for defining slightly more complex matching:
68
+ # To help define these implicit transitions, a set of helpers are available
69
+ # for slightly more complex matching:
70
70
  # * <tt>all</tt> - Matches every state in the machine
71
71
  # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
72
72
  # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
@@ -147,7 +147,7 @@ module StateMachine
147
147
  # requirements
148
148
  assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
149
149
 
150
- guards << guard = Guard.new(options)
150
+ guards << guard = Guard.new(options.merge(:on => name))
151
151
  @known_states |= guard.known_states
152
152
  guard
153
153
  end
@@ -162,15 +162,16 @@ module StateMachine
162
162
 
163
163
  # Finds and builds the next transition that can be performed on the given
164
164
  # object. If no transitions can be made, then this will return nil.
165
- def transition_for(object)
166
- from = machine.states.match(object).name
165
+ def transition_for(object, requirements = {})
166
+ requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
167
167
 
168
168
  guards.each do |guard|
169
- if match = guard.match(object, :from => from)
169
+ if match = guard.match(object, requirements)
170
170
  # Guard allows for the transition to occur
171
+ from = requirements[:from]
171
172
  to = match[:to].values.empty? ? from : match[:to].values.first
172
173
 
173
- return Transition.new(object, machine, name, from, to)
174
+ return Transition.new(object, machine, name, from, to, !custom_from_state)
174
175
  end
175
176
  end
176
177
 
@@ -244,7 +245,7 @@ module StateMachine
244
245
 
245
246
  # Fires the event, raising an exception if it fails
246
247
  machine.define_instance_method("#{qualified_name}!") do |machine, object, *args|
247
- object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match(object).name.inspect}")
248
+ object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match!(object).name.inspect}")
248
249
  end
249
250
  end
250
251
  end
@@ -34,6 +34,14 @@ module StateMachine
34
34
 
35
35
  # Gets the list of transitions that can be run on the given object.
36
36
  #
37
+ # Valid requirement options:
38
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
39
+ # are specified, then this will be the object's current state.
40
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
41
+ # specified, then this will match any to state.
42
+ # * <tt>:on</tt> - One or more events that fire the transition. If none
43
+ # are specified, then this will match any event.
44
+ #
37
45
  # == Examples
38
46
  #
39
47
  # class Vehicle
@@ -50,22 +58,25 @@ module StateMachine
50
58
  #
51
59
  # events = Vehicle.state_machine.events
52
60
  #
53
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
54
- # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
61
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
62
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
55
63
  #
56
64
  # vehicle.state = 'idling'
57
- # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
58
- def transitions_for(object)
59
- map {|event| event.transition_for(object)}.compact
65
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
66
+ #
67
+ # # Search for explicit transitions regardless of the current state
68
+ # events.transitions_for(vehicle, :from => :parked) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
69
+ def transitions_for(object, requirements = {})
70
+ map {|event| event.transition_for(object, requirements)}.compact
60
71
  end
61
72
 
62
73
  # Gets the transition that should be performed for the event stored in the
63
74
  # given object's event attribute. This also takes an additional parameter
64
- # for automatically invalidating the object if the event or transition
65
- # are invalid. By default, this is turned off.
75
+ # for automatically invalidating the object if the event or transition are
76
+ # invalid. By default, this is turned off.
66
77
  #
67
- # *Note* that if a transition has already been generated for the event,
68
- # then that transition will be used.
78
+ # *Note* that if a transition has already been generated for the event, then
79
+ # that transition will be used.
69
80
  #
70
81
  # == Examples
71
82
  #
@@ -97,7 +108,7 @@ module StateMachine
97
108
  if event = self[event_name.to_sym, :name]
98
109
  unless result = machine.read(object, :event_transition) || event.transition_for(object)
99
110
  # No valid transition: invalidate
100
- machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name]]) if invalidate
111
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name || 'nil']]) if invalidate
101
112
  result = false
102
113
  end
103
114
  else
@@ -22,15 +22,6 @@ module StateMachine
22
22
  end
23
23
 
24
24
  module InstanceMethods
25
- # Defines the initial values for state machine attributes. The values
26
- # will be set *after* the original initialize method is invoked. This is
27
- # necessary in order to ensure that the object is initialized before
28
- # dynamic initial attributes are evaluated.
29
- def initialize(*args, &block)
30
- super
31
- initialize_state_machines
32
- end
33
-
34
25
  # Runs one or more events in parallel. All events will run through the
35
26
  # following steps:
36
27
  # * Before callbacks
@@ -151,8 +142,8 @@ module StateMachine
151
142
  end
152
143
 
153
144
  protected
154
- def initialize_state_machines #:nodoc:
155
- self.class.state_machines.initialize_states(self)
145
+ def initialize_state_machines(options = {}) #:nodoc:
146
+ self.class.state_machines.initialize_states(self, options)
156
147
  end
157
148
  end
158
149
  end
@@ -24,6 +24,9 @@ module StateMachine
24
24
  # requirements contain a mapping of {:from => matcher, :to => matcher}.
25
25
  attr_reader :state_requirements
26
26
 
27
+ # The requirement for verifying the success of the event
28
+ attr_reader :success_requirement
29
+
27
30
  # A list of all of the states known to this guard. This will pull states
28
31
  # from the following options (in the same order):
29
32
  # * +from+ / +except_from+
@@ -39,6 +42,9 @@ module StateMachine
39
42
  # Build event requirement
40
43
  @event_requirement = build_matcher(options, :on, :except_on)
41
44
 
45
+ # Build success requirement
46
+ @success_requirement = options.delete(:include_failures) ? AllMatcher.instance : WhitelistMatcher.new([true])
47
+
42
48
  if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
43
49
  # Explicit from/to requirements specified
44
50
  @state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
@@ -185,11 +191,16 @@ module StateMachine
185
191
  def match_query(query)
186
192
  query ||= {}
187
193
 
188
- if match_event(query) && (state_requirement = match_states(query))
194
+ if match_success(query) && match_event(query) && (state_requirement = match_states(query))
189
195
  state_requirement.merge(:on => event_requirement)
190
196
  end
191
197
  end
192
198
 
199
+ # Verifies that the success requirement matches the given query
200
+ def match_success(query)
201
+ matches_requirement?(query, :success, success_requirement)
202
+ end
203
+
193
204
  # Verifies that the event requirement matches the given query
194
205
  def match_event(query)
195
206
  matches_requirement?(query, :on, event_requirement)
@@ -281,6 +281,14 @@ module StateMachine
281
281
  end
282
282
  end
283
283
 
284
+ # Forces the change in state to be recognized regardless of whether the
285
+ # state value actually changed
286
+ def write(object, attribute, value)
287
+ result = super
288
+ object.send("#{self.attribute}_will_change!") if attribute == :state && object.respond_to?("#{self.attribute}_will_change!")
289
+ result
290
+ end
291
+
284
292
  # Adds a validation error to the given object
285
293
  def invalidate(object, attribute, message, values = [])
286
294
  attribute = self.attribute(attribute)
@@ -308,13 +316,41 @@ module StateMachine
308
316
  callbacks[:after] << Callback.new {|object, transition| notify(:after, object, transition)}
309
317
  end
310
318
 
319
+ # Defines an initialization hook into the owner class for setting the
320
+ # initial state of the machine *before* any attributes are set on the
321
+ # object
322
+ def define_state_initializer
323
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
324
+ # Ensure that the attributes setter gets used to force initialization
325
+ # of the state machines
326
+ def initialize(attributes = nil, *args)
327
+ attributes ||= {}
328
+ super
329
+ end
330
+
331
+ # Hooks in to attribute initialization to set the states *prior*
332
+ # to the attributes being set
333
+ def attributes=(*args)
334
+ if new_record? && !@initialized_state_machines
335
+ @initialized_state_machines = true
336
+
337
+ initialize_state_machines(:dynamic => false)
338
+ super
339
+ initialize_state_machines(:dynamic => true)
340
+ else
341
+ super
342
+ end
343
+ end
344
+ end_eval
345
+ end
346
+
311
347
  # Skips defining reader/writer methods since this is done automatically
312
348
  def define_state_accessor
313
349
  name = self.name
314
350
 
315
351
  owner_class.validates_each(attribute) do |record, attr, value|
316
352
  machine = record.class.state_machine(name)
317
- machine.invalidate(record, attr, :invalid) unless machine.states.match(record)
353
+ machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
318
354
  end
319
355
  end
320
356
 
@@ -247,9 +247,24 @@ module StateMachine
247
247
 
248
248
  # Loads additional files specific to DataMapper
249
249
  def self.extended(base) #:nodoc:
250
+ require 'dm-core/version' unless ::DataMapper.const_defined?('VERSION')
250
251
  require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
251
252
  end
252
253
 
254
+ # Forces the change in state to be recognized regardless of whether the
255
+ # state value actually changed
256
+ def write(object, attribute, value)
257
+ result = super
258
+ if attribute == :state && owner_class.properties.detect {|property| property.name == self.attribute}
259
+ if ::DataMapper::VERSION =~ /^(0\.\d\.)/ # Match anything < 0.10
260
+ object.original_values[self.attribute] = "#{value}-ignored"
261
+ else
262
+ object.original_attributes[owner_class.properties[self.attribute]] = "#{value}-ignored"
263
+ end
264
+ end
265
+ result
266
+ end
267
+
253
268
  # Adds a validation error to the given object
254
269
  def invalidate(object, attribute, message, values = [])
255
270
  object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
@@ -257,7 +272,7 @@ module StateMachine
257
272
 
258
273
  # Resets any errors previously added when invalidating the given object
259
274
  def reset(object)
260
- object.errors.clear if object.respond_to?(:errors)
275
+ object.errors.clear if supports_validations?
261
276
  end
262
277
 
263
278
  protected
@@ -268,7 +283,7 @@ module StateMachine
268
283
 
269
284
  # Skips defining reader/writer methods since this is done automatically
270
285
  def define_state_accessor
271
- owner_class.property(attribute, String) unless owner_class.properties.has_property?(attribute)
286
+ owner_class.property(attribute, String) unless owner_class.properties.detect {|property| property.name == attribute}
272
287
 
273
288
  if supports_validations?
274
289
  name = self.name
@@ -226,6 +226,15 @@ module StateMachine
226
226
  require 'sequel/extensions/inflector' if ::Sequel.const_defined?('VERSION') && ::Sequel::VERSION >= '2.12.0'
227
227
  end
228
228
 
229
+ # Forces the change in state to be recognized regardless of whether the
230
+ # state value actually changed
231
+ def write(object, attribute, value)
232
+ result = super
233
+ column = self.attribute.to_sym
234
+ object.changed_columns << column if attribute == :state && owner_class.columns.include?(column) && !object.changed_columns.include?(column)
235
+ result
236
+ end
237
+
229
238
  # Adds a validation error to the given object
230
239
  def invalidate(object, attribute, message, values = [])
231
240
  object.errors.add(self.attribute(attribute), generate_message(message, values))
@@ -237,12 +246,34 @@ module StateMachine
237
246
  end
238
247
 
239
248
  protected
249
+ # Defines an initialization hook into the owner class for setting the
250
+ # initial state of the machine *before* any attributes are set on the
251
+ # object
252
+ def define_state_initializer
253
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
254
+ # Hooks in to attribute initialization to set the states *prior*
255
+ # to the attributes being set
256
+ def set(*args)
257
+ if new? && !@initialized_state_machines
258
+ @initialized_state_machines = true
259
+
260
+ initialize_state_machines(:dynamic => false)
261
+ result = super
262
+ initialize_state_machines(:dynamic => true)
263
+ result
264
+ else
265
+ super
266
+ end
267
+ end
268
+ end_eval
269
+ end
270
+
240
271
  # Skips defining reader/writer methods since this is done automatically
241
272
  def define_state_accessor
242
273
  name = self.name
243
274
  owner_class.validates_each(attribute) do |record, attr, value|
244
275
  machine = record.class.state_machine(name)
245
- machine.invalidate(record, attr, :invalid) unless machine.states.match(record)
276
+ machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
246
277
  end
247
278
  end
248
279
 
@@ -455,12 +455,6 @@ module StateMachine
455
455
  def owner_class=(klass)
456
456
  @owner_class = klass
457
457
 
458
- # Add class-/instance-level methods to the owner class for state initialization
459
- owner_class.class_eval do
460
- extend StateMachine::ClassMethods
461
- include StateMachine::InstanceMethods
462
- end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
463
-
464
458
  # Create modules for extending the class with state/event-specific methods
465
459
  class_helper_module = @class_helper_module = Module.new
466
460
  instance_helper_module = @instance_helper_module = Module.new
@@ -469,6 +463,16 @@ module StateMachine
469
463
  include instance_helper_module
470
464
  end
471
465
 
466
+ # Add class-/instance-level methods to the owner class for state initialization
467
+ unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
468
+ owner_class.class_eval do
469
+ extend StateMachine::ClassMethods
470
+ include StateMachine::InstanceMethods
471
+ end
472
+
473
+ define_state_initializer
474
+ end
475
+
472
476
  # Record this machine as matched to the name in the current owner class.
473
477
  # This will override any machines mapped to the same name in any superclasses.
474
478
  owner_class.state_machines[name] = self
@@ -479,7 +483,7 @@ module StateMachine
479
483
  # creation time.
480
484
  def initial_state=(new_initial_state)
481
485
  @initial_state = new_initial_state
482
- add_states([@initial_state]) unless @initial_state.is_a?(Proc)
486
+ add_states([@initial_state]) unless dynamic_initial_state?
483
487
 
484
488
  # Update all states to reflect the new initial state
485
489
  states.each {|state| state.initial = (state.name == @initial_state)}
@@ -565,7 +569,12 @@ module StateMachine
565
569
  # vehicle.force_idle = false
566
570
  # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
567
571
  def initial_state(object)
568
- states.fetch(@initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state)
572
+ states.fetch(dynamic_initial_state? ? @initial_state.call(object) : @initial_state)
573
+ end
574
+
575
+ # Whether a dynamic initial state is being used in the machine
576
+ def dynamic_initial_state?
577
+ @initial_state.is_a?(Proc)
569
578
  end
570
579
 
571
580
  # Customizes the definition of one or more states in the machine.
@@ -595,7 +604,7 @@ module StateMachine
595
604
  #
596
605
  # In the above state machine, there are two states automatically discovered:
597
606
  # :parked and :idling. These states, by default, will store their stringified
598
- # equivalents when an object moves into that states (e.g. "parked" / "idling").
607
+ # equivalents when an object moves into that state (e.g. "parked" / "idling").
599
608
  #
600
609
  # For legacy systems or when tying state machines into existing frameworks,
601
610
  # it's oftentimes necessary to need to store a different value for a state
@@ -878,7 +887,10 @@ module StateMachine
878
887
  # The following instance methods are generated when a new event is defined
879
888
  # (the "park" event is used as an example):
880
889
  # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
881
- # the current state of the object.
890
+ # the current state of the object. This will *not* run validations in
891
+ # ORM integrations. To check whether an event can fire *and* passes
892
+ # validations, use event attributes (e.g. state_event) as described in the
893
+ # "Events" documentation of each ORM integration.
882
894
  # * <tt>park_transition</tt> - Gets the next transition that would be
883
895
  # performed if the "park" event were to be fired now on the object or nil
884
896
  # if no transitions can be performed.
@@ -1070,6 +1082,17 @@ module StateMachine
1070
1082
  # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1071
1083
  # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1072
1084
  #
1085
+ # == Result requirements
1086
+ #
1087
+ # By default, after_transition callbacks will only be run if the transition
1088
+ # was performed successfully. A transition is successful if the machine's
1089
+ # action is not configured or does not return false when it is invoked.
1090
+ # In order to include failed attempts when running an after_transition
1091
+ # callback, the :include_failures option can be specified like so:
1092
+ #
1093
+ # after_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
1094
+ # after_transition :do => ... # Runs only on successful attempts to transition
1095
+ #
1073
1096
  # == Verbose Requirements
1074
1097
  #
1075
1098
  # Requirements can also be defined using verbose options rather than the
@@ -1163,8 +1186,10 @@ module StateMachine
1163
1186
  #
1164
1187
  # As can be seen, any number of transitions can be created using various
1165
1188
  # combinations of configuration options.
1166
- def before_transition(options = {}, &block)
1167
- add_callback(:before, options.is_a?(Hash) ? options : {:do => options}, &block)
1189
+ def before_transition(*args, &block)
1190
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1191
+ options[:do] = args if args.any?
1192
+ add_callback(:before, options, &block)
1168
1193
  end
1169
1194
 
1170
1195
  # Creates a callback that will be invoked *after* a transition is
@@ -1172,8 +1197,10 @@ module StateMachine
1172
1197
  #
1173
1198
  # See +before_transition+ for a description of the possible configurations
1174
1199
  # for defining callbacks.
1175
- def after_transition(options = {}, &block)
1176
- add_callback(:after, options.is_a?(Hash) ? options : {:do => options}, &block)
1200
+ def after_transition(*args, &block)
1201
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1202
+ options[:do] = args if args.any?
1203
+ add_callback(:after, options, &block)
1177
1204
  end
1178
1205
 
1179
1206
  # Marks the given object as invalid with the given message.
@@ -1182,7 +1209,7 @@ module StateMachine
1182
1209
  def invalidate(object, attribute, message, values = [])
1183
1210
  end
1184
1211
 
1185
- # Resets an errors previously added when invalidating the given object
1212
+ # Resets any errors previously added when invalidating the given object.
1186
1213
  #
1187
1214
  # By default, this is a no-op.
1188
1215
  def reset(object)
@@ -1197,7 +1224,7 @@ module StateMachine
1197
1224
  # Runs a transaction, rolling back any changes if the yielded block fails.
1198
1225
  #
1199
1226
  # This is only applicable to integrations that involve databases. By
1200
- # default, this will not run any transactions, since the changes aren't
1227
+ # default, this will not run any transactions since the changes aren't
1201
1228
  # taking place within the context of a database.
1202
1229
  def within_transaction(object)
1203
1230
  if use_transactions
@@ -1287,6 +1314,19 @@ module StateMachine
1287
1314
  end
1288
1315
  end
1289
1316
 
1317
+ # Defines the initial values for state machine attributes. Static values
1318
+ # are set prior to the original initialize method and dynamic values are
1319
+ # set *after* the initialize method in case it is dependent on it.
1320
+ def define_state_initializer
1321
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
1322
+ def initialize(*args)
1323
+ initialize_state_machines(:dynamic => false)
1324
+ super
1325
+ initialize_state_machines(:dynamic => true)
1326
+ end
1327
+ end_eval
1328
+ end
1329
+
1290
1330
  # Adds reader/writer methods for accessing the state attribute
1291
1331
  def define_state_accessor
1292
1332
  attribute = self.attribute