state_machine 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/CHANGELOG.rdoc +17 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +54 -84
  4. data/Rakefile +1 -1
  5. data/examples/Car_state.png +0 -0
  6. data/examples/Vehicle_state.png +0 -0
  7. data/examples/auto_shop.rb +11 -0
  8. data/examples/car.rb +19 -0
  9. data/examples/traffic_light.rb +9 -0
  10. data/examples/vehicle.rb +35 -0
  11. data/lib/state_machine.rb +65 -52
  12. data/lib/state_machine/assertions.rb +1 -1
  13. data/lib/state_machine/callback.rb +13 -9
  14. data/lib/state_machine/eval_helpers.rb +4 -3
  15. data/lib/state_machine/event.rb +51 -33
  16. data/lib/state_machine/extensions.rb +2 -2
  17. data/lib/state_machine/guard.rb +47 -41
  18. data/lib/state_machine/integrations.rb +67 -0
  19. data/lib/state_machine/integrations/active_record.rb +62 -36
  20. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  21. data/lib/state_machine/integrations/data_mapper.rb +23 -37
  22. data/lib/state_machine/integrations/data_mapper/observer.rb +23 -9
  23. data/lib/state_machine/integrations/sequel.rb +23 -24
  24. data/lib/state_machine/machine.rb +380 -277
  25. data/lib/state_machine/node_collection.rb +142 -0
  26. data/lib/state_machine/state.rb +114 -69
  27. data/lib/state_machine/state_collection.rb +38 -0
  28. data/lib/state_machine/transition.rb +36 -17
  29. data/test/active_record.log +2940 -85664
  30. data/test/functional/state_machine_test.rb +49 -53
  31. data/test/sequel.log +747 -11990
  32. data/test/unit/assertions_test.rb +2 -1
  33. data/test/unit/callback_test.rb +14 -12
  34. data/test/unit/eval_helpers_test.rb +25 -6
  35. data/test/unit/event_test.rb +144 -124
  36. data/test/unit/guard_test.rb +118 -140
  37. data/test/unit/integrations/active_record_test.rb +102 -68
  38. data/test/unit/integrations/data_mapper_test.rb +48 -37
  39. data/test/unit/integrations/sequel_test.rb +34 -25
  40. data/test/unit/integrations_test.rb +42 -0
  41. data/test/unit/machine_test.rb +460 -531
  42. data/test/unit/node_collection_test.rb +208 -0
  43. data/test/unit/state_collection_test.rb +167 -0
  44. data/test/unit/state_machine_test.rb +1 -1
  45. data/test/unit/state_test.rb +223 -200
  46. data/test/unit/transition_test.rb +81 -46
  47. metadata +17 -3
  48. data/test/data_mapper.log +0 -30860
@@ -1,66 +1,21 @@
1
1
  require 'state_machine/extensions'
2
+ require 'state_machine/assertions'
3
+ require 'state_machine/integrations'
4
+
2
5
  require 'state_machine/state'
3
6
  require 'state_machine/event'
4
7
  require 'state_machine/callback'
5
- require 'state_machine/assertions'
6
-
7
- # Load each available integration
8
- Dir["#{File.dirname(__FILE__)}/integrations/*.rb"].sort.each do |path|
9
- require "state_machine/integrations/#{File.basename(path)}"
10
- end
8
+ require 'state_machine/node_collection'
9
+ require 'state_machine/state_collection'
11
10
 
12
11
  module StateMachine
13
12
  # Represents a state machine for a particular attribute. State machines
14
13
  # consist of states, events and a set of transitions that define how the state
15
14
  # changes after a particular event is fired.
16
15
  #
17
- # A state machine may not necessarily know all of the possible states for
18
- # an object since they can be any arbitrary value. As a result, anything
19
- # that relies on a list of all possible states should keep in mind that if
20
- # a state has not been referenced *anywhere* in the state machine definition,
21
- # then it will *not* be a known state unless the +other_states+ helper is used.
22
- #
23
- # == State values
24
- #
25
- # While strings are the most common object type used for setting values on
26
- # the state of the machine, there are no restrictions on what can be used.
27
- # This means that symbols, integers, dates/times, etc. can all be used.
28
- #
29
- # With string states:
30
- #
31
- # class Vehicle
32
- # state_machine :initial => 'parked' do
33
- # event :ignite do
34
- # transition :to => 'idling', :from => 'parked'
35
- # end
36
- # end
37
- # end
38
- #
39
- # With symbolic states:
40
- #
41
- # class Vehicle
42
- # state_machine :initial => :parked do
43
- # event :ignite do
44
- # transition :to => :idling, :from => :parked
45
- # end
46
- # end
47
- # end
48
- #
49
- # With time states:
50
- #
51
- # class Switch
52
- # state_machine :activated_at
53
- # before_transition :to => nil, :do => lambda {...}
54
- #
55
- # event :activate do
56
- # transition :to => lambda {Time.now}
57
- # end
58
- #
59
- # event :deactivate do
60
- # transition :to => nil
61
- # end
62
- # end
63
- # end
16
+ # A state machine will not know all of the possible states for an object unless
17
+ # they are referenced *somewhere* in the state machine definition. As a result,
18
+ # any unused states should be defined with the +other_states+ or +state+ helper.
64
19
  #
65
20
  # == Callbacks
66
21
  #
@@ -88,8 +43,8 @@ module StateMachine
88
43
  # example,
89
44
  #
90
45
  # class Vehicle
91
- # state_machine, :initial => 'parked' do
92
- # before_transition :to => 'idling', :do => lambda {|vehicle| throw :halt}
46
+ # state_machine, :initial => :parked do
47
+ # before_transition :to => :idling, :do => lambda {|vehicle| throw :halt}
93
48
  # ...
94
49
  # end
95
50
  # end
@@ -109,7 +64,7 @@ module StateMachine
109
64
  # class Vehicle
110
65
  # state_machine do
111
66
  # event :park do
112
- # transition :to => 'parked', :from => 'idling'
67
+ # transition :to => :parked, :from => :idling
113
68
  # end
114
69
  # ...
115
70
  # end
@@ -188,23 +143,28 @@ module StateMachine
188
143
  # Attempts to find or create a state machine for the given class. For
189
144
  # example,
190
145
  #
191
- # StateMachine::Machine.find_or_create(Switch)
192
- # StateMachine::Machine.find_or_create(Switch, :initial => 'off')
193
- # StateMachine::Machine.find_or_create(Switch, 'status')
194
- # StateMachine::Machine.find_or_create(Switch, 'status', :initial => 'off')
146
+ # StateMachine::Machine.find_or_create(Vehicle)
147
+ # StateMachine::Machine.find_or_create(Vehicle, :initial => :parked)
148
+ # StateMachine::Machine.find_or_create(Vehicle, :status)
149
+ # StateMachine::Machine.find_or_create(Vehicle, :status, :initial => :parked)
195
150
  #
196
151
  # If a machine of the given name already exists in one of the class's
197
152
  # superclasses, then a copy of that machine will be created and stored
198
153
  # in the new owner class (the original will remain unchanged).
199
154
  def find_or_create(owner_class, *args, &block)
200
155
  options = args.last.is_a?(Hash) ? args.pop : {}
201
- attribute = (args.first || 'state').to_s
156
+ attribute = args.first || :state
202
157
 
203
158
  # Attempts to find an existing machine
204
159
  if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
205
- machine = machine.within_context(owner_class, options) unless machine.owner_class == owner_class
160
+ # Create a copy of the state machine if it's being created by a subclass
161
+ unless machine.owner_class == owner_class
162
+ machine = machine.clone
163
+ machine.initial_state = options[:initial] if options.include?(:initial)
164
+ machine.owner_class = owner_class
165
+ end
206
166
 
207
- # Evaluate caller block for DSL
167
+ # Evaluate DSL caller block
208
168
  machine.instance_eval(&block) if block_given?
209
169
  else
210
170
  # No existing machine: create a new one
@@ -218,10 +178,11 @@ module StateMachine
218
178
  # The given classes must be a comma-delimited string of class names.
219
179
  #
220
180
  # Configuration options:
221
- # * +file+ - A comma-delimited string of files to load that contain the state machine definitions to draw
222
- # * +path+ - The path to write the graph file to
223
- # * +format+ - The image format to generate the graph in
224
- # * +font+ - The name of the font to draw state names in
181
+ # * <tt>:file</tt> - A comma-delimited string of files to load that
182
+ # contain the state machine definitions to draw
183
+ # * <tt>:path</tt> - The path to write the graph file to
184
+ # * <tt>:format</tt> - The image format to generate the graph in
185
+ # * <tt>:font</tt> - The name of the font to draw state names in
225
186
  def draw(class_names, options = {})
226
187
  raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
227
188
 
@@ -246,35 +207,29 @@ module StateMachine
246
207
  end
247
208
 
248
209
  # The class that the machine is defined in
249
- attr_reader :owner_class
210
+ attr_accessor :owner_class
250
211
 
251
212
  # The attribute for which the machine is being defined
252
213
  attr_reader :attribute
253
214
 
254
- # The initial state that the machine will be in when an object is created
255
- attr_reader :initial_state
256
-
257
- # The events that trigger transitions
258
- #
259
- # Maps "name" => StateMachine::Event
215
+ # The events that trigger transitions. These are sorted, by default, in the
216
+ # order in which they were defined.
260
217
  attr_reader :events
261
218
 
262
- # Tracks the order in which events were defined. This is used to determine
263
- # in what order events are drawn on GraphViz visualizations.
264
- attr_reader :events_order
265
-
266
219
  # A list of all of the states known to this state machine. This will pull
267
- # state values from the following sources:
220
+ # states from the following sources:
268
221
  # * Initial state
269
222
  # * State behaviors
270
- # * Event transitions (:to, :from, :except_to, and :except_from options)
223
+ # * Event transitions (:to, :from, and :except_from options)
271
224
  # * Transition callbacks (:to, :from, :except_to, and :except_from options)
272
225
  # * Unreferenced states (using +other_states+ helper)
273
226
  #
274
- # Maps value => StateMachine::State
227
+ # These are sorted, by default, in the order in which they were referenced.
275
228
  attr_reader :states
276
229
 
277
230
  # The callbacks to invoke before/after a transition is performed
231
+ #
232
+ # Maps :before => callbacks and :after => callbacks
278
233
  attr_reader :callbacks
279
234
 
280
235
  # The action to invoke when an object transitions
@@ -291,87 +246,55 @@ module StateMachine
291
246
  assert_valid_keys(options, :initial, :action, :plural, :namespace, :integration)
292
247
 
293
248
  # Set machine configuration
294
- @attribute = (args.first || 'state').to_s
295
- @events = {}
296
- @events_order = []
297
- @states = {}
249
+ @attribute = args.first || :state
250
+ @events = NodeCollection.new
251
+ @states = StateCollection.new
298
252
  @callbacks = {:before => [], :after => []}
299
- @action = options[:action]
300
253
  @namespace = options[:namespace]
254
+ self.owner_class = owner_class
255
+ self.initial_state = options[:initial]
301
256
 
302
- # Add class-/instance-level methods to the owner class for state initialization
303
- owner_class.class_eval do
304
- extend StateMachine::ClassMethods
305
- include StateMachine::InstanceMethods
306
- end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
307
-
308
- # Initialize the class context of the machine
309
- set_context(owner_class, :initial => options[:initial], :integration => options[:integration], &block)
257
+ # Find an integration that matches this machine's owner class
258
+ if integration = options[:integration] ? StateMachine::Integrations.find(options[:integration]) : StateMachine::Integrations.match(owner_class)
259
+ extend integration
260
+ end
310
261
 
311
262
  # Set integration-specific configurations
312
- @action ||= default_action unless options.include?(:action)
263
+ @action = options.include?(:action) ? options[:action] : default_action
313
264
  define_attribute_accessor
314
265
  define_scopes(options[:plural])
315
266
 
316
267
  # Call after hook for integration-specific extensions
317
268
  after_initialize
318
269
 
319
- # Evaluate caller block for DSL
270
+ # Evaluate DSL caller block
320
271
  instance_eval(&block) if block_given?
321
272
  end
322
273
 
323
274
  # Creates a copy of this machine in addition to copies of each associated
324
- # event, so that the list of transitions for each event don't conflict
325
- # with different machines
275
+ # event/states/callback, so that the modifications to those collections do
276
+ # not affect the original machine.
326
277
  def initialize_copy(orig) #:nodoc:
327
278
  super
328
279
 
329
- @events = @events.inject({}) do |events, (name, event)|
330
- event = event.dup
331
- event.machine = self
332
- events[name] = event
333
- events
334
- end
335
- @events_order = @events_order.dup
336
- @states = @states.inject({}) do |states, (value, state)|
337
- state = state.dup
338
- state.machine = self
339
- states[value] = state
340
- states
341
- end
342
- @initial_state = @states[@initial_state.value]
280
+ @events = @events.dup
281
+ @events.machine = self
282
+ @states = @states.dup
283
+ @states.machine = self
343
284
  @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
344
285
  end
345
286
 
346
- # Creates a copy of this machine within the context of the given class.
347
- # This should be used for inheritance support of state machines.
348
- def within_context(owner_class, options = {}, &block) #:nodoc:
349
- machine = dup
350
- machine.set_context(owner_class, {:integration => @integration}.merge(options))
351
- machine
352
- end
353
-
354
- # Changes the context of this machine to the given class so that new
355
- # events and transitions are created in the proper context.
356
- #
357
- # Configuration options:
358
- # * +initial+ - The initial value to set the attribute to
359
- # * +integration+ - The name of the integration for extending this machine with library-specific behavior
360
- #
361
- # All other configuration options for the machine can only be set on
362
- # creation.
363
- def set_context(owner_class, options = {}) #:nodoc:
364
- assert_valid_keys(options, :initial, :integration)
365
-
366
- @owner_class = owner_class
367
- @initial_state = add_states([options[:initial]]).first if options.include?(:initial) || !@initial_state
368
- states.each {|name, state| state.initial = (state == @initial_state)}
287
+ # Sets the class which is the owner of this state machine. Any methods
288
+ # generated by states, events, or other parts of the machine will be defined
289
+ # on the given owner class.
290
+ def owner_class=(klass)
291
+ @owner_class = klass
369
292
 
370
- # Find an integration that can be used for implementing various parts
371
- # of the state machine that may behave differently in different libraries
372
- if @integration = options[:integration] || StateMachine::Integrations.constants.find {|name| StateMachine::Integrations.const_get(name).matches?(owner_class)}
373
- extend StateMachine::Integrations.const_get(@integration.to_s.gsub(/(?:^|_)(.)/) {$1.upcase})
374
- end
293
+ # Add class-/instance-level methods to the owner class for state initialization
294
+ owner_class.class_eval do
295
+ extend StateMachine::ClassMethods
296
+ include StateMachine::InstanceMethods
297
+ end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
375
298
 
376
299
  # Record this machine as matched to the attribute in the current owner
377
300
  # class. This will override any machines mapped to the same attribute
@@ -379,30 +302,40 @@ module StateMachine
379
302
  owner_class.state_machines[attribute] = self
380
303
  end
381
304
 
305
+ # Sets the initial state of the machine. This can be either the static name
306
+ # of a state or a lambda block which determines the initial state at
307
+ # creation time.
308
+ def initial_state=(new_initial_state)
309
+ @initial_state = new_initial_state
310
+ add_states([@initial_state]) unless @initial_state.is_a?(Proc)
311
+
312
+ # Update all states to reflect the new initial state
313
+ states.each {|state| state.initial = (state.name == @initial_state)}
314
+ end
315
+
382
316
  # Gets the initial state of the machine for the given object. If a dynamic
383
317
  # initial state was configured for this machine, then the object will be
384
- # passed into the lambda block to help determine the actual value of the
385
- # initial state.
318
+ # passed into the lambda block to help determine the actual state.
386
319
  #
387
320
  # == Examples
388
321
  #
389
322
  # With a static initial state:
390
323
  #
391
324
  # class Vehicle
392
- # state_machine :initial => 'parked' do
325
+ # state_machine :initial => :parked do
393
326
  # ...
394
327
  # end
395
328
  # end
396
329
  #
397
330
  # vehicle = Vehicle.new
398
- # Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
331
+ # Vehicle.state_machines[:state].initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
399
332
  #
400
333
  # With a dynamic initial state:
401
334
  #
402
335
  # class Vehicle
403
336
  # attr_accessor :force_idle
404
337
  #
405
- # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? 'idling' : 'parked'} do
338
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
406
339
  # ...
407
340
  # end
408
341
  # end
@@ -410,15 +343,116 @@ module StateMachine
410
343
  # vehicle = Vehicle.new
411
344
  #
412
345
  # vehicle.force_idle = true
413
- # Vehicle.state_machines['state'].initial_state(vehicle) # => "idling"
346
+ # Vehicle.state_machines[:state].initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
414
347
  #
415
348
  # vehicle.force_idle = false
416
- # Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
349
+ # Vehicle.state_machines[:state].initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
417
350
  def initial_state(object)
418
- @initial_state && @initial_state.value(object)
351
+ states.fetch(@initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state)
419
352
  end
420
353
 
421
- # Defines a series of behaviors to mixin with objects when the current
354
+ # Customizes the definition of one or more states in the machine.
355
+ #
356
+ # Configuration options:
357
+ # * <tt>:value</tt> - The actual value to store when an object transitions
358
+ # to the state. Default is the name (stringified).
359
+ # * <tt>:if</tt> - Determines whether an object's value matches the state
360
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
361
+ # By default, the configured value is matched.
362
+ #
363
+ # == Customizing the stored value
364
+ #
365
+ # Whenever a state is automatically discovered in the state machine, its
366
+ # default value is assumed to be the stringified version of the name. For
367
+ # example,
368
+ #
369
+ # class Vehicle
370
+ # state_machine :initial => :parked do
371
+ # event :ignite do
372
+ # transition :to => :idling, :from => :parked
373
+ # end
374
+ # end
375
+ # end
376
+ #
377
+ # In the above state machine, there are two states automatically discovered:
378
+ # :parked and :idling. These states, by default, will store their stringified
379
+ # equivalents when an object moves into that states (e.g. "parked" / "idling").
380
+ #
381
+ # For legacy systems or when tying state machines into existing frameworks,
382
+ # it's oftentimes necessary to need to store a different value for a state
383
+ # than the default. In order to continue taking advantage of an expressive
384
+ # state machine and helper methods, every defined state can be re-configured
385
+ # with a custom stored value. For example,
386
+ #
387
+ # class Vehicle
388
+ # state_machine :initial => :parked do
389
+ # event :ignite do
390
+ # transition :to => :idling, :from => :parked
391
+ # end
392
+ #
393
+ # state :idling, :value => 'IDLING'
394
+ # state :parked, :value => 'PARKED
395
+ # end
396
+ # end
397
+ #
398
+ # This is also useful if being used in association with a database and,
399
+ # instead of storing the state name in a column, you want to store the
400
+ # state's foreign key:
401
+ #
402
+ # class VehicleState < ActiveRecord::Base
403
+ # end
404
+ #
405
+ # class Vehicle < ActiveRecord::Base
406
+ # state_machine :state_id, :initial => :parked do
407
+ # event :ignite do
408
+ # transition :to => :idling, :from => :parked
409
+ # end
410
+ #
411
+ # states.each {|state| self.state(state.name, :value => VehicleState.find_by_name(state.name.to_s).id)}
412
+ # end
413
+ # end
414
+ #
415
+ # In the above example, each known state is configured to store it's
416
+ # associated database id in the +state_id+ attribute.
417
+ #
418
+ # === Dynamic values
419
+ #
420
+ # In addition to customizing states with other value types, lambda blocks
421
+ # can also be specified to allow for a state's value to be determined
422
+ # dynamically at runtime. For example,
423
+ #
424
+ # class Vehicle
425
+ # state_machine :purchased_at, :initial => :available do
426
+ # event :purchase do
427
+ # transition :to => :purchased
428
+ # end
429
+ #
430
+ # event :restock do
431
+ # transition :to => :available
432
+ # end
433
+ #
434
+ # state :available, :value => nil
435
+ # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
436
+ # end
437
+ # end
438
+ #
439
+ # In the above definition, the <tt>:purchased</tt> state is customized with
440
+ # both a dynamic value *and* a value matcher.
441
+ #
442
+ # When an object transitions to the purchased state, the value's lambda
443
+ # block will be called. This will get the current time and store it in the
444
+ # object's +purchased_at+ attribute.
445
+ #
446
+ # *Note* that the custom matcher is very important here. Since there's no
447
+ # way for the state machine to figure out an object's state when it's set to
448
+ # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
449
+ # were not configured for the state, then an ArgumentError exception would
450
+ # be raised at runtime, indicating that the state machine could not figure
451
+ # out what the current state of the object was.
452
+ #
453
+ # == Behaviors
454
+ #
455
+ # Behaviors defined a series of methods to mixin with objects when the current
422
456
  # state matches the given one(s). This allows instance methods to behave
423
457
  # a specific way depending on what the value of the object's state is.
424
458
  #
@@ -428,12 +462,12 @@ module StateMachine
428
462
  # attr_accessor :driver
429
463
  # attr_accessor :passenger
430
464
  #
431
- # state_machine :initial => 'parked' do
465
+ # state_machine :initial => :parked do
432
466
  # event :ignite do
433
- # transition :to => 'idling', :from => 'parked'
467
+ # transition :to => :idling, :from => :parked
434
468
  # end
435
469
  #
436
- # state 'parked' do
470
+ # state :parked do
437
471
  # def speed
438
472
  # 0
439
473
  # end
@@ -446,16 +480,18 @@ module StateMachine
446
480
  # end
447
481
  # end
448
482
  #
449
- # state 'idling', 'first_gear' do
483
+ # state :idling, :first_gear do
450
484
  # def speed
451
485
  # 20
452
486
  # end
453
487
  #
454
488
  # def rotate_driver
455
- # self.state = "parked"
489
+ # self.state = 'parked'
456
490
  # rotate_driver
457
491
  # end
458
492
  # end
493
+ #
494
+ # other_states :backing_up
459
495
  # end
460
496
  # end
461
497
  #
@@ -493,7 +529,7 @@ module StateMachine
493
529
  # implementations changed how they behave based on what the current state
494
530
  # of the vehicle was.
495
531
  #
496
- # == Invalid behaviors
532
+ # === Invalid behaviors
497
533
  #
498
534
  # If a specific behavior has not been defined for a state, then a
499
535
  # NoMethodError exception will be raised, indicating that that method would
@@ -502,66 +538,79 @@ module StateMachine
502
538
  # Using the example from before:
503
539
  #
504
540
  # vehicle = Vehicle.new
505
- # vehicle.state = "backing_up"
541
+ # vehicle.state = 'backing_up'
506
542
  # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
507
- def state(*values, &block)
508
- states = add_states(values)
509
- states.each {|state| state.context(&block)} if block_given?
543
+ def state(*names, &block)
544
+ options = names.last.is_a?(Hash) ? names.pop : {}
545
+ assert_valid_keys(options, :value, :if)
546
+
547
+ states = add_states(names)
548
+ states.each do |state|
549
+ if options.include?(:value)
550
+ state.value = options[:value]
551
+ self.states.update(state)
552
+ end
553
+
554
+ state.matcher = options[:if] if options.include?(:if)
555
+ state.context(&block) if block_given?
556
+ end
557
+
510
558
  states.length == 1 ? states.first : states
511
559
  end
560
+ alias_method :other_states, :state
512
561
 
513
- # Defines additional states that are possible in the state machine, but
514
- # which are derived outside of any events/transitions or possibly
515
- # dynamically via a lambda block. This allows the given states to be:
516
- # * Queried via instance-level predicates
517
- # * Included in GraphViz visualizations
518
- # * Used in :except_from and :except_to transition/callback conditionals
562
+ # Determines whether the given object is in a specific state. If the
563
+ # object's current value doesn't match the state, then this will return
564
+ # false, otherwise true. If the given state is unknown, then an ArgumentError
565
+ # exception will be raised.
519
566
  #
520
- # == Example
567
+ # == Examples
521
568
  #
522
569
  # class Vehicle
523
- # state_machine :initial => 'parked' do
524
- # event :ignite do
525
- # transition :to => 'idling', :from => 'parked'
526
- # end
527
- #
528
- # other_states %w(stalled stopped)
529
- # end
530
- #
531
- # def stop
532
- # self.state = 'stopped'
570
+ # state_machine :initial => :parked do
571
+ # other_states :idling
533
572
  # end
534
573
  # end
535
- #
536
- # In the above state machine, the known states would be:
537
- # * +idling+
538
- # * +parked+
539
- # * +stalled+
540
- # * +stopped+
541
- #
542
- # Since +stalled+ and +stopped+ are not referenced in any transitions or
543
- # callbacks, they are explicitly defined.
544
- def other_states(*args)
545
- add_states(args.flatten)
574
+ #
575
+ # machine = Vehicle.state_machines[:state]
576
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
577
+ #
578
+ # machine.state?(vehicle, :parked) # => true
579
+ # machine.state?(vehicle, :idling) # => false
580
+ # machine.state?(vehicle, :invalid) # => ArgumentError: :invalid is an invalid key for :name index
581
+ def state?(object, name)
582
+ states.fetch(name).matches?(object.send(attribute))
546
583
  end
547
584
 
548
- # Gets the order in which states should be displayed based on where they
549
- # were first referenced. This will order states in the following priority:
550
- #
551
- # 1. Initial state
552
- # 2. Event transitions (:to, :from, :except_to, :except_from options)
553
- # 3. States with behaviors
554
- # 4. States referenced via +other_states+
555
- # 5. States referenced in callbacks
556
- #
557
- # This order will determine how the GraphViz visualizations are rendered.
558
- def states_order
559
- order = [initial_state(nil)]
585
+ # Determines the current state of the given object as configured by this
586
+ # state machine. This will attempt to find a known state that matches
587
+ # the value of the attribute on the object. If no state is found, then
588
+ # an ArgumentError will be raised.
589
+ #
590
+ # == Examples
591
+ #
592
+ # class Vehicle
593
+ # state_machine :initial => :parked do
594
+ # other_states :idling
595
+ # end
596
+ # end
597
+ #
598
+ # machine = Vehicle.state_machines[:state]
599
+ #
600
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
601
+ # machine.state_for(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
602
+ #
603
+ # vehicle.state = 'idling'
604
+ # machine.state_for(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=true>
605
+ #
606
+ # vehicle.state = 'invalid'
607
+ # machine.state_for(vehicle) # => ArgumentError: "invalid" is not a known state value
608
+ def state_for(object)
609
+ value = object.send(attribute)
610
+ state = states[value, :value] || states.detect {|state| state.matches?(value)}
611
+ raise ArgumentError, "#{value.inspect} is not a known #{attribute} value" unless state
560
612
 
561
- events.each {|name, event| order |= event.known_states}
562
- order |= states.select {|value, state| state.methods.any?}.map {|state| state.first}
563
- order |= states.keys - callbacks.values.flatten.map {|callback| callback.known_states}.flatten
564
- order |= states.keys
613
+ state
565
614
  end
566
615
 
567
616
  # Defines one or more events for the machine and the transitions that can
@@ -571,10 +620,16 @@ module StateMachine
571
620
  #
572
621
  # The following instance methods are generated when a new event is defined
573
622
  # (the "park" event is used as an example):
574
- # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given the current state of the object.
575
- # * <tt>next_park_transition</tt> - Gets the next transition that would be performed if the "park" event were to be fired now on the object or nil if no transitions can be performed.
576
- # * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state.
577
- # * <tt>park!(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state. If the transition fails, then a StateMachine::InvalidTransition error will be raised.
623
+ # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
624
+ # the current state of the object.
625
+ # * <tt>next_park_transition</tt> - Gets the next transition that would be
626
+ # performed if the "park" event were to be fired now on the object or nil
627
+ # if no transitions can be performed.
628
+ # * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning
629
+ # from the current state to the next valid state.
630
+ # * <tt>park!(run_action = true)</tt> - Fires the "park" event, transitioning
631
+ # from the current state to the next valid state. If the transition fails,
632
+ # then a StateMachine::InvalidTransition error will be raised.
578
633
  #
579
634
  # With a namespace of "car", the above names map to the following methods:
580
635
  # * <tt>can_park_car?</tt>
@@ -588,11 +643,11 @@ module StateMachine
588
643
  # transitions that can happen as a result of that event. For example,
589
644
  #
590
645
  # event :park, :stop do
591
- # transition :to => 'parked', :from => 'idling'
646
+ # transition :to => :parked, :from => :idling
592
647
  # end
593
648
  #
594
649
  # event :first_gear do
595
- # transition :to => 'first_gear', :from => 'parked', :if => :seatbelt_on?
650
+ # transition :to => :first_gear, :from => :parked, :if => :seatbelt_on?
596
651
  # end
597
652
  #
598
653
  # See StateMachine::Event#transition for more information on
@@ -604,12 +659,12 @@ module StateMachine
604
659
  #
605
660
  # class Vehicle
606
661
  # def self.safe_states
607
- # %w(parked idling stalled)
662
+ # [:parked, :idling, :stalled]
608
663
  # end
609
664
  #
610
665
  # state_machine do
611
666
  # event :park do
612
- # transition :to => 'parked', :from => Vehicle.safe_states
667
+ # transition :to => :parked, :from => Vehicle.safe_states
613
668
  # end
614
669
  # end
615
670
  # end
@@ -620,23 +675,23 @@ module StateMachine
620
675
  # state_machine do
621
676
  # # The park, stop, and halt events will all share the given transitions
622
677
  # event :park, :stop, :halt do
623
- # transition :to => 'parked', :from => %w(idling backing_up)
678
+ # transition :to => :parked, :from => [:idling, :backing_up]
624
679
  # end
625
680
  #
626
681
  # event :stop do
627
- # transition :to => 'idling', :from => 'first_gear'
682
+ # transition :to => :idling, :from => :first_gear
628
683
  # end
629
684
  #
630
685
  # event :ignite do
631
- # transition :to => 'idling', :from => 'parked'
686
+ # transition :to => :idling, :from => :parked
632
687
  # end
633
688
  # end
634
689
  # end
635
690
  def event(*names, &block)
636
691
  events = names.collect do |name|
637
- name = name.to_s
638
- event = self.events[name] ||= Event.new(self, name)
639
- @events_order << name unless @events_order.include?(name)
692
+ unless event = self.events[name]
693
+ self.events << event = Event.new(self, name)
694
+ end
640
695
 
641
696
  if block_given?
642
697
  event.instance_eval(&block)
@@ -655,15 +710,25 @@ module StateMachine
655
710
  # order for the callback to get invoked.
656
711
  #
657
712
  # Configuration options:
658
- # * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
659
- # * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
660
- # * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
661
- # * +except_to+ - One more states *not* being transitioned to
662
- # * +except_from+ - One or more states *not* being transitioned from
663
- # * +except_on+ - One or more events that *did not* fire the transition
664
- # * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
665
- # * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
666
- # * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
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.
667
732
  #
668
733
  # The +except+ group of options (+except_to+, +exception_from+, and
669
734
  # +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
@@ -676,8 +741,8 @@ module StateMachine
676
741
  #
677
742
  # class Vehicle
678
743
  # state_machine do
679
- # before_transition :to => 'parked', :do => :set_alarm
680
- # before_transition :to => 'parked' do |vehicle, transition|
744
+ # before_transition :to => :parked, :do => :set_alarm
745
+ # before_transition :to => :parked do |vehicle, transition|
681
746
  # vehicle.set_alarm
682
747
  # end
683
748
  # ...
@@ -687,18 +752,18 @@ module StateMachine
687
752
  # === Accessing the transition
688
753
  #
689
754
  # In addition to passing the object being transitioned, the actual
690
- # transition describing the context (e.g. event, from state, to state)
691
- # can be accessed as well. This additional argument is only passed if the
692
- # callback allows for it.
755
+ # transition describing the context (e.g. event, from, to) can be accessed
756
+ # as well. This additional argument is only passed if the callback allows
757
+ # for it.
693
758
  #
694
759
  # For example,
695
760
  #
696
761
  # class Vehicle
697
762
  # # Only specifies one parameter (the object being transitioned)
698
- # before_transition :to => 'parked', :do => lambda {|vehicle| vehicle.set_alarm}
763
+ # before_transition :to => :parked, :do => lambda {|vehicle| vehicle.set_alarm}
699
764
  #
700
765
  # # Specifies 2 parameters (object being transitioned and actual transition)
701
- # before_transition :to => 'parked', :do => lambda {|vehicle, transition| vehicle.set_alarm(transition)}
766
+ # before_transition :to => :parked, :do => lambda {|vehicle, transition| vehicle.set_alarm(transition)}
702
767
  # end
703
768
  #
704
769
  # *Note* that the object in the callback will only be passed in as an
@@ -719,13 +784,13 @@ module StateMachine
719
784
  # before_transition :update_dashboard
720
785
  #
721
786
  # # Before specific transition:
722
- # before_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
787
+ # before_transition :to => :parked, :from => [:first_gear, :idling], :on => :park, :do => :take_off_seatbelt
723
788
  #
724
789
  # # With conditional callback:
725
- # before_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
790
+ # before_transition :to => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
726
791
  #
727
792
  # # Using :except counterparts:
728
- # before_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
793
+ # before_transition :except_to => :stalled, :except_from => :stalled, :except_on => :crash, :do => :update_dashboard
729
794
  # ...
730
795
  # end
731
796
  # end
@@ -742,15 +807,25 @@ module StateMachine
742
807
  # in order for the callback to get invoked.
743
808
  #
744
809
  # Configuration options:
745
- # * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
746
- # * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
747
- # * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
748
- # * +except_to+ - One more states *not* being transitioned to
749
- # * +except_from+ - One or more states *not* being transitioned from
750
- # * +except_on+ - One or more events that *did not* fire the transition
751
- # * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
752
- # * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
753
- # * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
810
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
811
+ # are specified, then all states will match.
812
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
813
+ # specified, then all states will match.
814
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
815
+ # are specified, then all events will match.
816
+ # * <tt>:except_from</tt> - One or more states *not* being transitioned from
817
+ # * <tt>:except_to</tt> - One more states *not* being transitioned to
818
+ # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
819
+ # * <tt>:do</tt> - The callback to invoke when a transition matches. This
820
+ # can be a method, proc or string.
821
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
822
+ # callback should occur (e.g. :if => :allow_callbacks, or
823
+ # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
824
+ # should return or evaluate to a true or false value.
825
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
826
+ # callback should not occur (e.g. :unless => :skip_callbacks, or
827
+ # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
828
+ # string should return or evaluate to a true or false value.
754
829
  #
755
830
  # The +except+ group of options (+except_to+, +exception_from+, and
756
831
  # +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
@@ -763,8 +838,8 @@ module StateMachine
763
838
  #
764
839
  # class Vehicle
765
840
  # state_machine do
766
- # after_transition :to => 'parked', :do => :set_alarm
767
- # after_transition :to => 'parked' do |vehicle, transition, result|
841
+ # after_transition :to => :parked, :do => :set_alarm
842
+ # after_transition :to => :parked do |vehicle, transition, result|
768
843
  # vehicle.set_alarm
769
844
  # end
770
845
  # ...
@@ -774,19 +849,18 @@ module StateMachine
774
849
  # === Accessing the transition / result
775
850
  #
776
851
  # In addition to passing the object being transitioned, the actual
777
- # transition describing the context (e.g. event, from state, to state) and
778
- # the result from calling the object's action can be optionally passed as
779
- # well. These additional arguments are only passed if the callback allows
780
- # for it.
852
+ # transition describing the context (e.g. event, from, to) and the result
853
+ # from calling the object's action can be optionally passed as well. These
854
+ # additional arguments are only passed if the callback allows for it.
781
855
  #
782
856
  # For example,
783
857
  #
784
858
  # class Vehicle
785
859
  # # Only specifies one parameter (the object being transitioned)
786
- # after_transition :to => 'parked', :do => lambda {|vehicle| vehicle.set_alarm}
860
+ # after_transition :to => :parked, :do => lambda {|vehicle| vehicle.set_alarm}
787
861
  #
788
862
  # # Specifies 3 parameters (object being transitioned, transition, and action result)
789
- # after_transition :to => 'parked', :do => lambda {|vehicle, transition, result| vehicle.set_alarm(transition) if result}
863
+ # after_transition :to => :parked, :do => lambda {|vehicle, transition, result| vehicle.set_alarm(transition) if result}
790
864
  # end
791
865
  #
792
866
  # *Note* that the object in the callback will only be passed in as an
@@ -807,13 +881,13 @@ module StateMachine
807
881
  # after_transition :update_dashboard
808
882
  #
809
883
  # # After specific transition:
810
- # after_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
884
+ # after_transition :to => :parked, :from => [:first_gear, :idling], :on => :park, :do => :take_off_seatbelt
811
885
  #
812
886
  # # With conditional callback:
813
- # after_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
887
+ # after_transition :to => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
814
888
  #
815
889
  # # Using :except counterparts:
816
- # after_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
890
+ # after_transition :except_to => :stalled, :except_from => :stalled, :except_on => :crash, :do => :update_dashboard
817
891
  # ...
818
892
  # end
819
893
  # end
@@ -840,10 +914,14 @@ module StateMachine
840
914
  # installed on the system.
841
915
  #
842
916
  # Configuration options:
843
- # * +name+ - The name of the file to write to (without the file extension). Default is "#{owner_class.name}_#{attribute}"
844
- # * +path+ - The path to write the graph file to. Default is the current directory (".").
845
- # * +format+ - The image format to generate the graph in. Default is "png'.
846
- # * +font+ - The name of the font to draw state names in. Default is "Arial'.
917
+ # * <tt>:name</tt> - The name of the file to write to (without the file extension).
918
+ # Default is "#{owner_class.name}_#{attribute}"
919
+ # * <tt>:path</tt> - The path to write the graph file to. Default is the
920
+ # current directory (".").
921
+ # * <tt>:format</tt> - The image format to generate the graph in.
922
+ # Default is "png'.
923
+ # * <tt>:font</tt> - The name of the font to draw state names in.
924
+ # Default is "Arial".
847
925
  def draw(options = {})
848
926
  options = {
849
927
  :name => "#{owner_class.name}_#{attribute}",
@@ -861,13 +939,13 @@ module StateMachine
861
939
  graph = GraphViz.new('G', :output => options[:format], :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"))
862
940
 
863
941
  # Add nodes
864
- Array(state(*states_order)).each do |state|
942
+ states.by_priority.each do |state|
865
943
  node = state.draw(graph)
866
944
  node.fontname = options[:font]
867
945
  end
868
946
 
869
947
  # Add edges
870
- Array(event(*events_order)).each do |event|
948
+ events.each do |event|
871
949
  edges = event.draw(graph)
872
950
  edges.each {|edge| edge.fontname = options[:font]}
873
951
  end
@@ -892,7 +970,7 @@ module StateMachine
892
970
  def default_action
893
971
  end
894
972
 
895
- # Adds reader/writer/prediate methods for accessing the attribute that
973
+ # Adds reader/writer/predicate methods for accessing the attribute that
896
974
  # this state machine is defined for.
897
975
  def define_attribute_accessor
898
976
  attribute = self.attribute
@@ -901,12 +979,15 @@ module StateMachine
901
979
  attr_reader attribute unless method_defined?(attribute) || private_method_defined?(attribute)
902
980
  attr_writer attribute unless method_defined?("#{attribute}=") || private_method_defined?("#{attribute}=")
903
981
 
904
- # Checks whether the current state is a given value. If the value
905
- # is not a known state, then an ArgumentError is raised.
982
+ # Checks whether the current state is a given value
906
983
  define_method("#{attribute}?") do |state|
907
- raise ArgumentError, "#{state.inspect} is not a known #{attribute} value" unless self.class.state_machines[attribute].states.include?(state)
908
- send(attribute) == state
984
+ self.class.state_machines[attribute].state?(self, state)
909
985
  end unless method_defined?("#{attribute}?") || private_method_defined?("#{attribute}?")
986
+
987
+ # Gets the state name for the current value
988
+ define_method("#{attribute}_name") do
989
+ self.class.state_machines[attribute].state_for(self).name
990
+ end
910
991
  end
911
992
  end
912
993
 
@@ -916,39 +997,61 @@ module StateMachine
916
997
  # automatically determined by either calling +pluralize+ on the attribute
917
998
  # name or adding an "s" to the end of the name.
918
999
  def define_scopes(custom_plural = nil)
1000
+ attribute = self.attribute
919
1001
  plural = custom_plural || (attribute.respond_to?(:pluralize) ? attribute.pluralize : "#{attribute}s")
920
1002
 
921
1003
  [attribute, plural].uniq.each do |name|
922
- define_with_scope("with_#{name}") unless owner_class.respond_to?("with_#{name}")
923
- define_without_scope("without_#{name}") unless owner_class.respond_to?("without_#{name}")
1004
+ [:with, :without].each do |kind|
1005
+ method = "#{kind}_#{name}"
1006
+
1007
+ if !owner_class.respond_to?(method) && scope = send("create_#{kind}_scope", method)
1008
+ (class << owner_class; self; end).class_eval do
1009
+ # Converts state names to their corresponding values so that
1010
+ # they can be looked up properly
1011
+ define_method(method) do |*states|
1012
+ machine_states = state_machines[attribute].states
1013
+ values = states.flatten.map {|state| machine_states.fetch(state).value}
1014
+
1015
+ # Invoke the original scope implementation
1016
+ scope.call(self, values)
1017
+ end
1018
+ end
1019
+ end
1020
+ end
924
1021
  end
925
1022
  end
926
1023
 
927
- # Defines a scope for finding objects *with* a particular value or
928
- # values for the attribute.
1024
+ # Creates a scope for finding objects *with* a particular value or values
1025
+ # for the attribute.
929
1026
  #
930
1027
  # This is only applicable to specific integrations.
931
- def define_with_scope(name)
1028
+ def create_with_scope(name)
932
1029
  end
933
1030
 
934
- # Defines a scope for finding objects *without* a particular value or
1031
+ # Creates a scope for finding objects *without* a particular value or
935
1032
  # values for the attribute.
936
1033
  #
937
1034
  # This is only applicable to specific integrations.
938
- def define_without_scope(name)
1035
+ def create_without_scope(name)
939
1036
  end
940
1037
 
941
1038
  # Adds a new transition callback of the given type.
942
1039
  def add_callback(type, options, &block)
943
- @callbacks[type] << callback = Callback.new(options, &block)
1040
+ callbacks[type] << callback = Callback.new(options, &block)
944
1041
  add_states(callback.known_states)
945
1042
  callback
946
1043
  end
947
1044
 
948
1045
  # Tracks the given set of states in the list of all known states for
949
1046
  # this machine
950
- def add_states(states)
951
- states.collect {|state| @states[state] ||= State.new(self, state)}
1047
+ def add_states(new_states)
1048
+ new_states.collect do |new_state|
1049
+ unless state = states[new_state]
1050
+ states << state = State.new(self, new_state)
1051
+ end
1052
+
1053
+ state
1054
+ end
952
1055
  end
953
1056
  end
954
1057
  end