module StateMachine # A path represents a sequence of transitions that can be run for a particular # object. Paths can walk to new transitions, revealing all of the possible # branches that can be encountered in the object's state machine. class Path < Array include Assertions # The object whose state machine is being walked attr_reader :object # The state machine this path is walking attr_reader :machine # Creates a new transition path for the given object. Initially this is an # empty path. In order to start walking the path, it must be populated with # an initial transition. # # Configuration options: # * :target - The target state to end the path on # * :guard - Whether to guard transitions with the if/unless # conditionals defined for each one def initialize(object, machine, options = {}) assert_valid_keys(options, :target, :guard) @object = object @machine = machine @target = options[:target] @guard = options[:guard] end def initialize_copy(orig) #:nodoc: super @transitions = nil end # The initial state name for this path def from_name first && first.from_name end # Lists all of the from states that can be reached through this path. # # For example, # # path.to_states # => [:parked, :idling, :first_gear, ...] def from_states map {|transition| transition.from_name}.uniq end # The end state name for this path. If a target state was specified for # the path, then that will be returned if the path is complete. def to_name last && last.to_name end # Lists all of the to states that can be reached through this path. # # For example, # # path.to_states # => [:parked, :idling, :first_gear, ...] def to_states map {|transition| transition.to_name}.uniq end # Lists all of the events that can be fired through this path. # # For example, # # path.events # => [:park, :ignite, :shift_up, ...] def events map {|transition| transition.event}.uniq end # Walks down the next transitions at the end of this path. This will only # walk down paths that are considered valid. def walk transitions.each {|transition| yield dup.push(transition)} end # Determines whether or not this path has completed. A path is considered # complete when one of the following conditions is met: # * The last transition in the path ends on the target state # * There are no more transitions remaining to walk and there is no target # state def complete? !empty? && (@target ? to_name == @target : transitions.empty?) end private # Calculates the number of times the given state has been walked to def times_walked_to(state) select {|transition| transition.to_name == state}.length end # Determines whether the given transition has been recently walked down in # this path. If a target is configured for this path, then this will only # look at transitions walked down since the target was last reached. def recently_walked?(transition) transitions = self if @target && @target != to_name && target_transition = detect {|t| t.to_name == @target} transitions = transitions[index(target_transition) + 1..-1] end transitions.include?(transition) end # Determines whether it's possible to walk to the given transition from # the current path. A transition can be walked to if: # * It has not been recently walked and # * If a target is specified, it has not been walked to twice yet def can_walk_to?(transition) !recently_walked?(transition) && (!@target || times_walked_to(@target) < 2) end # Get the next set of transitions that can be walked to starting from the # end of this path def transitions @transitions ||= empty? ? [] : machine.events.transitions_for(object, :from => to_name, :guard => @guard).select {|transition| can_walk_to?(transition)} end end end