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.
@@ -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
- # Looks up the ancestor class that has the given method defined. This
21
- # is used to find the method owner which is used to determine where to
22
- # define new methods.
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