require 'state_machine/guard'
require 'state_machine/eval_helpers'
module StateMachine
# Callbacks represent hooks into objects that allow you to trigger logic
# before or after a specific transition occurs.
class Callback
include EvalHelpers
class << self
# Determines whether to automatically bind the callback to the object being
# transitioned. This only applies to callbacks that are defined as
# lambda blocks (or Procs). Some integrations, such as DataMapper, handle
# callbacks by executing them bound to the object involved, while other
# integrations, such as ActiveRecord, pass the object as an argument to
# the callback. This can be configured on an application-wide basis by
# setting this configuration to +true+ or +false+. The default value
# is +false+.
#
# *Note* that the DataMapper and Sequel integrations automatically
# configure this value on a per-callback basis, so it does not have to
# be enabled application-wide.
#
# == Examples
#
# When not bound to the object:
#
# class Vehicle
# state_machine do
# before_transition do |vehicle|
# vehicle.set_alarm
# end
# end
#
# def set_alarm
# ...
# end
# end
#
# When bound to the object:
#
# StateMachine::Callback.bind_to_object = true
#
# class Vehicle
# state_machine do
# before_transition do
# self.set_alarm
# end
# end
#
# def set_alarm
# ...
# end
# end
attr_accessor :bind_to_object
end
# An optional block for determining whether to cancel the callback chain
# based on the return value of the callback. By default, the callback
# chain never cancels based on the return value (i.e. there is no implicit
# terminator). Certain integrations, such as ActiveRecord and Sequel,
# change this default value.
#
# == Examples
#
# Canceling the callback chain without a terminator:
#
# class Vehicle
# state_machine do
# before_transition do |vehicle|
# throw :halt
# end
# end
# end
#
# Canceling the callback chain with a terminator value of +false+:
#
# class Vehicle
# state_machine do
# before_transition do |vehicle|
# false
# end
# end
# end
attr_reader :terminator
# The guard that determines whether or not this callback can be invoked
# based on the context of the transition. The event, from state, and
# to state must all match in order for the guard to pass.
#
# See StateMachine::Guard for more information.
attr_reader :guard
# Creates a new callback that can get called based on the configured
# options.
#
# In addition to the possible configuration options for guards, the
# following options can be configured:
# * :bind_to_object - Whether to bind the callback to the object involved.
# If set to false, the object will be passed as a parameter instead.
# Default is integration-specific or set to the application default.
# * :terminator - A block/proc that determines what callback results
# should cause the callback chain to halt (if not using the default
# throw :halt technique).
#
# More information about how those options affect the behavior of the
# callback can be found in their attribute definitions.
def initialize(options = {}, &block)
if options.is_a?(Hash)
@method = options.delete(:do) || block
else
# Only the callback was configured
@method = options
options = {}
end
# The actual method to invoke must be defined
raise ArgumentError, ':do callback must be specified' unless @method
# Proxy the method so that it's bound to the object. Note that this only
# applies to lambda callbacks. All other callbacks ignore this option.
bind_to_object = !options.include?(:bind_to_object) && self.class.bind_to_object || options.delete(:bind_to_object)
@method = bound_method(@method) if @method.is_a?(Proc) && bind_to_object
@terminator = options.delete(:terminator)
@guard = Guard.new(options)
end
# Gets a list of the states known to this callback by looking at the
# guard's known states
def known_states
guard.known_states
end
# Runs the callback as long as the transition context matches the guard
# requirements configured for this callback.
def call(object, context = {}, *args)
# Only evaluate the method if the guard passes
if @guard.matches?(object, context)
result = evaluate_method(object, @method, *args)
# If a terminator has been configured and it matches the result from
# the evaluated method, then the callback chain should be halted
if @terminator && @terminator.call(result)
throw :halt
else
result
end
end
end
private
# Generates a method that can be bound to the object being transitioned
# when the callback is invoked
def bound_method(block)
# Generate a thread-safe unbound method that can be used on any object
# This is essentially a workaround for not having Ruby 1.9's instance_exec
unbound_method = Object.class_eval do
time = Time.now
method_name = "__bind_#{time.to_i}_#{time.usec}"
define_method(method_name, &block)
method = instance_method(method_name)
remove_method(method_name)
method
end
arity = unbound_method.arity
# Proxy calls to the method so that the method can be bound *and*
# the arguments are adjusted
lambda do |object, *args|
unbound_method.bind(object).call(*(arity == 0 ? [] : args))
end
end
end
end