state_machine 0.4.1 → 0.4.2

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.
@@ -15,7 +15,7 @@ module StateMachine
15
15
  attr_reader :requirements
16
16
 
17
17
  # A list of all of the states known to this guard. This will pull state
18
- # names from the following requirements:
18
+ # values from the following requirements:
19
19
  # * +to+
20
20
  # * +from+
21
21
  # * +except_to+
@@ -75,6 +75,41 @@ module StateMachine
75
75
  matches_query?(object, query) && matches_conditions?(object)
76
76
  end
77
77
 
78
+ # Draws a representation of this guard on the given graph. This will draw
79
+ # an edge between every state this guard matches *from* to either the
80
+ # configured to state or, if none specified, then a loopback to the from
81
+ # state.
82
+ #
83
+ # For example, if the following from states are configured:
84
+ # * +first_gear+
85
+ # * +idling+
86
+ # * +backing_up+
87
+ #
88
+ # ...and the to state is "parked", then the following edges will be created:
89
+ # * +first_gear+ -> +parked+
90
+ # * +idling+ -> +parked+
91
+ # * +backing_up+ -> +parked+
92
+ #
93
+ # Each edge will be labeled with the name of the event that would cause the
94
+ # transition.
95
+ #
96
+ # The collection of edges generated on the graph will be returned.
97
+ def draw(graph, event_name, valid_states)
98
+ # From states: :from, everything but :except states, or all states
99
+ from_states = requirements[:from] || requirements[:except_from] && (valid_states - requirements[:except_from]) || valid_states
100
+
101
+ # To state can be optional, otherwise it's a loopback
102
+ if to_state = requirements[:to]
103
+ to_state = State.id_for(to_state.first)
104
+ end
105
+
106
+ # Generate an edge between each from and to state
107
+ from_states.collect do |from_state|
108
+ from_state = State.id_for(from_state)
109
+ graph.add_edge(from_state, to_state || from_state, :label => event_name)
110
+ end
111
+ end
112
+
78
113
  protected
79
114
  # Verify that the from state, to state, and event match the query
80
115
  def matches_query?(object, query)
@@ -249,7 +249,8 @@ module StateMachine
249
249
  # This will always return true regardless of the results of the
250
250
  # callbacks.
251
251
  def notify(type, object, transition)
252
- ["#{type}_#{transition.event}", "#{type}_transition"].each do |method|
252
+ qualified_event = namespace ? "#{transition.event}_#{namespace}" : transition.event
253
+ ["#{type}_#{qualified_event}", "#{type}_transition"].each do |method|
253
254
  object.class.class_eval do
254
255
  @observer_peers.dup.each do |observer|
255
256
  observer.send(method, object, transition) if observer.respond_to?(method)
@@ -55,10 +55,17 @@ module StateMachine
55
55
  # # log message
56
56
  # end
57
57
  #
58
+ # # Target all state machines
58
59
  # before_transition :to => 'idling', :from => 'parked', :on => 'ignite' do
59
60
  # # put on seatbelt
60
61
  # end
61
62
  #
63
+ # # Target a specific state machine
64
+ # before_transition :state, :to => 'idling' do
65
+ # # put on seatbelt
66
+ # end
67
+ #
68
+ # # Target all state machines without requirements
62
69
  # before_transition do |transition|
63
70
  # # log message
64
71
  # end
@@ -104,10 +111,17 @@ module StateMachine
104
111
  # # log message
105
112
  # end
106
113
  #
114
+ # # Target all state machines
107
115
  # after_transition :to => 'idling', :from => 'parked', :on => 'ignite' do
108
116
  # # put on seatbelt
109
117
  # end
110
118
  #
119
+ # # Target a specific state machine
120
+ # after_transition :state, :to => 'idling' do
121
+ # # put on seatbelt
122
+ # end
123
+ #
124
+ # # Target all state machines without requirements
111
125
  # after_transition do |transition, saved|
112
126
  # if saved
113
127
  # # log message
@@ -1,4 +1,5 @@
1
1
  require 'state_machine/extensions'
2
+ require 'state_machine/state'
2
3
  require 'state_machine/event'
3
4
  require 'state_machine/callback'
4
5
  require 'state_machine/assertions'
@@ -10,7 +11,7 @@ end
10
11
 
11
12
  module StateMachine
12
13
  # Represents a state machine for a particular attribute. State machines
13
- # consist of events and a set of transitions that define how the state
14
+ # consist of states, events and a set of transitions that define how the state
14
15
  # changes after a particular event is fired.
15
16
  #
16
17
  # A state machine may not necessarily know all of the possible states for
@@ -95,7 +96,7 @@ module StateMachine
95
96
  #
96
97
  # vehicle = Vehicle.new
97
98
  # vehicle.park # => false
98
- # vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "idling"
99
+ # vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
99
100
  #
100
101
  # == Observers
101
102
  #
@@ -107,8 +108,8 @@ module StateMachine
107
108
  #
108
109
  # class Vehicle
109
110
  # state_machine do
110
- # event :ignite do
111
- # transition :to => 'idling', :from => 'parked'
111
+ # event :park do
112
+ # transition :to => 'parked', :from => 'idling'
112
113
  # end
113
114
  # ...
114
115
  # end
@@ -183,32 +184,6 @@ module StateMachine
183
184
  class Machine
184
185
  include Assertions
185
186
 
186
- # The class that the machine is defined in
187
- attr_reader :owner_class
188
-
189
- # The attribute for which the machine is being defined
190
- attr_reader :attribute
191
-
192
- # The initial state that the machine will be in when an object is created
193
- attr_reader :initial_state
194
-
195
- # The events that trigger transitions
196
- attr_reader :events
197
-
198
- # A list of all of the states known to this state machine. This will pull
199
- # state names from the following sources:
200
- # * Initial state
201
- # * Event transitions (:to, :from, :except_to, and :except_from options)
202
- # * Transition callbacks (:to, :from, :except_to, and :except_from options)
203
- # * Unreferenced states (using +other_states+ helper)
204
- attr_reader :states
205
-
206
- # The callbacks to invoke before/after a transition is performed
207
- attr_reader :callbacks
208
-
209
- # The action to invoke when an object transitions
210
- attr_reader :action
211
-
212
187
  class << self
213
188
  # Attempts to find or create a state machine for the given class. For
214
189
  # example,
@@ -263,24 +238,66 @@ module StateMachine
263
238
  end
264
239
 
265
240
  # Draw each of the class's state machines
266
- klass.state_machines.values.each do |machine|
241
+ klass.state_machines.each do |name, machine|
267
242
  machine.draw(options)
268
243
  end
269
244
  end
270
245
  end
271
246
  end
272
247
 
248
+ # The class that the machine is defined in
249
+ attr_reader :owner_class
250
+
251
+ # The attribute for which the machine is being defined
252
+ attr_reader :attribute
253
+
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
260
+ attr_reader :events
261
+
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
+ # A list of all of the states known to this state machine. This will pull
267
+ # state values from the following sources:
268
+ # * Initial state
269
+ # * State behaviors
270
+ # * Event transitions (:to, :from, :except_to, and :except_from options)
271
+ # * Transition callbacks (:to, :from, :except_to, and :except_from options)
272
+ # * Unreferenced states (using +other_states+ helper)
273
+ #
274
+ # Maps value => StateMachine::State
275
+ attr_reader :states
276
+
277
+ # The callbacks to invoke before/after a transition is performed
278
+ attr_reader :callbacks
279
+
280
+ # The action to invoke when an object transitions
281
+ attr_reader :action
282
+
283
+ # An identifier that forces all methods (including state predicates and
284
+ # event methods) to be generated with the value prefixed or suffixed,
285
+ # depending on the context.
286
+ attr_reader :namespace
287
+
273
288
  # Creates a new state machine for the given attribute
274
289
  def initialize(owner_class, *args, &block)
275
290
  options = args.last.is_a?(Hash) ? args.pop : {}
276
- assert_valid_keys(options, :initial, :action, :plural, :integration)
291
+ assert_valid_keys(options, :initial, :action, :plural, :namespace, :integration)
277
292
 
278
293
  # Set machine configuration
279
294
  @attribute = (args.first || 'state').to_s
280
295
  @events = {}
281
- @states = []
296
+ @events_order = []
297
+ @states = {}
282
298
  @callbacks = {:before => [], :after => []}
283
299
  @action = options[:action]
300
+ @namespace = options[:namespace]
284
301
 
285
302
  # Add class-/instance-level methods to the owner class for state initialization
286
303
  owner_class.class_eval do
@@ -315,7 +332,14 @@ module StateMachine
315
332
  events[name] = event
316
333
  events
317
334
  end
318
- @states = @states.dup
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]
319
343
  @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
320
344
  end
321
345
 
@@ -340,8 +364,8 @@ module StateMachine
340
364
  assert_valid_keys(options, :initial, :integration)
341
365
 
342
366
  @owner_class = owner_class
343
- @initial_state = options[:initial] if options[:initial]
344
- add_states([@initial_state])
367
+ @initial_state = add_states([options[:initial]]).first if options.include?(:initial) || !@initial_state
368
+ states.each {|name, state| state.initial = (state == @initial_state)}
345
369
 
346
370
  # Find an integration that can be used for implementing various parts
347
371
  # of the state machine that may behave differently in different libraries
@@ -391,7 +415,99 @@ module StateMachine
391
415
  # vehicle.force_idle = false
392
416
  # Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
393
417
  def initial_state(object)
394
- @initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state
418
+ @initial_state && @initial_state.value(object)
419
+ end
420
+
421
+ # Defines a series of behaviors to mixin with objects when the current
422
+ # state matches the given one(s). This allows instance methods to behave
423
+ # a specific way depending on what the value of the object's state is.
424
+ #
425
+ # For example,
426
+ #
427
+ # class Vehicle
428
+ # attr_accessor :driver
429
+ # attr_accessor :passenger
430
+ #
431
+ # state_machine :initial => 'parked' do
432
+ # event :ignite do
433
+ # transition :to => 'idling', :from => 'parked'
434
+ # end
435
+ #
436
+ # state 'parked' do
437
+ # def speed
438
+ # 0
439
+ # end
440
+ #
441
+ # def rotate_driver
442
+ # driver = self.driver
443
+ # self.driver = passenger
444
+ # self.passenger = driver
445
+ # true
446
+ # end
447
+ # end
448
+ #
449
+ # state 'idling', 'first_gear' do
450
+ # def speed
451
+ # 20
452
+ # end
453
+ #
454
+ # def rotate_driver
455
+ # self.state = "parked"
456
+ # rotate_driver
457
+ # end
458
+ # end
459
+ # end
460
+ # end
461
+ #
462
+ # In the above example, there are two dynamic behaviors defined for the
463
+ # class:
464
+ # * +speed+
465
+ # * +rotate_driver+
466
+ #
467
+ # Each of these behaviors are instance methods on the Vehicle class. However,
468
+ # which method actually gets invoked is based on the current state of the
469
+ # object. Using the above class as the example:
470
+ #
471
+ # vehicle = Vehicle.new
472
+ # vehicle.driver = 'John'
473
+ # vehicle.passenger = 'Jane'
474
+ #
475
+ # # Behaviors in the "parked" state
476
+ # vehicle.state # => "parked"
477
+ # vehicle.speed # => 0
478
+ # vehicle.rotate_driver # => true
479
+ # vehicle.driver # => "Jane"
480
+ # vehicle.passenger # => "John"
481
+ #
482
+ # vehicle.ignite # => true
483
+ #
484
+ # # Behaviors in the "idling" state
485
+ # vehicle.state # => "idling"
486
+ # vehicle.speed # => 20
487
+ # vehicle.rotate_driver # => true
488
+ # vehicle.driver # => "John"
489
+ # vehicle.passenger # => "Jane"
490
+ # vehicle.state # => "parked"
491
+ #
492
+ # As can be seen, both the +speed+ and +rotate_driver+ instance method
493
+ # implementations changed how they behave based on what the current state
494
+ # of the vehicle was.
495
+ #
496
+ # == Invalid behaviors
497
+ #
498
+ # If a specific behavior has not been defined for a state, then a
499
+ # NoMethodError exception will be raised, indicating that that method would
500
+ # not normally exist for an object with that state.
501
+ #
502
+ # Using the example from before:
503
+ #
504
+ # vehicle = Vehicle.new
505
+ # vehicle.state = "backing_up"
506
+ # 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?
510
+ states.length == 1 ? states.first : states
395
511
  end
396
512
 
397
513
  # Defines additional states that are possible in the state machine, but
@@ -429,7 +545,27 @@ module StateMachine
429
545
  add_states(args.flatten)
430
546
  end
431
547
 
432
- # Defines an event for the machine.
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)]
560
+
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
565
+ end
566
+
567
+ # Defines one or more events for the machine and the transitions that can
568
+ # be performed when those events are run.
433
569
  #
434
570
  # == Instance methods
435
571
  #
@@ -440,13 +576,19 @@ module StateMachine
440
576
  # * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state.
441
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.
442
578
  #
579
+ # With a namespace of "car", the above names map to the following methods:
580
+ # * <tt>can_park_car?</tt>
581
+ # * <tt>next_park_car_transition</tt>
582
+ # * <tt>park_car</tt>
583
+ # * <tt>park_car!</tt>
584
+ #
443
585
  # == Defining transitions
444
586
  #
445
587
  # +event+ requires a block which allows you to define the possible
446
588
  # transitions that can happen as a result of that event. For example,
447
589
  #
448
- # event :park do
449
- # transition :to => 'parked', :from => 'idle'
590
+ # event :park, :stop do
591
+ # transition :to => 'parked', :from => 'idling'
450
592
  # end
451
593
  #
452
594
  # event :first_gear do
@@ -476,19 +618,35 @@ module StateMachine
476
618
  #
477
619
  # class Vehicle
478
620
  # state_machine do
479
- # event :park do
480
- # transition :to => 'parked', :from => %w(first_gear reverse)
621
+ # # The park, stop, and halt events will all share the given transitions
622
+ # event :park, :stop, :halt do
623
+ # transition :to => 'parked', :from => %w(idling backing_up)
624
+ # end
625
+ #
626
+ # event :stop do
627
+ # transition :to => 'idling', :from => 'first_gear'
628
+ # end
629
+ #
630
+ # event :ignite do
631
+ # transition :to => 'idling', :from => 'parked'
481
632
  # end
482
- # ...
483
633
  # end
484
634
  # end
485
- def event(name, &block)
486
- name = name.to_s
487
- event = events[name] ||= Event.new(self, name)
488
- event.instance_eval(&block)
489
- add_states(event.known_states)
635
+ def event(*names, &block)
636
+ 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)
640
+
641
+ if block_given?
642
+ event.instance_eval(&block)
643
+ add_states(event.known_states)
644
+ end
645
+
646
+ event
647
+ end
490
648
 
491
- event
649
+ events.length == 1 ? events.first : events
492
650
  end
493
651
 
494
652
  # Creates a callback that will be invoked *before* a transition is
@@ -702,56 +860,21 @@ module StateMachine
702
860
 
703
861
  graph = GraphViz.new('G', :output => options[:format], :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"))
704
862
 
705
- # Tracks unique identifiers for dynamic states (via lambda blocks)
706
- dynamic_states = {}
707
-
708
863
  # Add nodes
709
- states.each do |state|
710
- shape = state == @initial_state ? 'doublecircle' : 'circle'
711
-
712
- # Use GraphViz-friendly name/label for dynamic/nil states
713
- if state.is_a?(Proc)
714
- name = "lambda#{dynamic_states.keys.length}"
715
- label = '*'
716
- dynamic_states[state] = name
717
- else
718
- name = label = state.nil? ? 'nil' : state.to_s
719
- end
720
-
721
- graph.add_node(name, :label => label, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
864
+ Array(state(*states_order)).each do |state|
865
+ node = state.draw(graph)
866
+ node.fontname = options[:font]
722
867
  end
723
868
 
724
869
  # Add edges
725
- events.values.each do |event|
726
- event.guards.each do |guard|
727
- # From states: :from, everything but :except states, or all states
728
- from_states = guard.requirements[:from] || guard.requirements[:except_from] && (states - guard.requirements[:except_from]) || states
729
- if to_state = guard.requirements[:to]
730
- to_state = to_state.first
731
-
732
- # Convert to GraphViz-friendly name
733
- to_state = case to_state
734
- when Proc; dynamic_states[to_state]
735
- when nil; 'nil'
736
- else; to_state.to_s; end
737
- end
738
-
739
- from_states.each do |from_state|
740
- # Convert to GraphViz-friendly name
741
- from_state = case from_state
742
- when Proc; dynamic_states[from_state]
743
- when nil; 'nil'
744
- else; from_state.to_s; end
745
-
746
- graph.add_edge(from_state, to_state || from_state, :label => event.name, :fontname => options[:font])
747
- end
748
- end
870
+ Array(event(*events_order)).each do |event|
871
+ edges = event.draw(graph)
872
+ edges.each {|edge| edge.fontname = options[:font]}
749
873
  end
750
874
 
751
875
  # Generate the graph
752
876
  graph.output
753
-
754
- true
877
+ graph
755
878
  rescue LoadError
756
879
  $stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
757
880
  false
@@ -825,23 +948,7 @@ module StateMachine
825
948
  # Tracks the given set of states in the list of all known states for
826
949
  # this machine
827
950
  def add_states(states)
828
- new_states = states - @states
829
- @states += new_states
830
-
831
- # Add state predicates
832
- attribute = self.attribute
833
- new_states.each do |state|
834
- if state && (state.is_a?(String) || state.is_a?(Symbol))
835
- name = "#{state}?"
836
-
837
- owner_class.class_eval do
838
- # Checks whether the current state is equal to the given value
839
- define_method(name) do
840
- self.send(attribute) == state
841
- end unless method_defined?(name) || private_method_defined?(name)
842
- end
843
- end
844
- end
951
+ states.collect {|state| @states[state] ||= State.new(self, state)}
845
952
  end
846
953
  end
847
954
  end