module Arca::Collector

Include Arca::Collector in an ActiveRecord class in order to collect data about how callbacks are being used.

Constants

ARCA_CALLBACK_FINDER_REGEXP

Internal: Regular expression used for extracting the file path and line number from a caller line.

ARCA_CONDITIONALS

Internal: Array of conditional symbols.

Public Class Methods

included(base) click to toggle source

ruby-doc.org/core-2.2.1/Module.html#method-i-included

# File lib/arca/collector.rb, line 17
def self.included(base)
  base.class_eval do
    define_singleton_method(:arca_callback_data) do
      @arca_callback_data ||= Hash.new {|k,v| k[v] = [] }
    end

    # Find the callback methods defined on this class.
    callback_method_symbols = singleton_methods.grep /^(after|around|before)\_/

    callback_method_symbols.each do |callback_symbol|
      # Find the UnboundMethod for the callback.
      callback_method = singleton_class.instance_method(callback_symbol)

      # Redefine the callback method so that data can be collected each time
      # the callback is used for this class.
      define_singleton_method(callback_method.name) do |*args, &block|
        # Duplicate args before modifying.
        args_copy = args.dup

        # Add target_symbol :inline to args_copy if a block, Proc, or Class
        # was given.
        if block
          args_copy.unshift(:inline)
        elsif args_copy.first.kind_of?(Proc)
          args_copy.shift
          args_copy.unshift(:inline)
        elsif !args_copy.first.kind_of?(Symbol)
          class_or_instance = args_copy.shift

          if class_or_instance.class == Class
            args_copy.unshift(class_or_instance.name.to_sym)
          else
            args_copy.unshift(class_or_instance.class.name.to_sym)
          end
        end

        # Get the options hash from the end of the args Array if it exists.
        options = args_copy.pop if args[-1].is_a?(Hash)

        # Get the callback file path and line number from the caller stack.
        callback_file_path, callback_line_number = ARCA_CALLBACK_FINDER_REGEXP.match(caller.first)[1..2]

        # Extract the model file path from the caller stack.
        caller.each do |line|
          if match = /\A(.+):\d+:in\s`<class:#{name.split("::").last}>'/.match(line)
            self.arca_callback_data[:model_file_path] = match[1]
            break
          end
        end

        # Iterate through the rest of the args. Each remaining arguement is
        # a Symbol representing the callback target method.
        args_copy.each do |target_symbol|
          # Find the conditional symbol if it exists in the options hash.
          conditional_symbol = ARCA_CONDITIONALS.
            find {|conditional| options && options.has_key?(conditional) }

          # Find the conditional target symbol if there is a conditional.
          conditional_target_symbol = if conditional_symbol
            options[conditional_symbol]
          end

          # Set the collector hash for this callback_symbol to an empty
          # Array if it has not already been set.
          arca_callback_data[callback_symbol] ||= []

          # Add the collected callback data to the collector Array for
          # this callback_symbol.
          arca_callback_data[callback_symbol] << {
            :callback_symbol                => callback_symbol,
            :callback_file_path             => callback_file_path,
            :callback_line_number           => callback_line_number.to_i,
            :target_symbol                  => target_symbol,
            :conditional_symbol             => conditional_symbol,
            :conditional_target_symbol      => conditional_target_symbol
          }
        end

        # Bind the callback method to self and call it with args.
        callback_method.bind(self).call(*args, &block)
      end
    end
  end
end