module ActionService # :nodoc: module Invocation # :nodoc: ConcreteInvocation = :concrete VirtualInvocation = :virtual UnpublishedConcreteInvocation = :unpublished_concrete class InvocationError < ActionService::ActionServiceError # :nodoc: end def self.append_features(base) # :nodoc: super base.extend(ClassMethods) base.send(:include, ActionService::Invocation::InstanceMethods) end # Invocation interceptors provide a means to execute custom code before # and after method invocations on ActionService::Base objects. # # When running in _Direct_ dispatching mode, ActionController filters # should be used for this functionality. # # The semantics of invocation interceptors are the same as ActionController # filters, and accept the same parameters and options. # # A _before_ interceptor can also cancel execution by returning +false+, # or returning a [false, "cancel reason"] array if it wishes to supply # a reason for canceling the request. # # === Example # # class CustomService < ActionService::Base # before_invocation :intercept_add, :only => [:add] # # def add(a, b) # a + b # end # # private # def intercept_add # return [false, "permission denied"] # cancel it # end # end # # Options: # [:except] A list of methods for which the interceptor will NOT be called # [:only] A list of methods for which the interceptor WILL be called module ClassMethods # Appends the given +interceptors+ to be called # _before_ method invocation. def append_before_invocation(*interceptors, &block) conditions = extract_conditions!(interceptors) interceptors << block if block_given? add_interception_conditions(interceptors, conditions) append_interceptors_to_chain("before", interceptors) end # Prepends the given +interceptors+ to be called # _before_ method invocation. def prepend_before_invocation(*interceptors, &block) conditions = extract_conditions!(interceptors) interceptors << block if block_given? add_interception_conditions(interceptors, conditions) prepend_interceptors_to_chain("before", interceptors) end alias :before_invocation :append_before_invocation # Appends the given +interceptors+ to be called # _after_ method invocation. def append_after_invocation(*interceptors, &block) conditions = extract_conditions!(interceptors) interceptors << block if block_given? add_interception_conditions(interceptors, conditions) append_interceptors_to_chain("after", interceptors) end # Prepends the given +interceptors+ to be called # _after_ method invocation. def prepend_after_invocation(*interceptors, &block) conditions = extract_conditions!(interceptors) interceptors << block if block_given? add_interception_conditions(interceptors, conditions) prepend_interceptors_to_chain("after", interceptors) end alias :after_invocation :append_after_invocation def before_invocation_interceptors # :nodoc: read_inheritable_attribute("before_invocation_interceptors") end def after_invocation_interceptors # :nodoc: read_inheritable_attribute("after_invocation_interceptors") end def included_intercepted_methods # :nodoc: read_inheritable_attribute("included_intercepted_methods") || {} end def excluded_intercepted_methods # :nodoc: read_inheritable_attribute("excluded_intercepted_methods") || {} end private def append_interceptors_to_chain(condition, interceptors) write_inheritable_array("#{condition}_invocation_interceptors", interceptors) end def prepend_interceptors_to_chain(condition, interceptors) interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors") write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors) end def extract_conditions!(interceptors) return nil unless interceptors.last.is_a? Hash interceptors.pop end def add_interception_conditions(interceptors, conditions) return unless conditions included, excluded = conditions[:only], conditions[:except] write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded end def condition_hash(interceptors, *methods) interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})} end end module InstanceMethods # :nodoc: def self.append_features(base) super base.class_eval do alias_method :perform_invocation_without_interception, :perform_invocation alias_method :perform_invocation, :perform_invocation_with_interception end end def perform_invocation_with_interception(invocation, &block) return if before_invocation(invocation.method_name, invocation.params, &block) == false result = perform_invocation_without_interception(invocation) after_invocation(invocation.method_name, invocation.params, result) result end def perform_invocation(invocation) if invocation.concrete? unless self.respond_to?(invocation.method_name) && \ self.class.service_api.has_api_method?(invocation.method_name) raise InvocationError, "no such service method '#{invocation.method_name}'" end end params = invocation.params if invocation.concrete? || invocation.unpublished_concrete? self.send(invocation.method_name, *params) else if invocation.block params = invocation.block_params + params invocation.block.call(invocation.public_method_name, *params) else self.send(invocation.method_name, *params) end end end def before_invocation(name, args, &block) call_interceptors(self.class.before_invocation_interceptors, [name, args], &block) end def after_invocation(name, args, result) call_interceptors(self.class.after_invocation_interceptors, [name, args, result]) end private def call_interceptors(interceptors, interceptor_args, &block) if interceptors and not interceptors.empty? interceptors.each do |interceptor| next if method_exempted?(interceptor, interceptor_args[0].to_s) result = case when interceptor.is_a?(Symbol) self.send(interceptor, *interceptor_args) when interceptor_block?(interceptor) interceptor.call(self, *interceptor_args) when interceptor_class?(interceptor) interceptor.intercept(self, *interceptor_args) else raise( InvocationError, "Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method" ) end reason = nil if result.is_a?(Array) reason = result[1] if result[1] result = result[0] end if result == false block.call(reason) if block && reason return false end end end end def interceptor_block?(interceptor) interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1) end def interceptor_class?(interceptor) interceptor.respond_to?("intercept") end def method_exempted?(interceptor, method_name) case when self.class.included_intercepted_methods[interceptor] !self.class.included_intercepted_methods[interceptor].include?(method_name) when self.class.excluded_intercepted_methods[interceptor] self.class.excluded_intercepted_methods[interceptor].include?(method_name) end end end class InvocationRequest # :nodoc: attr_accessor :type attr :public_method_name attr_accessor :method_name attr_accessor :params attr_accessor :block attr :block_params def initialize(type, public_method_name, method_name, params=nil) @type = type @public_method_name = public_method_name @method_name = method_name @params = params || [] @block = nil @block_params = [] end def concrete? @type == ConcreteInvocation ? true : false end def unpublished_concrete? @type == UnpublishedConcreteInvocation ? true : false end end end end