pboling/debug_logging

View on GitHub
lib/debug_logging/util.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module DebugLogging
  module Util
    module_function

    def debug_time(time_or_monotonic)
      case time_or_monotonic
      when Time, DateTime
        time_or_monotonic
      when Numeric
        Time.at(time_or_monotonic)
      when String
        Time.parse(time_or_monotonic)
      else
        # Garbage in, Sweet Nourishing Gruel Out
        Time.now
      end
    end

    # methods_to_log may be an array of a single method name, followed by config options and payload,
    #   or it could be an array of method names followed by config options and payload to be shared by the whole set.
    def extract_payload_and_config(method_names:, payload: nil, config: nil)
      # When scoped config is present it will always be a new configuration instance per method
      # When scoped config is not present it will reuse the class' configuration object
      scoped_payload = (method_names.is_a?(Array) && method_names.last.is_a?(Hash) && method_names.pop.clone(freeze: false)) || {}
      payload = if payload
        payload.merge(scoped_payload)
      else
        scoped_payload
      end
      config_opts = config&.clone(freeze: false) || {}
      # puts "[EPAC] config: #{config}, scoped_payload: #{scoped_payload}, payload: #{payload}, config_opts: #{config_opts}"
      unless payload.empty?
        DebugLogging::Configuration::CONFIG_KEYS.each { |k| config_opts[k] = payload.delete(k) if payload.key?(k) }
      end
      method_names =
        case method_names
        when Symbol
          method_names
        when String
          method_names.to_sym
        when Array
          if method_names.first.is_a?(Array)
            # Array of arrays?
            method_names.shift
          elsif method_names.size == 1 && method_names.first.is_a?(Symbol)
            # when set as i_methods: [[:i_with_dsplat_payload, { tags: %w[blue green] }], ...]
            method_names.shift.to_sym
          else
            # Or an array of method name symbols?
            # logged :meth1, :meth2, :meth3 without options is valid
            method_names
          end
        else
          raise ArgumentError, "unknown type for method_names: #{method_names.class}"
        end
      [method_names, payload, config_opts]
    end

    def payload_instance_variable_hydration(scope:, payload:)
      paydirt = {}
      # TODO: Could make instance variable introspection configurable before or after method execution
      if payload.key?(:instance_variables)
        paydirt.merge!(payload.reject { |k| k == :instance_variables })
        payload[:instance_variables].each do |k|
          paydirt[k] = scope.send(:instance_variable_get, "@#{k}") if scope.send(:instance_variable_defined?, "@#{k}")
        end
      else
        paydirt.merge!(payload)
      end
      paydirt
    end

    def config_proxy_finder(scope:, method_name:, proxy_ref:, config_opts: {}, &block)
      proxy = scope.send(
        :instance_variable_get,
        DebugLogging::Configuration.config_pointer(
          proxy_ref,
          method_name,
        ),
      )
      # short circuit on subsequent calls is required
      #   so we only register notifications once
      return proxy if proxy

      base = scope.respond_to?(:debug_config) ? scope.debug_config : DebugLogging.debug_logging_configuration
      proxy = if config_opts.empty?
        base
      else
        DebugLogging::Configuration.new(**base.to_hash.merge(config_opts))
      end
      proxy.register(method_name)
      scope.send(
        :instance_variable_set,
        DebugLogging::Configuration.config_pointer(proxy_ref, method_name),
        proxy,
      )
      yield proxy if block
      proxy
    end
  end
end