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
Xpath Expression for Regex-Like Matching
Trying to Install Ruby-Filemagic on Snow Leopard Using Brew Rather Than Ports
Sort a Collection of Objects by Number (Highest First) Then by Letter (Alphabetical)
How to Check If a Ruby Array Includes One of Several Values
Putting the Results of Pp (Or Anything Outputted to Console) into a String
Use of Caret Symbol (^) in Ruby
How to Test 'Rand()' with Rspec
Returning Data from Forked Processes
Ruby Gets/Puts Only for Strings
How to Have Rspec Test for My Default Scope
How to Find Each Instance of a Class in Ruby
Creating Permutations from a Multi-Dimensional Array in Ruby
Rails - Pass Id Parameter on a Link_To
Why Is Enumerable#Each_With_Object Deprecated
Get Sidekiq to Execute a Job Immediately