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 +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
|