MCP
MCP

Reputation: 4536

Ruby: trying to object.send a module function with parameters

I have a module that has a function defined within:

module Settings
def find_by_name(name, start)
  puts "find_by_name: Found me!"
end

In miniTest I'm trying to test "sending" to this function in a manner that simulates it dynamically being defined/called at runtime. Here's what I'm trying to do in MiniTest.

require 'settings'

class TestFileUtilities < MiniTest::Unit::TestCase

  def test_dynamic_call_to_Settings
    foreign_function = 'Settings::find_by_name'
    send(foreign_function, 'name.txt', 'Users/MCP/Desktop/Projects/TestDir')
  end

What I am expecting is that send() will call the "foreign_function" method, converting the string to a symbol, and then will add the two parameters to the call. All I want is no errors, and a puts to my console so that I know it's working...then I'll start defining "assertions". Right now though I get:

NoMethodError: undefined method `Settings::find_by_name' for #<TestFileUtilities:0x007fdf59840310>

This is the first time I'm playing around with Ruby's ability to dynamically call methods and I was able to get it to work when I wasn't using the scope resolution operator and was include Settings inside of TestFileUtilities but I'd rather use scope so that user defined functions don't conflict with anything inside of my class (not just conflict with MiniTest, but the actual FileUtilities class). Any thoughts/help would be greatly apprecaited! For anyone who's stumbled here looking for info, this is what got me started.

Upvotes: 4

Views: 4539

Answers (3)

MCP
MCP

Reputation: 4536

I this specific instance I had trouble trying to include functions that were in another module, not within any class. I was requireing the module I was trying to send to, but when I would do ModuleName::send(:function_name) I was getting an error stating "no method exists by that name" (or something close to that error). What fixed this problem was to define the functions in the module that I was trying to "send" to as "self.function_name".

module Something
  def self.my_function
    puts "Silliness"
  end
end

Then in the sending file/module/class:

require '../lib/something.rb'

    class MyClass
      def this_crazy_thing
        funct = 'my_function'
        Something::send(funct)
      end
    end

So now the above works...by simply defining the function in module Something as self. Which as per my understanding has to do with how objects are instantiated in Ruby. What got me here is that, not using the dynamic send functionality...if I knew the name of the function in the module that I wanted to call, I could call it with:

ModuleName::function_name(params)

so I expected something similar to work with send. My guess is that since I'm trying to send to an object, the function needs to be tied to the object I'm sending to, namely the module, so that it can be called as an instance of that object. Clearly I'm still wrapping my head around the fact that everything is an object in Ruby. Hopefully this at least gets someone on the right path. If someone can explain why exactly this works:

module ModuleSomething
  def a_function
   puts "hi"
  end
end

require '../lib/modulesomething'
class MyClass
  def this_thing
    ModuleSomething::a_function
  end
end

but not:

require 'ModuleSomething'
class MyClass
  def this_string
    funct = 'a_function'
    ModuleSomething::send(funct)
  end
end

That might help clear it up a little more as to why my solution worked. When I find out more I will add to this. Thanks guys!

Upvotes: 1

dbenhur
dbenhur

Reputation: 20408

There are several issues with your example.

  1. You haven't done anything apparent to make find_by_name a class method of Blueprints, so even skipping the send part you should get NoMethodError if you tried to call Blueprints::find_by_name('name.txt', 'Users/MCP/Desktop/Projects/TestDir') To remedy this you would have to say Blueprints.extend(Settings) which would mixin the Settings module to the Blueprints class with its instance methods becoming class methods of Blueprints.

  2. A method name is a symbol, Object#send expects a symbol as its first arg, if it's a string it converts it to a symbol. You're passing the string 'Blueprints::find_by_name' which is a very odd name for a method and not one you've defined. I think what you intended is to call the method find_by_name on the class or module Blueprints. To do this, you can say foreign_function = 'find_by_name'; Blueprints.send(foreign_function, 'name.txt', 'Users/MCP/Desktop/Projects/TestDir') If you need to resolve the class/module name dynamically too, you want Module#const_get.

So this is what I think you mean to say:

module Settings
  def find_by_name(name, start)
    puts "find_by_name: Found me!"
  end
end

class Blueprints
  extend Settings
end

require 'settings'

class TestFileUtilities < MiniTest::Unit::TestCase

  def test_dynamic_call_to_actual_Blueprints
    klass = 'Blueprints'
    func = 'find_by_name'
    Object.const_get(klass).send(func, 'name.txt', 'Users/MCP/Desktop/Projects/TestDir')
  end

end

Upvotes: 3

psanford
psanford

Reputation: 5670

Its not clear from your question how the class Blueprints relates to Settings. I'll assume that either Blueprints is a subclass of Settings or you meant to use Settings instead.

What I am expecting is that send() will call the "foreign_function" method, converting the string to a symbol, and then will add the two parameters to the call.

You need to call send() on the object (or class) that you want to send() to:

foreign_function = 'find_by_name'
Blueprints.send(foreign_function, 'name.txt', 'Users/MCP/Desktop/Projects/TestDir')

Upvotes: 1

Related Questions