state_machine 0.7.6 → 0.8.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.
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