Module: Kinda::Hookable

Includes:
Core
Defined in:
lib/hookable.rb

Defined Under Namespace

Classes: HookedMethod

Constant Summary collapse

ClassMethods =
inheritable_extend do
  [:before, :after, :around].each do |kind|
    define_method(kind) do |method_name, &block|
      add_hook(kind, method_name, &block)
    end
  end

  alias_method :on, :after

  def method_hooked?(method_name)
    self_and_ancestors.each do |ancestor|
      ancestor.is_a?(Module) ? next : break unless ancestor.respond_to?(:hooked_methods)
      return true if ancestor.hooked_methods.include?(method_name)
    end
    false
  end

  protected

  def hooked_methods
    @hooked_methods ||= Hash.new do |hash, method_name|
      hash[method_name] = Hookable::HookedMethod.new(self, method_name)
    end
  end

  def patch_method(method_name)
    return if Thread.current[:__adding_method__]
    # puts "Patching method ##{method_name} in #{self.inspect}"
    original_method = instance_method(method_name)
    Thread.current[:__adding_method__] = true
    define_method(method_name) do |*args, &block|
      singleton_class_send(:exec_method, method_name, original_method, self, *args, &block)
    end
    Thread.current[:__adding_method__] = false
    hooked_methods[method_name].method_patched = true
  end

  private

  def add_hook(kind, method_name, &block)
    hooked_methods[method_name.to_sym].hooks[kind] << block
    patch_method_if_necessary(method_name.to_sym)
  end

  def exec_method(method_name, original_method, original_self, *args, &block)
    result = nil

    chained_hooks = [] << lambda do |*args, &block|
      find_hooks(method_name, :before).each do |hook|
        original_self.call_with_this(hook, *args, &block)
      end
      result = original_method.bind(original_self).call(*args, &block) if original_method
      find_hooks(method_name, :after).each do |hook|
        original_self.call_with_this(hook, *args, &block)
      end
    end

    hooks = find_hooks(method_name, :around)
    hooks.each_index do |index|
      chained_hooks << lambda do |*args, &block|
        original_self.call_with_this(hooks[index], chained_hooks[index], *args, &block)
      end
    end
    chained_hooks.last.call(*args, &block)

    result
  end

  def find_hooks(method_name, kind)
    hooks = []
    self_and_ancestors.each do |ancestor|
      ancestor.is_a?(Module) ? next : break unless ancestor.respond_to?(:hooked_methods)
      next unless ancestor.hooked_methods.include?(method_name)
      next unless ancestor.hooked_methods[method_name].hooks.include?(kind)
      hooks.concat(ancestor.hooked_methods[method_name].hooks[kind])
    end
    hooks
  end
  
  def method_added(method_name)
    super
    if method_hooked?(method_name)
      patch_method(method_name)
    end
  end

  def patch_method_if_necessary(method_name)
    self_and_ancestors.each do |ancestor|
      ancestor.is_a?(Module) ? next : break unless ancestor.respond_to?(:patch_method)
      method_defined_in_this_class = ancestor.instance_method(method_name).owner == ancestor rescue false
      if method_defined_in_this_class
        ancestor.patch_method(method_name) unless ancestor.hooked_methods[method_name].method_patched
        break
      end
    end
  end
end

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object (private)



117
118
119
120
121
122
123
# File 'lib/hookable.rb', line 117

def method_missing(method_name, *args, &block)
  if method_hooked?(method_name)
    singleton_class_send(:exec_method, method_name, nil, self, *args, &block)
  else
    super
  end
end

Instance Method Details

#respond_to?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


111
112
113
# File 'lib/hookable.rb', line 111

def respond_to?(method_name, include_private=false)
  super || method_hooked?(method_name)
end