Как установить крючок для запуска кода в конце определения класса Ruby?

Я создаю плагин, который позволит разработчику добавлять в класс различные функции с простым объявлением в определении класса (по шаблону normal act_as).

Например, код, потребляющий плагин, может выглядеть как

class YourClass
  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end

Возникает вопрос, потому что я хочу, чтобы ошибка проверяла, что значение, указанное для параметра: specific_method_to_use, существует как метод, но способ, как правило, организовывается и загружается, метод еще не существует.

Код в моем плагине выглядит примерно так:

module MyPlugin
  extend ActiveSupport::Concern

  module ClassMethods
    def consumes_my_plugin(options = {})
      raise ArgumentError.new("#{options[:specific_method_to_use]} is not defined") if options[:specific_method_to_use].present? && !self.respond_to?(options[:specific_method_to_use])
    end
  end
end

Это будет работать:

class YourClass
  def your_method; true; end

  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end

Но так большинство людей пишут код, и это не будет:

class YourClass
  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method

  def your_method; true; end
end

Как я могу терпеть неудачу при загрузке YourClass? Я хочу, чтобы это ошибка, а не во время выполнения с помощью NoMethodError. Могу ли я отложить выполнение строки, которая вызывает ArgumentError до загрузки всего класса, или сделать что-то еще умное для достижения этого?

Ответы

Ответ 1

Используйте TracePoint, чтобы отслеживать, когда ваш класс отправляет событие :end.


Общее решение

Этот модуль позволит вам создать обратный вызов self.finalize в любом классе.

module Finalize
  def self.extended(obj)
    TracePoint.trace(:end) do |t|
      if obj == t.self
        obj.finalize
        t.disable
      end
    end
  end
end

Теперь вы можете расширить свой класс и определить self.finalize, который запустится, как только закончится определение класса:

class Foo
  puts "Top of class"

  extend Finalize

  def self.finalize
    puts "Finalizing #{self}"
  end

  puts "Bottom of class"
end

puts "Outside class"

# output:
#   Top of class
#   Bottom of class
#   Finalizing Foo
#   Outside class

Конкретное решение проблемы ОП

Вот как вы можете вставить TracePoint непосредственно в ранее существовавший модуль.

require 'active_support/all'

module MyPlugin
  extend ActiveSupport::Concern

  module ClassMethods
    def consumes_my_plugin(**options)
      m = options[:specific_method_to_use]

      TracePoint.trace(:end) do |t|
        break unless self == t.self

        raise ArgumentError.new("#{m} is not defined") unless instance_methods.include?(m)

        t.disable
      end
    end
  end
end

Приведенные ниже примеры демонстрируют, что он работает как указано:

# 'def' before 'consumes': evaluates without errors
class MethodBeforePlugin
  include MyPlugin
  def your_method; end
  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end

# 'consumes' before 'def': evaluates without errors
class PluginBeforeMethod
  include MyPlugin
  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
  def your_method; end
end

# 'consumes' with no 'def': throws ArgumentError at load time
class PluginWithoutMethod
  include MyPlugin
  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end