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