require 'state_machines/branch' require 'state_machines/eval_helpers' module StateMachines # Callbacks represent hooks into objects that allow logic to be triggered # before, after, or around a specific set of transitions. 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: # # StateMachines::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 # The application-wide terminator to use for callbacks when not # explicitly defined. Terminators determine whether to cancel a # callback chain based on the return value of the callback. # # See StateMachines::Callback#terminator for more information. attr_accessor :terminator end # The type of callback chain this callback is for. This can be one of the # following: # * +before+ # * +after+ # * +around+ # * +failure+ attr_accessor :type # 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 branch 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 branch to pass. # # See StateMachines::Branch for more information. attr_reader :branch # Creates a new callback that can get called based on the configured # options. # # In addition to the possible configuration options for branches, 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(type, *args, &block) @type = type raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless [:before, :after, :around, :failure].include?(type) options = args.last.is_a?(Hash) ? args.pop : {} @methods = args @methods.concat(Array(options.delete(:do))) @methods << block if block_given? raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any? options = {bind_to_object: self.class.bind_to_object, terminator: self.class.terminator}.merge(options) # Proxy lambda blocks so that they're bound to the object bind_to_object = options.delete(:bind_to_object) @methods.map! do |method| bind_to_object && method.is_a?(Proc) ? bound_method(method) : method end @terminator = options.delete(:terminator) @branch = Branch.new(options) end # Gets a list of the states known to this callback by looking at the # branch's known states def known_states branch.known_states end # Runs the callback as long as the transition context matches the branch # requirements configured for this callback. If a block is provided, it # will be called when the last method has run. # # If a terminator has been configured and it matches the result from the # evaluated method, then the callback chain should be halted. def call(object, context = {}, *args, &block) if @branch.matches?(object, context) run_methods(object, context, 0, *args, &block) true else false end end private # Runs all of the methods configured for this callback. # # When running +around+ callbacks, this will evaluate each method and # yield when the last method has yielded. The callback will only halt if # one of the methods does not yield. # # For all other types of callbacks, this will evaluate each method in # order. The callback will only halt if the resulting value from the # method passes the terminator. def run_methods(object, context = {}, index = 0, *args, &block) if type == :around current_method = @methods[index] if current_method yielded = false evaluate_method(object, current_method, *args) do yielded = true run_methods(object, context, index + 1, *args, &block) end throw :halt unless yielded else yield if block_given? end else @methods.each do |method| result = evaluate_method(object, method, *args) throw :halt if @terminator && @terminator.call(result) end end end # Generates a method that can be bound to the object being transitioned # when the callback is invoked def bound_method(block) type = self.type arity = block.arity arity += 1 if arity >= 0 # Make sure the object gets passed arity += 1 if arity == 1 && type == :around # Make sure the block gets passed method = lambda { |object, *args| object.instance_exec(*args, &block) } # Proxy arity to the original block ( class << method; self; end).class_eval do define_method(:arity) { arity } end method end end end