How to handle memoization with nil values with Sorbet? - ruby

I have this method:
def current_organization
return #current_organization if defined?(#current_organization)
subdomain = request.subdomain.to_s
return #current_organization = nil if subdomain.blank?
return #current_organization = nil if Allowable::DeniedSlugs.include?(subdomain)
#current_organization = Organization.find_by(slug: subdomain)
end
Because this file is typed: strict, I need to declare #current_organization, but this would mean that checking defined? would always be true. What is the correct pattern for handling nil for methods like this

You will have to declare #current_organization after checking defined? which can be done just above the find_by call
def current_organization
return #current_organization if defined?(#current_organization)
...
#current_organization = T.let(#current_organization, T.nilable(Organization))
#current_organization = Organization.find_by(slug: subdomain)
end
Example on sorbet.run

Related

How to avoid iterating over Nil value

I'm trying to scrape a website's content to instantiate objects out of the data, and I'm running into a problem with a dead link on the page I'm scraping. I want to figure out how I can simply not iterate over that link and avoid scraping it altogether.
I tried using this, but it didn't work:
name = li.css("strong a").text.strip unless li.nil?
url = li.css("a")[0].attr("href") unless li.nil?
Player.new(name,url)
class HomepageScraper
BASE_URL = "https://www.nba.com/history/nba-at-50/top-50-players"
def self.scrape_players
page = open(BASE_URL)
parsed_HTML = Nokogiri::HTML(page)
name_lis = parsed_HTML.css("div.field-item li")
name_lis.each do |li|
name = li.css("strong a").text.strip
url = li.css("a")[0].attr("href")
Player.new(name,url)
end
end
end
I expected example output to be:
#name = "Shaquille o neal", #url = "www.nba..."
But received:
#name = "Shaquille o neal", #url = nil
The error message is:
undefined method `attr' for nil:NilClass (NoMethodError)
If you run at least Ruby 2.3, do a
url = li.css("a")[0]&.attr("href")
This sets url to nil, if the part to the left of &. is nil, and applies attr otherwise.
You should use the compact method on Array.
It is a useful method if you need to remove nil values from an array.
For example:
[1, nil, 2, nil].compact => [1, 2]
In your case:
name_lis.compact.each do |li|
end

Dynamically check if a field in JSON is nil without using eval

Here's an extract of the code that I am using:
def retrieve(user_token, quote_id, check="quotes")
end_time = Time.now + 15
match = false
until Time.now > end_time || match
#response = http_request.get(quote_get_url(quote_id, user_token))
eval("match = !JSON.parse(#response.body)#{field(check)}.nil?")
end
match.eql?(false) ? nil : #response
end
private
def field (check)
hash = {"quotes" => '["quotes"][0]',
"transaction-items" => '["quotes"][0]["links"]["transactionItems"]'
}
hash[check]
end
I was informed that using eval in this manner is not good practice. Could anyone suggest a better way of dynamically checking the existence of a JSON node (field?). I want this to do:
psudo: match = !JSON.parse(#response.body) + dynamic-path + .nil?
Store paths as arrays of path elements (['quotes', 0]). With a little helper function you'll be able to avoid eval. It is, indeed, completely inappropriate here.
Something along these lines:
class Hash
def deep_get(path)
path.reduce(self) do |memo, path_element|
return unless memo
memo[path_element]
end
end
end
path = ['quotes', 0]
hash = JSON.parse(response.body)
match = !hash.deep_get(path).nil?

Net::ftp getbinaryfile() saving to file vs saving to variable

Using the following ftp_download method works, but if I change
ftp.getbinaryfile(file,localdir,1024) #=> Saves the file to localdir
to
ftp.getbinaryfile(file) #=> returns nil
I get nil returned. According to
http://www.ruby-doc.org/stdlib-2.0/libdoc/net/ftp/rdoc/Net/FTP.html#method-i-getbinaryfile
inilf I set localfile to nil as above, the data should be retrieved and returned by the method. What am I doing wrong?
def ftp_download(domain,remotedir,filename_regex,user=nil,pwd=nil)
ftp = Net::FTP::new(domain)
if user && pwd
ftp.login(user, pwd)
end
ftp.chdir(remotedir)
fileList = ftp.nlst(filename_regex)
fileList.each do |file|
localdir=File.join(remotedir,file)
localdir=localdir[1..-1] if localdir[0]="/"
FileUtils.mkdir_p(File.dirname(localdir))
ftp.getbinaryfile(file,localdir,1024)
end
ftp.close
end
If you look at the getbinaryfile method signature you will notice that the default value for the second parameter (localfile) is not nil but File.basename(remotefile)
getbinaryfile(remotefile,
localfile=File.basename(remotefile),
blocksize=DEFAULT_BLOCKSIZE)
If you want localfile to be nil you have to pass it explicitly:
ftp.getbinaryfile(file, nil)

How to return a particular value from a method?

I have this code that tries to return a value from a method:
temp = "123"
return temp
and I have this line that calls the method and assigns the return value:
person_connections = #client.get_person_connections(:id => current_user_id )
but when I try to inspect person_connections, it shows some different object string. Any idea how to return the actual value of the temp variable?
def get_person_connections(options = {})
person_id = options[:id]
path = "/people/id=" + person_id + ":(num-connections)"
query_connections(path, options)
self
end
and
private
def query_connections(path, options={})
fields = options.delete(:fields) || LinkedIn.default_profile_fields
if options.delete(:public)
path +=":public"
elsif fields
path +=":(#{fields.map{ |f| f.to_s.gsub("_","-") }.join(',')})"
end
headers = options.delete(:headers) || {}
params = options.map { |k,v| v.is_a?(Array) ? v.map{|i| "#{k}=#{i}"}.join("&") : "#{k}=#{v}" }.join("&")
path += "?#{params}" if not params.empty?
temp_var = get(path, headers)
hash = JSON.parse(temp_var)
conn = hash["numConnections"]
end
As Samy said in a comment:
In Ruby, the last statement will be returned.
So if we take a look at get_person_connections, we see that the last line is self. What it means is that it returns the instance on which the method was called, #client in this case.
Additional notes: the solution would be to remove self, although if the method is used elsewhere be careful as returning self is often used to allow chaining of methods (though it hardly makes sense to do that on a get method).

Preserving case in HTTP headers with Ruby's Net:HTTP

Although the HTTP spec says that headers are case insensitive; Paypal, with their new adaptive payments API require their headers to be case-sensitive.
Using the paypal adaptive payments extension for ActiveMerchant (http://github.com/lamp/paypal_adaptive_gateway) it seems that although the headers are set in all caps, they are sent in mixed case.
Here is the code that sends the HTTP request:
headers = {
"X-PAYPAL-REQUEST-DATA-FORMAT" => "XML",
"X-PAYPAL-RESPONSE-DATA-FORMAT" => "JSON",
"X-PAYPAL-SECURITY-USERID" => #config[:login],
"X-PAYPAL-SECURITY-PASSWORD" => #config[:password],
"X-PAYPAL-SECURITY-SIGNATURE" => #config[:signature],
"X-PAYPAL-APPLICATION-ID" => #config[:appid]
}
build_url action
request = Net::HTTP::Post.new(#url.path)
request.body = #xml
headers.each_pair { |k,v| request[k] = v }
request.content_type = 'text/xml'
proxy = Net::HTTP::Proxy("127.0.0.1", "60723")
server = proxy.new(#url.host, 443)
server.use_ssl = true
server.start { |http| http.request(request) }.body
(i added the proxy line so i could see what was going on with Charles - http://www.charlesproxy.com/)
When I look at the request headers in charles, this is what i see:
X-Paypal-Application-Id ...
X-Paypal-Security-Password...
X-Paypal-Security-Signature ...
X-Paypal-Security-Userid ...
X-Paypal-Request-Data-Format XML
X-Paypal-Response-Data-Format JSON
Accept */*
Content-Type text/xml
Content-Length 522
Host svcs.sandbox.paypal.com
I verified that it is not Charles doing the case conversion by running a similar request using curl. In that test the case was preserved.
The RFC does specify that header keys are case-insensitive, so unfortunately you seem to have hit an annoying requirement with the PayPal API.
Net::HTTP is what is changing the case, although I'm surprised they're not all getting downcased:
# File net/http.rb, line 1160
def []=(key, val)
unless val
#header.delete key.downcase
return val
end
#header[key.downcase] = [val]
end
"Sets the header field corresponding to the case-insensitive key."
As the above is a simple class it could be monkey-patched. I will think further for a nicer solution.
Use following code to force case sensitive headers.
class CaseSensitivePost < Net::HTTP::Post
def initialize_http_header(headers)
#header = {}
headers.each{|k,v| #header[k.to_s] = [v] }
end
def [](name)
#header[name.to_s]
end
def []=(name, val)
if val
#header[name.to_s] = [val]
else
#header.delete(name.to_s)
end
end
def capitalize(name)
name
end
end
Usage example:
post = CaseSensitivePost.new(url, {myCasedHeader: '1'})
post.body = body
http = Net::HTTP.new(host, port)
http.request(post)
If you are still looking for an answer that works. Newer versions have introduced some changes to underlying capitalize method by using to_s. Fix is to make the to_s and to_str return the self so that the returned object is an instance of ImmutableKey instead of the base string class.
class ImmutableKey < String
def capitalize
self
end
def to_s
self
end
alias_method :to_str, :to_s
end
Ref: https://jatindhankhar.in/blog/custom-http-header-and-ruby-standard-library/
I got several issues with the code proposed by #kaplan-ilya because the Net::HTTP library tries to detect the post content-type, and the I ended up with 2 content-type and other fields repeated with different cases.
So the code below should ensure than once a particular case has been choosen, it will stick to the same.
class Post < Net::HTTP::Post
def initialize_http_header(headers)
#header = {}
headers.each { |k, v| #header[k.to_s] = [v] }
end
def [](name)
_k, val = header_insensitive_match name
val
end
def []=(name, val)
key, _val = header_insensitive_match name
key = name if key.nil?
if val
#header[key] = [val]
else
#header.delete(key)
end
end
def capitalize(name)
name
end
def header_insensitive_match(name)
#header.find { |key, _value| key.match Regexp.new(name.to_s, Regexp::IGNORECASE) }
end
end

Resources