verborghs-state_machine 0.9.4
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 +360 -0
- data/LICENSE +20 -0
- data/README.rdoc +635 -0
- data/Rakefile +77 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +241 -0
- data/lib/state_machine/condition_proxy.rb +106 -0
- data/lib/state_machine/eval_helpers.rb +83 -0
- data/lib/state_machine/event.rb +267 -0
- data/lib/state_machine/event_collection.rb +122 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/guard.rb +230 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +5 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +45 -0
- data/lib/state_machine/integrations/active_model.rb +445 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record.rb +522 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
- data/lib/state_machine/integrations/data_mapper.rb +379 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
- data/lib/state_machine/integrations/sequel.rb +356 -0
- data/lib/state_machine/integrations.rb +83 -0
- data/lib/state_machine/machine.rb +1645 -0
- data/lib/state_machine/machine_collection.rb +64 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +260 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +399 -0
- data/lib/state_machine/transition_collection.rb +244 -0
- data/lib/state_machine.rb +421 -0
- data/lib/tasks/state_machine.rake +1 -0
- data/lib/tasks/state_machine.rb +27 -0
- data/test/files/en.yml +9 -0
- data/test/files/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +980 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +728 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +222 -0
- data/test/unit/event_collection_test.rb +324 -0
- data/test/unit/event_test.rb +795 -0
- data/test/unit/guard_test.rb +909 -0
- data/test/unit/integrations/active_model_test.rb +956 -0
- data/test/unit/integrations/active_record_test.rb +1918 -0
- data/test/unit/integrations/data_mapper_test.rb +1814 -0
- data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
- data/test/unit/integrations/sequel_test.rb +1492 -0
- data/test/unit/integrations_test.rb +50 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +565 -0
- data/test/unit/machine_test.rb +2349 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +848 -0
- data/test/unit/transition_collection_test.rb +2098 -0
- data/test/unit/transition_test.rb +1384 -0
- metadata +176 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'state_machine/node_collection'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# Represents a collection of states in a state machine
|
5
|
+
class StateCollection < NodeCollection
|
6
|
+
def initialize(machine) #:nodoc:
|
7
|
+
super(machine, :index => [:name, :value])
|
8
|
+
end
|
9
|
+
|
10
|
+
# Determines whether the given object is in a specific state. If the
|
11
|
+
# object's current value doesn't match the state, then this will return
|
12
|
+
# false, otherwise true. If the given state is unknown, then an IndexError
|
13
|
+
# will be raised.
|
14
|
+
#
|
15
|
+
# == Examples
|
16
|
+
#
|
17
|
+
# class Vehicle
|
18
|
+
# state_machine :initial => :parked do
|
19
|
+
# other_states :idling
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# states = Vehicle.state_machine.states
|
24
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
25
|
+
#
|
26
|
+
# states.matches?(vehicle, :parked) # => true
|
27
|
+
# states.matches?(vehicle, :idling) # => false
|
28
|
+
# states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
|
29
|
+
def matches?(object, name)
|
30
|
+
fetch(name).matches?(machine.read(object, :state))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Determines the current state of the given object as configured by this
|
34
|
+
# state machine. This will attempt to find a known state that matches
|
35
|
+
# the value of the attribute on the object.
|
36
|
+
#
|
37
|
+
# == Examples
|
38
|
+
#
|
39
|
+
# class Vehicle
|
40
|
+
# state_machine :initial => :parked do
|
41
|
+
# other_states :idling
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# states = Vehicle.state_machine.states
|
46
|
+
#
|
47
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
48
|
+
# states.match(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
|
49
|
+
#
|
50
|
+
# vehicle.state = 'idling'
|
51
|
+
# states.match(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=true>
|
52
|
+
#
|
53
|
+
# vehicle.state = 'invalid'
|
54
|
+
# states.match(vehicle) # => nil
|
55
|
+
def match(object)
|
56
|
+
value = machine.read(object, :state)
|
57
|
+
self[value, :value] || detect {|state| state.matches?(value)}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Determines the current state of the given object as configured by this
|
61
|
+
# state machine. If no state is found, then an ArgumentError will be
|
62
|
+
# raised.
|
63
|
+
#
|
64
|
+
# == Examples
|
65
|
+
#
|
66
|
+
# class Vehicle
|
67
|
+
# state_machine :initial => :parked do
|
68
|
+
# other_states :idling
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# states = Vehicle.state_machine.states
|
73
|
+
#
|
74
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
75
|
+
# states.match!(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
|
76
|
+
#
|
77
|
+
# vehicle.state = 'invalid'
|
78
|
+
# states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
|
79
|
+
def match!(object)
|
80
|
+
match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Gets the order in which states should be displayed based on where they
|
84
|
+
# were first referenced. This will order states in the following priority:
|
85
|
+
#
|
86
|
+
# 1. Initial state
|
87
|
+
# 2. Event transitions (:from, :except_from, :to, :except_to options)
|
88
|
+
# 3. States with behaviors
|
89
|
+
# 4. States referenced via +state+ or +other_states+
|
90
|
+
# 5. States referenced in callbacks
|
91
|
+
#
|
92
|
+
# This order will determine how the GraphViz visualizations are rendered.
|
93
|
+
def by_priority
|
94
|
+
order = select {|state| state.initial}.map {|state| state.name}
|
95
|
+
|
96
|
+
machine.events.each {|event| order += event.known_states}
|
97
|
+
order += select {|state| state.methods.any?}.map {|state| state.name}
|
98
|
+
order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
|
99
|
+
order += keys(:name)
|
100
|
+
|
101
|
+
order.uniq!
|
102
|
+
order.map! {|name| self[name]}
|
103
|
+
order
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
# Gets the value for the given attribute on the node
|
108
|
+
def value(node, attribute)
|
109
|
+
attribute == :value ? node.value(false) : super
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,399 @@
|
|
1
|
+
require 'state_machine/transition_collection'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# An invalid transition was attempted
|
5
|
+
class InvalidTransition < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# A transition represents a state change for a specific attribute.
|
9
|
+
#
|
10
|
+
# Transitions consist of:
|
11
|
+
# * An event
|
12
|
+
# * A starting state
|
13
|
+
# * An ending state
|
14
|
+
class Transition
|
15
|
+
# The object being transitioned
|
16
|
+
attr_reader :object
|
17
|
+
|
18
|
+
# The state machine for which this transition is defined
|
19
|
+
attr_reader :machine
|
20
|
+
|
21
|
+
# The original state value *before* the transition
|
22
|
+
attr_reader :from
|
23
|
+
|
24
|
+
# The new state value *after* the transition
|
25
|
+
attr_reader :to
|
26
|
+
|
27
|
+
# The arguments passed in to the event that triggered the transition
|
28
|
+
# (does not include the +run_action+ boolean argument if specified)
|
29
|
+
attr_accessor :args
|
30
|
+
|
31
|
+
# The result of invoking the action associated with the machine
|
32
|
+
attr_reader :result
|
33
|
+
|
34
|
+
# Whether the transition is only existing temporarily for the object
|
35
|
+
attr_writer :transient
|
36
|
+
|
37
|
+
# Creates a new, specific transition
|
38
|
+
def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
|
39
|
+
@object = object
|
40
|
+
@machine = machine
|
41
|
+
@args = []
|
42
|
+
@transient = false
|
43
|
+
|
44
|
+
@event = machine.events.fetch(event)
|
45
|
+
@from_state = machine.states.fetch(from_name)
|
46
|
+
@from = read_state ? machine.read(object, :state) : @from_state.value
|
47
|
+
@to_state = machine.states.fetch(to_name)
|
48
|
+
@to = @to_state.value
|
49
|
+
|
50
|
+
reset
|
51
|
+
end
|
52
|
+
|
53
|
+
# The attribute which this transition's machine is defined for
|
54
|
+
def attribute
|
55
|
+
machine.attribute
|
56
|
+
end
|
57
|
+
|
58
|
+
# The action that will be run when this transition is performed
|
59
|
+
def action
|
60
|
+
machine.action
|
61
|
+
end
|
62
|
+
|
63
|
+
# The event that triggered the transition
|
64
|
+
def event
|
65
|
+
@event.name
|
66
|
+
end
|
67
|
+
|
68
|
+
# The fully-qualified name of the event that triggered the transition
|
69
|
+
def qualified_event
|
70
|
+
@event.qualified_name
|
71
|
+
end
|
72
|
+
|
73
|
+
# The human-readable name of the event that triggered the transition
|
74
|
+
def human_event
|
75
|
+
@event.human_name(@object.class)
|
76
|
+
end
|
77
|
+
|
78
|
+
# The state name *before* the transition
|
79
|
+
def from_name
|
80
|
+
@from_state.name
|
81
|
+
end
|
82
|
+
|
83
|
+
# The fully-qualified state name *before* the transition
|
84
|
+
def qualified_from_name
|
85
|
+
@from_state.qualified_name
|
86
|
+
end
|
87
|
+
|
88
|
+
# The human-readable state name *before* the transition
|
89
|
+
def human_from_name
|
90
|
+
@from_state.human_name(@object.class)
|
91
|
+
end
|
92
|
+
|
93
|
+
# The new state name *after* the transition
|
94
|
+
def to_name
|
95
|
+
@to_state.name
|
96
|
+
end
|
97
|
+
|
98
|
+
# The new fully-qualified state name *after* the transition
|
99
|
+
def qualified_to_name
|
100
|
+
@to_state.qualified_name
|
101
|
+
end
|
102
|
+
|
103
|
+
# The new human-readable state name *after* the transition
|
104
|
+
def human_to_name
|
105
|
+
@to_state.human_name(@object.class)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Does this transition represent a loopback (i.e. the from and to state
|
109
|
+
# are the same)
|
110
|
+
#
|
111
|
+
# == Example
|
112
|
+
#
|
113
|
+
# machine = StateMachine.new(Vehicle)
|
114
|
+
# StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
|
115
|
+
# StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
|
116
|
+
def loopback?
|
117
|
+
from_name == to_name
|
118
|
+
end
|
119
|
+
|
120
|
+
# Is this transition existing for a short period only? If this is set, it
|
121
|
+
# indicates that the transition (or the event backing it) should not be
|
122
|
+
# written to the object if it fails.
|
123
|
+
def transient?
|
124
|
+
@transient
|
125
|
+
end
|
126
|
+
|
127
|
+
# A hash of all the core attributes defined for this transition with their
|
128
|
+
# names as keys and values of the attributes as values.
|
129
|
+
#
|
130
|
+
# == Example
|
131
|
+
#
|
132
|
+
# machine = StateMachine.new(Vehicle)
|
133
|
+
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
134
|
+
# transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
|
135
|
+
def attributes
|
136
|
+
@attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
|
137
|
+
end
|
138
|
+
|
139
|
+
# Runs the actual transition and any before/after callbacks associated
|
140
|
+
# with the transition. The action associated with the transition/machine
|
141
|
+
# can be skipped by passing in +false+.
|
142
|
+
#
|
143
|
+
# == Examples
|
144
|
+
#
|
145
|
+
# class Vehicle
|
146
|
+
# state_machine :action => :save do
|
147
|
+
# ...
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# vehicle = Vehicle.new
|
152
|
+
# transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
|
153
|
+
# transition.perform # => Runs the +save+ action after setting the state attribute
|
154
|
+
# transition.perform(false) # => Only sets the state attribute
|
155
|
+
# transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
|
156
|
+
# transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
|
157
|
+
def perform(*args)
|
158
|
+
run_action = [true, false].include?(args.last) ? args.pop : true
|
159
|
+
self.args = args
|
160
|
+
|
161
|
+
# Run the transition
|
162
|
+
!!TransitionCollection.new([self], :actions => run_action).perform
|
163
|
+
end
|
164
|
+
|
165
|
+
# Runs a block within a transaction for the object being transitioned.
|
166
|
+
# By default, transactions are a no-op unless otherwise defined by the
|
167
|
+
# machine's integration.
|
168
|
+
def within_transaction
|
169
|
+
machine.within_transaction(object) do
|
170
|
+
yield
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Runs the before / after callbacks for this transition. If a block is
|
175
|
+
# provided, then it will be executed between the before and after callbacks.
|
176
|
+
#
|
177
|
+
# Configuration options:
|
178
|
+
# * +after+ - Whether to run after callbacks. If false, then any around
|
179
|
+
# callbacks will be paused until called again with +after+ enabled.
|
180
|
+
# Default is true.
|
181
|
+
#
|
182
|
+
# This will return true if all before callbacks gets executed. After
|
183
|
+
# callbacks will not have an effect on the result.
|
184
|
+
def run_callbacks(options = {}, &block)
|
185
|
+
options = {:after => true}.merge(options)
|
186
|
+
@success = false
|
187
|
+
|
188
|
+
halted = pausable { before(options[:after], &block) }
|
189
|
+
|
190
|
+
# After callbacks are only run if:
|
191
|
+
# * An around callback didn't halt after yielding
|
192
|
+
# * They're enabled or the run didn't succeed
|
193
|
+
after if !(@before_run && halted) && (options[:after] || !@success)
|
194
|
+
|
195
|
+
@before_run
|
196
|
+
end
|
197
|
+
|
198
|
+
# Transitions the current value of the state to that specified by the
|
199
|
+
# transition. Once the state is persisted, it cannot be persisted again
|
200
|
+
# until this transition is reset.
|
201
|
+
#
|
202
|
+
# == Example
|
203
|
+
#
|
204
|
+
# class Vehicle
|
205
|
+
# state_machine do
|
206
|
+
# event :ignite do
|
207
|
+
# transition :parked => :idling
|
208
|
+
# end
|
209
|
+
# end
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# vehicle = Vehicle.new
|
213
|
+
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
214
|
+
# transition.persist
|
215
|
+
#
|
216
|
+
# vehicle.state # => 'idling'
|
217
|
+
def persist
|
218
|
+
unless @persisted
|
219
|
+
machine.write(object, :state, to)
|
220
|
+
@persisted = true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Rolls back changes made to the object's state via this transition. This
|
225
|
+
# will revert the state back to the +from+ value.
|
226
|
+
#
|
227
|
+
# == Example
|
228
|
+
#
|
229
|
+
# class Vehicle
|
230
|
+
# state_machine :initial => :parked do
|
231
|
+
# event :ignite do
|
232
|
+
# transition :parked => :idling
|
233
|
+
# end
|
234
|
+
# end
|
235
|
+
# end
|
236
|
+
#
|
237
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
|
238
|
+
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
239
|
+
#
|
240
|
+
# # Persist the new state
|
241
|
+
# vehicle.state # => "parked"
|
242
|
+
# transition.persist
|
243
|
+
# vehicle.state # => "idling"
|
244
|
+
#
|
245
|
+
# # Roll back to the original state
|
246
|
+
# transition.rollback
|
247
|
+
# vehicle.state # => "parked"
|
248
|
+
def rollback
|
249
|
+
reset
|
250
|
+
machine.write(object, :state, from)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Resets any tracking of which callbacks have already been run and whether
|
254
|
+
# the state has already been persisted
|
255
|
+
def reset
|
256
|
+
@before_run = @persisted = @after_run = false
|
257
|
+
@paused_block = nil
|
258
|
+
end
|
259
|
+
|
260
|
+
# Generates a nicely formatted description of this transitions's contents.
|
261
|
+
#
|
262
|
+
# For example,
|
263
|
+
#
|
264
|
+
# transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
|
265
|
+
# transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
266
|
+
def inspect
|
267
|
+
"#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
|
268
|
+
end
|
269
|
+
|
270
|
+
private
|
271
|
+
# Runs a block that may get paused. If the block doesn't pause, then
|
272
|
+
# execution will continue as normal. If the block gets paused, then it
|
273
|
+
# will take care of switching the execution context when it's resumed.
|
274
|
+
#
|
275
|
+
# This will return true if the given block halts for a reason other than
|
276
|
+
# getting paused.
|
277
|
+
def pausable
|
278
|
+
begin
|
279
|
+
halted = !catch(:halt) { yield; true }
|
280
|
+
rescue Exception => error
|
281
|
+
raise unless @resume_block
|
282
|
+
end
|
283
|
+
|
284
|
+
if @resume_block
|
285
|
+
@resume_block.call(halted, error)
|
286
|
+
else
|
287
|
+
halted
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Pauses the current callback execution. This should only occur within
|
292
|
+
# around callbacks when the remainder of the callback will be executed at
|
293
|
+
# a later point in time.
|
294
|
+
def pause
|
295
|
+
unless @resume_block
|
296
|
+
require 'continuation' unless defined?(callcc)
|
297
|
+
callcc do |block|
|
298
|
+
@paused_block = block
|
299
|
+
throw :halt, true
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Resumes the execution of a previously paused callback execution. Once
|
305
|
+
# the paused callbacks complete, the current execution will continue.
|
306
|
+
def resume
|
307
|
+
if @paused_block
|
308
|
+
halted, error = callcc do |block|
|
309
|
+
@resume_block = block
|
310
|
+
@paused_block.call
|
311
|
+
end
|
312
|
+
|
313
|
+
@resume_block = @paused_block = nil
|
314
|
+
|
315
|
+
raise error if error
|
316
|
+
!halted
|
317
|
+
else
|
318
|
+
true
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Runs the machine's +before+ callbacks for this transition. Only
|
323
|
+
# callbacks that are configured to match the event, from state, and to
|
324
|
+
# state will be invoked.
|
325
|
+
#
|
326
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
327
|
+
# is reset.
|
328
|
+
def before(complete = true, index = 0, &block)
|
329
|
+
unless @before_run
|
330
|
+
while callback = machine.callbacks[:before][index]
|
331
|
+
index += 1
|
332
|
+
|
333
|
+
if callback.type == :around
|
334
|
+
# Around callback: need to handle recursively. Execution only gets
|
335
|
+
# paused if:
|
336
|
+
# * The block fails and the callback doesn't run on failures OR
|
337
|
+
# * The block succeeds, but after callbacks are disabled (in which
|
338
|
+
# case a continuation is stored for later execution)
|
339
|
+
return if catch(:cancel) do
|
340
|
+
callback.call(object, context, self) do
|
341
|
+
before(complete, index, &block)
|
342
|
+
|
343
|
+
pause if @success && !complete
|
344
|
+
throw :cancel, true unless callback.matches_success?(@success)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
else
|
348
|
+
# Normal before callback
|
349
|
+
callback.call(object, context, self)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
@before_run = true
|
354
|
+
end
|
355
|
+
|
356
|
+
action = {:success => true}.merge(block_given? ? yield : {})
|
357
|
+
@result, @success = action[:result], action[:success]
|
358
|
+
end
|
359
|
+
|
360
|
+
# Runs the machine's +after+ callbacks for this transition. Only
|
361
|
+
# callbacks that are configured to match the event, from state, and to
|
362
|
+
# state will be invoked.
|
363
|
+
#
|
364
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
365
|
+
# is reset.
|
366
|
+
#
|
367
|
+
# == Halting
|
368
|
+
#
|
369
|
+
# If any callback throws a <tt>:halt</tt> exception, it will be caught
|
370
|
+
# and the callback chain will be automatically stopped. However, this
|
371
|
+
# exception will not bubble up to the caller since +after+ callbacks
|
372
|
+
# should never halt the execution of a +perform+.
|
373
|
+
def after
|
374
|
+
unless @after_run
|
375
|
+
# First resume previously paused callbacks
|
376
|
+
if resume
|
377
|
+
catch(:halt) do
|
378
|
+
after_context = context.merge(:success => @success)
|
379
|
+
machine.callbacks[:after].each {|callback| callback.call(object, after_context, self)}
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
@after_run = true
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Gets a hash of the context defining this unique transition (including
|
388
|
+
# event, from state, and to state).
|
389
|
+
#
|
390
|
+
# == Example
|
391
|
+
#
|
392
|
+
# machine = StateMachine.new(Vehicle)
|
393
|
+
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
394
|
+
# transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
|
395
|
+
def context
|
396
|
+
@context ||= {:on => event, :from => from_name, :to => to_name}
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|