module StateMachine
# An invalid transition was attempted
class InvalidTransition < StandardError
end
# A transition represents a state change for a specific attribute.
#
# Transitions consist of:
# * An event
# * A starting state
# * An ending state
class Transition
class << self
# Runs one or more transitions in parallel. All transitions will run
# through the following steps:
# 1. Before callbacks
# 2. Persist state
# 3. Invoke action
# 4. After callbacks (if configured)
# 5. Rollback (if action is unsuccessful)
#
# Configuration options:
# * :action - Whether to run the action configured for each transition
# * :after - Whether to run after callbacks
#
# If a block is passed to this method, that block will be called instead
# of invoking each transition's action.
def perform(transitions, options = {})
# Validate that the transitions are for separate machines / attributes
attributes = transitions.map {|transition| transition.attribute}.uniq
raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != transitions.length
success = false
# Run before callbacks. If any callback halts, then the entire chain
# is halted for every transition.
if transitions.all? {|transition| transition.before}
# Persist the new state for each attribute
transitions.each {|transition| transition.persist}
# Run the actions associated with each machine
begin
results = {}
success =
if block_given?
# Block was given: use the result for each transition
result = yield
transitions.each {|transition| results[transition.action] = result}
!!result
elsif options[:action] == false
# Skip the action
true
else
# Run each transition's action (only once)
object = transitions.first.object
transitions.all? do |transition|
action = transition.action
action && !results.include?(action) ? results[action] = object.send(action) : true
end
end
rescue Exception
# Action failed: rollback
transitions.each {|transition| transition.rollback}
raise
end
# Run after callbacks even when the actions failed. The :after option
# is ignored if the transitions were unsuccessful.
transitions.each {|transition| transition.after(results[transition.action], success)} unless options[:after] == false && success
# Rollback the transitions if the transaction was unsuccessful
transitions.each {|transition| transition.rollback} unless success
end
success
end
# Runs one or more transitions within a transaction. See StateMachine::Transition.perform
# for more information.
def perform_within_transaction(transitions, options = {})
success = false
transitions.first.within_transaction do
success = perform(transitions, options)
end
success
end
end
# The object being transitioned
attr_reader :object
# The state machine for which this transition is defined
attr_reader :machine
# The event that triggered the transition
attr_reader :event
# The fully-qualified name of the event that triggered the transition
attr_reader :qualified_event
# The original state value *before* the transition
attr_reader :from
# The original state name *before* the transition
attr_reader :from_name
# The original fully-qualified state name *before* transition
attr_reader :qualified_from_name
# The new state value *after* the transition
attr_reader :to
# The new state name *after* the transition
attr_reader :to_name
# The new fully-qualified state name *after* the transition
attr_reader :qualified_to_name
# The arguments passed in to the event that triggered the transition
# (does not include the +run_action+ boolean argument if specified)
attr_accessor :args
# The result of invoking the action associated with the machine
attr_reader :result
# Creates a new, specific transition
def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
@object = object
@machine = machine
@args = []
# Event information
event = machine.events.fetch(event)
@event = event.name
@qualified_event = event.qualified_name
# From state information
from_state = machine.states.fetch(from_name)
@from = read_state ? machine.read(object, :state) : from_state.value
@from_name = from_state.name
@qualified_from_name = from_state.qualified_name
# To state information
to_state = machine.states.fetch(to_name)
@to = to_state.value
@to_name = to_state.name
@qualified_to_name = to_state.qualified_name
end
# The attribute which this transition's machine is defined for
def attribute
machine.attribute
end
# The action that will be run when this transition is performed
def action
machine.action
end
# Does this transition represent a loopback (i.e. the from and to state
# are the same)
#
# == Example
#
# machine = StateMachine.new(Vehicle)
# StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
# StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
def loopback?
from_name == to_name
end
# A hash of all the core attributes defined for this transition with their
# names as keys and values of the attributes as values.
#
# == Example
#
# machine = StateMachine.new(Vehicle)
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
# transition.attributes # => {:object => #, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
def attributes
@attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
end
# Runs the actual transition and any before/after callbacks associated
# with the transition. The action associated with the transition/machine
# can be skipped by passing in +false+.
#
# == Examples
#
# class Vehicle
# state_machine :action => :save do
# ...
# end
# end
#
# vehicle = Vehicle.new
# transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
# transition.perform # => Runs the +save+ action after setting the state attribute
# transition.perform(false) # => Only sets the state attribute
def perform(*args)
run_action = [true, false].include?(args.last) ? args.pop : true
self.args = args
# Run the transition
self.class.perform_within_transaction([self], :action => run_action)
end
# Runs a block within a transaction for the object being transitioned.
# By default, transactions are a no-op unless otherwise defined by the
# machine's integration.
def within_transaction
machine.within_transaction(object) do
yield
end
end
# Runs the machine's +before+ callbacks for this transition. Only
# callbacks that are configured to match the event, from state, and to
# state will be invoked.
#
# Once the callbacks are run, they cannot be run again until this transition
# is reset.
#
# == Example
#
# class Vehicle
# state_machine do
# before_transition :on => :ignite, :do => lambda {|vehicle| ...}
# end
# end
#
# vehicle = Vehicle.new
# transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
# transition.before
def before
result = false
catch(:halt) do
unless @before_run
callback(:before)
@before_run = true
end
result = true
end
result
end
# Transitions the current value of the state to that specified by the
# transition. Once the state is persisted, it cannot be persisted again
# until this transition is reset.
#
# == Example
#
# class Vehicle
# state_machine do
# event :ignite do
# transition :parked => :idling
# end
# end
# end
#
# vehicle = Vehicle.new
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
# transition.persist
#
# vehicle.state # => 'idling'
def persist
unless @persisted
machine.write(object, :state, to)
@persisted = true
end
end
# Runs the machine's +after+ callbacks for this transition. Only
# callbacks that are configured to match the event, from state, and to
# state will be invoked.
#
# The result can be used to indicate whether the associated machine action
# was executed successfully.
#
# Once the callbacks are run, they cannot be run again until this transition
# is reset.
#
# == Halting
#
# If any callback throws a :halt exception, it will be caught
# and the callback chain will be automatically stopped. However, this
# exception will not bubble up to the caller since +after+ callbacks
# should never halt the execution of a +perform+.
#
# == Example
#
# class Vehicle
# state_machine do
# after_transition :on => :ignite, :do => lambda {|vehicle| ...}
#
# event :ignite do
# transition :parked => :idling
# end
# end
# end
#
# vehicle = Vehicle.new
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
# transition.after(true)
def after(result = nil, success = true)
@result = result
catch(:halt) do
unless @after_run
callback(:after, :success => success)
@after_run = true
end
end
true
end
# Rolls back changes made to the object's state via this transition. This
# will revert the state back to the +from+ value.
#
# == Example
#
# class Vehicle
# state_machine :initial => :parked do
# event :ignite do
# transition :parked => :idling
# end
# end
# end
#
# vehicle = Vehicle.new # => #
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
#
# # Persist the new state
# vehicle.state # => "parked"
# transition.persist
# vehicle.state # => "idling"
#
# # Roll back to the original state
# transition.rollback
# vehicle.state # => "parked"
def rollback
reset
machine.write(object, :state, from)
end
# Resets any tracking of which callbacks have already been run and whether
# the state has already been persisted
def reset
@before_run = @persisted = @after_run = false
end
# Generates a nicely formatted description of this transitions's contents.
#
# For example,
#
# transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
# transition # => #
def inspect
"#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
end
protected
# Gets a hash of the context defining this unique transition (including
# event, from state, and to state).
#
# == Example
#
# machine = StateMachine.new(Vehicle)
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
# transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
def context
@context ||= {:on => event, :from => from_name, :to => to_name}
end
# Runs the callbacks of the given type for this transition. This will
# only invoke callbacks that exactly match the event, from state, and
# to state that describe this transition.
#
# Additional callback parameters can be specified. By default, this
# transition is also passed into callbacks.
def callback(type, context = {})
context = self.context.merge(context)
machine.callbacks[type].each do |callback|
callback.call(object, context, self)
end
end
end
end