Ruby/Rails ActiveRecord attribute find boolean - ruby-on-rails

I am looking for a nice way to find if a ActiveRecord attribute is a boolean, i have this method here which will check of an summary attribute exists or has data, it does this by the value.nil? || value.zero? but now we have an exception where the value could be a boolean. so it blows up with a NoMethodError: undefined method zero?' for false:FalseClass
def self.summary_attribute_status(element)
# Check if all summary attributes zero or nil
result = element.class.attributes_for_tag(:summary_attributes).all? do |attribute|
value = element.send(attri bute)
next element
value.nil? || value.zero?
end
return result ? nil : 'something'
end
I could do it like this:
value.nil? || value.zero? || (!value.is_a? TrueClass || !value.is_a? FalseClass)
But its seems like there should be a better way of finding its general type, i tried finding if they shared a boolean type parent or superclass but they both inherit from Object < BasicObject. is there no other way?

There is another way to detect if a value is a boolean in ruby.
!!value == value #true if boolean
But I don't think there is any method built-in for that.

If you have for example a User class that has a boolean attribute admin than you can do something like this:
User.columns_hash['admin'].type
# => :boolean
Read more about columns_hash here: http://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-columns_hash

Related

How use varaible as column name in Rails 6

I would make this code shorter.
def foo type, user
if type == "foo"
user.foo
elsif type == "bar"
user.bar
end
end
user is ActiveRecord object
type is string "foo" or "bar"
If I have more types then I need to make more elsif. It's possible to make it shorter (without using the model method or any other additional code) using type variable as a column name? Something like:
user.{type} # ???
You may use public_send to achieve this:
user.public_send(type)

Ruby try not handling exception

I have the following example ruby code:
def example_method(obj)
ExampleClass.new(color1: #theme.try(obj['color1'].to_sym))
end
Which should pass either the obj['color1'] as a symbol or nil to the class.
However if color1 is not passed I get the error: NoMethodError: undefined method 'to_sym' for nil:NilClass.
Shouldn't the try method be handling the exception?
Update: based on comments... I solved it by doing a ternary:
ExampleClass.new(color1: obj['color1'].present? ? #brand_theme.try(obj['color1'].try(:to_sym)) : nil)
You could write a helper method:
def theme_color(name)
return unless name
return unless #theme.respond_to?(name)
#theme.public_send(name)
end
def example_method(obj)
ExampleClass.new(color1: theme_color(obj['color1']))
end
theme_color returns nil if the argument is nil, i.e. obj['color1']. It also returns nil if theme does not respond to the given method. Otherwise, it invokes the method specified by name.
Note that respond_to? and public_send accept either a string or a symbol, so no to_sym is needed.
You could also define the helper method as an instance method of your #theme class:
class Theme
def color(name)
return unless name
return unless respond_to?(name)
public_send(name)
end
def red
'FF0000'
end
end
#theme = Theme.new
#theme.red #=> "FF0000"
#theme.color(:red) #=> "FF0000"
#theme.color('red') #=> "FF0000"
#theme.color('green') #=> nil
#theme.color(nil) #=> nil
And invoke it via:
def example_method(obj)
ExampleClass.new(color1: #theme.color(obj['color1']))
end
Keep in mind that these approaches (using public_send or try) allow you to invoke arbitrary methods on your #theme object. It might be safer to keep the colors in a hash.
From comments:
In this example obj['color1'] would be nil. So we'd be passing nil to the try.
Yes, that's an error. You can't call a method with no name. Technically, you could avoid the error by doing .try(obj['color'].to_s), but it's super-wrong.
I would check for presence explicitly and bail early if it's not there.
def example_method(obj)
return unless obj['color1'].present?
ExampleClass.new(color1: #theme.try(obj['color1']))
end
The try is called on the #theme object. The nil error is thrown because obj['color1'] returns nil and then to_sym is called on nil.
You'd have to alter the code to
ExampleClass.new(color1: #theme.try(obj['color1'].try(:to_sym) || ''))
to catch that.
And then you would have to prettify the code.
How the prettification works will depend on the use case, so I can only offer some general pointer. One way would be to have a default value to avoid having to deal with the null object
Instead of passing around nil, one simply returns a default value:
color_key = obj.fetch('color') { 'default_color' }.to_sym
ExampleClass.new(color1: #theme.send(color_key)))
This makes use of the fetch method which enables returning a default value. That way you will always have a value defined.

Use merge! or deep_merge! to update hash in activerecord model

I have a JSON column in my model that by default is an empty hash.
I want to check if specific keys are present and if not to merge the empty hash with a default hash with the keys.
In my model I am checking if a utility (water, gas, or electric is present) and if not then insert this default hash:
def default_config
{:config => {"features" => {"utilities" => {"water" => true, "gas" => true, "electric" => true}}}}
end
this is how I'm checking for whether a utility key is present:
def water
has_water? || parent.has_water?
end
which in turn calls these methods (all in my model):
def utility(util)
self[:config].try(:fetch, "features", nil).try(:fetch, "utilities", nil).try(:fetch, "#{util}", nil)
end
def has_water?
utility("water") == true
end
This is in order to be able to configure the JSON column whether or not the keys already present, which I'm attempting here:
def set_water(boolean)
new_val = cleaned_boolean(boolean)
water ? nil : self[:config].deep_merge!(default_config)
self[:config]["features"]["utilities"]["water"] = new_val
end
When I test this I'm getting
undefined method `[]=' for nil:NilClass
error when trying to set a utility value indicating that my default_config is not being merged into the existing empty hash.
reverse_merge! is the usual way to set defaults for a Hash in rails.
self[:config].reverse_merge!(default_config)
this is essentially equal to:
default_config.merge!(self[:config])
Leaving everything in self[:config] untouched and just merging in the missing key value pairs from default_config.
Also this ternary expression:
water ? nil : self[:config].deep_merge!(default_config)
is more idiomatically written as (using Hash#reverse_merge!)
self[:config].reverse_merge!(default_config) unless water
and since water returns a boolean value it is generally written as a question e.g. water? (like in has_water?) Not sure if the water method is used frequently but I would refactor as
def has_water?(include_parent=false)
utility("water") == true || (include_parent && parent.has_water?)
end
Then call as:
self[:config].reverse_merge!(default_config) unless has_water?(true)

For a hash of objects in ruby, hash.keys.index(obj) works but hash.has_key?(obj) does not. Why?

Here's my Rails class
class SkinnyEmployee
include ActiveModel::Validations
attr_accessor :uid, :name
validates :uid, :presence => true
def initialize(id, name)
#uid = id
#name = name
end
def ==(other)
puts "Calling =="
raise ArgumentError.new("other is nil or bad in "+self.to_s) if other.nil? or !other.instance_of?(SkinnyEmployee)
return (self.class == other.class && self.uid == other.uid)
end
alias :eql? :==
end
I have a hash of SkinnyEmployee objects. E.g.,
skinny_hash = {SkinnyEmployee.new("123", "xyz") => 1, SkinnyEmployee.new("456", "abc") => 2}
I have another SkinnyEmployee object that I want to look up. E.g.,
entry = SkinnyEmployee.new("456", "abc")
When I do
skinny_hash.keys.index(entry)
I get 1, as expected. But when I do
skinny_hash.has_key?(entry)
I get false.
Why is that? Doesn't has_key? also use == or eql? to find whether a key exists in a hash?
Thanks much for the help!
First, this drove me nuts. What you're doing looked absolutely correct to me, and, as you already know, doesn't work.
I can take you part of the way to a solution:
http://ruby-doc.org/core-2.0.0/Object.html#method-i-hash
quoting:
Generates a Fixnum hash value for this object. This function must have the property that a.eql?(b) implies a.hash == b.hash.
The hash value is used along with eql? by the Hash class to determine if two objects reference the same hash key. Any hash value that exceeds the capacity of a Fixnum will be truncated before being used.
I added:
def hash
1
end
to your SkinnyEmployee Class, and has_key? started returning true. Obviously that's not a solution, but I'm thinking it at least puts you on the path to one.
You have overwritten the eql? method used by Array#index but not the hash method used by Hash#has_key?.
From Ruby docs for Object#hash
Generates a Fixnum hash value for this object. This function must have the property that a.eql?(b) implies a.hash == b.hash.
The Object#hash and Object#eql? methods return equal if and only if the objects occupy the same space in memory. Some classes like Array overwrite both methods to return true if the compared array's have same elements.
For your case you can define the hash method like:
def hash
"#{self.class}_#{self.uid}".hash
end
This would satisfy the docs criteria for hash method given above.
That is happening because the object you are using as a key and they one you are using to search the key are different.
Every time you call SkinnyEmployee.new it will create a new, different, object. For example
employee_1 = SkinnyEmployee.new("123", "xyz")
employee_2 = SkinnyEmployee.new("123", "xyz")
employee_1 == employee_1 #=> true
employee_2 == employee_2 #=> true
employee_2 == employee_1 #=> false
If you call object_id on both employee_1 and employee_2 you will notice that it gives you different id's.
Using has_key? will check for the exact same object, and that won't be the case if you use SkinnyEmployee.new("456", "abc").
You would need a way to retrieve the exact same object, store it in a variable or in the DB, you are using as a key and use it as an attribute for has_key? for it to work.
Hope this can help you.

How do I submit a boolean parameter in Rails?

I'm submitting a parameter show_all with the value true. This value isn't associated with a model.
My controller is assigning this parameter to an instance variable:
#show_all = params[:show_all]
However, #show_all.is_a? String, and if #show_all == true always fails.
What values does Rails parse as booleans? How can I explicitly specify that my parameter is a boolean, and not a string?
UPDATE: Rails 5:
ActiveRecord::Type::Boolean.new.deserialize('0')
UPDATE: Rails 4.2 has public API for this:
ActiveRecord::Type::Boolean.new.type_cast_from_user("0") # false
PREVIOUS ANSWER:
ActiveRecord maintains a list of representations for true/false in https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/column.rb
2.0.0-p247 :005 > ActiveRecord::ConnectionAdapters::Column.value_to_boolean("ON")
2.0.0-p247 :006 > ActiveRecord::ConnectionAdapters::Column.value_to_boolean("F")
This is not part of Rails' public API, so I wrapped it into a helper method:
class ApplicationController < ActionController::Base
private
def parse_boolean(value)
ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
end
and added a basic test:
class ApplicationControllerTest < ActionController::TestCase
test "parses boolean params" do
refute ApplicationController.new.send(:parse_boolean, "OFF")
assert ApplicationController.new.send(:parse_boolean, "T")
end
end
I wanted to comment on zetetic answer but as I can't do that yet I'll post this as an answer.
If you use
#show_all = params[:show_all] == "1"
then you can drop ? true : false because params[:show_all] == "1" statement itself will evaluate to true or false and thus ternary operator is not needed.
This question is rather old, but since I came across this issue a couple of times, and didn't like any of the solutions proposed, I hacked something myself which allows to use multiple strings for true such as 'yes', 'on', 't' and the opposite for false.
Monkey patch the class String, and add a method to convert them to boolean, and put this file in /config/initializers as suggested here: Monkey Patching in Rails 3
class String
def to_bool
return true if ['true', '1', 'yes', 'on', 't'].include? self
return false if ['false', '0', 'no', 'off', 'f'].include? self
return nil
end
end
Notice that if the value is none of the valid ones either for true or false, then it returns nil. It's not the same to search for ?paid=false (return all records not paid) than ?paid= (I don't specify if it has to be paid or not -- so discard this).
Then, following this example, the logic in your controller would look like this:
Something.where(:paid => params[:paid].to_bool) unless params[:paid].try(:to_bool).nil?
It's pretty neat, and helps to keep controllers/models clean.
#show_all = params[:show_all] == "1" ? true : false
This should work nicely if you're passing the value in from a checkbox -- a missing key in a hash generates nil, which evaluates to false in a conditional.
EDIT
As pointed out here, the ternary operator is not necessary, so this can just be:
#show_all = params[:show_all] == "1"
You could change your equality statement to:
#show_all == "true"
If you want it to be a boolean you could create a method on the string class to convert a string to a boolean.
I think the simplest solution is to test "boolean" parameters against their String representation.
#show_all = params[:show_all]
if #show_all.to_s == "true"
# do stuff
end
Regardless of whether Rails delivers the parameter as the String "true" or "false" or an actual TrueClass or FalseClass, this test will always work.
You could just do
#show_all = params[:show_all].downcase == 'true'
It's worth noting that if you're passing down a value to an ActiveModel in Rails > 5.2, the simpler solution is to use attribute,
class Model
include ActiveModel::Attributes
attribute :show_all, :boolean
end
Model.new(show_all: '0').show_all # => false
As can be seen here.
Before 5.2 I use:
class Model
include ActiveModel::Attributes
attribute_reader :show_all
def show_all=(value)
#show_all = ActiveModel::Type::Boolean.new.cast(value)
end
end
Model.new(show_all: '0').show_all # => false
Another approach is to pass only the key without a value. Although using ActiveRecord::Type::Boolean.new.type_cast_from_user(value) is pretty neat, there might be a situation when assigning a value to the param key is redundant.
Consider the following:
On my products index view by default I want to show only scoped collection of products (e.g. those that are in the stock). That is if I want to return all the products, I may send myapp.com/products?show_all=true and typecast the show_all parameter for a boolean value.
However the opposite option - myapp.com/products?show_all=false just makes no sense since it will return the same product collection as myapp.com/products would have returned.
An alternative:
if I want to return the whole unscoped collection, then I send myapp.com/products?all and in my controller define
private
def show_all?
params.key?(:all)
end
If the key is present in params, then regardless of its value, I will know that I need to return all products, no need to typecast value.
You can add the following to your model:
def show_all= value
#show_all = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
You could convert all your boolean params to real booleans like this:
%w(show_all, show_featured).each do |bool_param|
params[bool_param.to_sym] = params[bool_param.to_sym] == "true"
end
In this solution, nil parameters would become false.
While not explicitly what the question is about I feel this is appropriately related; If you're trying to pass true boolean variables in a rails test then you're going to want the following syntax.
post :update, params: { id: user.id }, body: { attribute: true }.to_json, as: :json
I arrived at this thread looking for exactly this syntax, so I hope it helps someone looking for this as well. Credit to Lukom

Resources