require 'state_machine/transition'
require 'state_machine/guard'
require 'state_machine/assertions'
require 'state_machine/matcher_helpers'
module StateMachine
# An event defines an action that transitions an attribute from one state to
# another. The state that an attribute is transitioned to depends on the
# guards configured for the event.
class Event
include Assertions
include MatcherHelpers
# The state machine for which this event is defined
attr_accessor :machine
# The name of the action that fires the event
attr_reader :name
# The list of guards that determine what state this event transitions
# objects to when fired
attr_reader :guards
# A list of all of the states known to this event using the configured
# guards/transitions as the source
attr_reader :known_states
# Creates a new event within the context of the given machine
def initialize(machine, name) #:nodoc:
@machine = machine
@name = name
@guards = []
@known_states = []
add_actions
end
# Creates a copy of this event in addition to the list of associated
# guards to prevent conflicts across events within a class hierarchy.
def initialize_copy(orig) #:nodoc:
super
@guards = @guards.dup
@known_states = @known_states.dup
end
# Creates a new transition that determines what to change the current state
# to when this event fires.
#
# == Defining transitions
#
# The options for a new transition uses the Hash syntax to map beginning
# states to ending states. For example,
#
# transition :parked => :idling, :idling => :first_gear
#
# In this case, when the event is fired, this transition will cause the
# state to be +idling+ if it's current state is +parked+ or +first_gear+ if
# it's current state is +idling+.
#
# To help defining these implicit transitions, a set of helpers are available
# for defining slightly more complex matching:
# * all - Matches every state in the machine
# * all - [:parked, :idling, ...] - Matches every state except those specified
# * any - An alias for +all+ (matches every state in the machine)
# * same - Matches the same state being transitioned from
#
# See StateMachine::MatcherHelpers for more information.
#
# Examples:
#
# transition all => nil # Transitions to nil regardless of the current state
# transition all => :idling # Transitions to :idling regardless of the current state
# transition all - [:idling, :first_gear] => :idling # Transitions every state but :idling and :first_gear to :idling
# transition nil => :idling # Transitions to :idling from the nil state
# transition :parked => :idling # Transitions to :idling if :parked
# transition [:parked, :stalled] => :idling # Transitions to :idling if :parked or :stalled
#
# transition :parked => same # Loops :parked back to :parked
# transition [:parked, :stalled] => same # Loops either :parked or :stalled back to the same state
# transition all - :parked => same # Loops every state but :parked back to the same state
#
# == Verbose transitions
#
# Transitions can also be defined use an explicit set of deprecated
# configuration options:
# * :from - A state or array of states that can be transitioned from.
# If not specified, then the transition can occur for *any* state.
# * :to - The state that's being transitioned to. If not specified,
# then the transition will simply loop back (i.e. the state will not change).
# * :except_from - A state or array of states that *cannot* be
# transitioned from.
#
# Examples:
#
# transition :to => nil
# transition :to => :idling
# transition :except_from => [:idling, :first_gear], :to => :idling
# transition :from => nil, :to => :idling
# transition :from => [:parked, :stalled], :to => :idling
#
# transition :from => :parked
# transition :from => [:parked, :stalled]
# transition :except_from => :parked
#
# Notice that the above examples are the verbose equivalent of the examples
# described initially.
#
# == Conditions
#
# In addition to the state requirements for each transition, a condition
# can also be defined to help determine whether that transition is
# available. These options will work on both the normal and verbose syntax.
#
# Configuration options:
# * :if - A method, proc or string to call to determine if the
# transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
# The condition should return or evaluate to true or false.
# * :unless - A method, proc or string to call to determine if the
# transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
# The condition should return or evaluate to true or false.
#
# Examples:
#
# transition :parked => :idling, :if => :moving?
# transition :parked => :idling, :unless => :stopped?
#
# transition :from => :parked, :to => :idling, :if => :moving?
# transition :from => :parked, :to => :idling, :unless => :stopped?
#
# == Order of operations
#
# Transitions are evaluated in the order in which they're defined. As a
# result, if more than one transition applies to a given object, then the
# first transition that matches will be performed.
def transition(options)
raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
# Only a certain subset of explicit options are allowed for transition
# requirements
assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
guards << guard = Guard.new(options)
@known_states |= guard.known_states
guard
end
# Determines whether any transitions can be performed for this event based
# on the current state of the given object.
#
# If the event can't be fired, then this will return false, otherwise true.
def can_fire?(object)
!next_transition(object).nil?
end
# Finds and builds the next transition that can be performed on the given
# object. If no transitions can be made, then this will return nil.
def next_transition(object)
from = machine.state_for(object).name
guards.each do |guard|
if match = guard.match(object, :from => from)
# Guard allows for the transition to occur
to = match[:to].values.empty? ? from : match[:to].values.first
return Transition.new(object, machine, name, from, to)
end
end
# No transition matched
nil
end
# Attempts to perform the next available transition on the given object.
# If no transitions can be made, then this will return false, otherwise
# true.
#
# Any additional arguments are passed to the StateMachine::Transition#perform
# instance method.
def fire(object, *args)
machine.reset(object)
if transition = next_transition(object)
transition.perform(*args)
else
machine.invalidate(object, self)
false
end
end
# Attempts to perform the next available transition on the given object.
# If no transitions can be made, then a StateMachine::InvalidTransition
# exception will be raised, otherwise true will be returned.
def fire!(object, *args)
fire(object, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.attribute} via :#{name} from #{machine.state_for(object).name.inspect}")
end
# Draws a representation of this event on the given graph. This will
# create 1 or more edges on the graph for each guard (i.e. transition)
# configured.
#
# A collection of the generated edges will be returned.
def draw(graph)
valid_states = machine.states.by_priority.map {|state| state.name}
guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
end
# Generates a nicely formatted description of this events's contents.
#
# For example,
#
# event = StateMachine::Event.new(machine, :park)
# event.transition all - :idling => :parked, :idling => same
# event # => # :parked, :idling => same]>
def inspect
transitions = guards.map do |guard|
guard.state_requirements.map do |state_requirement|
"#{state_requirement[:from].description} => #{state_requirement[:to].description}"
end * ', '
end
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
end
protected
# Add the various instance methods that can transition the object using
# the current event
def add_actions
qualified_name = name = self.name
qualified_name = "#{name}_#{machine.namespace}" if machine.namespace
# Checks whether the event can be fired on the current object
machine.define_instance_method("can_#{qualified_name}?") do |machine, object|
machine.event(name).can_fire?(object)
end
# Gets the next transition that would be performed if the event were
# fired now
machine.define_instance_method("next_#{qualified_name}_transition") do |machine, object|
machine.event(name).next_transition(object)
end
# Fires the event
machine.define_instance_method(qualified_name) do |machine, object, *args|
machine.event(name).fire(object, *args)
end
# Fires the event, raising an exception if it fails
machine.define_instance_method("#{qualified_name}!") do |machine, object, *args|
machine.event(name).fire!(object, *args)
end
end
end
end