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 +17 -0
- data/Rakefile +1 -1
- data/lib/state_machine.rb +11 -8
- data/lib/state_machine/callback.rb +1 -1
- data/lib/state_machine/event.rb +9 -8
- data/lib/state_machine/event_collection.rb +21 -10
- data/lib/state_machine/extensions.rb +2 -11
- data/lib/state_machine/guard.rb +12 -1
- data/lib/state_machine/integrations/active_record.rb +37 -1
- data/lib/state_machine/integrations/data_mapper.rb +17 -2
- data/lib/state_machine/integrations/sequel.rb +32 -1
- data/lib/state_machine/machine.rb +56 -16
- data/lib/state_machine/machine_collection.rb +7 -5
- data/lib/state_machine/state.rb +1 -1
- data/lib/state_machine/transition.rb +41 -14
- data/test/unit/assertions_test.rb +3 -3
- data/test/unit/eval_helpers_test.rb +13 -22
- data/test/unit/event_collection_test.rb +24 -0
- data/test/unit/event_test.rb +94 -0
- data/test/unit/guard_test.rb +46 -0
- data/test/unit/integrations/active_record_test.rb +247 -18
- data/test/unit/integrations/data_mapper_test.rb +143 -1
- data/test/unit/integrations/sequel_test.rb +279 -10
- data/test/unit/machine_collection_test.rb +42 -19
- data/test/unit/machine_test.rb +55 -0
- data/test/unit/state_test.rb +1 -1
- data/test/unit/transition_test.rb +106 -7
- metadata +2 -2
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.
|
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
|
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
|
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,
|
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
|
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
|
-
#
|
373
|
-
#
|
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|
|
data/lib/state_machine/event.rb
CHANGED
@@ -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
|
69
|
-
# for
|
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,
|
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
|
54
|
-
# events.transitions_for(vehicle)
|
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)
|
58
|
-
|
59
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
data/lib/state_machine/guard.rb
CHANGED
@@ -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,
|
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
|
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.
|
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,
|
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
|
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(
|
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
|
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(
|
1167
|
-
|
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(
|
1176
|
-
|
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
|
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
|
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
|