Dynamic Method Calling in Ruby

Dynamic method calling in Ruby


is there any reason to ever use send?

call needs a method object, send doesn't:

class Foo
def method_missing(name)
"#{name} called"
end
end

Foo.new.send(:bar) #=> "bar called"
Foo.new.method(:bar).call #=> undefined method `bar' for class `Foo' (NameError)

is there any reason to ever use eval?

eval evaluates arbitrary expressions, it's not just for calling a method.


Regarding benchmarks, send seems to be faster than method + call:

require 'benchmark'

class Foo
def bar; end
end

Benchmark.bm(4) do |b|
b.report("send") { 1_000_000.times { Foo.new.send(:bar) } }
b.report("call") { 1_000_000.times { Foo.new.method(:bar).call } }
end

Result:

           user     system      total        real
send 0.210000 0.000000 0.210000 ( 0.215181)
call 0.740000 0.000000 0.740000 ( 0.739262)

How to call methods dynamically based on their name?

What you want to do is called dynamic dispatch. It’s very easy in Ruby, just use public_send:

method_name = 'foobar'
obj.public_send(method_name) if obj.respond_to? method_name

If the method is private/protected, use send instead, but prefer public_send.

This is a potential security risk if the value of method_name comes from the user. To prevent vulnerabilities, you should validate which methods can be actually called. For example:

if obj.respond_to?(method_name) && %w[foo bar].include?(method_name)
obj.send(method_name)
end

Dynamically call a method in a module?

I see the only way, if you wish to get a module by name defined as a String, and don't use the #eval:

Object.const_get( 'Notification' ).send( 'send_notification', 'hello', 'there' )
# hellothere

If you wish to use #eval that is strongly non-recommended in many cases:

eval( 'Notification' ).send( 'send_notification', 'hello', 'there' )

How to call methods dynamically based on time

You should write a rake task and add it's execution to your crontab on server. You can use whenever gem to simplify crontab scripting and auto update on each deploy (whenever-capistrano/whenever-mina). Example of your rake task:

namespace :raffle do

task :check do
Raffle.get_winners.each do |w|
Mailer.send_win_mail(w).deliver_later
end
end

end

deliver_later is background execution in queue by the queue driver you use (DelayedJob/Rescue/Backburner etc)

Dynamic method calling with arguments

What you are trying to do can be really dangerous, so I recommend you filter the params[:method] before.

allowed_methods = {
method_one: ->(ex){ex.method_one(params[:value])}
method_two: ->(ex){ex.method_two}
}
allowed_methods[params[:method]]&.call(ex)

I defined an Hash mapping the methods name to a lambda calling the method, which handles arguments and any special case you want.

I only get a lambda if params[:method] is in the allowed_methods hash as a key.

The &. syntax is the new safe navigation operator in ruby 2.3, and - for short - executes the following method if the receiver is not nil (i.e. the result of allowed_methods[params[:method]])
If you're not using ruby >= 2.3, you can use try instead, which have a similar behavior in this case :

allowed_methods[params[:method]].try(:call, ex)

If you don't filter the value of params[:method], then a user can just pass :destroy for example to delete your entry, which is certainly not what you want.

Also, by calling ex.send ..., you bypass the object's encapsulation, which you usually shouldn't. To use only the public interface, prefer using public_send.


Another point on the big security flaw of you code:

eval is a private method defined on Object (actually inherited from Kernel), so you can use it this way on any object :

object = Object.new
object.send(:eval, '1+1') #=> 2

Now, with your code, imagine the user puts eval as the value of params[:method] and an arbitrary ruby code in params[:value], he can actually do whatever he wants inside your application.

ruby - how to dynamically call instance method in a module

Disclaimer: Bad practice and it makes little to no sense:

MyModule.instance_method(:Foo) # #<UnboundMethod: MyModule#Foo>
.bind(Object) # #<Method: MyModule#Foo>
.call #=> hello

You can't call an unbound method, thus you have to first bind it to some object.

References:

  • Module#instance_method

  • UnboundMethod#bind

  • Method#call


It would be much easier if you make the method singleton method:

module MyModule
def self.Foo
puts "hello"
end
end

MyModule.Foo
#=> "hello"

Another option is to use module_function (read up on the method's visibility for the including object when using this option):

module MyModule
def Foo
puts "hello"
end
module_function :Foo
end

MyModule.Foo
#=> "hello"

And yet another one is using extend self:

module MyModule
def Foo
puts "hello"
end

extend self
end

MyModule.Foo
#=> "hello"

P.S. It is not conventional to use capital letters in method names. It is very rare case in Ruby where it can be used and your case is 100% not the one :)

How to call a Class method dynamically in ruby

Use constantize:

class_name.singularize.constantize.columns

How to invoke a method in Ruby when the method name is dynamic?

In Ruby, you can use send similar to this:

thumb = :video_thumbnail
@video_thumbnail = @video.send(thumb)

However, you should be aware of the potential security implications of this approach. If the contents of the thumb variable is in any way controllable by the user (e.g. by setting it from params), any user might have the possibility to call arbitrary methods on your video object. And given the large amount of meta-programming in Rails, you could easily have the ability to call and run arbitrary code that way. Because of that, you should make sure, that you tightly control the contents of the thumb variable.

If possible, you should user other means to call the methods, e.g. by using separate controller actions and proper routes to call them.

Calling methods dynamically (Crystal-lang)

I'm not sure that this way most simple and convenient (perhaps more experienced developers will correct me below) but i would use the Proc:

def method1
puts "i'm method1"
end

def method2
puts "i'm method2"
end

def method3
puts "i'm method3"
end

hash = {
"ctrl": -> { method1 },
"shift": -> { method2 },
"alt": -> { method3 }
}

binding = ["ctrl", "shift", "alt"].sample
hash[binding].call #=> i'm method2

See working example



Related Topics



Leave a reply



Submit