如何在Ruby类定义的末尾设置一个钩子来运行代码?

时间:2022-10-08 02:23:15

I'm building a plugin that will allow a developer to add various features to a class with a simple declaration in the class definition (following the normal acts_as pattern).

我正在构建一个插件,允许开发人员在类定义中使用简单声明向类添加各种功能(遵循正常的acts_as模式)。

For example, code consuming the plugin might look like

例如,使用插件的代码可能看起来像

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

My question arises because I want to error check that the value provided for the :specific_method_to_use parameter exists as a method, but the way code is typically organized and loaded, the method doesn't exist yet.

我的问题出现了,因为我想错误地检查为:specific_method_to_use参数提供的值是否作为方法存在,但代码通常是组织和加载的方式,该方法尚不存在。

The code in my plugin tentatively looks like this:

我的插件中的代码暂时看起来像这样:

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

This would work:

这可行:

class YourClass
  def your_method; true; end

  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end

But this is how most people write code, and it would not:

但这是大多数人编写代码的方式,它不会:

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

  def your_method; true; end
end

How can I fail at YourClass load time? I want it to error then, not at run time with a NoMethodError. Can I defer execution of the line that raises the ArgumentError until the entire class is loaded, or do something else clever to achieve that?

如何在YourClass加载时失败?我希望它出错,而不是在运行时使用NoMethodError。我可以推迟执行引发ArgumentError的行,直到加载整个类,或者做一些其他聪明的事情来实现吗?

1 个解决方案

#1


5  

Use TracePoint to track when your class sends up an :end event.

使用TracePoint跟踪类何时发送:end事件。


Specific solution to your problem

具体解决您的问题

Here's how you can fit TracePoint directly into your pre-existing module.

以下是如何将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

The examples below demonstrate that it works as specified:

以下示例表明它按指定的方式工作:

# `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

General solution

一般解决方案

This module will let you create a self.finalize callback in any class.

此模块将允许您在任何类中创建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

Now you can extend your class and define self.finalize, which will run as soon as the class definition ends:

现在,您可以扩展您的类并定义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

#1


5  

Use TracePoint to track when your class sends up an :end event.

使用TracePoint跟踪类何时发送:end事件。


Specific solution to your problem

具体解决您的问题

Here's how you can fit TracePoint directly into your pre-existing module.

以下是如何将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

The examples below demonstrate that it works as specified:

以下示例表明它按指定的方式工作:

# `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

General solution

一般解决方案

This module will let you create a self.finalize callback in any class.

此模块将允许您在任何类中创建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

Now you can extend your class and define self.finalize, which will run as soon as the class definition ends:

现在,您可以扩展您的类并定义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