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)

  end
  def method_two

  end
end

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])
  end

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])
      else
        ex.send(params[:method])
      end
    end
  end

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}
}
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.

Answer 2


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

def method_two _ = nil
end

or

def method_two *
end

It works as well the other way round:

def method_one *args
end
def method_two *
end

and:

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)