Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I set a hook to run code at the end of a Ruby class definition?

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?

like image 246
LikeMaBell Avatar asked Aug 26 '15 18:08

LikeMaBell


1 Answers

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
like image 198
user513951 Avatar answered Sep 17 '22 22:09

user513951