What is the best practice for handling nil objects and properties? - ruby-on-rails

Say I have a User object, which has an email property, and I need the upper cased last letter of their email:
u = User.find(1)
letter = u.email.upcase.last
If u or email is nil in this chain, then I get a NoMethodError: undefined method 'blah' for nil:Nilclass. I should be able to work around it in most cases, but sometimes, a nil gets where it shouldn't or its hard to contain. One way would be verbose:
u = User.find(1)
letter = nil
if u && u.email
letter = u.email.upcase.last
end
But this gets annoying and hazardous in a view, or in a long chain of a.bunch.of.properties.down.a.hierarchy. I read about try method in Rails:
u = User.find(1)
letter = u.try(:email).try(:upcase).try(:last)
This is less verbose, but I feel icky writing all those tries. And once I put try in the chain, I have to use them all the way down. Is there a better way?

I like to use the Null Object Pattern. Avdi has a great post explaining this, but the basic idea is you have a little class that can stand in for an object and respond reasonably to the messages you might pass the original object. I've found these are useful not only for avoiding NoMethodErrors but also for setting default values/nice messages.
For instance, you could do:
class NilUser
def email
"(no email address)"
end
end
u = User.find(1) || NilUser.new
u.email.upcase.last # => No errors!

I just wanted to update this thread with one more option: Ruby now (as of 2.3) gives us a safe navigation operator, the &. syntax.
So:
u.email.upcase
Would become:
u.email&.upcase
Similarly to Rail's try method, the whole chain will return nil if it encounters NoMethodError on a nil.

User.find(1)
Will raise exception if user with id 1 not exist so you don't need to worry about nil here
u.email
If you have in your model
validates :email, presence: true
You don't need to worry about nil because User without email cant be in database
But I think you are asking about general way of handling nils in ruby code. Lately I'm using Null Object pattern
http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/
http://robots.thoughtbot.com/post/20907555103/rails-refactoring-example-introduce-null-object
Rails: replacing try with the Null Object Pattern
https://github.com/martinciu/nullobject

You could also map the result of find
[User.find(1)].map{|u| (u != nil ? u.mail : "no mail")}[0]

Related

Fetch ActiveRecord query result as an array of hashes with chosen attributes

The model User has first, last and login as attributes. It also has a method called name that joins first and last.
What I want is to iterate through the Users records and create an array of hashes with the attributes I want. Like so:
results = []
User.all.map do |user|
record = {}
record["login"] = user.login
record["name"] = user.name
results << record
end
Is there a cleaner way in Ruby to do this?
Trying to map over User.all is going to cause performance issues (later, if not now). To avoid instantiating all User objects, you can use pluck to get the data directly out of the DB and then map it.
results = User.all.pluck(:login, :first, :last).map do |login, first, last|
{ 'login' => login, 'name' => first << last }
end
Instantiating all the users is going to be problematic. Even the as_json relation method is going to do that. It may even be a problem using this method, depending on how many users there are.
Also, this assumes that User#name really just does first + last. If it's different, you can change the logic in the block.
You can use ActiveRecord::QueryMethods#select and ActiveRecord::Relation#as_json:
User.select(:login, '(first || last) as name').as_json(except: :id)
I would write:
results = User.all.map { |u| { login: u.login, name: u.name } }
The poorly named and poorly documented method ActiveRecord::Result#to_hash does what you want, I think.
User.select(:login, :name).to_hash
Poorly named because it does in fact return an array of Hash, which seems pretty poor form for a method named to_hash.

Ruby 1.9.2 non-existent hash element

I'm on Rails 3.0.x, Ruby 1.9.2 and needs a way to test for params that may or may not exists, e.g.,
params[:user] #exists
params[:user][:login] #may not exist
What's the proper Ruby syntax for the 2nd check so it doesn't barf?
Try following:
params.has_key? :user #=> true because exists
params[:user].has_key? :login #=> true if exist otherwise false
#WarHog has it right, pretty much. It's very unusual for an item in params to sometimes return a string but other times return a Hash, but regardless you can handle that easily enough:
if params.has_key?(:user) && params[:user].respond_to?(:has_key?)
do_something_with params[:user][:login]
end
Instead of respond_to? :has_key? you could also do respond_to? :[] or just is_a? Hash. Mostly a matter of preference.
You would just get nil in the second case.. that shouldn't be a problem, no?
e.g. params[:user][:login] just returns nil, which evaluates to false if the :user entry exists in the first Hash.
However if the nesting would be one or more levels deeper, and the missing hash entry was somewhere in the middle, you would have problems. e.g.:
params[:user][:missing_key][:something]
in that case Ruby would try to evaluate nil[:something] and raise an exception
you could do something like this:
begin
x = params[:user][:missing_key][:something]
rescue
x = nil
end
... which you could further abstract...

Protecting against an undefined chained method

I have a long loop that results in this:
csv_code = CSV.generate do |csv|
csv << ["Product ID","Name", "Url"]
#all_products.each do |product|
if product.page_url("en_US") != nil
turl = product.page_url("en_US")
end
csv << [product.name,product.d_id, turl]
end
end
The method uses products 1-17 works great resulting in a url printed. When I get to my 18th record I have problems
Product.find(18) // product found!
product.find(18).page_url("en_US")
NoMethodError: undefined method `page_url' for nil:NilClass
How can I protect against these undefined events?
url = product.page_url("en_US")
The issue is that a product is nil:
undefined method 'page_url' for nil:NilClass". Solution:
(It has nothing to do with page_url maybe returning nil.)
Make sure product can't be nil: but be wary that this may be a deeper issue. In any case, "fixing" this issue is easy to deal with.
Consider either using a collection restriction (such as Enumerable#reject):
#all_products.reject(&:nil?).each do {
...
}
The above uses the Symbol#to_proc "Rails magic", but could just as easily have been {|x| x.nil?} as the restriction. The downside is it's not practical to use this for a "no URL" condition per-product although Enumerable#partition could help with that: use the right tool for the job.
Another solution is to expand the conditional check itself:
if product && product.page_url("en_US")
# yay
else
# uhm
end
The short-circuit nature of && will ensure page_url is only invoked upon a truthy value (which excludes nil).
I also took the liberty of assuming page_url can't return false as I find this makes the intent more clear.
Happy coding.
Try this:
product.find(18).try(:page_url, "en_US")
But it's a perf killer.
Are you sure Product.find(18) doesn't return nil ?
Anyway, you could do:
url = product.nil? ? "no_url" : product.page_url("en_US")

If string is empty then return some default value

Often I need to check if some value is blank and write that "No data present" like that:
#user.address.blank? ? "We don't know user's address" : #user.address
And when we have got about 20-30 fields that we need to process this way it becomes ugly.
What I've made is extended String class with or method
class String
def or(what)
self.strip.blank? ? what : self
end
end
#user.address.or("We don't know user's address")
Now it is looking better. But it is still raw and rough
How it would be better to solve my problem. Maybe it would be better to extend ActiveSupport class or use helper method or mixins or anything else. What ruby idealogy, your experience and best practices can tell to me.
ActiveSupport adds a presence method to all objects that returns its receiver if present? (the opposite of blank?), and nil otherwise.
Example:
host = config[:host].presence || 'localhost'
Phrogz sort of gave me the idea in PofMagicfingers comment, but what about overriding | instead?
class String
def |(what)
self.strip.blank? ? what : self
end
end
#user.address | "We don't know user's address"
Since you're doing this in Ruby on Rails, it looks like you're working with a model. If you wanted a reasonable default value everywhere in your app, you could (for example) override the address method for your User model.
I don't know ActiveRecord well enough to provide good code for this; in Sequel it would be something like:
class User < Sequel::Model
def address
if (val=self[:address]).empty?
"We don't know user's address"
else
val
end
end
end
...but for the example above this seems like you'd be mixing view logic into your model, which is not a good idea.
Your or method might have some unwanted side-effects, since the alternative (default) value is always evaluated, even if the string is not empty.
For example
#user.address.or User.make_a_long_and_painful_SQL_query_here
would make extra work even if address is not empty. Maybe you could update that a bit (sorry about confusing one-liner, trying to keep it short):
class String
def or what = ""
self.strip.empty? ? block_given? ? yield : what : self
end
end
#user.address.or "We don't know user's address"
#user.address.or { User.make_a_long_and_painful_SQL_query_here }
It is probably better to extend ActiveRecord or individual models instead of String.
In your view, you might prefer a more explicit pattern like
#user.attr_or_default :address, "We don't know the user's address"
Ruby:
unless my_str.empty? then my_str else 'default' end
RoR:
unless my_str.blank? then my_str else 'default' end
I recommend to use options.fetch(:myOption, defaultValue) because it works great with boolean flags like the ones mentioned above and therefore seems better to use in general.
Examples
value = {}
puts !!(value.fetch(:condition, true)) # Print true
value = {}
value[:condition] = false
puts !!(value.fetch(:condition, true)) # Print false
value = {}
value[:condition] = true
puts !!(value.fetch(:condition, true)) # Print true
value = {}
value[:condition] = nil
puts !!(value.fetch(:condition, true)) # Print false

debugging a ruby on rails error

I'm some what new with ruby on rails, so I'm attempting to debug this error that I'm getting but, from my understanding, is working on the prod code.
The error:
NoMethodError in JoinController#index
undefined method `join_template' for nil:NilClass
/app/controllers/join_controller.rb:5:in `index'
Okay, so line 5 in index is:
elsif current_brand.join_template == 'tms'
So clearly current_brand is nil. The controller is a child class of AppController, so checking that out I see that current_brand is a method:
def current_brand
return #current_brand if defined?(#current_brand)
url_array = request.env['HTTP_HOST'].split('.').reverse
url = url_array[1] << "." << url_array[0]
#current_brand = Brand.find(:first, :conditions => ["url LIKE ?", "%" << url << "%"])
end
It seems that #current_brand is always returned, yet it's continuing to be Nil. What could the problem be?
It may be your query is not returning anything. You can use a debugger, but it's pretty easy to just output #current_brand and see what it evaluates to.
logger.debug(#current_brand)
You must check two things:
Does Rails build the SQL query properly with the url passed in on the last line of that method?
Does the record exist in the brands table? you're not actually checking for that.
Also, passing in the url like that opens you up to a potential SQL injection attack.
You need to rewrite your definition with a rescue or if/else so if you do get a nil element then it won't be a fatal error.
This is your problem:
#current_brand = Brand.find(:first, :conditions => ["url LIKE ?", "%" << url << "%"])
#current_brand is not finding anything so make sure you find something.
#current_brand = Brand.find(:first)
If this fixes the problem then you know it is not finding anything and you will need to change your code that if it returns nil then it doesn't provide the function or it finds a default brand such as what I suggested.

Resources