lib/state_machines/path_collection.rb
module StateMachines
# Represents a collection of paths that are generated based on a set of
# requirements regarding what states to start and end on
class PathCollection < Array
# The object whose state machine is being walked
attr_reader :object
# The state machine these path are walking
attr_reader :machine
# The initial state to start each path from
attr_reader :from_name
# The target state for each path
attr_reader :to_name
# Creates a new collection of paths with the given requirements.
#
# Configuration options:
# * <tt>:from</tt> - The initial state to start from
# * <tt>:to</tt> - The target end state
# * <tt>:deep</tt> - Whether to enable deep searches for the target state.
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
# conditionals defined for each one
def initialize(object, machine, options = {})
options = {deep: false, from: machine.states.match!(object).name}.merge(options)
options.assert_valid_keys( :from, :to, :deep, :guard)
@object = object
@machine = machine
@from_name = machine.states.fetch(options[:from]).name
@to_name = options[:to] && machine.states.fetch(options[:to]).name
@guard = options[:guard]
@deep = options[:deep]
initial_paths.each { |path| walk(path) }
end
# Lists all of the states that can be transitioned from through the paths in
# this collection.
#
# For example,
#
# paths.from_states # => [:parked, :idling, :first_gear, ...]
def from_states
flat_map(&:from_states).uniq
end
# Lists all of the states that can be transitioned to through the paths in
# this collection.
#
# For example,
#
# paths.to_states # => [:idling, :first_gear, :second_gear, ...]
def to_states
flat_map(&:to_states).uniq
end
# Lists all of the events that can be fired through the paths in this
# collection.
#
# For example,
#
# paths.events # => [:park, :ignite, :shift_up, ...]
def events
flat_map(&:events).uniq
end
private
# Gets the initial set of paths to walk
def initial_paths
machine.events.transitions_for(object, from: from_name, guard: @guard).map do |transition|
path = Path.new(object, machine, target: to_name, guard: @guard)
path << transition
path
end
end
# Walks down the given path. Each new path that matches the configured
# requirements will be added to this collection.
def walk(path)
self << path if path.complete?
path.walk { |next_path| walk(next_path) } unless to_name && path.complete? && !@deep
end
end
end