Pass a constant name to another method - ruby

I have read about sending method names as parameters in ruby, however in my case I want to allow the user to select a Digest format and use the answer to set the digest type:
def hash_digest(file, argument)
format = Digest::argument.new #using the argument here
...
end
hash_digest(filename, :MD5)
I have tried:
def hash_digest(file, argument)
format = Digest::send(argument).new
...
end
hash_digest(filename, :MD5)
But I get a no method `md5' error, despite MD5 being a valid algorithm in the Digest method. The exact error is:
send': undefined method MD5' for Digest:Module (NoMethodError) –
Is this possible in ruby?

I believe this works:
require 'digest'
def hash_digest(file, digest_algorithm)
format = Digest.const_get(digest_algorithm).new
end
hash_digest('test', 'SHA256')
Also since the number of options is fairly small you could always just use a case statement:
require 'digest'
def hash_digest(file, digest_algorithm)
format = case digest_algorithm
when 'HMAC' then Digest::HMAC.new
when 'MD5' then Digest::MD5.new
when 'RMD160' then Digest::RMD160.new
when 'SHA1' then Digest::SHA1.new
when 'SHA256' then Digest::SHA256.new
when 'SHA384' then Digest::SHA384.new
when 'SHA512' then Digest::SHA512.new
end
end
hash_digest('test', 'SHA256')

Related

Can you pass a block of code that returns an error to a method?

I often find myself dealing with these kind of scenarios:
require 'nokogiri'
require "open-uri"
url = "https://www.random_website.com/contains_info_I_want_to_parse"
nokodoc = Nokogiri::HTML(open(url))
# Let's say one of the following line breaks the ruby script
# because the element I'm searching doesn't contain an attribute.
a = nokodoc.search('#element-1').attribute('href').text
b = nokodoc.search('#element-2').attribute('href').text.gsub("a", "A")
c = nokodoc.search('#element-3 h1').attribute('style').text.strip
What happens is that I'll be creating about 30 variables all searching for different elements in a page, and I'll be looping that code over multiple pages. However, a few of these pages may have an ever-so-slightly different layout and won't have one of those div. This will break my code (because you can't call .attribute or .gsub on nil for example). But I can never guess which line before-hand.
My go-to solution is usually surround each line with:
begin
line #n
rescue
puts "line #n caused an error"
end
I'd like to be able to do something like:
url = "https://www.random_website.com/contains_info_I_want_to_parse"
nokodoc = Nokogiri::HTML(open(url))
catch_error(a, nokodoc.search('#element-1').attribute('href').text)
catch_error(b, nokodoc.search('#element-2').attribute('href').text.gsub("a", "A"))
catch_error(c, nokodoc.search('#element-3 h1').attribute('style').text.strip)
def catch_error(variable_name, code)
begin
variable_name = code
rescue
puts "Code in #{variable_name} caused an error"
end
variable_name
end
I know that putting & before each new method works:
nokodoc.search('#element-1')&.attribute('href')&.text
But I want to be able to display the error with a 'puts' in my terminal to see when my code gives an error.
Is it possible?
You can't pass your code as a regular argument to a method because it'll be evaluated (and raise an exception) before it gets passed to your catch_error method. You could pass it as a block--something like
a = catch_error('element_1 href text') do
nokodoc.search('#element-1').attribute('href').text
end
def catch_error(error_description)
yield
rescue
puts "#{error_description} caused an error"
end
Note that you can't pass a to the method as variable_name: it hasn't been defined anywhere before calling that method, so you'll get an undefined local variable or method error. Even if you define a earlier, it won't work correctly. If your code works without raising an exception, the method will return the right value but the value won't get stored anywhere outside the method scope. If there is an exception, variable_name will have whatever value a had before the method (nil if you defined it without setting it), so your error message would output something like Code in caused an error. That's why I added an error_description parameter.
You could also try logging the message and backtrace if you didn't want to have to specify an error description every time.
a = catch_error(nokodoc) do |doc|
doc.search('#element-1').attribute('href').text
end
def catch_error(doc)
yield doc
rescue => ex
puts doc.title # Or something else that identifies the document
puts ex.message
puts ex.backtrace.join("\n")
end
I made one additional change here: passing the document in as a parameter so that rescue could easily log something that identifies the document, in case that's important.

How do I call a function in Ruby?

I'm trying to call but I keep getting an error. This is my code:
require 'rubygems'
require 'net/http'
require 'uri'
require 'json'
class AlchemyAPI
#Setup the endpoints
##ENDPOINTS = {}
##ENDPOINTS['taxonomy'] = {}
##ENDPOINTS['taxonomy']['url'] = '/url/URLGetRankedTaxonomy'
##ENDPOINTS['taxonomy']['text'] = '/text/TextGetRankedTaxonomy'
##ENDPOINTS['taxonomy']['html'] = '/html/HTMLGetRankedTaxonomy'
##BASE_URL = 'http://access.alchemyapi.com/calls'
def initialize()
begin
key = File.read('C:\Users\KVadher\Desktop\api_key.txt')
key.strip!
if key.empty?
#The key file should't be blank
puts 'The api_key.txt file appears to be blank, please copy/paste your API key in the file: api_key.txt'
puts 'If you do not have an API Key from AlchemyAPI please register for one at: http://www.alchemyapi.com/api/register.html'
Process.exit(1)
end
if key.length != 40
#Keys should be exactly 40 characters long
puts 'It appears that the key in api_key.txt is invalid. Please make sure the file only includes the API key, and it is the correct one.'
Process.exit(1)
end
#apiKey = key
rescue => err
#The file doesn't exist, so show the message and create the file.
puts 'API Key not found! Please copy/paste your API key into the file: api_key.txt'
puts 'If you do not have an API Key from AlchemyAPI please register for one at: http://www.alchemyapi.com/api/register.html'
#create a blank file to hold the key
File.open("api_key.txt", "w") {}
Process.exit(1)
end
end
# Categorizes the text for a URL, text or HTML.
# For an overview, please refer to: http://www.alchemyapi.com/products/features/text-categorization/
# For the docs, please refer to: http://www.alchemyapi.com/api/taxonomy/
#
# INPUT:
# flavor -> which version of the call, i.e. url, text or html.
# data -> the data to analyze, either the the url, text or html code.
# options -> various parameters that can be used to adjust how the API works, see below for more info on the available options.
#
# Available Options:
# showSourceText -> 0: disabled (default), 1: enabled.
#
# OUTPUT:
# The response, already converted from JSON to a Ruby object.
#
def taxonomy(flavor, data, options = {})
unless ##ENDPOINTS['taxonomy'].key?(flavor)
return { 'status'=>'ERROR', 'statusInfo'=>'Taxonomy info for ' + flavor + ' not available' }
end
#Add the URL encoded data to the options and analyze
options[flavor] = data
return analyze(##ENDPOINTS['taxonomy'][flavor], options)
print
end
**taxonomy(text,"trees",1)**
end
In ** ** I have entered my call. Am I doing something incorrect. The error I receive is:
C:/Users/KVadher/Desktop/testrub:139:in `<class:AlchemyAPI>': undefined local variable or method `text' for AlchemyAPI:Class (NameError)
from C:/Users/KVadher/Desktop/testrub:6:in `<main>'
I feel as though I'm calling as normal and that there is something wrong with the api code itself? Although I may be wrong.
Yes, as jon snow says, the function (method) call must be outside of the class. The methods are defined along with the class.
Also, Options should be a Hash, not a number, as you call options[flavor] = data, which is going to cause you another problem.
I believe maybe you meant to put text in quotes, as that is one of your flavors.
Furthermore, because you declared a class, this is called an instance method, and you must make an instance of the class to use this:
my_instance = AlchemyAPI.new
my_taxonomy = my_instance.taxonomy("text", "trees")
That's enough to get it to work, it seems like you have a ways to go to get this all working though. Good luck!

Minitest: How to stub/mock the file result of Kernel.open on a URL

I have been trying to use Minitest to test my code (full repo) but am having trouble with one method which downloads a SHA1 hash from a .txt file on a website and returns the value.
Method:
def download_remote_sha1
#log.info('Downloading Elasticsearch SHA1.')
#remote_sha1 = ''
Kernel.open(#verify_url) do |file|
#remote_sha1 = file.read
end
#remote_sha1 = #remote_sha1.split(/\s\s/)[0]
#remote_sha1
end
You can see that I log what is occurring to the command line, create an object to hold my SHA1 value, open the url (e.g. https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.4.2.deb.sha1.txt)
I then split the string so that I only have the SHA1 value.
The problem is that during a test, I want to stub the Kernel.open which uses OpenURI to open the URL. I would like to ensure that I'm not actually reaching out to download any file, but rather I'm just passing the block my own mock IO object testing just that it correctly splits stuff.
I attempted it like the block below but when #remote_sha1 = file.read occurs the file item is nil.
#mock_file = Minitest::Mock.new
#mock_file.expect(:read, 'd377e39343e5cc277104beee349e1578dc50f7f8 elasticsearch-1.4.2.deb')
Kernel.stub :open, #mock_file do
#downloader = ElasticsearchUpdate::Downloader.new(hash, true)
#downloader.download_remote_sha1.must_equal 'd377e39343e5cc277104beee349e1578dc50f7f8'
end
I was working on this question too, but matt figured it out first. To add to what matt posted:
When you write:
Kernel.stub(:open, #mock_file) do
#block code
end
...that means when Kernel.open() is called--in any code, anywhere before the stub() block ends--the return value of Kernel.open() will be #mock_file. However, you never use the return value of Kernel.open() in your code:
Kernel.open(#verify_url) do |f|
#remote_sha1 = f.read
end
If you wanted to use the return value of Kernel.open(), you would have to write:
return_val = Kernel.open(#verify_url) do |f|
#remote_sha1 = f.read
end
#do something with return_val
Therefore, the return value of Kernel.open() is irrelevant in your code--which means the second argument of stub() is irrelevant.
A careful examination of the source code for stub() reveals that stub() takes a third argument--an argument which will be passed to a block specified after the stubbed method call. You, in fact, have specified a block after your stubbed Kernel.open() method call:
stubbed method call -+ +- start of block
| | |
V V V
Kernel.open(#verify_url) do |f|
#remote_sha1 = f.read
end
^
|
end of block
So, in order to pass #mockfile to the block you need to specify it as the third argument to Kernel.stub():
Kernel.stub(:open, 'irrelevant', #mock_file) do
end
Here is a full example for future searchers:
require 'minitest/autorun'
class Dog
def initialize
#verify_url = 'http://www.google.com'
end
def download_remote_sha1
#remote_sha1 = ''
Kernel.open(#verify_url) do |f|
#remote_sha1 = f.read
end
#puts #remote_sha1[0..300]
#remote_sha1 = #remote_sha1.split(" ")[0] #Using a single space for the split() pattern will split on contiguous whitespace.
end
end
#Dog.new.download_remote_sha1
describe 'downloaded file' do
it 'should be an sha1 code' do
#mock_file = Minitest::Mock.new
#mock_file.expect(:read, 'd377e39343e5cc277104beee349e1578dc50f7f8 elasticsearch-1.4.2.deb')
Kernel.stub(:open, 'irrelevant', #mock_file) do
#downloader = Dog.new
#downloader.download_remote_sha1.must_equal 'd377e39343e5cc277104beee349e1578dc50f7f8'
end
end
end
xxx
The second argument to stub is what you want the return value to be for the duration of your test, but the way Kernel.open is used here requires the value it yields to the block to be changed instead.
You can achieve this by providing a third argument. Try changing the call to Kernel.stub to
Kernel.stub :open, true, #mock_file do
#...
Note the extra argument true, so that #mock_file is now the third argument and will be yielded to the block. The actual value of the second argument doesn’t really matter in this case, you might want to use #mock_file there too to more closely correspond to how open behaves.

Multiple Parameters

I have an object that allows users to connect to HDFS. One method allows them to download data, and the other upload data. The initialize method looks something like this:
def initialize(file=nil,set=nil,action)
end
How can I change the way arguments are passed to be more efficient? The action param is required every time, but file and file_set are only required depending on the action. I.e., if they want to upload data they need to pass set, if they want to download data, they just pass file.
Since Ruby 2.0 you can use keyword parameters:
def initialize(action, file: nil,set: nil)
unless file.nil?
# do stuff with file
end
end
So you can call it:
MyClass.new(some_action)
MyClass.new(some_action, set: some_set)
MyClass.new(some_action, file: some_file)
MyClass.new(some_action, set: some_set, file: some_file)
...
First of all you should pass requred parameter first e.g.:
def initialize(action, file = nil, set = nil)
end
Then you may want to use hash to pass optional params:
def initialize(action, options = {})
end
The passing hash is a common way when you need to pass more than one optional parameter.
When you need to pass file, set or both you may call initialize method as follow (assume that this method is defened in the class MyModel):
MyModel.new(action, {file: ''})
MyModel.new(action, {set: ''})
MyModel.new(action, {file: '', set: ''})
Or when you don't want to pass any optional params, simply call:
MyModel.new(action)
In this case you will have empty options hash passed in you initialize method.
Quite common is to use positional parameters when they are mandatory, and an options hash
for the rest. You just need to check action to verify given params are then present:
def initialize(action, opts={})
if action == 'foo'
raise ArgumentError, "requires either :file or :set" unless
([:file, :set] & opts.keys).size == 1
end
...
end

Ruby: how can use the dump method to output data to a csv file?

I try to use the ruby standard csv lib to dump out the arr of object to a csv.file , called 'a.csv'
http://ruby-doc.org/stdlib-1.9.3/libdoc/csv/rdoc/CSV.html#method-c-dump
dump(ary_of_objs, io = "", options = Hash.new)
but in this method, how can i dump into a file?
there is no such examples exists and help. I google it no example to do for me...
Also, the docs said that...
The next method you can provide is an instance method called
csv_headers(). This method is expected to return the second line of
the document (again as an Array), which is to be used to give each
column a header. By default, ::load will set an instance variable if
the field header starts with an # character or call send() passing the
header as the method name and the field value as an argument. This
method is only called on the first object of the Array.
Anyone knows how to pass the instance method csv_headers() to this dump function?
I haven't tested this out yet, but it looks like io should be set to a file. According to the doc you linked "The io parameter can be used to serialize to a File"
Something like:
f = File.open("filename")
dump(ary_of_objs, io = f, options = Hash.new)
The accepted answer doesn't really answer the question so I thought I'd give a useful example.
First of all if you look at the docs at http://ruby-doc.org/stdlib-1.9.3/libdoc/csv/rdoc/CSV.html, if you hover over the method name for dump you see you can click to show source. If you do that you'll see that the dump method attempts to call csv_headers on the first object you pass in from ary_of_objs:
obj_template = ary_of_objs.first
...snip...
headers = obj_template.csv_headers
Then later you see that the method will call csv_dump on each object in ary_of_objs and pass in the headers:
ary_of_objs.each do |obj|
begin
csv << obj.csv_dump(headers)
rescue NoMethodError
csv << headers.map do |var|
if var[0] == #
obj.instance_variable_get(var)
else
obj[var[0..-2]]
end
end
end
end
So we need to augment each entry in array_of_objs to respond to those two methods. Here's an example wrapper class that would take a Hash, and return the hash keys as the CSV headers and then be able to dump each row based on the headers.
class CsvRowDump
def initialize(row_hash)
#row = row_hash
end
def csv_headers
#row.keys
end
def csv_dump(headers)
headers.map { |h| #row[h] }
end
end
There's one more catch though. This dump method wants to write an extra line at the top of the CSV file before the headers, and there's no way to skip that if you call this method due to this code at the top:
# write meta information
begin
csv << obj_template.class.csv_meta
rescue NoMethodError
csv << [:class, obj_template.class]
end
Even if you return '' from CsvRowDump.csv_meta that will still be a blank line where a parse expects the headers. So instead lets let dump write that line and then remove it afterwards when we call dump. This example assumes you have an array of hashes that all have the same keys (which will be the CSV header).
#rows = #hashes.map { |h| CsvRowDump.new(h) }
File.open(#filename, "wb") do |f|
str = CSV::dump(#rows)
f.write(str.split(/\n/)[1..-1].join("\n"))
end

Resources