Ruby Dynamic Arguments in Dynamically Created Methods

How to dynamically create methods with parameter in ruby?

You should use public_send to call methods based on their name:

  ['id', 'message', 'votes_count'].each do |method|
define_method "#{method}" do |parameter|
parameter.public_send(method)
end
end

define_method: How to dynamically create methods with arguments

If I understand your question correctly, you want something like this:

class Product
class << self
[:name, :brand].each do |attribute|
define_method :"find_by_#{attribute}" do |value|
all.find {|prod| prod.public_send(attribute) == value }
end
end
end
end

(I'm assuming that the all method returns an Enumerable.)

The above is more-or-less equivalent to defining two class methods like this:

class Product
def self.find_by_name(value)
all.find {|prod| prod.name == value }
end

def self.find_by_brand(value)
all.find {|prod| prod.brand == value }
end
end

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 / Passing arguments to a dynamically created class

It is unclear why you want to create the class in the first place. If there are good reasons for it, my answer is kind of invalid.

You can have the desired behaviour using "standard" OOP techniques and working with instances

class Fetcher
def initialize(resource_name)
@resource_name = resource_name
end

def all
"http://some.remote.resource/#{@resource_name}/all"
end
end

xyz_fetcher = Fetcher.new('xyz')
xyz_fetcher.all

Otherwise, your code is more or less what you would/should do, I guess. Just, I would let the Fetcher class act as a singleton (not use an instance of Fetcher):

class Fetcher < ApplicationService
# make a singleton by privatizing initialize (read this up somewhere else)

def self.build(resource_name)
Class.new(BaseClass) do
@@resource_name = resource_name

class << self
def all
"http://some.remote.resource/#{@@resource_name}/all"
end
end
end
end
end

Then

Xyz = Fetcher.build('xyz')
Xyz.all

Now, you have the stuff with ApplicationService which more or less achieves that (and passes a block), so probably we readers miss some parts of the bigger picture ... please clarify if that is the case.

Besides singletonization, you could also work with modules instead (thanks @max for the comment).

How to dynamically create instance methods at runtime?

I'm particularly fond of using method_missing, especially when the code you want to use is very similar across the various method calls. Here's an example from this site - whenever somebody calls x.boo and boo doesn't exist, method_missing is called with boo, the arguments to boo, and (optionally) a block:

class ActiveRecord::Base
def method_missing(meth, *args, &block)
if meth.to_s =~ /^find_by_(.+)$/
run_find_by_method($1, *args, &block)
else
super # You *must* call super if you don't handle the
# method, otherwise you'll mess up Ruby's method
# lookup.
end
end

def run_find_by_method(attrs, *args, &block)
# Make an array of attribute names
attrs = attrs.split('_and_')

# #transpose will zip the two arrays together like so:
# [[:a, :b, :c], [1, 2, 3]].transpose
# # => [[:a, 1], [:b, 2], [:c, 3]]
attrs_with_args = [attrs, args].transpose

# Hash[] will take the passed associative array and turn it
# into a hash like so:
# Hash[[[:a, 2], [:b, 4]]] # => { :a => 2, :b => 4 }
conditions = Hash[attrs_with_args]

# #where and #all are new AREL goodness that will find all
# records matching our conditions
where(conditions).all
end
end

define_method also looks like it would work for you, but I have less experience with it than method_missing. Here's the example from the same link:

%w(user email food).each do |meth|
define_method(meth) { @data[meth.to_sym] }
end

Ruby Default Arguments: Static or Dynamic?

It is dynamic because Ruby is interpreted, not compiled language.

✎ require 'date'
✎ def test param = DateTime.now
puts param
end
✎ 3.times { test; sleep(1) }
2018-12-14T18:10:08+01:00
2018-12-14T18:10:09+01:00
2018-12-14T18:10:10+01:00


Related Topics



Leave a reply



Submit