Asked  7 Months ago    Answers:  5   Viewed   30 times

How can I call a method dynamically when its name is contained in a string variable? For example:

class MyClass
  def foo; end
  def bar; end
end

obj = MyClass.new
str = get_data_from_user  # e.g. `gets`, `params`, DB access, etc.
str  #=> "foo"
# somehow call `foo` on `obj` using the value in `str`.

How can I do this? Is doing so a security risk?

 Answers

80

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
Tuesday, June 1, 2021
 
medhybrid
answered 7 Months ago
57

The name of a class is simply the name of the first constant that refers to it.

I.e. if I do myclass = Class.new and then MyClass = myclass, the name of the class will become MyClass. However I can't do MyClass = if I don't know the name of the class until runtime.

So instead you can use Module#const_set, which dynamically sets the value of a const. Example:

dynamic_name = "ClassName"
Object.const_set(dynamic_name, Class.new { def method1() 42 end })
ClassName.new.method1 #=> 42
Saturday, July 10, 2021
 
Shobit
answered 5 Months ago
20

No, this is not possible not directly possible. The calling convention is:

<xsl:call-template name="QName" />

Where a QName is defined as:

QName ::= PrefixedName | UnprefixedName

PrefixedName   ::= Prefix ':' LocalPart
UnprefixedName ::= LocalPart

Prefix         ::= NCName
LocalPart      ::= NCName

Basically this boils down to "characters only, no expressions". As the other answers highlight, there are in fact ways to do something equivalent, but the straightforward approach/naïve approach will not work.

Monday, August 2, 2021
 
ojrac
answered 4 Months ago
59

Use a dictionary for the start row and another for the end row. It is then straightforward to determine the range of duplicate rows for each name and copy them to a new sheet.

Sub CopyDuplicates()

    Dim wb As Workbook, ws As Worksheet
    Dim irow As Long, iLastRow As Long

    Dim dictFirstRow As Object, dictLastRow As Object, sKey As String
    Set dictFirstRow = CreateObject("Scripting.Dictionary") ' first row for name
    Set dictLastRow = CreateObject("Scripting.Dictionary") ' last row for name

    Set wb = ThisWorkbook
    Set ws = wb.Sheets("Sheet1")
    iLastRow = ws.Range("A" & Rows.Count).End(xlUp).Row

    ' build dictionaries
    For irow = 1 To iLastRow
        sKey = ws.Cells(irow, 1)
        If dictFirstRow.exists(sKey) Then
           dictLastRow(sKey) = irow
        Else
           dictFirstRow.Add sKey, irow
           dictLastRow.Add sKey, irow
        End If
    Next

    ' copy range of duplicates
    Dim k, iFirstRow As Long, rng As Range, wsNew As Worksheet
    For Each k In dictFirstRow.keys

        iFirstRow = dictFirstRow(k)
        iLastRow = dictLastRow(k)

        ' only copy duplicates
        If iLastRow > iFirstRow Then
            Set wsNew = wb.Worksheets.Add(after:=wb.Sheets(wb.Sheets.Count))
            wsNew.Name = k

            Set rng = ws.Rows(iFirstRow & ":" & iLastRow).EntireRow
            rng.Copy wsNew.Range("A1")
            Debug.Print k, iFirstRow, iLastRow, rng.Address
        End If
    Next

    MsgBox "Done"

End Sub

Friday, August 20, 2021
 
oroshnivskyy
answered 4 Months ago
99

Here's a fairly full example of using define_method in a module that you use to extend your class:

module VerboseSetter
  def make_verbose_setter(*names)
    names.each do |name|
      define_method("#{name}=") do |val|
        puts "@#{name} was set to #{val}"
        instance_variable_set("@#{name}", val)
      end
    end
  end
end

class Foo
  extend VerboseSetter

  make_verbose_setter :bar, :quux
end

f = Foo.new
f.bar = 5
f.quux = 10

Output:

@bar was set to 5
@quux was set to 10

You were close, but you don't want to include the argument of the method inside the arguments of your call to define_method. The arguments go in the block you pass to define_method.

Sunday, September 12, 2021
 
Migwell
answered 3 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share