am-kantox/kungfuig

View on GitHub
lib/kungfuig/prepender.rb

Summary

Maintainability
C
1 day
Test Coverage
module Kungfuig
  module I★I
  end

  LAMBDA = lambda do |λ, e, **hash|
    begin
      Kungfuig::Prepender.error! e, **hash
      λ[:on_error].call(e, **hash) if λ[:on_error]
    rescue => e
      Kungfuig::Prepender.error! e, reason: :on_error
    end
  end

  class Prepender
    class MalformedTarget < StandardError
      def initialize msg, args
        super "#{msg}. Target arguments: [#{args.inspect}]."
      end
    end

    AGRESSIVE_ERRORS = true

    class << self
      def errors
        @errors ||= []
      end

      def error! e, **hash
        errors << [e, hash]
      end

      def anteponer *args
        fail MalformedTarget.new "Factory requires a block; use Prepender#new for more accurate tuning", args unless block_given?
        Prepender.new(*args, &Proc.new).hook!
      end
    end

    attr_reader :method, :receiver, :options, :λ

    # Parameters might be:
    # • 1
    #   — method instance
    #   — string in form "Class#method"
    # • 2
    #   — class (String, Symbol or Class), method name (String, Symbol)
    #   — instance (Object), method name (String, Symbol)
    def initialize *args, **params
      @λ = { before: nil, after: nil, on_hook: nil, on_error: nil }
      @klazz, @method, @receiver =  case args.size
                                    when 1
                                      case args.first
                                      when Method then [(class << args.first.receiver ; self ; end), args.first.name, args.first.receiver]
                                      when UnboundMethod then [args.first.owner, args.first.name]
                                      when String
                                        k, m = args.first.split('#')
                                        [k, m && m.to_sym]
                                      end
                                    when 2
                                      case args.first
                                      when Module, String then [args.first, args.last.to_sym]
                                      when Symbol then [args.first.to_s.split('_').map(&:capitalize).join, args.last.to_sym]
                                      else
                                        [(class << args.first ; self ; end), args.last.to_sym, args.first]
                                      end
                                    end

      @options = params
      after(Proc.new) if block_given? # assign the block to after by default

      fail MalformedTarget.new "Unable to lookup class", args unless @klazz
      fail MalformedTarget.new "Unable to lookup method", args unless @method
    end

    def before λ = nil
      tap { @λ[__callee__] = λ || (block_given? ? Proc.new : nil) }
    end
    alias_method :after, :before
    alias_method :on_hook, :before
    alias_method :on_error, :before

    def hook!
      tap { postpone_hook }
    end

    protected

    def klazz
      return @klazz if @klazz.is_a?(Module)
      @klazz = Kernel.const_get(@klazz) if Kernel.const_defined?(@klazz)
      @klazz
    end

    def ready?
      @receiver && @receiver.respond_to?(@method) ||
        klazz.is_a?(Module) && klazz.instance_methods.include?(@method)
    end

    def to_hash
      {
        klazz: klazz.to_s,
        method: @method,
        lambdas: @λ
      }
    end

    # rubocop:disable Style/NestedTernaryOperator
    def hook
      status = {}
      λ = (hash = to_hash).delete(:lambdas)

      p = Module.new do
        include Kungfuig::I★I
        define_method(hash[:method]) do |*args, **params, &cb|
          before_params = hash.merge(receiver: self, ★: args, ★★: params, λ: cb)
          begin
            λ[:before].call(**before_params) if λ[:before]
          rescue => e
            status[:before] = e
            LAMBDA.call λ, e, **hash
          end

          (args.empty? ? (params.empty? ? super(&cb) : super(**params)) : super(*args, **params, &cb)).tap do |result|
            begin
              λ[:after].call(**before_params.merge(result: result)) if λ[:after]
            rescue => e
              status[:after] = e
              LAMBDA.call λ, e, **hash
            end
          end
        end
      end

      klazz.send(:include, Kungfuig) unless klazz.ancestors.include? Kungfuig
      klazz.send(:prepend, p)
    rescue => e
      status[:rescued] = e
      raise MalformedTarget.new e.message, "#{@klazz}##{@method}" if AGRESSIVE_ERRORS
    ensure
      begin
        λ[:on_hook].call(status) if λ[:on_hook]
      rescue => e
        LAMBDA.call λ, e, reason: :on_hook
      end
    end
    # rubocop:enable Style/NestedTernaryOperator

    def postpone_hook
      return hook if ready?

      TracePoint.new(:end) do |tp|
        if tp.self.name == @klazz && ready?
          hook
          tp.disable
        end
      end.enable
    end
  end
end