How can I set a hook to run code at the end of a R

2020-03-10 05:56发布

问题:

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).

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.

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?

回答1:

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


General solution

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

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:

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

Specific solution to OP's problem

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

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