How can i trap invocation of methods in ruby - ruby

I want to trap invocation of a method and then display output.
class A
end
If i run new A.see it should trap and print 'unkown method'.
I am new to the language

When you send a message to an object, the object executes the first method it finds on its method lookup path with the same name as the message. If it fails to find any such method, it raises a NoMethodError exception, unless you have provided the object with a method called method_missing. The method_missing method is passed the symbol of the non-existent method, an array of the arguments that were passed in the original call and any block passed to the original method.
class A
def method_missing(m, *args, &block)
puts "There's no method called #{m} here -- please try again."
super
end
end

This already triggers a NoMethodError that by default halts your program.
NoMethodError: undefined method `see' for A:Class

Related

NoMethodError: undefined method `alias_method'

I am using some metaprogramming (using ruby 2.3.1) so that i can call a method before calling the actual method i want to call - like a before_filter/before_action.
The pseudocode below explains what i am trying to achieve
module Api
def call_this_method_everytime
if A
go ahead and call the actual method being called
else
stop here do not call he method it was supposed to call
end
end
def when_i_call_this_method
end
def or_this_method
end
end
With the help of a SO member i was able to theoretically understand what i want to do using metaprogramming - which i arrived at the code below.
module Api
def heartbeat
...
end
def interceptor(name)
original_method = "original #{name}"
alias_method original_method, name
define_method(name) do |*args|
heartbeat
result = send original_method, *args
puts "The method #{name} called!"
result
end
end
end
Instead of calling the method i want - i call interceptor method with the name of the actual function i want to call as an argument. I would then first call heartbeat function and if the check is OK then i proceed with actually calling the actual function.
However having limited knowledge of metaprogramming i am getting this error
NoMethodError: undefined method 'alias_method'
Searching around did not help. Any help appreciated.
A simpler way of doing the same thing:
def interceptor(name, *args)
if 1 == 1
send name, args
end
end
interceptor(:puts, "oi")

Defining a method in Rubymine returns "undefined method" error

I am executing a class with only this code in rubymine:
def saythis(x)
puts x
end
saythis('words')
It returns an error: undefined method `saythis', rather than printing the string 'words'. What am I missing here? Replicating this code in irb prints the string 'words'.
I assume you wrote a class like the one below and did not write that code into a irb console. The problem is that you define an instance method, but try to call the method from the class level.
class Foo
def say_this(x) # <= defines an instance method
puts x
end
say_this('words') # <= calls a class method
end
There a two ways to "fix" this:
Define a class method instead of an instance method: def self.say_this(x)
Call the instance method instead of the class method call: new.say_this(x)

Metaprogramming: method_missing and __send__

I've found an oddness in Ruby. Using method_missing/respond_to_missing? to dynamically handle method calls initiated via __send__ fails when Kernel defines the same method:
class Testerize
def method_missing(method, *args, &block)
if method == :system
puts 'yay'
else
super
end
end
def respond_to_missing?(method, internal)
return true if method == :system
end
end
t = Testerize.new
puts t.respond_to?(:system)
# Prints true
t.system
# Prints 'yay'
t.__send__(:system)
# Exception: wrong number of arguments (ArgumentError)
Kernel.system is somehow getting in the mix. Does anyone know what's going on here? I would have expected the :system "message" to get posted to the Testerize instance, hit method_missing, and voila. Why isn't my method_missing getting called when using __send__ when it is with direct invocation?
I'm using Ruby 1.9.3, if that is relevant.
with '__send__' or 'send' we can even call private methods of the object.
In your script do:
t.private_methods.grep /system/
You will see system method, while with
t.methods.grep /system/
you will see an empty array.
__send__ tries to call the private method inherited from Kernel in the inheritance chain, hence instead of using __send__ use Ruby's public_send method.
If you look up the ancestor chain of your class (Testerize.ancestors), you will find:
[Testerize, Object, Kernel, BasicObject]
Since new classes inherit from Object by default, and Object inherits from Kernel, all of the instance methods available to Kernel are available in your instances.

Ruby any way to catch messages before method_missing?

I understand that method_missing is something of a last resort when Ruby is processing messages. My understanding is that it goes up the Object hierarchy looking for a declared method matching the symbol, then back down looking for the lowest declared method_missing. This is much slower than a standard method call.
Is it possible to intercept sent messages before this point? I tried overriding send, and this works when the call to send is explicit, but not when it is implicit.
Not that I know of.
The most performant bet is usually to use method_missing to dynamically add the method being to a called to the class so that the overhead is only ever incurred once. From then on it calls the method like any other method.
Such as:
class Foo
def method_missing(name, str)
# log something out when we call method_missing so we know it only happens once
puts "Defining method named: #{name}"
# Define the new instance method
self.class.class_eval <<-CODE
def #{name}(arg1)
puts 'you passed in: ' + arg1.to_s
end
CODE
# Run the instance method we just created to return the value on this first run
send name, str
end
end
# See if it works
f = Foo.new
f.echo_string 'wtf'
f.echo_string 'hello'
f.echo_string 'yay!'
Which spits out this when run:
Defining method named: echo_string
you passed in: wtf
you passed in: hello
you passed in: yay!

Ruby Methods: how to return an usage string when insufficient arguments are given

After I have created a serious bunch of classes (with initialize methods), I am loading these into IRb to test each of them. I do so by creating simple instances and calling their methods to learn their behavior. However sometimes I don't remember exactly what order I was supposed to give the arguments when I call the .new method on the class. It requires me to look back at the code. However, I think it should be easy enough to return a usage message, instead of seeing:
ArgumentError: wrong number of arguments (0 for 9)
So I prefer to return a string with the human readable arguments, by example using "puts" or just a return of a string. Now I have seen the rescue keyword inside begin-end code, but I wonder how I could catch the ArgumentError when the initialize method is called.
Thank you for your answers, feedback and comments!
It is possible to hook into object creation by overriding the Class#new method e.g.
class Class
# alias the original 'new' method before overriding it
alias_method :old_new, :new
def new(*args)
return old_new(*args)
rescue ArgumentError => ae
if respond_to?(:usage)
raise ArgumentError.new(usage)
else
raise ae
end
end
end
This overriden method calls the normal new method but catches ArgumentError and if the class of the object being created provides a usage method then it will raise an ArgumentError with the usage message otherwise it will reraise the original ArgumentError.
Here is an example of it in action. Define a Person class:
class Person
def initialize(name, age)
end
def self.usage
"Person.new should be called with 2 arguments: name and age"
end
end
and then try and instantiate it without the required arguments:
irb(main):019:0> p = Person.new
ArgumentError: Person.new should be called with 2 arguments: name and age
from (irb):8:in `new'
from (irb):22
Note: this isn't perfect. The main problem being that it is possible that the ArgumentError we catch has been caused by something other than an incorrect number of arguments being passed to initialize which would lead to a misleading message. However it should do what you want in most cases.

Resources