state_machine 0.5.2 → 0.6.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 +9 -0
- data/README.rdoc +34 -26
- data/Rakefile +1 -1
- data/examples/auto_shop.rb +2 -2
- data/examples/car.rb +4 -4
- data/examples/traffic_light.rb +1 -3
- data/examples/vehicle.rb +7 -11
- data/lib/state_machine.rb +5 -5
- data/lib/state_machine/callback.rb +1 -1
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/event.rb +92 -23
- data/lib/state_machine/guard.rb +90 -57
- data/lib/state_machine/integrations/active_record.rb +3 -3
- data/lib/state_machine/integrations/data_mapper.rb +3 -3
- data/lib/state_machine/integrations/data_mapper/observer.rb +8 -6
- data/lib/state_machine/integrations/sequel.rb +3 -3
- data/lib/state_machine/machine.rb +136 -130
- data/lib/state_machine/matcher_helpers.rb +53 -0
- data/lib/state_machine/state.rb +3 -3
- data/test/active_record.log +23221 -0
- data/test/classes/switch.rb +2 -2
- data/test/functional/state_machine_test.rb +25 -29
- data/test/sequel.log +10610 -0
- data/test/unit/callback_test.rb +36 -6
- data/test/unit/condition_proxy_test.rb +301 -0
- data/test/unit/event_test.rb +31 -17
- data/test/unit/guard_test.rb +246 -13
- data/test/unit/integrations/active_record_test.rb +29 -0
- data/test/unit/integrations/data_mapper_test.rb +45 -0
- data/test/unit/integrations/sequel_test.rb +29 -0
- data/test/unit/machine_test.rb +25 -3
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/node_collection_test.rb +0 -4
- data/test/unit/state_collection_test.rb +0 -4
- metadata +8 -2
data/lib/state_machine/guard.rb
CHANGED
@@ -17,58 +17,60 @@ module StateMachine
|
|
17
17
|
# The condition that must *not* be met on an object
|
18
18
|
attr_reader :unless_condition
|
19
19
|
|
20
|
-
# The requirement for verifying the event being guarded
|
21
|
-
# :except_on).
|
20
|
+
# The requirement for verifying the event being guarded
|
22
21
|
attr_reader :event_requirement
|
23
22
|
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
attr_reader :state_requirement
|
23
|
+
# One or more requrirements for verifying the states being guarded. All
|
24
|
+
# requirements contain a mapping of {:from => matcher, :to => matcher}.
|
25
|
+
attr_reader :state_requirements
|
28
26
|
|
29
27
|
# A list of all of the states known to this guard. This will pull states
|
30
|
-
# from the following options
|
28
|
+
# from the following options (in the same order):
|
31
29
|
# * +from+ / +except_from+
|
32
30
|
# * +to+ / +except_to+
|
33
31
|
attr_reader :known_states
|
34
32
|
|
35
33
|
# Creates a new guard
|
36
34
|
def initialize(options = {}) #:nodoc:
|
37
|
-
assert_valid_keys(options, :from, :to, :on, :except_from, :except_to, :except_on, :if, :unless)
|
38
|
-
|
39
35
|
# Build conditionals
|
40
|
-
assert_exclusive_keys(options, :if, :unless)
|
41
36
|
@if_condition = options.delete(:if)
|
42
37
|
@unless_condition = options.delete(:unless)
|
43
38
|
|
44
|
-
# Build event
|
39
|
+
# Build event requirement
|
45
40
|
@event_requirement = build_matcher(options, :on, :except_on)
|
46
|
-
|
41
|
+
|
42
|
+
if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
|
43
|
+
# Explicit from/to requirements specified
|
44
|
+
@state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
|
45
|
+
else
|
46
|
+
# Separate out the event requirement
|
47
|
+
options.delete(:on)
|
48
|
+
options.delete(:except_on)
|
49
|
+
|
50
|
+
# Implicit from/to requirements specified
|
51
|
+
@state_requirements = options.collect do |from, to|
|
52
|
+
from = WhitelistMatcher.new(from) unless from.is_a?(Matcher)
|
53
|
+
to = WhitelistMatcher.new(to) unless to.is_a?(Matcher)
|
54
|
+
{:from => from, :to => to}
|
55
|
+
end
|
56
|
+
end
|
47
57
|
|
48
58
|
# Track known states. The order that requirements are iterated is based
|
49
59
|
# on the priority in which tracked states should be added.
|
50
60
|
@known_states = []
|
51
|
-
|
61
|
+
@state_requirements.each do |state_requirement|
|
62
|
+
[:from, :to].each {|option| @known_states |= state_requirement[option].values}
|
63
|
+
end
|
52
64
|
end
|
53
65
|
|
54
|
-
#
|
66
|
+
# Determines whether the given object / query matches the requirements
|
55
67
|
# configured for this guard. In addition to matching the event, from state,
|
56
68
|
# and to state, this will also check whether the configured :if/:unless
|
57
69
|
# conditions pass on the given object.
|
58
70
|
#
|
59
|
-
# This will return true or false depending on whether a match is found.
|
60
|
-
#
|
61
|
-
# Query options:
|
62
|
-
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
63
|
-
# are specified, then this will always match.
|
64
|
-
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
65
|
-
# specified, then this will always match.
|
66
|
-
# * <tt>:on</tt> - One or more events that fired the transition. If none
|
67
|
-
# are specified, then this will always match.
|
68
|
-
#
|
69
71
|
# == Examples
|
70
72
|
#
|
71
|
-
# guard = StateMachine::Guard.new(:
|
73
|
+
# guard = StateMachine::Guard.new(:parked => :idling, :on => :ignite)
|
72
74
|
#
|
73
75
|
# # Successful
|
74
76
|
# guard.matches?(object, :on => :ignite) # => true
|
@@ -85,7 +87,36 @@ module StateMachine
|
|
85
87
|
# guard.matches?(object, :from => :parked, :to => :first_gear) # => false
|
86
88
|
# guard.matches?(object, :on => :park, :from => :parked, :to => :idling) # => false
|
87
89
|
def matches?(object, query = {})
|
88
|
-
|
90
|
+
!match(object, query).nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Attempts to match the given object / query against the set of requirements
|
94
|
+
# configured for this guard. In addition to matching the event, from state,
|
95
|
+
# and to state, this will also check whether the configured :if/:unless
|
96
|
+
# conditions pass on the given object.
|
97
|
+
#
|
98
|
+
# If a match is found, then the event/state requirements that the query
|
99
|
+
# passed successfully will be returned. Otherwise, nil is returned if there
|
100
|
+
# was no match.
|
101
|
+
#
|
102
|
+
# Query options:
|
103
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
104
|
+
# are specified, then this will always match.
|
105
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
106
|
+
# specified, then this will always match.
|
107
|
+
# * <tt>:on</tt> - One or more events that fired the transition. If none
|
108
|
+
# are specified, then this will always match.
|
109
|
+
#
|
110
|
+
# == Examples
|
111
|
+
#
|
112
|
+
# guard = StateMachine::Guard.new(:parked => :idling, :on => :ignite)
|
113
|
+
#
|
114
|
+
# guard.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
|
115
|
+
# guard.match(object, :on => :park) # => nil
|
116
|
+
def match(object, query = {})
|
117
|
+
if (match = match_query(query)) && matches_conditions?(object)
|
118
|
+
match
|
119
|
+
end
|
89
120
|
end
|
90
121
|
|
91
122
|
# Draws a representation of this guard on the given graph. This will draw
|
@@ -108,26 +139,28 @@ module StateMachine
|
|
108
139
|
#
|
109
140
|
# The collection of edges generated on the graph will be returned.
|
110
141
|
def draw(graph, event, valid_states)
|
111
|
-
edges
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
142
|
+
state_requirements.inject([]) do |edges, state_requirement|
|
143
|
+
# From states determined based on the known valid states
|
144
|
+
from_states = state_requirement[:from].filter(valid_states)
|
145
|
+
|
146
|
+
# If a to state is not specified, then it's a loopback and each from
|
147
|
+
# state maps back to itself
|
148
|
+
if state_requirement[:to].values.empty?
|
149
|
+
loopback = true
|
150
|
+
else
|
151
|
+
to_state = state_requirement[:to].values.first
|
152
|
+
to_state = to_state ? to_state.to_s : 'nil'
|
153
|
+
loopback = false
|
154
|
+
end
|
155
|
+
|
156
|
+
# Generate an edge between each from and to state
|
157
|
+
from_states.each do |from_state|
|
158
|
+
from_state = from_state ? from_state.to_s : 'nil'
|
159
|
+
edges << graph.add_edge(from_state, loopback ? from_state : to_state, :label => event.to_s)
|
160
|
+
end
|
161
|
+
|
162
|
+
edges
|
128
163
|
end
|
129
|
-
|
130
|
-
edges
|
131
164
|
end
|
132
165
|
|
133
166
|
protected
|
@@ -149,20 +182,25 @@ module StateMachine
|
|
149
182
|
# Verifies that all configured requirements (event and state) match the
|
150
183
|
# given query. If a match is return, then a hash containing the
|
151
184
|
# event/state requirements that passed will be returned; otherwise, nil.
|
152
|
-
def
|
185
|
+
def match_query(query)
|
153
186
|
query ||= {}
|
154
|
-
|
187
|
+
|
188
|
+
if match_event(query) && (state_requirement = match_states(query))
|
189
|
+
state_requirement.merge(:on => event_requirement)
|
190
|
+
end
|
155
191
|
end
|
156
192
|
|
157
193
|
# Verifies that the event requirement matches the given query
|
158
|
-
def
|
194
|
+
def match_event(query)
|
159
195
|
matches_requirement?(query, :on, event_requirement)
|
160
196
|
end
|
161
197
|
|
162
198
|
# Verifies that the state requirements match the given query. If a
|
163
199
|
# matching requirement is found, then it is returned.
|
164
|
-
def
|
165
|
-
|
200
|
+
def match_states(query)
|
201
|
+
state_requirements.detect do |state_requirement|
|
202
|
+
[:from, :to].all? {|option| matches_requirement?(query, option, state_requirement[option])}
|
203
|
+
end
|
166
204
|
end
|
167
205
|
|
168
206
|
# Verifies that an option in the given query matches the values required
|
@@ -174,13 +212,8 @@ module StateMachine
|
|
174
212
|
# Verifies that the conditionals for this guard evaluate to true for the
|
175
213
|
# given object
|
176
214
|
def matches_conditions?(object)
|
177
|
-
|
178
|
-
|
179
|
-
elsif unless_condition
|
180
|
-
!evaluate_method(object, unless_condition)
|
181
|
-
else
|
182
|
-
true
|
183
|
-
end
|
215
|
+
Array(if_condition).all? {|condition| evaluate_method(object, condition)} &&
|
216
|
+
!Array(unless_condition).any? {|condition| evaluate_method(object, condition)}
|
184
217
|
end
|
185
218
|
end
|
186
219
|
end
|
@@ -10,7 +10,7 @@ module StateMachine
|
|
10
10
|
# class Vehicle < ActiveRecord::Base
|
11
11
|
# state_machine :initial => :parked do
|
12
12
|
# event :ignite do
|
13
|
-
# transition :
|
13
|
+
# transition :parked => :idling
|
14
14
|
# end
|
15
15
|
# end
|
16
16
|
# end
|
@@ -95,7 +95,7 @@ module StateMachine
|
|
95
95
|
#
|
96
96
|
# class Vehicle < ActiveRecord::Base
|
97
97
|
# state_machine :initial => :parked do
|
98
|
-
# before_transition
|
98
|
+
# before_transition any => :idling do |vehicle|
|
99
99
|
# vehicle.put_on_seatbelt
|
100
100
|
# end
|
101
101
|
#
|
@@ -104,7 +104,7 @@ module StateMachine
|
|
104
104
|
# end
|
105
105
|
#
|
106
106
|
# event :ignite do
|
107
|
-
# transition :
|
107
|
+
# transition :parked => :idling
|
108
108
|
# end
|
109
109
|
# end
|
110
110
|
#
|
@@ -16,7 +16,7 @@ module StateMachine
|
|
16
16
|
#
|
17
17
|
# state_machine :initial => :parked do
|
18
18
|
# event :ignite do
|
19
|
-
# transition :
|
19
|
+
# transition :parked => :idling
|
20
20
|
# end
|
21
21
|
# end
|
22
22
|
# end
|
@@ -122,7 +122,7 @@ module StateMachine
|
|
122
122
|
# property :state, String
|
123
123
|
#
|
124
124
|
# state_machine :initial => :parked do
|
125
|
-
# before_transition
|
125
|
+
# before_transition any => :idling do
|
126
126
|
# put_on_seatbelt
|
127
127
|
# end
|
128
128
|
#
|
@@ -131,7 +131,7 @@ module StateMachine
|
|
131
131
|
# end
|
132
132
|
#
|
133
133
|
# event :ignite do
|
134
|
-
# transition :
|
134
|
+
# transition :parked => :idling
|
135
135
|
# end
|
136
136
|
# end
|
137
137
|
#
|
@@ -37,6 +37,8 @@ module StateMachine
|
|
37
37
|
# If dm-observer is not available, then this feature will be skipped.
|
38
38
|
#
|
39
39
|
module Observer
|
40
|
+
include MatcherHelpers
|
41
|
+
|
40
42
|
# Creates a callback that will be invoked *before* a transition is
|
41
43
|
# performed, so long as the given configuration options match the
|
42
44
|
# transition. Each part of the transition (event, to state, from state)
|
@@ -55,7 +57,7 @@ module StateMachine
|
|
55
57
|
#
|
56
58
|
# state_machine :initial => :parked do
|
57
59
|
# event :ignite do
|
58
|
-
# transition :
|
60
|
+
# transition :parked => :idling
|
59
61
|
# end
|
60
62
|
# end
|
61
63
|
# end
|
@@ -70,12 +72,12 @@ module StateMachine
|
|
70
72
|
# end
|
71
73
|
#
|
72
74
|
# # Target all state machines
|
73
|
-
# before_transition :
|
75
|
+
# before_transition :parked => :idling, :on => :ignite do
|
74
76
|
# # put on seatbelt
|
75
77
|
# end
|
76
78
|
#
|
77
79
|
# # Target a specific state machine
|
78
|
-
# before_transition :state,
|
80
|
+
# before_transition :state, any => :idling do
|
79
81
|
# # put on seatbelt
|
80
82
|
# end
|
81
83
|
#
|
@@ -111,7 +113,7 @@ module StateMachine
|
|
111
113
|
#
|
112
114
|
# state_machine :initial => :parked do
|
113
115
|
# event :ignite do
|
114
|
-
# transition :
|
116
|
+
# transition :parked => :idling
|
115
117
|
# end
|
116
118
|
# end
|
117
119
|
# end
|
@@ -126,12 +128,12 @@ module StateMachine
|
|
126
128
|
# end
|
127
129
|
#
|
128
130
|
# # Target all state machines
|
129
|
-
# after_transition :
|
131
|
+
# after_transition :parked => :idling, :on => :ignite do
|
130
132
|
# # put on seatbelt
|
131
133
|
# end
|
132
134
|
#
|
133
135
|
# # Target a specific state machine
|
134
|
-
# after_transition :state,
|
136
|
+
# after_transition :state, any => :idling do
|
135
137
|
# # put on seatbelt
|
136
138
|
# end
|
137
139
|
#
|
@@ -10,7 +10,7 @@ module StateMachine
|
|
10
10
|
# class Vehicle < Sequel::Model
|
11
11
|
# state_machine :initial => :parked do
|
12
12
|
# event :ignite do
|
13
|
-
# transition :
|
13
|
+
# transition :parked => :idling
|
14
14
|
# end
|
15
15
|
# end
|
16
16
|
# end
|
@@ -101,7 +101,7 @@ module StateMachine
|
|
101
101
|
#
|
102
102
|
# class Vehicle < Sequel::Model
|
103
103
|
# state_machine :initial => :parked do
|
104
|
-
# before_transition
|
104
|
+
# before_transition any => :idling do
|
105
105
|
# put_on_seatbelt
|
106
106
|
# end
|
107
107
|
#
|
@@ -110,7 +110,7 @@ module StateMachine
|
|
110
110
|
# end
|
111
111
|
#
|
112
112
|
# event :ignite do
|
113
|
-
# transition :
|
113
|
+
# transition :parked => :idling
|
114
114
|
# end
|
115
115
|
# end
|
116
116
|
#
|
@@ -7,6 +7,7 @@ require 'state_machine/event'
|
|
7
7
|
require 'state_machine/callback'
|
8
8
|
require 'state_machine/node_collection'
|
9
9
|
require 'state_machine/state_collection'
|
10
|
+
require 'state_machine/matcher_helpers'
|
10
11
|
|
11
12
|
module StateMachine
|
12
13
|
# Represents a state machine for a particular attribute. State machines
|
@@ -44,7 +45,7 @@ module StateMachine
|
|
44
45
|
#
|
45
46
|
# class Vehicle
|
46
47
|
# state_machine, :initial => :parked do
|
47
|
-
# before_transition
|
48
|
+
# before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
|
48
49
|
# ...
|
49
50
|
# end
|
50
51
|
# end
|
@@ -64,7 +65,7 @@ module StateMachine
|
|
64
65
|
# class Vehicle
|
65
66
|
# state_machine do
|
66
67
|
# event :park do
|
67
|
-
# transition :
|
68
|
+
# transition :idling => :parked
|
68
69
|
# end
|
69
70
|
# ...
|
70
71
|
# end
|
@@ -115,8 +116,8 @@ module StateMachine
|
|
115
116
|
# end
|
116
117
|
# end
|
117
118
|
#
|
118
|
-
# Additional observer-like behavior may be exposed by the various
|
119
|
-
#
|
119
|
+
# Additional observer-like behavior may be exposed by the various integrations
|
120
|
+
# available. See below for more information.
|
120
121
|
#
|
121
122
|
# == Integrations
|
122
123
|
#
|
@@ -138,6 +139,7 @@ module StateMachine
|
|
138
139
|
# constants defined under the StateMachine::Integrations namespace.
|
139
140
|
class Machine
|
140
141
|
include Assertions
|
142
|
+
include MatcherHelpers
|
141
143
|
|
142
144
|
class << self
|
143
145
|
# Attempts to find or create a state machine for the given class. For
|
@@ -369,7 +371,7 @@ module StateMachine
|
|
369
371
|
# class Vehicle
|
370
372
|
# state_machine :initial => :parked do
|
371
373
|
# event :ignite do
|
372
|
-
# transition :
|
374
|
+
# transition :parked => :idling
|
373
375
|
# end
|
374
376
|
# end
|
375
377
|
# end
|
@@ -387,7 +389,7 @@ module StateMachine
|
|
387
389
|
# class Vehicle
|
388
390
|
# state_machine :initial => :parked do
|
389
391
|
# event :ignite do
|
390
|
-
# transition :
|
392
|
+
# transition :parked => :idling
|
391
393
|
# end
|
392
394
|
#
|
393
395
|
# state :idling, :value => 'IDLING'
|
@@ -405,7 +407,7 @@ module StateMachine
|
|
405
407
|
# class Vehicle < ActiveRecord::Base
|
406
408
|
# state_machine :state_id, :initial => :parked do
|
407
409
|
# event :ignite do
|
408
|
-
# transition :
|
410
|
+
# transition :parked => :idling
|
409
411
|
# end
|
410
412
|
#
|
411
413
|
# states.each {|state| self.state(state.name, :value => VehicleState.find_by_name(state.name.to_s).id)}
|
@@ -424,11 +426,11 @@ module StateMachine
|
|
424
426
|
# class Vehicle
|
425
427
|
# state_machine :purchased_at, :initial => :available do
|
426
428
|
# event :purchase do
|
427
|
-
# transition
|
429
|
+
# transition all => :purchased
|
428
430
|
# end
|
429
431
|
#
|
430
432
|
# event :restock do
|
431
|
-
# transition
|
433
|
+
# transition all => :available
|
432
434
|
# end
|
433
435
|
#
|
434
436
|
# state :available, :value => nil
|
@@ -464,7 +466,7 @@ module StateMachine
|
|
464
466
|
#
|
465
467
|
# state_machine :initial => :parked do
|
466
468
|
# event :ignite do
|
467
|
-
# transition :
|
469
|
+
# transition :parked => :idling
|
468
470
|
# end
|
469
471
|
#
|
470
472
|
# state :parked do
|
@@ -540,6 +542,39 @@ module StateMachine
|
|
540
542
|
# vehicle = Vehicle.new
|
541
543
|
# vehicle.state = 'backing_up'
|
542
544
|
# vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
|
545
|
+
#
|
546
|
+
# == State-aware class methods
|
547
|
+
#
|
548
|
+
# In addition to defining scopes for instance methods that are state-aware,
|
549
|
+
# the same can be done for certain types of class methods.
|
550
|
+
#
|
551
|
+
# Some libraries have support for class-level methods that only run certain
|
552
|
+
# behaviors based on a conditions hash passed in. For example:
|
553
|
+
#
|
554
|
+
# class Vehicle < ActiveRecord::Base
|
555
|
+
# state_machine do
|
556
|
+
# ...
|
557
|
+
# state :first_gear, :second_gear, :third_gear do
|
558
|
+
# validates_presence_of :speed
|
559
|
+
# validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
|
560
|
+
# end
|
561
|
+
# end
|
562
|
+
# end
|
563
|
+
#
|
564
|
+
# In the above ActiveRecord model, two validations have been defined which
|
565
|
+
# will *only* run when the Vehicle object is in one of the three states:
|
566
|
+
# +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
|
567
|
+
# conditions can continue to be used.
|
568
|
+
#
|
569
|
+
# This functionality is not library-specific and can work for any class-level
|
570
|
+
# method that is defined like so:
|
571
|
+
#
|
572
|
+
# def validates_presence_of(args, options = {})
|
573
|
+
# ...
|
574
|
+
# end
|
575
|
+
#
|
576
|
+
# The minimum requirement is that the last argument in the method be an
|
577
|
+
# options hash which contains at least <tt>:if</tt> condition support.
|
543
578
|
def state(*names, &block)
|
544
579
|
options = names.last.is_a?(Hash) ? names.pop : {}
|
545
580
|
assert_valid_keys(options, :value, :if)
|
@@ -616,6 +651,9 @@ module StateMachine
|
|
616
651
|
# Defines one or more events for the machine and the transitions that can
|
617
652
|
# be performed when those events are run.
|
618
653
|
#
|
654
|
+
# This method is also aliased as +on+ for improved compatibility with
|
655
|
+
# using a domain-specific language.
|
656
|
+
#
|
619
657
|
# == Instance methods
|
620
658
|
#
|
621
659
|
# The following instance methods are generated when a new event is defined
|
@@ -643,11 +681,11 @@ module StateMachine
|
|
643
681
|
# transitions that can happen as a result of that event. For example,
|
644
682
|
#
|
645
683
|
# event :park, :stop do
|
646
|
-
# transition :
|
684
|
+
# transition :idling => :parked
|
647
685
|
# end
|
648
686
|
#
|
649
687
|
# event :first_gear do
|
650
|
-
# transition :
|
688
|
+
# transition :parked => :first_gear, :if => :seatbelt_on?
|
651
689
|
# end
|
652
690
|
#
|
653
691
|
# See StateMachine::Event#transition for more information on
|
@@ -664,7 +702,7 @@ module StateMachine
|
|
664
702
|
#
|
665
703
|
# state_machine do
|
666
704
|
# event :park do
|
667
|
-
# transition
|
705
|
+
# transition Vehicle.safe_states => :parked
|
668
706
|
# end
|
669
707
|
# end
|
670
708
|
# end
|
@@ -675,15 +713,15 @@ module StateMachine
|
|
675
713
|
# state_machine do
|
676
714
|
# # The park, stop, and halt events will all share the given transitions
|
677
715
|
# event :park, :stop, :halt do
|
678
|
-
# transition :
|
716
|
+
# transition [:idling, :backing_up] => :parked
|
679
717
|
# end
|
680
718
|
#
|
681
719
|
# event :stop do
|
682
|
-
# transition :
|
720
|
+
# transition :first_gear => :idling
|
683
721
|
# end
|
684
722
|
#
|
685
723
|
# event :ignite do
|
686
|
-
# transition :
|
724
|
+
# transition :parked => :idling
|
687
725
|
# end
|
688
726
|
# end
|
689
727
|
# end
|
@@ -703,108 +741,71 @@ module StateMachine
|
|
703
741
|
|
704
742
|
events.length == 1 ? events.first : events
|
705
743
|
end
|
744
|
+
alias_method :on, :event
|
706
745
|
|
707
746
|
# Creates a callback that will be invoked *before* a transition is
|
708
|
-
# performed so long as the given
|
709
|
-
# Each part of the transition (event, to state, from state) must match in
|
710
|
-
# order for the callback to get invoked.
|
711
|
-
#
|
712
|
-
# Configuration options:
|
713
|
-
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
714
|
-
# are specified, then all states will match.
|
715
|
-
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
716
|
-
# specified, then all states will match.
|
717
|
-
# * <tt>:on</tt> - One or more events that fired the transition. If none
|
718
|
-
# are specified, then all events will match.
|
719
|
-
# * <tt>:except_from</tt> - One or more states *not* being transitioned from
|
720
|
-
# * <tt>:except_to</tt> - One more states *not* being transitioned to
|
721
|
-
# * <tt>:except_on</tt> - One or more events that *did not* fire the transition
|
722
|
-
# * <tt>:do</tt> - The callback to invoke when a transition matches. This
|
723
|
-
# can be a method, proc or string.
|
724
|
-
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
725
|
-
# callback should occur (e.g. :if => :allow_callbacks, or
|
726
|
-
# :if => lambda {|user| user.signup_step > 2}). The method, proc or string
|
727
|
-
# should return or evaluate to a true or false value.
|
728
|
-
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
729
|
-
# callback should not occur (e.g. :unless => :skip_callbacks, or
|
730
|
-
# :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
|
731
|
-
# string should return or evaluate to a true or false value.
|
732
|
-
#
|
733
|
-
# The +except+ group of options (+except_to+, +exception_from+, and
|
734
|
-
# +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
|
735
|
-
# +from+, and +on+, respectively)
|
747
|
+
# performed so long as the given requirements match the transition.
|
736
748
|
#
|
737
749
|
# == The callback
|
738
750
|
#
|
739
|
-
#
|
740
|
-
#
|
751
|
+
# Callbacks must be defined as either the only argument, in the :do option,
|
752
|
+
# or as a block. For example,
|
741
753
|
#
|
742
754
|
# class Vehicle
|
743
755
|
# state_machine do
|
744
|
-
# before_transition :
|
745
|
-
# before_transition
|
756
|
+
# before_transition :set_alarm
|
757
|
+
# before_transition all => :parked :do => :set_alarm
|
758
|
+
# before_transition all => :parked do |vehicle, transition|
|
746
759
|
# vehicle.set_alarm
|
747
760
|
# end
|
748
761
|
# ...
|
749
762
|
# end
|
750
763
|
# end
|
751
764
|
#
|
752
|
-
#
|
765
|
+
# == State requirements
|
753
766
|
#
|
754
|
-
#
|
755
|
-
#
|
756
|
-
#
|
757
|
-
# for it.
|
767
|
+
# Callbacks can require that the machine be transitioning from and to
|
768
|
+
# specific states. These requirements use a Hash syntax to map beginning
|
769
|
+
# states to ending states. For example,
|
758
770
|
#
|
759
|
-
#
|
771
|
+
# before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
|
760
772
|
#
|
761
|
-
#
|
762
|
-
#
|
763
|
-
# before_transition :to => :parked, :do => lambda {|vehicle| vehicle.set_alarm}
|
764
|
-
#
|
765
|
-
# # Specifies 2 parameters (object being transitioned and actual transition)
|
766
|
-
# before_transition :to => :parked, :do => lambda {|vehicle, transition| vehicle.set_alarm(transition)}
|
767
|
-
# end
|
773
|
+
# In this case, the +set_alarm+ callback will only be called if the machine
|
774
|
+
# is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
|
768
775
|
#
|
769
|
-
#
|
770
|
-
#
|
771
|
-
#
|
776
|
+
# To help define state requirements, a set of helpers are available for
|
777
|
+
# slightly more complex matching:
|
778
|
+
# * <tt>all</tt> - Matches every state/event in the machine
|
779
|
+
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
|
780
|
+
# * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
|
781
|
+
# * <tt>same</tt> - Matches the same state being transitioned from
|
772
782
|
#
|
773
|
-
# See StateMachine::
|
774
|
-
# attributes available on the transition.
|
783
|
+
# See StateMachine::MatcherHelpers for more information.
|
775
784
|
#
|
776
|
-
#
|
785
|
+
# Examples:
|
777
786
|
#
|
778
|
-
#
|
779
|
-
#
|
787
|
+
# before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
|
788
|
+
# before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
|
789
|
+
# before_transition all => :parked, :do => ... # Matches all states to parked
|
790
|
+
# before_transition any => same, :do => ... # Matches every loopback
|
780
791
|
#
|
781
|
-
#
|
782
|
-
# state_machine do
|
783
|
-
# # Before all transitions
|
784
|
-
# before_transition :update_dashboard
|
785
|
-
#
|
786
|
-
# # Before specific transition:
|
787
|
-
# before_transition :to => :parked, :from => [:first_gear, :idling], :on => :park, :do => :take_off_seatbelt
|
788
|
-
#
|
789
|
-
# # With conditional callback:
|
790
|
-
# before_transition :to => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
|
791
|
-
#
|
792
|
-
# # Using :except counterparts:
|
793
|
-
# before_transition :except_to => :stalled, :except_from => :stalled, :except_on => :crash, :do => :update_dashboard
|
794
|
-
# ...
|
795
|
-
# end
|
796
|
-
# end
|
792
|
+
# == Event requirements
|
797
793
|
#
|
798
|
-
#
|
799
|
-
#
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
#
|
805
|
-
#
|
806
|
-
#
|
807
|
-
#
|
794
|
+
# In addition to state requirements, an event requirement can be defined so
|
795
|
+
# that the callback is only invoked on specific events using the +on+
|
796
|
+
# option. This can also use the same matcher helpers as the state
|
797
|
+
# requirements.
|
798
|
+
#
|
799
|
+
# Examples:
|
800
|
+
#
|
801
|
+
# before_transition :on => :ignite, :do => ... # Matches only on ignite
|
802
|
+
# before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
|
803
|
+
# before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
|
804
|
+
#
|
805
|
+
# == Verbose Requirements
|
806
|
+
#
|
807
|
+
# Requirements can also be defined using verbose options rather than the
|
808
|
+
# implicit Hash syntax and helper methods described above.
|
808
809
|
#
|
809
810
|
# Configuration options:
|
810
811
|
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
@@ -816,8 +817,18 @@ module StateMachine
|
|
816
817
|
# * <tt>:except_from</tt> - One or more states *not* being transitioned from
|
817
818
|
# * <tt>:except_to</tt> - One more states *not* being transitioned to
|
818
819
|
# * <tt>:except_on</tt> - One or more events that *did not* fire the transition
|
819
|
-
#
|
820
|
-
#
|
820
|
+
#
|
821
|
+
# Examples:
|
822
|
+
#
|
823
|
+
# before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
|
824
|
+
# before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
|
825
|
+
#
|
826
|
+
# == Conditions
|
827
|
+
#
|
828
|
+
# In addition to the state/event requirements, a condition can also be
|
829
|
+
# defined to help determine whether the callback should be invoked.
|
830
|
+
#
|
831
|
+
# Configuration options:
|
821
832
|
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
822
833
|
# callback should occur (e.g. :if => :allow_callbacks, or
|
823
834
|
# :if => lambda {|user| user.signup_step > 2}). The method, proc or string
|
@@ -827,40 +838,26 @@ module StateMachine
|
|
827
838
|
# :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
|
828
839
|
# string should return or evaluate to a true or false value.
|
829
840
|
#
|
830
|
-
#
|
831
|
-
# +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
|
832
|
-
# +from+, and +on+, respectively)
|
841
|
+
# Examples:
|
833
842
|
#
|
834
|
-
#
|
835
|
-
#
|
836
|
-
# When defining additional configuration options, callbacks must be defined
|
837
|
-
# in either the :do option or as a block. For example,
|
843
|
+
# before_transition :parked => :idling, :if => :moving?
|
844
|
+
# before_transition :on => :ignite, :unless => :seatbelt_on?
|
838
845
|
#
|
839
|
-
#
|
840
|
-
# state_machine do
|
841
|
-
# after_transition :to => :parked, :do => :set_alarm
|
842
|
-
# after_transition :to => :parked do |vehicle, transition, result|
|
843
|
-
# vehicle.set_alarm
|
844
|
-
# end
|
845
|
-
# ...
|
846
|
-
# end
|
847
|
-
# end
|
848
|
-
#
|
849
|
-
# === Accessing the transition / result
|
846
|
+
# === Accessing the transition
|
850
847
|
#
|
851
848
|
# In addition to passing the object being transitioned, the actual
|
852
|
-
# transition describing the context (e.g. event, from, to)
|
853
|
-
#
|
854
|
-
#
|
849
|
+
# transition describing the context (e.g. event, from, to) can be accessed
|
850
|
+
# as well. This additional argument is only passed if the callback allows
|
851
|
+
# for it.
|
855
852
|
#
|
856
853
|
# For example,
|
857
854
|
#
|
858
855
|
# class Vehicle
|
859
856
|
# # Only specifies one parameter (the object being transitioned)
|
860
|
-
#
|
857
|
+
# before_transition :to => :parked, :do => lambda {|vehicle| vehicle.set_alarm}
|
861
858
|
#
|
862
|
-
# # Specifies
|
863
|
-
#
|
859
|
+
# # Specifies 2 parameters (object being transitioned and actual transition)
|
860
|
+
# before_transition :to => :parked, :do => lambda {|vehicle, transition| vehicle.set_alarm(transition)}
|
864
861
|
# end
|
865
862
|
#
|
866
863
|
# *Note* that the object in the callback will only be passed in as an
|
@@ -872,28 +869,37 @@ module StateMachine
|
|
872
869
|
#
|
873
870
|
# == Examples
|
874
871
|
#
|
875
|
-
# Below is an example of a
|
876
|
-
# of +
|
872
|
+
# Below is an example of a class with one state machine and various types
|
873
|
+
# of +before+ transitions defined for it:
|
877
874
|
#
|
878
875
|
# class Vehicle
|
879
876
|
# state_machine do
|
880
|
-
# #
|
881
|
-
#
|
877
|
+
# # Before all transitions
|
878
|
+
# before_transition :update_dashboard
|
882
879
|
#
|
883
|
-
# #
|
884
|
-
#
|
880
|
+
# # Before specific transition:
|
881
|
+
# before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
|
885
882
|
#
|
886
883
|
# # With conditional callback:
|
887
|
-
#
|
884
|
+
# before_transition :to => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
|
888
885
|
#
|
889
|
-
# # Using :
|
890
|
-
#
|
886
|
+
# # Using helpers:
|
887
|
+
# before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
|
891
888
|
# ...
|
892
889
|
# end
|
893
890
|
# end
|
894
891
|
#
|
895
892
|
# As can be seen, any number of transitions can be created using various
|
896
893
|
# combinations of configuration options.
|
894
|
+
def before_transition(options = {}, &block)
|
895
|
+
add_callback(:before, options.is_a?(Hash) ? options : {:do => options}, &block)
|
896
|
+
end
|
897
|
+
|
898
|
+
# Creates a callback that will be invoked *after* a transition is
|
899
|
+
# performed so long as the given requirements match the transition.
|
900
|
+
#
|
901
|
+
# See +before_transition+ for a description of the possible configurations
|
902
|
+
# for defining callbacks.
|
897
903
|
def after_transition(options = {}, &block)
|
898
904
|
add_callback(:after, options.is_a?(Hash) ? options : {:do => options}, &block)
|
899
905
|
end
|