TAGS :Viewed: 2 - Published at: a few seconds ago

[ Dynamic method calling with arguments ]

For example I have class with two methods:

class Example < ActiveRecord::Base
  def method_one(value)

  def method_two


and method in controller where I call them:

  def example
    ex = Example.find(params[:id])
    ex.send(params[:method], params[:value]) if ex.respond_to?(params[:method])

But the problem comes when I try to call method_two

ArgumentError (wrong number of arguments (1 for 0))

It happens because params[:value] returns nil. The easiest solution is:

  def example
    ex = Example.find(params[:id])
    if ex.respond_to?(params[:method])
      if params[:value].present?
        ex.send(params[:method], params[:value])

I wonder if there is any better workaround to do not pass argument if it's null.

Answer 1

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}

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.

Answer 2

If you understand what you are doing, there are easier workarounds:

def method_two _ = nil


def method_two *

It works as well the other way round:

def method_one *args
def method_two *


ex.public_send(params[:method], *[params[:value]]) \
  if ex.respond_to?(params[:method])

Sidenote: prefer public_send over send unless you are explicitly calling private method.

Using splatted params without modifying the methods signatures:

ex.public_send(*[params[:method], params[:value]].compact)