module TinyHooks::ClassMethods

Class methods

Public Instance Methods

define_hook(kind, target, hook_method_name = nil, terminator: :abort, if: nil, class_method: false, &block) click to toggle source

Define hook with kind and target method

@param [Symbol, String] kind the kind of the hook, possible values are: :before, :after and :around @param [Symbol, String] target the name of the targeted method @param hook_method_name [Symbol, String] the name of a method which should be called as a hook @param [Symbol] terminator choice for terminating execution, default is throwing abort symbol @param [Symbol] if condition to determine if it should define callback. Block is evaluated in context of self @param class_method [Boolean] treat target as class method

# File lib/tiny_hooks.rb, line 54
def define_hook(kind, target, hook_method_name = nil, terminator: :abort, if: nil, class_method: false, &block) # rubocop:disable Naming/MethodParameterName
  raise ArgumentError, 'You must provide a block or hook_method_name' unless block || hook_method_name
  raise ArgumentError, 'terminator must be one of the following: :abort or :return_false' unless %i[abort return_false].include? terminator.to_sym
  raise TinyHooks::TargetError, "Hook for #{target} is not allowed" if @_targets != UNDEFINED_TARGETS && !@_targets.include?(target)

  if class_method
    is_private = private_methods.include?(target.to_sym)

    begin
      original_method = @_public_only ? public_method(target) : method(target)
    rescue NameError => e
      raise unless e.message.include?('private')

      raise TinyHooks::PrivateError, "Public only mode is on and hooks for private methods (#{target} for this time) are not available."
    end
    @_class_originals[target.to_sym] = original_method unless @_class_originals[target.to_sym]

    block ||= -> { __send__(hook_method_name) }
    body = method_body(kind, original_method, terminator, binding.local_variable_get(:if), &block)
    singleton_class.class_eval do
      undef_method(target)
      define_method(target, &body)
      private target if is_private
    end
  else # instance method
    is_private = private_instance_methods.include?(target.to_sym)

    begin
      original_method = @_public_only ? public_instance_method(target) : instance_method(target)
    rescue NameError => e
      raise unless e.message.include?('private')

      raise TinyHooks::PrivateError, "Public only mode is on and hooks for private methods (#{target} for this time) are not available."
    end
    @_originals[target.to_sym] = original_method unless @_originals[target.to_sym]

    block ||= -> { __send__(hook_method_name) }

    undef_method(target)
    define_method(target, &method_body(kind, original_method, terminator, binding.local_variable_get(:if), &block))
    private target if is_private
  end
end
include_private!() click to toggle source

Disable public only mode

# File lib/tiny_hooks.rb, line 140
def include_private!
  @_public_only = false
end
public_only!() click to toggle source

Enable public only mode

# File lib/tiny_hooks.rb, line 135
def public_only!
  @_public_only = true
end
restore_original(target, class_method: false) click to toggle source

Restore original method

@param [Symbol, String] target @param class_method [Boolean] treat target as class method

# File lib/tiny_hooks.rb, line 102
def restore_original(target, class_method: false)
  if class_method
    original_method = @_class_originals[target.to_sym] || method(target)
    singleton_class.class_eval do
      undef_method(target)
      define_method(target, original_method)
    end
  else
    original_method = @_originals[target.to_sym] || instance_method(target)
    undef_method(target)
    define_method(target, original_method)
  end
end
target!(include_pattern: nil, exclude_pattern: nil) click to toggle source

Defines target for hooks @param include_pattern [Regexp] @param exclude_pattern [Regexp]

# File lib/tiny_hooks.rb, line 119
def target!(include_pattern: nil, exclude_pattern: nil)
  raise ArgumentError if include_pattern.nil? && exclude_pattern.nil?

  candidates = @_public_only ? instance_methods : instance_methods + private_instance_methods
  candidates += @public_only ? methods : methods + private_methods
  @_targets = if include_pattern && exclude_pattern
                targets = candidates.grep(include_pattern)
                targets.grep_v(exclude_pattern)
              elsif include_pattern
                candidates.grep(include_pattern)
              else
                candidates.grep_v(exclude_pattern)
              end
end

Private Instance Methods

_after(original_method, if_proc:, &block) click to toggle source
# File lib/tiny_hooks.rb, line 189
def _after(original_method, if_proc:, &block)
  if RUBY_VERSION >= '2.7'
    proc do |*args, **kwargs, &blk|
      original_method.is_a?(UnboundMethod) ? original_method.bind_call(self, *args, **kwargs, &blk) : original_method.call(*args, **kwargs, &blk)
      instance_exec(*args, **kwargs, &block) if if_proc.nil? || instance_exec(&if_proc) != false
    end
  else
    proc do |*args, &blk|
      original_method = original_method.bind(self) if original_method.is_a?(UnboundMethod)
      original_method.call(*args, &blk)
      instance_exec(*args, &block) if if_proc.nil? || instance_exec(&if_proc) != false
    end
  end
end
_around(original_method, if_proc:, &block) click to toggle source
# File lib/tiny_hooks.rb, line 204
def _around(original_method, if_proc:, &block)
  if RUBY_VERSION >= '2.7'
    proc do |*args, **kwargs, &blk|
      wrapper = lambda do
        original_method.is_a?(UnboundMethod) ? original_method.bind_call(self, *args, **kwargs, &blk) : original_method.call(*args, **kwargs, &blk)
      end
      instance_exec(wrapper, *args, **kwargs, &block) if if_proc.nil? || instance_exec(&if_proc) != false
    end
  else
    proc do |*args, &blk|
      wrapper = lambda do
        original_method = original_method.bind(self) if original_method.is_a?(UnboundMethod)
        original_method.call(*args, &blk)
      end
      instance_exec(wrapper, *args, &block) if if_proc.nil? || instance_exec(&if_proc) != false
    end
  end
end
_before(original_method, terminator:, if_proc:, &block) click to toggle source
# File lib/tiny_hooks.rb, line 156
def _before(original_method, terminator:, if_proc:, &block)
  if RUBY_VERSION >= '2.7'
    proc do |*args, **kwargs, &blk|
      if if_proc.nil? || instance_exec(&if_proc) != false
        hook_result = nil
        abort_result = catch :abort do
          hook_result = instance_exec(*args, **kwargs, &block)
          true
        end
        return if abort_result.nil? && terminator == :abort
        return if hook_result == false && terminator == :return_false
      end

      original_method.is_a?(UnboundMethod) ? original_method.bind_call(self, *args, **kwargs, &blk) : original_method.call(*args, **kwargs, &blk)
    end
  else
    proc do |*args, &blk|
      if if_proc.nil? || instance_exec(&if_proc) != false
        hook_result = nil
        abort_result = catch :abort do
          hook_result = instance_exec(*args, &block)
          true
        end
        return if abort_result.nil? && terminator == :abort
        return if hook_result == false && terminator == :return_false
      end

      original_method = original_method.bind(self) if original_method.is_a?(UnboundMethod)
      original_method.call(*args, &blk)
    end
  end
end
inherited(subclass) click to toggle source
Calls superclass method
# File lib/tiny_hooks.rb, line 223
def inherited(subclass)
  super
  subclass.instance_variable_set(:@_originals, instance_variable_get(:@_originals).clone)
  subclass.instance_variable_set(:@_class_originals, instance_variable_get(:@_class_originals).clone)
  subclass.instance_variable_set(:@_targets, instance_variable_get(:@_targets).clone)
  subclass.instance_variable_set(:@_public_only, instance_variable_get(:@_public_only).clone)
end
method_body(kind, original_method, terminator, if_proc, &block) click to toggle source
# File lib/tiny_hooks.rb, line 146
def method_body(kind, original_method, terminator, if_proc, &block)
  case kind.to_sym
  when :before then _before(original_method, terminator: terminator, if_proc: if_proc, &block)
  when :after  then _after(original_method, if_proc: if_proc, &block)
  when :around then _around(original_method, if_proc: if_proc, &block)
  else
    raise Error, "#{kind} is not supported."
  end
end