module Behavior module Filters class BehaviorError < StandardError; end def self.included(base) base.extend ClassMethods base.send(:include, Behavior::Filters::InstanceMethods) end module ClassMethods # :nodoc: # The passed filters will be appended to the array of filters that's run _before_ # the behavior is processed def append_before_filter(*filters, &block) # :doc: conditions = extract_conditions!(filters) filters << block if block_given? append_filter_to_chain('before', filters, conditions) end # Short-hand for append_before_filter since that's the most common of the two. alias :before_filter :append_before_filter # The passed filters will be appended to the array of filters that's run _after_ actions # the behavior is processed def append_after_filter(*filters, &block) conditions = extract_conditions!(filters) filters << block if block_given? append_filter_to_chain('after', filters, conditions) end # Short-hand for append_after_filter since that's the most common of the two. alias :after_filter :append_after_filter # The passed filters will have their +before+ method appended to the array of filters that's run before this # behavior is processed and have their +after+ method prepended to the after filter lest. The filter objects must all # respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like: # # B#before # A#before # A#after # B#after def append_around_filter(*filters) conditions = extract_conditions!(filters) for filter in filters.flatten ensure_filter_responds_to_before_and_after(filter) append_before_filter(conditions || {}) { |c| filter.before(c) } append_after_filter(conditions || {}) { |c| filter.after(c) } end end # Short-hand for append_around_filter since that's the most common of the two. alias :around_filter :append_around_filter # Returns all the before filters for this class and all its ancestors. def before_filters(state) #:nodoc: read_inheritable_attribute("before_#{state.to_s}_filters") || [] end # Returns all the after filters for this class and all its ancestors. def after_filters(state) #:nodoc: read_inheritable_attribute("after_#{state.to_s}_filters") || [] end private def append_filter_to_chain(state, filters, conditions) write_inheritable_array("#{state}_descendants_filters", filters) if conditions.include?(:descendants) or conditions.include?(:all) write_inheritable_array("#{state}_children_filters", filters) if conditions.include?(:children) or conditions.include?(:all) write_inheritable_array("#{state}_self_filters", filters) if conditions.include?(:self) or conditions.include?(:all) end def ensure_filter_responds_to_before_and_after(filter) unless filter.respond_to?(:before) && filter.respond_to?(:after) raise BehaviorError, "Filter object must respond to both before and after" end end def extract_conditions!(filters) filters_for = :self if filters.last.is_a? Hash h = filters.pop filters_for = h[:for] if h.has_key? :for end Array.[]( filters_for ).flatten end end module InstanceMethods # :nodoc: def self.included(base) base.class_eval do alias_method :process_without_filters, :process alias_method :process, :process_with_filters end end attr_reader :before_filter_chain_aborted def process_with_filters(request, response) @request, @response = request, response before_process_result = before_process(request, response) unless before_process_result == false process_without_filters(request, response) after_process(request, response) end @before_filter_chain_aborted = (before_process_result == false) end # Calls all the defined before-filter filters, which are added by using "before_filter :method". # If any of the filters return false, no more filters will be executed and the action is aborted. def before_process(request, response) #:doc: filter_result = true # if it's the direct parent, call :children, then :descendants # for all parents call :descendants # if it's this behavior, call :self parent_chain = page.ancestors.reverse parent_chain.each_with_index do |parent_page, i| filter_result = parent_page.behavior.call_before_process_filters( request, response, :children ) if (i+1) == parent_chain.length filter_result = parent_page.behavior.call_before_process_filters( request, response, :descendants ) unless filter_result == false return filter_result if filter_result == false end call_before_process_filters(request, response, :self) end def call_before_process_filters(request, response, state) call_filters(self.class.before_filters(state), request, response) end # Calls all the defined after-filter filters, which are added by using "after_filter :method". # If any of the filters return false, no more filters will be executed. def after_process(request, response) #:doc: # if it's the direct parent, call :children, then :descendants # for all parents call :descendants # if it's this behavior, call :self filter_result = call_after_process_filters(request, response, :self) parent_chain = page.ancestors parent_chain.each_with_index do |parent_page, i| filter_result = parent_page.behavior.call_after_process_filters( request, response, :children ) if i == 0 filter_result = parent_page.behavior.call_after_process_filters( request, response, :descendants ) unless filter_result == false return filter_result if filter_result == false end if filter_result end def call_after_process_filters(request, response, state) call_filters(self.class.after_filters(state), request, response) end private def call_filters(filters, request, response) filters.each do |filter| # can call 'next' if needed... filter_result = case when filter.is_a?(Symbol) self.send(filter, request, response) when filter_block?(filter) filter.call(self, request, response) when filter_class?(filter) filter.filter(self, request, response) else raise( BehaviorError, 'Filters need to be either a symbol, proc/method, or class implementing a static filter method' ) end if filter_result == false # M@: logger is nil... Could be useful to add logger object to Behavior::Base, eh? #logger.info "Filter chain halted as [#{filter}] returned false" if logger STDERR.puts "Filter chain halted as [#{filter}] returned false" return false end true end end def filter_block?(filter) filter.respond_to?('call') && (filter.arity == 1 || filter.arity == -1) end def filter_class?(filter) filter.respond_to?('filter') end end end end