/
event.rb
254 lines (228 loc) · 10.4 KB
/
event.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
require 'state_machine/transition'
require 'state_machine/guard'
require 'state_machine/assertions'
require 'state_machine/matcher_helpers'
module StateMachine
# An event defines an action that transitions an attribute from one state to
# another. The state that an attribute is transitioned to depends on the
# guards configured for the event.
class Event
include Assertions
include MatcherHelpers
# The state machine for which this event is defined
attr_accessor :machine
# The name of the action that fires the event
attr_reader :name
# The list of guards that determine what state this event transitions
# objects to when fired
attr_reader :guards
# A list of all of the states known to this event using the configured
# guards/transitions as the source
attr_reader :known_states
# Creates a new event within the context of the given machine
def initialize(machine, name) #:nodoc:
@machine = machine
@name = name
@guards = []
@known_states = []
add_actions
end
# Creates a copy of this event in addition to the list of associated
# guards to prevent conflicts across events within a class hierarchy.
def initialize_copy(orig) #:nodoc:
super
@guards = @guards.dup
@known_states = @known_states.dup
end
# Creates a new transition that determines what to change the current state
# to when this event fires.
#
# == Defining transitions
#
# The options for a new transition uses the Hash syntax to map beginning
# states to ending states. For example,
#
# transition :parked => :idling, :idling => :first_gear
#
# In this case, when the event is fired, this transition will cause the
# state to be +idling+ if it's current state is +parked+ or +first_gear+ if
# it's current state is +idling+.
#
# To help defining these implicit transitions, a set of helpers are available
# for defining slightly more complex matching:
# * <tt>all</tt> - Matches every state in the machine
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
# * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
# * <tt>same</tt> - Matches the same state being transitioned from
#
# See StateMachine::MatcherHelpers for more information.
#
# Examples:
#
# transition all => nil # Transitions to nil regardless of the current state
# transition all => :idling # Transitions to :idling regardless of the current state
# transition all - [:idling, :first_gear] => :idling # Transitions every state but :idling and :first_gear to :idling
# transition nil => :idling # Transitions to :idling from the nil state
# transition :parked => :idling # Transitions to :idling if :parked
# transition [:parked, :stalled] => :idling # Transitions to :idling if :parked or :stalled
#
# transition :parked => same # Loops :parked back to :parked
# transition [:parked, :stalled] => same # Loops either :parked or :stalled back to the same state
# transition all - :parked => same # Loops every state but :parked back to the same state
#
# == Verbose transitions
#
# Transitions can also be defined use an explicit set of deprecated
# configuration options:
# * <tt>:from</tt> - A state or array of states that can be transitioned from.
# If not specified, then the transition can occur for *any* state.
# * <tt>:to</tt> - The state that's being transitioned to. If not specified,
# then the transition will simply loop back (i.e. the state will not change).
# * <tt>:except_from</tt> - A state or array of states that *cannot* be
# transitioned from.
#
# Examples:
#
# transition :to => nil
# transition :to => :idling
# transition :except_from => [:idling, :first_gear], :to => :idling
# transition :from => nil, :to => :idling
# transition :from => [:parked, :stalled], :to => :idling
#
# transition :from => :parked
# transition :from => [:parked, :stalled]
# transition :except_from => :parked
#
# Notice that the above examples are the verbose equivalent of the examples
# described initially.
#
# == Conditions
#
# In addition to the state requirements for each transition, a condition
# can also be defined to help determine whether that transition is
# available. These options will work on both the normal and verbose syntax.
#
# Configuration options:
# * <tt>:if</tt> - A method, proc or string to call to determine if the
# transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
# The condition should return or evaluate to true or false.
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
# transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
# The condition should return or evaluate to true or false.
#
# Examples:
#
# transition :parked => :idling, :if => :moving?
# transition :parked => :idling, :unless => :stopped?
#
# transition :from => :parked, :to => :idling, :if => :moving?
# transition :from => :parked, :to => :idling, :unless => :stopped?
#
# == Order of operations
#
# Transitions are evaluated in the order in which they're defined. As a
# result, if more than one transition applies to a given object, then the
# first transition that matches will be performed.
def transition(options)
raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
# Only a certain subset of explicit options are allowed for transition
# requirements
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?
guards << guard = Guard.new(options)
@known_states |= guard.known_states
guard
end
# Determines whether any transitions can be performed for this event based
# on the current state of the given object.
#
# If the event can't be fired, then this will return false, otherwise true.
def can_fire?(object)
!next_transition(object).nil?
end
# Finds and builds the next transition that can be performed on the given
# object. If no transitions can be made, then this will return nil.
def next_transition(object)
from = machine.state_for(object).name
guards.each do |guard|
if match = guard.match(object, :from => from)
# Guard allows for the transition to occur
to = match[:to].values.empty? ? from : match[:to].values.first
return Transition.new(object, machine, name, from, to)
end
end
# No transition matched
nil
end
# Attempts to perform the next available transition on the given object.
# If no transitions can be made, then this will return false, otherwise
# true.
#
# Any additional arguments are passed to the StateMachine::Transition#perform
# instance method.
def fire(object, *args)
if transition = next_transition(object)
transition.perform(*args)
else
machine.invalidate(object, self)
false
end
end
# Attempts to perform the next available transition on the given object.
# If no transitions can be made, then a StateMachine::InvalidTransition
# exception will be raised, otherwise true will be returned.
def fire!(object, *args)
fire(object, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.attribute} via :#{name} from #{machine.state_for(object).name.inspect}")
end
# Draws a representation of this event on the given graph. This will
# create 1 or more edges on the graph for each guard (i.e. transition)
# configured.
#
# A collection of the generated edges will be returned.
def draw(graph)
valid_states = machine.states.by_priority.map {|state| state.name}
guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
end
# Generates a nicely formatted description of this events's contents.
#
# For example,
#
# event = StateMachine::Event.new(machine, :park)
# event.transition all - :idling => :parked, :idling => same
# event # => #<StateMachine::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
def inspect
transitions = guards.map do |guard|
guard.state_requirements.map do |state_requirement|
"#{state_requirement[:from].description} => #{state_requirement[:to].description}"
end * ', '
end
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
end
protected
# Add the various instance methods that can transition the object using
# the current event
def add_actions
attribute = machine.attribute
qualified_name = name = self.name
qualified_name = "#{name}_#{machine.namespace}" if machine.namespace
machine.owner_class.class_eval do
# Checks whether the event can be fired on the current object
define_method("can_#{qualified_name}?") do
self.class.state_machines[attribute].event(name).can_fire?(self)
end
# Gets the next transition that would be performed if the event were
# fired now
define_method("next_#{qualified_name}_transition") do
self.class.state_machines[attribute].event(name).next_transition(self)
end
# Fires the event
define_method(qualified_name) do |*args|
self.class.state_machines[attribute].event(name).fire(self, *args)
end
# Fires the event, raising an exception if it fails
define_method("#{qualified_name}!") do |*args|
self.class.state_machines[attribute].event(name).fire!(self, *args)
end
end
end
end
end