st0012/object_tracer

View on GitHub
lib/object_tracer/trackable.rb

Summary

Maintainability
A
30 mins
Test Coverage
class ObjectTracer
  module Trackable
    def tap_init!(object, options = {}, &block)
      ObjectTracer::Trackers::InitializationTracker.new(options, &block).track(object)
    end

    def tap_passed!(object, options = {}, &block)
      ObjectTracer::Trackers::PassedTracker.new(options, &block).track(object)
    end

    def tap_assoc!(object, options = {}, &block)
      ObjectTracer::Trackers::AssociactionCallTracker.new(options, &block).track(object)
    end

    def tap_on!(object, options = {}, &block)
      ObjectTracer::Trackers::MethodCallTracker.new(options, &block).track(object)
    end

    def tap_mutation!(object, options = {}, &block)
      ObjectTracer::Trackers::MutationTracker.new(options, &block).track(object)
    end

    # each combination generates 3 methods: (uses action "print" and subject "call" as example)
    #
    # - #print_calls(object)
    # - #with_print_calls
    # - #print_instance_calls(klass)
    #
    [:print, :write].each do |output_action|
      [:calls, :traces, :mutations].each do |subject|
        helper_method_name = "#{output_action}_#{subject}"

        define_method helper_method_name do |target, options = {}|
          send("output_#{subject}", target, options, output_action: "and_#{output_action}")
        end

        define_method "with_#{helper_method_name}" do |options = {}|
          send(helper_method_name, self, options)
          self
        end

        define_method "#{output_action}_instance_#{subject}" do |target_klass, options = {}|
          collection_proxy = AsyncCollectionProxy.new

          tap_init!(target_klass, options.merge(force_recording: true)) do |payload|
            collection_proxy << send(helper_method_name, payload.return_value, options)
          end

          collection_proxy
        end
      end
    end

    private

    def output_calls(target, options = {}, output_action:)
      device_options, output_options = separate_options(options)

      tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
        output_payload.detail_call_info(output_options)
      end
    end

    def output_traces(target, options = {}, output_action:)
      device_options, output_options = separate_options(options)
      device_options[:event_type] = :call

      device_1 = tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
        "Called #{output_payload.method_name_and_location(output_options)}\n"
      end
      device_2 = tap_passed!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
        output_payload.passed_at(output_options)
      end
      CollectionProxy.new([device_1, device_2])
    end

    def output_mutations(target, options = {}, output_action:)
      device_options, output_options = separate_options(options)

      tap_mutation!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
        output_payload.call_info_with_ivar_changes(output_options)
      end
    end

    def separate_options(options)
      output_options = Output::DEFAULT_OPTIONS.keys.each_with_object({}) do |key, hash|
        hash[key] = options.fetch(key, ObjectTracer.config[key])
        options.delete(key)
      end

      [options, output_options]
    end

    # CollectionProxy delegates chained actions to multiple devices
    class CollectionProxy
      CHAINABLE_ACTIONS = [:stop!, :stop_when, :with]

      def initialize(devices)
        @devices = devices
      end

      CHAINABLE_ACTIONS.each do |method|
        define_method method do |&block|
          @devices.each do |device|
            device.send(method, &block)
          end
        end
      end
    end

    # AsyncCollectionProxy delegates chained actions to multiple device "asyncronously"
    # when we use tapping methods like `tap_init!` to create sub-devices
    # we need to find a way to pass the chained actions to every sub-device that's created
    # and this can only happen asyncronously as we won't know when'll that happen
    class AsyncCollectionProxy < CollectionProxy
      def initialize(devices = [])
        super
        @blocks = {}
      end

      CHAINABLE_ACTIONS.each do |method|
        define_method method do |&block|
          super(&block)
          @blocks[method] = block
        end
      end

      def <<(device)
        @devices << device

        @blocks.each do |method, block|
          device.send(method, &block)
        end
      end
    end
  end
end

include ObjectTracer::Trackable