require 'state_machine/transition'
require 'state_machine/guard'
require 'state_machine/assertions'
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
# 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 will be evaluated when the event is fired.
#
# 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.
# * :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.
#
# == 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.
#
# == Examples
#
# transition :from => nil, :to => :parked
# transition :from => [:first_gear, :reverse]
# transition :except_from => :parked
# transition :to => nil
# transition :to => :parked
# transition :to => :parked, :from => :first_gear
# transition :to => :parked, :from => [:first_gear, :reverse]
# transition :to => :parked, :from => :first_gear, :if => :moving?
# transition :to => :parked, :from => :first_gear, :unless => :stopped?
# transition :to => :parked, :except_from => :parked
def transition(options)
assert_valid_keys(options, :from, :to, :except_from, :if, :unless)
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
if guard = guards.find {|guard| guard.matches?(object, :from => from)}
# Guard allows for the transition to occur
to = guard.state_requirement[:to].values.any? ? guard.state_requirement[:to].values.first : from
Transition.new(object, machine, name, from, to)
end
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)
if transition = next_transition(object)
transition.perform(*args)
else
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 :to => :parked, :from => :idling
# event # => # :parked]>
def inspect
transitions = guards.map do |guard|
"#{guard.state_requirement[:from].description} => #{guard.state_requirement[:to].description}"
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
attribute = machine.attribute
qualified_name = name = self.name
qualified_name = "#{name}_#{machine.namespace}" if machine.namespace
machine.owner_class.class_eval do
# Checks whether the event can be fired on the current object
define_method("can_#{qualified_name}?") do
self.class.state_machines[attribute].event(name).can_fire?(self)
end
# Gets the next transition that would be performed if the event were
# fired now
define_method("next_#{qualified_name}_transition") do
self.class.state_machines[attribute].event(name).next_transition(self)
end
# Fires the event
define_method(qualified_name) do |*args|
self.class.state_machines[attribute].event(name).fire(self, *args)
end
# Fires the event, raising an exception if it fails
define_method("#{qualified_name}!") do |*args|
self.class.state_machines[attribute].event(name).fire!(self, *args)
end
end
end
end
end