module RubyExt::Callbacks class AbstractCallback attr_reader :executor def executor= executor @executor = executor.must_be.a Symbol, Proc end attr_reader :conditions def conditions= conditions @conditions = {} conditions.each do |k, v| @conditions[k] = if v.is_a? Symbol v elsif v.is_a? Array raise "method names must be symbols (#{v})!" unless v.all?{|e| e.is_a? Symbol} v else v end end @conditions end def run? target, data if cond = conditions[:if] evaluate_if(cond, target, data) elsif cond = conditions[:unless] !evaluate_if(cond, target, data) elsif cond = conditions[:only] evaluate_only(cond, data) elsif cond = conditions[:except] !evaluate_only(cond, data) else true end end alias_method :deep_clone, :clone protected def evaluate_if cond, target, data if cond.is_a? Symbol target.send cond elsif cond.is_a? Proc cond.call target, data else must_be.never_called end end def evaluate_only cond, data method = data[:method] method.must_be.a Symbol if method if cond.is_a? Symbol cond == method elsif cond.is_a? Array cond.include? method else must_be.never_called end end end class BeforeCallback < AbstractCallback attr_accessor :terminator def build_block target, data, &block -> do if run? target, data block.call if run target, data else block.call end end end def run target, data callback_result = if executor.is_a? Symbol target.send executor elsif executor.is_a? Proc executor.call target else must_be.never_called end !terminate?(target, callback_result) end protected def terminate? target, result unless terminator.nil? if terminator.is_a? Proc terminator.call target, result else result == terminator end else false end end end class AfterCallback < AbstractCallback def build_block target, data, &block -> do if run? target, data result = block.call run target, data result else block.call end end end def run target, data if executor.is_a? Symbol target.send executor elsif executor.is_a? Proc executor.call target else must_be.never_called end end end class AroundCallback < AbstractCallback def build_block target, data, &block -> do if run? target, data run target, data, &block else block.call end end end def run target, data, &block if executor.is_a? Symbol target.send executor, &block elsif executor.is_a? Proc executor.call target, block else must_be.never_called end end end def run_before_callbacks callback_name, data = {} callback_name.must_be.a Symbol self.class.callbacks[callback_name].try :each do |callback| if callback.is_a? BeforeCallback return false unless callback.run self, data end end true end def run_after_callbacks callback_name, data = {} callback_name.must_be.a Symbol self.class.callbacks[callback_name].try :each do |callback| callback.run self, data if callback.is_a? AfterCallback end end def run_callbacks callback_name, data = {}, &block callback_name.must_be.a Symbol if callbacks = self.class.callbacks[callback_name] chain = block || -> {} chain = callbacks.reverse.reduce chain do |chain, callback| callback.build_block self, data, &chain end chain.call else block.call if block end end # We need to prevent callback from rinning multiple times if nested # (need this for wrapping methods with callbacks to correctly call super). def run_callbacks_only_once callback_name, data = {}, &block set = Thread.current[callback_name] ||= {} if set.include? object_id block.call if block else begin set[object_id] = true run_callbacks callback_name, data, &block ensure set.delete object_id end end end module ClassMethods inheritable_accessor :callbacks, {} def set_callback callback_name, type, *executor_or_options, &block callback_name.must_be.a Symbol type = type.to_sym # Parsing arguments. opt = executor_or_options.extract_options! "You can't provide both method name and block for filter!" if block and !executor_or_options.empty? executor = block || executor_or_options.first type.must_be.in [:before, :around, :after] executor.must_be.defined # Creating callback. callback = AbstractCallback.new callback = case type when :before then BeforeCallback.new when :around then AroundCallback.new when :after then AfterCallback.new end callback.executor = executor callback.terminator = opt.delete :terminator if type == :before callback.conditions = opt (self.callbacks[callback_name] ||= []) << callback end def wrap_method_with_callbacks method, callback method_without_callback = :"#{method}_without_#{callback}_of_#{self.alias}" if method_defined? method_without_callback raise "can't wrap method #{method} with #{callback} of #{self.alias} twice!" end alias_method method_without_callback, method define_method method do |*args, &block| # We can't use run_callbacks, because in case of the `super` # call it will be runned twice. run_callbacks_only_once callback do send method_without_callback, *args, &block end end end def wrap_with_callback callback instance_methods(false).each do |method| wrap_method_with_callbacks method, callback end end end end