Reputation: 1243
I'm writing a library which wraps methods with spies (basically transparently allows you to add callbacks to methods). I currently have it working fine for dealing with bound methods
class FakeClass
def self.hello_world
'hello world'
end
def age
25
end
end
module Spy
def self.on(receiver, msg)
original = receiver.method(msg)
wrapped = Proc.new {|*args| original.call(*args)}
original.owner.instance_eval do
define_method msg, wrapped
end
end
end
# works
Spy.on(FakeClass, :hello_world)
# also works
Spy.on(FakeClass.new, :age)
# what I want to do
Spy.on_any_instance(FakeClass, :age)
So far I've noticed a couple things:
FakeClass.methods.include? :age
# => false
FakeClass.instance_method :age
# => #<UnboundMethod FakeClass#age>
Which leads me to my question:
Since instance_method
returns an UnboundMethod, how can I define a replacement method to override that (e.g. I'd like a define_instance_method
)
Also how can I wrap the UnboundMethod so that it is bound to the instance when the instance is created?
Can I do any of this without messing with the initialize
method?
EDIT:
With Ajedi32's advice I was able to change my method to something like this (the API is slightly more complex in my library, but you'll get the gist):
def wrap_original
context = self
@original.owner.instance_eval do
define_method context.msg do |*args|
if context.original.respond_to? :bind
result = context.original.bind(self).call(*args)
else
result = context.original.call(*args)
end
context.after_call(result, *args)
result
end
end
end
def unwrap_original
context = self
@original.owner.instance_eval do
define_method context.msg, context.original
end
end
EDIT:
Link to my gem in the wild. This is the main entry point (API) which interacts with Core and then instantiates a new Instance https://github.com/jbodah/spy_rb/blob/a40ed2a67b088bfb7d40d12c5b6ffc882a5097c8/lib/spy/api.rb
Upvotes: 2
Views: 365
Reputation: 48368
define_method
actually does define an instance method by default. The only reason it's defining a class method instead in your case is because you're calling it inside of instance_eval
. (For details on the exact behavior of instance_eval
, you may want to read this article). A simpler way to do what you want in that first case is to define a singleton method on the receiver object using define_singleton_method
:
module Spy
def self.on(receiver, msg)
original = receiver.method(msg)
# Use `define_singleton_method` to define a method directly on the receiver
receiver.define_singleton_method(msg) do |*args, &block|
puts "Calling #{msg} with args #{args}" # To verify it's working
original.call(*args, &block) # Call with &block to preserve block args
end
end
end
Spy.on(FakeClass, :hello_world)
FakeClass.hello_world # Prints: Calling hello_world with args []
f = FakeClass.new
Spy.on(f, :age)
f.age # Prints: Calling age with args []
As for your on_any_instance
method, you can use the regular define_method
method for that. You will have to call it using send
though, since define_method
is private. And since the original method you're trying to call is an UnboundMethod
, you'll have to bind it to the instance you want to call it on before calling it:
module Spy
def self.on_any_instance(receiver, msg)
original = receiver.instance_method(msg) # Get `UnboundMethod`
# We must call `define_method` with `send`, since it's private
receiver.send(:define_method, msg) do |*args, &block|
puts "Calling #{msg} with args #{args}"
# Method gets bound to `self`, the current instance
original.bind(self).call(*args, &block)
end
end
end
Spy.on_any_instance(FakeClass, :age)
FakeClass.new.age # Prints: Calling age with args []
Upvotes: 1
Reputation: 1243
So it turns out I can do this:
original = FakeClass.instance_method(:age)
FakeClass.instance_eval {:define_method, :age, original}
But I need to see how I can wrap the method now
Upvotes: 0