1

I'm trying to define a macro that wraps another method, then pass the instance to the macro. Here's a contrived example...

module Wrapper
  def wrap(method_name, options: {})
    class_eval do
      new_method = "_wrap_#{method_name}"
      alias_method new_method, method_name

      define_method method_name do |*args, &block|
        # binding.pry
        puts "Options... #{options}"
        
        send new_method, *args, &block
      end
    end
  end
end

class Thing
  extend Wrapper

  class << self
    extend Wrapper

    def bar
      puts 'Bar!'
    end
    wrap :bar, options: -> { bar_options }

    private

    def bar_options
      { bar: 2 }
    end
  end

  def foo
    puts 'Foo!'
  end
  wrap :foo, options: -> { foo_options }

  private

  def foo_options
    { foo: 1 }
  end
end

So, I know I have access to the instance from within define_method, but I don't think I should have to create / rename / alias methods within my classes to conform to a module - I'd like to pass the config in, so to speak. I feel like instance_exec/eval is my friend, here, but I can't seem to get the incantation correct. I tried with passing a block to the code as well, but yield behaved the same as the proc. Maybe binding, but I can't wrap my head around that at all, for some reason.

This is from within the define_method call...

> Thing.new.foo
=> Options... #<Proc:0x00007fcb0782cfd8@(irb):24>
Foo!

> self.class
=> Thing

> self
=> #<Thing:0x00007fdfb78b88e8>

> options
=> #<Proc:0x00007fcb0782cfd8@(irb):24>

> options.call
# NameError: undefined local variable or method `foo_options` for Thing:Class

> foo_options
=> {:foo=>1}

> self.class.instance_eval &options
# NameError: undefined local variable or method `foo_options` for Thing:Class

> self.class.instance_exec &options
# NameError: undefined local variable or method `foo_options` for Thing:Class

I understand how a proc captures scope to use for later, so I can see how the use of a proc/lambda is incorrect here. When the class loads, the wrap method "wraps" the method and captures foo_options at the class level to be called for later - which doesn't exist at the class level. Calling options: foo_options does the same thing, just blows up on load.

Any bit helps... Thanks!

Dudo
  • 4,002
  • 8
  • 32
  • 57
  • 1
    This example/use case doesn't make a ton of sense to me. You are calling `instance_eval` and `instance_exec` as class methods, but they're meant to be called on an instance (e.g., [instance_eval docu](https://ruby-doc.org/core-2.5.3/BasicObject.html#instance_eval-method)). You're also overriding the definition of params that would automatically exist in a controller. Might want to check out the docu on [Concerns](https://api.rubyonrails.org/v6.1.3.2/classes/ActiveSupport/Concern.html) and update this with a use case for a model. – Allison Jun 29 '21 at 23:52
  • Clarified. And yes, that's exactly the problem! – Dudo Jun 30 '21 at 04:26

1 Answers1

2

I was so close... While within define_method, I have access to the instance, and can call instance_exec on self, instead of self.class! Also, there is a newer, preferred approach to wrapping methods since ruby 2.0.

module Wrapper
  def wrap(method_name, options: {})
    proxy = Module.new
    proxy.define_method(method_name) do |*args, &block|
      options = instance_exec(&options) if options.is_a?(Proc)
      target = is_a?(Module) ? "#{self}." : "#{self.class}#"
      puts "#{target}#{method_name} is about to be called. `wrap` options #{options}"
      super *args, &block
    end
    self.prepend proxy
  end
end

Output:

> Thing.new.foo
Thing#foo is about to be called. `wrap` options {:foo=>1}
Foo!
=> nil

> Thing.bar
Thing.bar is about to be called. `wrap` options {:bar=>2}
Bar!
=> nil

This is much cleaner than the "old way" that Sergio mentions in the comments. This Question helped me out!

Another great benefit of this approach is that you can define the macro at the top of the file (where they belong, arguably).

Dudo
  • 4,002
  • 8
  • 32
  • 57