Sepp
Sepp

Reputation: 347

Get all instance methods on top of the class definition

I'm trying to wrap all instance methods of TestClass to perform code before and after an instance method is called. So far, this code is working:

module Wrapper
  def wrap(*methods)
    prependable_module = Module.new do
      methods.each do |m|
        define_method(m) do |*args, &block|
          p 1
          super(*args, &block)
          p 3
        end
      end
    end

    prepend prependable_module
  end
end

class TestClass
  extend Wrapper
  wrap :instance_method1

  def instance_method1
    p 2
  end
end

TestClass.new.instance_method1 # => 1, 2, 3

I can call wrap with all method names as arguments. If I try to wrap all methods without listing them individually, I need to call it using instance_methods(false) at the bottom of the class definition.

class TestClass
  extend Wrapper
    
  def instance_method1
    p 2
  end
  
  wrap(*instance_methods(false))
end

In Rails, all callback methods like before_action or after_create are usually called on top of the class definition. My goal is to call wrap on top of the class definition as well (without listing all methods individually). In this case, I can't call instance_methods(false) on top of the class definition, because at this point no method has been defined.

Thanks for your help!

Update

Thanks to Kimmo Lehto's approach I can wrap every instance method using the method_added hook. I don't want to prepend a new module for every defined method, so I add all overridden methods to the same module.

module Wrapper
  def method_added(method_name)
    tmp_module = find_or_initialize_module
    return if tmp_module.instance_methods(false).include?(method_name)

    tmp_module.define_method(method_name) do |*args, &block|
      p 1
      super(*args, &block)
      p 3
    end
  end

  def find_or_initialize_module
    module_name  = "#{name}Wrapper"
    module_idx   = ancestors.map(&:to_s).index(module_name)

    unless module_idx
      prepend Object.const_set(module_name, Module.new)
      return find_or_initialize_module
    end

    ancestors[module_idx]
  end
end

class TestClass
  extend Wrapper

  def instance_method1
    p 2
  end
end

tc = TestClass.new
tc.instance_method1 # => 1, 2, 3

Upvotes: 2

Views: 168

Answers (1)

Kimmo Lehto
Kimmo Lehto

Reputation: 6041

You could use the Module#method_added hook to automatically wrap any methods that are added.

You will need some magic to not get a stack overflow from an infinite loop.

Another option is to use TracePoint to trigger the wrapping once the class has been defined. You can use the Module#extended to set up the tracepoint. Something like:

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

  def finalize
    wrap(*instance_methods(false))
  end
end

Classes are usually not exactly "closed" unless you explicitly .freeze them so it's a bit of a hacky solution and will not trigger if methods are added afterwards. method_added is probably your best bet.

Upvotes: 1

Related Questions