state_machines 0.100.4 → 0.200.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.
- checksums.yaml +4 -4
- data/lib/state_machines/async_mode/async_transition_collection.rb +19 -24
- data/lib/state_machines/eval_helpers.rb +7 -23
- data/lib/state_machines/machine/callbacks.rb +301 -27
- data/lib/state_machines/machine/configuration.rb +4 -12
- data/lib/state_machines/machine/event_methods.rb +380 -3
- data/lib/state_machines/machine/integration.rb +22 -0
- data/lib/state_machines/machine/state_methods.rb +297 -0
- data/lib/state_machines/machine/utilities.rb +4 -3
- data/lib/state_machines/machine.rb +0 -1031
- data/lib/state_machines/syntax_validator.rb +10 -13
- data/lib/state_machines/test_helper.rb +107 -227
- data/lib/state_machines/transition.rb +16 -32
- data/lib/state_machines/version.rb +1 -1
- metadata +6 -6
|
@@ -6,6 +6,37 @@ module StateMachines
|
|
|
6
6
|
# Gets the initial state of the machine for the given object. If a dynamic
|
|
7
7
|
# initial state was configured for this machine, then the object will be
|
|
8
8
|
# passed into the lambda block to help determine the actual state.
|
|
9
|
+
#
|
|
10
|
+
# == Examples
|
|
11
|
+
#
|
|
12
|
+
# With a static initial state:
|
|
13
|
+
#
|
|
14
|
+
# class Vehicle
|
|
15
|
+
# state_machine :initial => :parked do
|
|
16
|
+
# ...
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# vehicle = Vehicle.new
|
|
21
|
+
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
|
|
22
|
+
#
|
|
23
|
+
# With a dynamic initial state:
|
|
24
|
+
#
|
|
25
|
+
# class Vehicle
|
|
26
|
+
# attr_accessor :force_idle
|
|
27
|
+
#
|
|
28
|
+
# state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
|
|
29
|
+
# ...
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# vehicle = Vehicle.new
|
|
34
|
+
#
|
|
35
|
+
# vehicle.force_idle = true
|
|
36
|
+
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=false>
|
|
37
|
+
#
|
|
38
|
+
# vehicle.force_idle = false
|
|
39
|
+
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
|
|
9
40
|
def initial_state(object)
|
|
10
41
|
states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
|
|
11
42
|
end
|
|
@@ -37,6 +68,272 @@ module StateMachines
|
|
|
37
68
|
end
|
|
38
69
|
|
|
39
70
|
# Customizes the definition of one or more states in the machine.
|
|
71
|
+
#
|
|
72
|
+
# Configuration options:
|
|
73
|
+
# * <tt>:value</tt> - The actual value to store when an object transitions
|
|
74
|
+
# to the state. Default is the name (stringified).
|
|
75
|
+
# * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
|
|
76
|
+
# then setting this to true will cache the evaluated result
|
|
77
|
+
# * <tt>:if</tt> - Determines whether an object's value matches the state
|
|
78
|
+
# (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
|
|
79
|
+
# By default, the configured value is matched.
|
|
80
|
+
# * <tt>:human_name</tt> - The human-readable version of this state's name.
|
|
81
|
+
# By default, this is either defined by the integration or stringifies the
|
|
82
|
+
# name and converts underscores to spaces.
|
|
83
|
+
#
|
|
84
|
+
# == Customizing the stored value
|
|
85
|
+
#
|
|
86
|
+
# Whenever a state is automatically discovered in the state machine, its
|
|
87
|
+
# default value is assumed to be the stringified version of the name. For
|
|
88
|
+
# example,
|
|
89
|
+
#
|
|
90
|
+
# class Vehicle
|
|
91
|
+
# state_machine :initial => :parked do
|
|
92
|
+
# event :ignite do
|
|
93
|
+
# transition :parked => :idling
|
|
94
|
+
# end
|
|
95
|
+
# end
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
# In the above state machine, there are two states automatically discovered:
|
|
99
|
+
# :parked and :idling. These states, by default, will store their stringified
|
|
100
|
+
# equivalents when an object moves into that state (e.g. "parked" / "idling").
|
|
101
|
+
#
|
|
102
|
+
# For legacy systems or when tying state machines into existing frameworks,
|
|
103
|
+
# it's oftentimes necessary to need to store a different value for a state
|
|
104
|
+
# than the default. In order to continue taking advantage of an expressive
|
|
105
|
+
# state machine and helper methods, every defined state can be re-configured
|
|
106
|
+
# with a custom stored value. For example,
|
|
107
|
+
#
|
|
108
|
+
# class Vehicle
|
|
109
|
+
# state_machine :initial => :parked do
|
|
110
|
+
# event :ignite do
|
|
111
|
+
# transition :parked => :idling
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# state :idling, :value => 'IDLING'
|
|
115
|
+
# state :parked, :value => 'PARKED
|
|
116
|
+
# end
|
|
117
|
+
# end
|
|
118
|
+
#
|
|
119
|
+
# This is also useful if being used in association with a database and,
|
|
120
|
+
# instead of storing the state name in a column, you want to store the
|
|
121
|
+
# state's foreign key:
|
|
122
|
+
#
|
|
123
|
+
# class VehicleState < ActiveRecord::Base
|
|
124
|
+
# end
|
|
125
|
+
#
|
|
126
|
+
# class Vehicle < ActiveRecord::Base
|
|
127
|
+
# state_machine :attribute => :state_id, :initial => :parked do
|
|
128
|
+
# event :ignite do
|
|
129
|
+
# transition :parked => :idling
|
|
130
|
+
# end
|
|
131
|
+
#
|
|
132
|
+
# states.each do |state|
|
|
133
|
+
# self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
|
|
134
|
+
# end
|
|
135
|
+
# end
|
|
136
|
+
# end
|
|
137
|
+
#
|
|
138
|
+
# In the above example, each known state is configured to store it's
|
|
139
|
+
# associated database id in the +state_id+ attribute. Also, notice that a
|
|
140
|
+
# lambda block is used to define the state's value. This is required in
|
|
141
|
+
# situations (like testing) where the model is loaded without any existing
|
|
142
|
+
# data (i.e. no VehicleState records available).
|
|
143
|
+
#
|
|
144
|
+
# One caveat to the above example is to keep performance in mind. To avoid
|
|
145
|
+
# constant db hits for looking up the VehicleState ids, the value is cached
|
|
146
|
+
# by specifying the <tt>:cache</tt> option. Alternatively, a custom
|
|
147
|
+
# caching strategy can be used like so:
|
|
148
|
+
#
|
|
149
|
+
# class VehicleState < ActiveRecord::Base
|
|
150
|
+
# cattr_accessor :cache_store
|
|
151
|
+
# self.cache_store = ActiveSupport::Cache::MemoryStore.new
|
|
152
|
+
#
|
|
153
|
+
# def self.find_by_name(name)
|
|
154
|
+
# cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
|
|
155
|
+
# end
|
|
156
|
+
# end
|
|
157
|
+
#
|
|
158
|
+
# === Dynamic values
|
|
159
|
+
#
|
|
160
|
+
# In addition to customizing states with other value types, lambda blocks
|
|
161
|
+
# can also be specified to allow for a state's value to be determined
|
|
162
|
+
# dynamically at runtime. For example,
|
|
163
|
+
#
|
|
164
|
+
# class Vehicle
|
|
165
|
+
# state_machine :purchased_at, :initial => :available do
|
|
166
|
+
# event :purchase do
|
|
167
|
+
# transition all => :purchased
|
|
168
|
+
# end
|
|
169
|
+
#
|
|
170
|
+
# event :restock do
|
|
171
|
+
# transition all => :available
|
|
172
|
+
# end
|
|
173
|
+
#
|
|
174
|
+
# state :available, :value => nil
|
|
175
|
+
# state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
|
|
176
|
+
# end
|
|
177
|
+
# end
|
|
178
|
+
#
|
|
179
|
+
# In the above definition, the <tt>:purchased</tt> state is customized with
|
|
180
|
+
# both a dynamic value *and* a value matcher.
|
|
181
|
+
#
|
|
182
|
+
# When an object transitions to the purchased state, the value's lambda
|
|
183
|
+
# block will be called. This will get the current time and store it in the
|
|
184
|
+
# object's +purchased_at+ attribute.
|
|
185
|
+
#
|
|
186
|
+
# *Note* that the custom matcher is very important here. Since there's no
|
|
187
|
+
# way for the state machine to figure out an object's state when it's set to
|
|
188
|
+
# a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
|
|
189
|
+
# were not configured for the state, then an ArgumentError exception would
|
|
190
|
+
# be raised at runtime, indicating that the state machine could not figure
|
|
191
|
+
# out what the current state of the object was.
|
|
192
|
+
#
|
|
193
|
+
# == Behaviors
|
|
194
|
+
#
|
|
195
|
+
# Behaviors define a series of methods to mixin with objects when the current
|
|
196
|
+
# state matches the given one(s). This allows instance methods to behave
|
|
197
|
+
# a specific way depending on what the value of the object's state is.
|
|
198
|
+
#
|
|
199
|
+
# For example,
|
|
200
|
+
#
|
|
201
|
+
# class Vehicle
|
|
202
|
+
# attr_accessor :driver
|
|
203
|
+
# attr_accessor :passenger
|
|
204
|
+
#
|
|
205
|
+
# state_machine :initial => :parked do
|
|
206
|
+
# event :ignite do
|
|
207
|
+
# transition :parked => :idling
|
|
208
|
+
# end
|
|
209
|
+
#
|
|
210
|
+
# state :parked do
|
|
211
|
+
# def speed
|
|
212
|
+
# 0
|
|
213
|
+
# end
|
|
214
|
+
#
|
|
215
|
+
# def rotate_driver
|
|
216
|
+
# driver = self.driver
|
|
217
|
+
# self.driver = passenger
|
|
218
|
+
# self.passenger = driver
|
|
219
|
+
# true
|
|
220
|
+
# end
|
|
221
|
+
# end
|
|
222
|
+
#
|
|
223
|
+
# state :idling, :first_gear do
|
|
224
|
+
# def speed
|
|
225
|
+
# 20
|
|
226
|
+
# end
|
|
227
|
+
#
|
|
228
|
+
# def rotate_driver
|
|
229
|
+
# self.state = 'parked'
|
|
230
|
+
# rotate_driver
|
|
231
|
+
# end
|
|
232
|
+
# end
|
|
233
|
+
#
|
|
234
|
+
# other_states :backing_up
|
|
235
|
+
# end
|
|
236
|
+
# end
|
|
237
|
+
#
|
|
238
|
+
# In the above example, there are two dynamic behaviors defined for the
|
|
239
|
+
# class:
|
|
240
|
+
# * +speed+
|
|
241
|
+
# * +rotate_driver+
|
|
242
|
+
#
|
|
243
|
+
# Each of these behaviors are instance methods on the Vehicle class. However,
|
|
244
|
+
# which method actually gets invoked is based on the current state of the
|
|
245
|
+
# object. Using the above class as the example:
|
|
246
|
+
#
|
|
247
|
+
# vehicle = Vehicle.new
|
|
248
|
+
# vehicle.driver = 'John'
|
|
249
|
+
# vehicle.passenger = 'Jane'
|
|
250
|
+
#
|
|
251
|
+
# # Behaviors in the "parked" state
|
|
252
|
+
# vehicle.state # => "parked"
|
|
253
|
+
# vehicle.speed # => 0
|
|
254
|
+
# vehicle.rotate_driver # => true
|
|
255
|
+
# vehicle.driver # => "Jane"
|
|
256
|
+
# vehicle.passenger # => "John"
|
|
257
|
+
#
|
|
258
|
+
# vehicle.ignite # => true
|
|
259
|
+
#
|
|
260
|
+
# # Behaviors in the "idling" state
|
|
261
|
+
# vehicle.state # => "idling"
|
|
262
|
+
# vehicle.speed # => 20
|
|
263
|
+
# vehicle.rotate_driver # => true
|
|
264
|
+
# vehicle.driver # => "John"
|
|
265
|
+
# vehicle.passenger # => "Jane"
|
|
266
|
+
#
|
|
267
|
+
# As can be seen, both the +speed+ and +rotate_driver+ instance method
|
|
268
|
+
# implementations changed how they behave based on what the current state
|
|
269
|
+
# of the vehicle was.
|
|
270
|
+
#
|
|
271
|
+
# === Invalid behaviors
|
|
272
|
+
#
|
|
273
|
+
# If a specific behavior has not been defined for a state, then a
|
|
274
|
+
# NoMethodError exception will be raised, indicating that that method would
|
|
275
|
+
# not normally exist for an object with that state.
|
|
276
|
+
#
|
|
277
|
+
# Using the example from before:
|
|
278
|
+
#
|
|
279
|
+
# vehicle = Vehicle.new
|
|
280
|
+
# vehicle.state = 'backing_up'
|
|
281
|
+
# vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
|
|
282
|
+
#
|
|
283
|
+
# === Using matchers
|
|
284
|
+
#
|
|
285
|
+
# The +all+ / +any+ matchers can be used to easily define behaviors for a
|
|
286
|
+
# group of states. Note, however, that you cannot use these matchers to
|
|
287
|
+
# set configurations for states. Behaviors using these matchers can be
|
|
288
|
+
# defined at any point in the state machine and will always get applied to
|
|
289
|
+
# the proper states.
|
|
290
|
+
#
|
|
291
|
+
# For example:
|
|
292
|
+
#
|
|
293
|
+
# state_machine :initial => :parked do
|
|
294
|
+
# ...
|
|
295
|
+
#
|
|
296
|
+
# state all - [:parked, :idling, :stalled] do
|
|
297
|
+
# validates_presence_of :speed
|
|
298
|
+
#
|
|
299
|
+
# def speed
|
|
300
|
+
# gear * 10
|
|
301
|
+
# end
|
|
302
|
+
# end
|
|
303
|
+
# end
|
|
304
|
+
#
|
|
305
|
+
# == State-aware class methods
|
|
306
|
+
#
|
|
307
|
+
# In addition to defining scopes for instance methods that are state-aware,
|
|
308
|
+
# the same can be done for certain types of class methods.
|
|
309
|
+
#
|
|
310
|
+
# Some libraries have support for class-level methods that only run certain
|
|
311
|
+
# behaviors based on a conditions hash passed in. For example:
|
|
312
|
+
#
|
|
313
|
+
# class Vehicle < ActiveRecord::Base
|
|
314
|
+
# state_machine do
|
|
315
|
+
# ...
|
|
316
|
+
# state :first_gear, :second_gear, :third_gear do
|
|
317
|
+
# validates_presence_of :speed
|
|
318
|
+
# validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
|
|
319
|
+
# end
|
|
320
|
+
# end
|
|
321
|
+
# end
|
|
322
|
+
#
|
|
323
|
+
# In the above ActiveRecord model, two validations have been defined which
|
|
324
|
+
# will *only* run when the Vehicle object is in one of the three states:
|
|
325
|
+
# +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
|
|
326
|
+
# conditions can continue to be used.
|
|
327
|
+
#
|
|
328
|
+
# This functionality is not library-specific and can work for any class-level
|
|
329
|
+
# method that is defined like so:
|
|
330
|
+
#
|
|
331
|
+
# def validates_presence_of(attribute, options = {})
|
|
332
|
+
# ...
|
|
333
|
+
# end
|
|
334
|
+
#
|
|
335
|
+
# The minimum requirement is that the last argument in the method be an
|
|
336
|
+
# options hash which contains at least <tt>:if</tt> condition support.
|
|
40
337
|
def state(*names, &)
|
|
41
338
|
options = names.last.is_a?(Hash) ? names.pop : {}
|
|
42
339
|
StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name)
|
|
@@ -17,9 +17,10 @@ module StateMachines
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
20
|
+
# Determines whether there's already a helper method defined within the
|
|
21
|
+
# given scope. This is true only if one of the owner's ancestors defines
|
|
22
|
+
# the method and is further along in the ancestor chain than this
|
|
23
|
+
# machine's helper module.
|
|
23
24
|
def owner_class_ancestor_has_method?(scope, method)
|
|
24
25
|
return false unless owner_class_has_method?(scope, method)
|
|
25
26
|
|