Search code examples
rubymetaprogrammingmonkeypatching

calling method that was undefined in ruby


using external API that has defined as special class where almost all standard methods are undefined to use for building xml. Where #method_missing is responsible to generate elements based on the missing method name called over the object.

Basically in class body there is something to the effect of:

undef_method :send

Now I want to programatically call method by name. I guess I can use eval "obj.#{something}" but I really don't like eval. I was thinking there must be some dark-side technique to revert the undefining of method #send so I can alias it to #__send__ and undefine it again. That way I can happily call methods by name without eval. e.g. obj.__send__(:something, params).

So my question is how do I use monkey patching to revert the #send method. I can't find anything to that effect. Even can't find anybody asking about it either.

UPDATE: My original problem was non-problem because there is a method #__send__ anyway, I knew only about #send. The other part of the question was how to restore an undefined method. With help of @philomory asnwer here's what I've got:

[46] pry(#<CucuShift::DefaultWorld>)> class A
[46] pry(#<CucuShift::DefaultWorld>)*   def gah
[46] pry(#<CucuShift::DefaultWorld>)*     puts "gah"
[46] pry(#<CucuShift::DefaultWorld>)*   end  
[46] pry(#<CucuShift::DefaultWorld>)* end  
=> :gah
[49] pry(#<CucuShift::DefaultWorld>)> class C < A
[49] pry(#<CucuShift::DefaultWorld>)*   undef_method :gah
[49] pry(#<CucuShift::DefaultWorld>)* end  
=> C
[50] pry(#<CucuShift::DefaultWorld>)> C.new.gah
NoMethodError: undefined method `gah' for #<C:0x000000070d7918>
[57] pry(#<CucuShift::DefaultWorld>)> class C
[57] pry(#<CucuShift::DefaultWorld>)*   define_method :fff , A.instance_method(:gah)
[57] pry(#<CucuShift::DefaultWorld>)* end  
=> :fff
[58] pry(#<CucuShift::DefaultWorld>)> C.new.fff
gah

Solution

  • The dark magic you are looking for is the UnboundMethod class. Use it like this (assuming obj is your builder API object):

    send_method = BasicObject.instance_method(:__send__)
    bound_method = send_method.bind(obj)
    bound_method.call(:method_you_are_calling_dynamically,*arguments)
    

    Of course, depending on what you're trying to accomplish, you could just get the specific method you want and bind it directly, like

    id_method = BasicObject.intance_method(:__id__)
    bound_method = id_method.bind(obj)
    bound_method.call
    

    Or, if the builder class is using method_missing and you just want to dynamically dispatch to that, you don't need send or anything like it at all, just call obj.method_missing(:my_dynamic_method_name)


    UPDATE:

    It's worth noting that you should not need to alias send to __send__ as __send__ is already provided as part of BasicObject and it is very unusual to un-define it. Though I suppose if you are using an earlier version of Ruby without BasicObject and __send__ you do it this way:

    def obj.__send__(*args,&blk)
      Object.instance_method(:send).bind(self).call(*args,&blk)
    end