Multiple Parameters - ruby

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

Related

Pass a constant name to another method

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')

In Ruby the aws-sdk gem, is it ok to pass a nil value for a parameter?

In the Ruby aws-sdk gem, is it ok to pass a nil value for a parameter, if the parameter is not required?
I want to do
s3_client.list_objects(bucket: bucket_name, prefix: path_to_files, marker: marker)
and I want to know if it's ok, for the first list_objects request, to have a marker value of nil (which is the same as marker: nil). Or do I have to do s3_client.list_objects(bucket: bucket_name, prefix: path_to_files) without marker: marker if marker is nil?
The documentation has
:marker (String) — Specifies the key to start with when listing objects in a bucket.
so I know that :marker is not required, but is passing in a nil value supported behaviour (after all, it's not a String), and is it guaranteed to give the same results as not passing in a value at all?
Looking at the source for aws-sdk-v1 and doing a project search for :marker shows where they use this parameter key, it seems to be guarded by checks to see if its non falsy. So I'd assume it's safe to pass in nil in this case. More specifically, the definition for the list_objects method in lib/aws/s3/client.rb:
# #overload list_objects(options = {})
# #param [Hash] options
# #option options [required,String] :bucket_name
# #option options [String] :delimiter
# #option options [String] :marker
# #option options [String] :max_keys
# #option options [String] :prefix
# #return [Core::Response]
bucket_method(:list_objects, :get, XML::ListObjects) do
configure_request do |req, options|
super(req, options)
params = %w(delimiter marker max_keys prefix)
params.each do |param|
if options[param.to_sym]
req.add_param(param.gsub(/_/, '-'), options[param.to_sym])
end
end
end
end
Notice how every parameter is checked for falsiness before being added to the request if options[param.to_sym]. However, I didn't check to see if aws actually requires this param.
So try it. Does it work without :marker? If so, and the results are as expected, then it's safe to use nil there. Indeed, it would be exactly the same as not passing it in at all, since, according to the above implementation, the add_param method is only called if it's non falsy.

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.

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