Calling private methods by symbol name in Ruby - ruby

I have the symbol name of a method that I'd like to call with some arguments. What I'm really trying to do boils down to this code snippet:
method.to_proc.call(method)
In this case, method is the symbol name of a method on the object. In my case, I'm trying to call a method that happens to be private on the object.
This is the error output that I get:
>$ ruby symbol_methods.rb
symbol_methods.rb:33:in `call': private method `test_value_1' called for "value":String (NoMethodError)
from symbol_methods.rb:33:in `block (2 levels) in <main>'
from symbol_methods.rb:30:in `each'
from symbol_methods.rb:30:in `block in <main>'
from symbol_methods.rb:29:in `each'
from symbol_methods.rb:29:in `<main>'
Here's a self-contained example that demonstrates this behavior:
data = [
["value", true],
["any value here", true],
["Value", true],
]
def matches_value(string)
string == "value"
end
def contains_value(string)
string.gsub(/.*?value.*?/, "\\1")
end
def matches_value_ignore_case(string)
string.downcase == "value"
end
#tests
[:matches_value, :contains_value, :matches_value_ignore_case].each_with_index do |method, index|
test = data[index]
value = test[0]
expected_result = test[1]
result = method.to_proc.call(value) # <<== HERE
puts "#{method}: #{result == expected_result ? 'Pass' : 'Fail'}: '#{value}'"
end
The important bit is in the block marked #tests. The data variable is a set of inputs and expected results. The test_value_* methods are private methods that are the tests to run.
I've tried public_send(method, value) and method.to_proc.call(value), but both result in the private method error.
What would be the right way to call a private method named as a symbol in this case? I'm looking for both an explanation and a syntactically correct answer.

use send instead.
puts "#{method}: #{send(method, value) == expected_result ? 'Pass' : 'Fail'}: '#{value}'"

After a fair amount of searching, I found an alternative answer than Object#send, that has an unanticipated feature benefit. The solution is to use the Object#method to return a Method object for the symbol name.
A Method object is a Proc-like callable object, so it implements the #call interface, which fits the bill nicely. Object has many such useful helpers defined in its interface.
In context of the original question, this is how it works:
#tests
[:test_value_1, :test_value_2, :test_value_3].each do |method|
data.each do |test|
value = test[0]
expected_result = test[1]
puts "#{method}: #{self.method(method).call(value) == expected_result ? 'Pass' : 'Fail'}: '#{value}'"
end
end
The important bits are:
self.method(method).call(value)
This will convert the symbol name to a Method object, and then invoke the method with value supplied as the parameter. This works roughly equivalently to the send method solution, in functional terms. However, there are some differences to note.
send is going to be somewhat more efficient, as there's no overhead in the conversion to a Method. Method#call and send use different internal calling mechanisms, and it appears that send has less call overhead, as well.
The unanticipated feature of using Object#method is that the Method object is easily converted to a Proc object (using Method#to_proc). As such, it can be stored and passed as a first-class object. This means that it can be supplied in place of a block or provided as a callback, making it useful for implementing flexible dispatch solutions.

Related

How does the syntax MODULE::METHODNAME('string') work

I recently had cause to use the nokogiri gem to parse html but while i going through their documentation, i came across this ruby syntax that i hadn't seen before
html_doc = Nokogiri::HTML('<html><body><h1>Mr. Belvedere Fan Club</h1></body></html>')
xml_doc = Nokogiri::XML('<root><aliens><alien><name>Alf</name></alien></aliens></root>')
The part of interest for me is Nokogiri::HTML('...'). This looks very much like a method invocation but i know ruby method names cannot be in capital letters. So i looked through code files nokogiri gem and i came across the following definition
module Nokogiri
class << self
###
# Parse HTML. Convenience method for Nokogiri::HTML::Document.parse
def HTML thing, url = nil, encoding = nil, options = XML::ParseOptions::DEFAULT_HTML, &block
Nokogiri::HTML::Document.parse(thing, url, encoding, options, &block)
end
end
# more code
end
I tried reproducing the same code
module How
class << self
def DOESTHISWORK
puts "In How Method"
end
end
end
How::DOESTHISWORK
But it keeps coming back with the error "uninitialized constant How::DOESTHISWORK (NameError)". I know it has to do with the method name starting in capitals but i just haven't been able to figure out how it works in nokogiri.
The difference is in the Nokogiri example the method is being called with parentheses and a parameter value which identifies it as a method call. Your DOESTHISWORK method takes no parameters but can be called with empty parentheses e.g.
irb(main):028:0> How::DOESTHISWORK()
In How Method
=> nil
If you add a parameter to your method that can also serve to identify it as a method like so:
irb(main):036:0> How::DOESTHISWORK 'some param'
Starting method names with a lowercase letter is good practice but isn't enforced. Something that begins with a capital letter is assumed to be a constant and will be looked up as such, this is why the parentheses or parameter is needed to indicate a method is being referred to. Another example:
irb(main):051:0> def Example
irb(main):052:1> puts "An example!"
irb(main):053:1> end
=> nil
irb(main):054:0> Example
NameError: uninitialized constant Example
from (irb):54
from /Users/mike/.rbenv/versions/1.9.3-p194/bin/irb:12:in `<main>'
irb(main):055:0> Example()
An example!
=> nil
I also found this post to be very helpful
What are the restrictions for method names in Ruby?
It's good practice, while not mandatory, to start the method name with
a lower-case character, because names that start with capital letters
are constants in Ruby. It's still possible to use a constant name for
a method, but you won't be able to invoke it without parentheses,
because the interpeter will look-up for the name as a constant

rspec stub to allow [hash_key] to be passed

How do you create a rspec method stub to allow a response from a method that takes in the hash key to return its value?
This is the line I want to test
sub_total = menu.menu_items[item] * quantity
and I'm using this line in rspec as my test stub on a double.
allow(menu).to receive(:menu_items[item]).and_return(2.0)
My env is set up with ruby 2.2.0 and spec 3.1.7
However I keep on getting a
NameError: undefined local variable or method `item'
Ruby code
def place_order(item, quantity, menu)
sub_total = menu.menu_items[item] * quantity
#customer_order << [item, quantity, sub_total]
end
Rspec code
let(:menu) { double :menu }
it "should allow 1 order of beer to placed" do
order = Order.new
allow(menu).to receive(:menu_items[item]).and_return(2.0)
order.place_order(:Beer, 1, 2.0)
expect(order.customer_order).to eq [[:Beer, 1, 2.0]]
end
Failures:
1) Order should allow 1 order of beer to placed
Failure/Error: allow(menu).to receive(:menu_items[item]).and_return(2.0)
NameError:
undefined local variable or method `item' for #<RSpec::ExampleGroups::Order:0x007fbb62917ee8 #__memoized=nil>
# ./spec/order_spec.rb:9:in `block (2 levels) in <top (required)>'
I've tried a number of things but nothing has worked
allow(menu).to receive(:menu_items).and_return(2.0)
allow(menu).to receive(:menu_items).with(item).and_return(2.0)
allow(menu).to receive(:menu_items).with("item").and_return(2.0)
allow(menu).to receive(:menu_items).with([item]).and_return(2.0)
I've run my code in irb and I can see it works but I can't find a way to get my class double to recerive the hash key.
you can do this:
allow(menu.menu_items).to receive(:[]).and_return({Beer: 2.0})
You can also pass an specific item if you need:
allow(menu.menu_items).to receive(:[]).with(1).and_return({Beer: 2.0})
The line menu.menu_items[item] is in reality composed by 3 method calls. [] is a call to the method [] on the Hash returned by menu_items.
I assume menu.menu_items returns a Hash and not an Array, given in the spec item is a Symbol.
That means your stub requires a little bit more work.
allow(menu).to receive(:menu_items).and_return({ Beer: 2.0 })
Also note, the error
undefined local variable or method `item'
is because you were using item in the spec, but item is not defined outside your method.
you're going a little too deep with your stub, think of this instead
allow(menu).to receive(:menu_items).and_return({Beer: 2.0})
Thanks to #SimoneCarletti's answer, I was able to easily stub an instance of PublicActivity. I add this answer only as a more brief (re)statement of the OP's problem and the simplicity of the solution.
Code I want to mimic with a stub:
self.entity = activity.parameters['entity_string']
And the salient parts of the test double:
activity = double('activity') # PublicActivity
allow(activity).to receive(:parameters).and_return({'entity_string' => "some entity name"})
Full code:
class ActivityRenderer
attr_accessor :time
attr_accessor :user
attr_accessor :action
attr_accessor :entity
def initialize(activity)
self.entity = activity.parameters['entity_string']
self.time = activity.updated_at
self.user = User.find(activity.owner_id)
self.action = activity.key
end
end
RSpec.describe ActivityRenderer do
let(:user) { ...factory girl stuff... }
let(:now) { Time.zone.now }
before do
Timecop.freeze
end
it 'provides an activity renderer' do
activity = double('activity') # PublicActivity
allow(activity).to receive(:parameters).and_return({'entity_string' => "some entity name"})
allow(activity).to receive(:updated_at).and_return(now)
allow(activity).to receive(:owner_id).and_return(user._id)
allow(activity).to receive(:key).and_return('some activity?')
ar = ActivityRenderer.new(activity)
expect(ar.user).to eql(user)
expect(ar.time).to eql(now)
expect(ar.action).to eql('some activity?')
expect(ar.entity).to eql("some entity name")
end
end

Ruby : Converting String to a Method Call

I am new to Ruby. I'm trying to convert a string to a method call in
Ruby. I intend to store all my function calls in an Excel Worksheet and
use the extracted strings to make the actual method call. But I am not
able to convert the string obtained from the excel and use it as a
function call. I read somewhere that the Send() method helps convert
strings to method calls. But I am not able to use it correctly. For the
code mentioned below I obtain a "in <top (required)>': undefined method
Execute_Statement(5)' for main:Object (NoMethodError)"
begin
def Execute_Statement(var1)
puts("Hello",var1)
end
end
x='Execute_Statement(5)' #This would be fed from the Excel Worksheet
send(x)
What am I doing wrong ?
You can either adopt the bad practice, i.e. Just do eval(x). If don't want to adopt it, do some more work as below :
def Execute_Statement(var1)
puts("Hello",var1)
end
s = "Execute_Statement(5)" # I hope this is coming from your excel cell.
method_name,number = s[/.*(?=\()/],s[/\d+/]
send(method_name,number.to_i)
Remove the begin..end block, it is not needed for your case.
You should be passing your parameter as part of the send call. Also the method name needs to be a symbol. In other words define the function name as a symbol and define your parameter as a separate variable, then call send like so:
def Execute_Statement(var1)
puts("Hello",var1)
end
method_name = 'Execute_Statement'.to_sym
parameter = 5
send(method_name,parameter)
As commenter above says this does not seem like a great idea.

RSpec stop method

Quick rspec question here.
Given the following test...
it 'sets the correct params' do
property = 'blahblah'
expect(ConnectionClass).to receive(:new).with({ opt1: param1, opt2: property })
subject.send(:do_things, nil, nil).and_return_nil
end
and the following method...
private
def do_things(param1, param2)
connection = ConnectionClass.new(opt1: param1, opt2: param2)
##
# How do i stop the test from continue beyond this point? :(
##
some_var = connection.build_request do |blah|
# ...
end
some_var.some_attribute
end
Running the test results in the following failure:
Failures:
1) Klass#do_things sets the correct params
Failure/Error: subject.send(:do_things, nil, nil).and_return_nil
NoMethodError:
undefined method `build_request' for nil:NilClass
# ./lib/blah/Klass.rb:46:in `make_request'
# ./spec/lib/blah/Klass_spec.rb:79:in `block (3 levels) in <top (required)>'
For this test, all i care about is that ConnectionClass is initialized correctly -- how can i prevent the call to build_request and eventual some_attribute?
Thank you
This is generally a pretty good signal that you either need to refactor your code, or you are testing things that you need to not be testing. Rather than testing the implementation ("This class is instantiated with these parameters") consider testing your outputs (For input X, do_things should return output Y and have side effects Z). If you think "I want this test to stop in the middle of this method", you need to refactor the method you're testing so that you can discretely test just the bits you want to test.
That said, if you don't want to change your approach here, you could just return a double from your stub, so that it can complete the method.
some_var = double(some_attribute: "value")
connection = double(build_request: some_var)
expect(ConnectionClass).to receive(:new).with({ opt1: param1, opt2: property }).
and_return(connection)

Array.find method problem

I find this line in the ZenTest source code:
result = #test_mappings.find { |file_re, ignored| filename =~ file_re }
The #test_mappings and result here are both Array object, but I didn't found 'find' method on Array class in ruby doc. I also tried it on irb:
irb(main):014:0> Array.respond_to? :find
=> false
irb(main):015:0> [1,2,3].find
LocalJumpError: no block given
from (irb):15:in `find'
from (irb):15:in `each'
from (irb):15:in `find'
from (irb):15
irb(main):016:0> [1,2,3].find{|x| x>1}
=> 2
Could any one explain it to me? How could find method also return an Array object? thanks in advance.
Array includes the Enumerable module, which adds the find method.
In your example you tested Array.respond_to. This will only return true for class methods of Array. find is an instance method, so respond_to? must be invoked on an instance of the class.
>> a = Array.new
=> []
>> a.respond_to? :find
=> true
Another sometimes useful trick is calling the 'methods' function which lists all the methods available to the instance of the object and using the grep method to filter out for something specific. It also gives you a good picture of what standard methods are provided by base classes without referring to docs.
a = Array.new
=> []
>> a.methods.grep /find/
=> ["find", "find_all"]

Resources