Rails 4 - ActionController can't require 2 keys - ruby-on-rails

Looking at the documentation for ActionController::Parameters for the require method I read the followiing
When given an array of keys, the method tries to require each one of them in order. If it succeeds, an array with the respective return values is returned:
params = ActionController::Parameters.new(user: { ... }, profile: { ... })
user_params, profile_params = params.require(:user, :profile)
but when I run this code with rails console, my output is very different
[70] pry(main)> params = ActionController::Parameters.new(user: { a: 1 }, profile: { b: 2 })
=> {"user"=>{"a"=>1}, "profile"=>{"b"=>2}}
[71] pry(main)> user_params, profile_params = params.require(:user, :profile)
ArgumentError: wrong number of arguments (2 for 1)
from /home/myuser/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/actionpack-4.2.1/lib/action_controller/metal/strong_parameters.rb:244:in `require'
When I read when given an array of keys and saw the example, I thought that maybe they made a mistake when writing the example, so I tried this as well, but it did not work either.
[72] pry(main)> user_params, profile_params = params.require([:user, :profile])
ActionController::ParameterMissing: param is missing or the value is empty: [:user, :profile]
from /home/myuser/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/actionpack-4.2.1/lib/action_controller/metal/strong_parameters.rb:249:in `require'
What is going on here?

You're looking at the API docs for the current version of Rails (which is Rails 5), and you're using Rails 4.2.1. The Rails 4 docs do not specify multiple arguments to require like that:
http://api.rubyonrails.org/v4.2.1/classes/ActionController/Parameters.html#method-i-require

Have you tried:
def user_params
params.require(:user)
end
def profile_params
params.require(:profile)
end
This way you have two separate rules for each model.

Related

Requiring properties in array of hash without looping in Rails

In Rails 4.2, I'd like to validate that every hash of an array passed as a parameter to my action has certain attributes.
For now I could only find how to filter out unwanted attributes, such as:
ActionController::Parameters.new(
points: [{lat: 42, foo: 0}, {lng: 43, bar: 100}]
).permit(
points: [:lat, :lng]
)
# => {"points"=>[{"lat"=>42}, {"lng"=>43}]}
What I'd like to do is making sure every member of points has both lat and lng without having to loop over it. Is this possible using permit or a similar method?
There is a method called require that has the same signature as permit:
params.require(:lat, :lng)
Note that you can chain this with permit
Also, you can use select or reject on the params hash, secure params is mostly sugar for this anyway.
def my_params
required_attrs = %w{lat lng}
missing_params = required_attrs.select do |key|
params.has_key?(key)
end
missing_params.empty? ? params : raise(RuntimeError, "missing params: #{missing_params.join(",")}")
end

Is there a lightweight way to lock down a set of keys on a Hash?

To be clear, I'm perfectly happy implementing this functionality as a custom class myself, but I want to make sure I'm not overlooking some bit of ruby or rails magic. I have googled every meaningful permutation of the keywords "ruby rails hash keys values immutable lock freeze". But no luck so far!
Problem: I need to give a Hash a set of keys, possibly at run time, and then lock the set of keys without locking their values. Something like the following:
to_lock = {}
to_lock[:name] = "Bill"
to_lock[:age] = 42
to_lock.freeze_keys # <-- this is what I'm after, so that:
to_lock[:name] = "Bob" # <-- this works fine,
to_lock[:height] # <-- this returns nil, and
to_lock[:height] = 175 # <-- this throws some RuntimeError
Question: Is there a bit of ruby or rails tooling to allow this?
I know of Object#freeze and of Immutable::Hash, but both lock keys and values.
Sticking with out-of-the-box ruby, the use case could be mostly met by manipulating the methods or accessors of classes at runtime, as in this or this, then overriding #method_missing. But that feels quite a bit clunkier. Those techniques also don't really "lock" the set of methods or accessors, it's just awkward to add more. At that point it'd be better to simply write a class that exactly implements the snippet above and maintain it as needed.
You can achieve this by defining a custom []= for your "to-lock" instance of a hash, after you've added the allowed keys:
x = { name: nil, age: nil }
def x.[]=(key, value)
# blow up unless the key already exists in the hash
raise 'no' unless keys.include?(key)
super
end
x[:name] # nil
x[:name] = "Bob" # "Bob"
x[:size] # nil
x[:size] = "large" # raise no
Note that this won't prevent you from inadvertently adding keys using something like merge!.
#meagar has offered an interesting solution, but has pointed out that it only works when attempting to add a key-value pair using Hash#[]. Moreover, it does not prevent keys from being deleted.
Here's another way, but it's rather kludgy, so I think you should probably be looking for a different way to skin your cat.
class Hash
def frozen_keys_create
self.merge(self) { |*_,v| [v] }.freeze
end
def frozen_keys_get_value(k)
self[k].first
end
def frozen_keys_put_value(k, new_value)
self[k].replace [new_value]
self
end
def frozen_keys_to_unfrozen
self.merge(self) { |*_,v| v.first }
end
end
Now let's put them to use.
Create a frozen hash with each value wrapped in an array
sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
#=> {:cat=>["meow"], :dog=>["woof"]}
sounds.frozen?
#=> true
This prevents keys from being added:
sounds[:pig] = "oink"
#=> RuntimeError: can't modify frozen Hash
sounds.update(:pig=>"oink")
#=> RuntimeError: can't modify frozen Hash
or deleted:
sounds.delete(:cat)
#=> RuntimeError: can't modify frozen Hash
sounds.reject! { |k,_| k==:cat }
#=> RuntimeError: can't modify frozen Hash
Get a value
sounds.frozen_keys_get_value(:cat)
#=> "meow"
Change a value
sounds.frozen_keys_put_value(:dog, "oooooowwwww")
#=> {:cat=>["meow"], :dog=>["oooooowwwww"]}
Convert to a hash whose keys are not frozen
new_sounds = sounds.frozen_keys_to_unfrozen
#=> {:cat=>"meow", :dog=>"oooooowwwww"}
new_sounds.frozen?
#=> false
Add and delete keys
Maybe even add (private, perhaps) methods to add or delete key(s) to override the desired behaviour.
class Hash
def frozen_keys_add_key_value(k, value)
frozen_keys_to_unfrozen.tap { |h| h[k] = value }.frozen_keys_create
end
def frozen_keys_delete_key(k)
frozen_keys_to_unfrozen.reject! { |key| key == k }.frozen_keys_create
end
end
sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
#=> {:cat=>["meow"], :dog=>["oooowwww"]}
new_sounds = sounds.frozen_keys_add_key_value(:pig, "oink")
#=> {:cat=>["meow"], :dog=>["woof"], :pig=>["oink"]}
new_sounds.frozen?
#=> true
newer_yet = new_sounds.frozen_keys_delete_key(:cat)
#=> {:dog=>["woof"], :pig=>["oink"]}
newer_yet.frozen?
#=> true
Sounds like a great use-case for the built-in Struct
irb(main):001:0> s = Struct.new(:name, :age).new('Bill', 175)
=> #<struct name="Bill", age=175>
irb(main):002:0> s.name = 'Bob'
=> "Bob"
irb(main):003:0> s.something_else
NoMethodError: undefined method `something_else' for #<struct name="Bob", age=175>
from (irb):3
from /home/jtzero/.rbenv/versions/2.3.0/bin/irb:11:in `<main>'

Why is this create statement giving the error "ArgumentError: wrong number of arguments (1 for 2)"?

Having trouble with this seemingly simple task of importing a CSV file via a Rake task, and storing in multiple tables.
My rake task:
desc "Imports the COGCC CSV file into wikifrac database"
task :import_cogcc => :environment do
require 'csv'
CSV.foreach('public/partial.csv', :headers => true) do |row|
# create records in independent tables
# create the Company object
this_company_name = row['name'].strip!
this_operator_num = row['operator_num']
if !(Companies.exists?(:company_name => this_company_name))
Companies.create(:company_name => this_company_name, :operator_num => this_operator_num)
end
thecompany = Companies.find(:first, :conditions => ["this_company_name = ?", this_company_name])
company_id = thecompany.id
...
My Companies model:
class Companies < ActiveRecord::Base
has_many :facilities, dependent: :destroy
attr_accessor :company_name, :operator_num
def initialize(company_name, operator_num)
#operator_num = operator_num
#company_name = company_name
end
end
But when I run rake import_partial, I get this error:
rake aborted! ArgumentError: wrong number of arguments (1 for 2)
wfrails/app/models/companies.rb:5:in
initialize'
wfrails/lib/tasks/import_partial.rake:47:in
block (2 levels) in '
wfrails/lib/tasks/import_partial.rake:26:in
`block in ' Tasks: TOP => import_cogcc
Can anyone tell me what's wrong with this? Have been going around and around with this; many similar examples in SO, but cannot quite solve my error....
The error is because you are calling create with a hash in your rake task. The hash is counted as 1 object (the syntax you use has implicit curly braces around it, so you are really passing in {:company_name => this_company_name, :operator_num => this_operator_num}), whereas you have overridden the initialize method to take 2 arguments separately. Hence, it complaining it only find 1 parameter where it expects 2.
Easy way
Since you are using activerecord, I would do what BroiSatse says and just remove your initialize overridden method. This will allow the hash to work correctly, and will automatically set the attributes you send in.
Special case
If for some reason, you want to set those two variables as part of the initialization, I would make sure you call super so that the rest of the object is set correctly.
ruby
def initialize(params={})
super
self.operator_num = params[:operator_num]
self.company_name = params[:company_name]
end
But I only do this if I need these attributes set before I run any of my callbacks.

Stringify keys error when building a hash

I am getting a stringify_keys error.
Currently I am calling the following method which works perfectly:
def attributes
{
city: #content[1..20].strip,
streetname: #content[21..40].strip,
house_number: #content[41..46].strip.to_i
}
end
Now that I am refactoring my code, I need to build the hash from the ground up where the keys and values are populating the hash based on certain conditions (conditions are not written yet).
def attributes
test = {}
test["city"] = #content[1..20].strip
test["streetname"] = #content[21..40].strip
test["house_number"] = #content[41..46].strip.to_i
end
Now I am getting the stringify_keys error. I checked the docs for clues on how to build a hash but there isn't anything that could help me.
Where is the problem? If you need more code, please ask.
The key is symbol in your first piece of code, and you have to return test at last in your second piece of code.
def attributes
test = {}
test[:city] = #content[1..20].strip
test[:streetname] = #content[21..40].strip
test[:house_number] = #content[41..46].strip.to_i
test
end
In Rails with active support you can use symbolize_keys and stringify_keys look example:
=> hash = {"foo" => 1, 'baz' => 13}
=> {"foo"=>1, "baz"=>13}
=> hash.symbolize_keys
=> {:foo=>1, :baz=>13}
and back:
=> hash.symbolize_keys.stringify_keys
=> {"foo"=>1, "baz"=>13}

Equivalent of .try() for a hash to avoid "undefined method" errors on nil? [duplicate]

This question already has answers here:
How to avoid NoMethodError for nil elements when accessing nested hashes? [duplicate]
(4 answers)
Closed 7 years ago.
In Rails we can do the following in case a value doesn't exist to avoid an error:
#myvar = #comment.try(:body)
What is the equivalent when I'm digging deep into a hash and don't want to get an error?
#myvar = session[:comments][#comment.id]["temp_value"]
# [:comments] may or may not exist here
In the above case, session[:comments]try[#comment.id] doesn't work. What would?
You forgot to put a . before the try:
#myvar = session[:comments].try(:[], #comment.id)
since [] is the name of the method when you do [#comment.id].
The announcement of Ruby 2.3.0-preview1 includes an introduction of Safe navigation operator.
A safe navigation operator, which already exists in C#, Groovy, and
Swift, is introduced to ease nil handling as obj&.foo. Array#dig and
Hash#dig are also added.
This means as of 2.3 below code
account.try(:owner).try(:address)
can be rewritten to
account&.owner&.address
However, one should be careful that & is not a drop in replacement of #try. Take a look at this example:
> params = nil
nil
> params&.country
nil
> params = OpenStruct.new(country: "Australia")
#<OpenStruct country="Australia">
> params&.country
"Australia"
> params&.country&.name
NoMethodError: undefined method `name' for "Australia":String
from (pry):38:in `<main>'
> params.try(:country).try(:name)
nil
It is also including a similar sort of way: Array#dig and Hash#dig. So now this
city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)
can be rewritten to
city = params.dig(:country, :state, :city)
Again, #dig is not replicating #try's behaviour. So be careful with returning values. If params[:country] returns, for example, an Integer, TypeError: Integer does not have #dig method will be raised.
The most beautiful solution is an old answer by Mladen Jablanović, as it lets you to dig in the hash deeper than you could with using direct .try() calls, if you want the code still look nice:
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc}
end
end
You should be careful with various objects (especially params), because Strings and Arrays also respond to :[], but the returned value may not be what you want, and Array raises exception for Strings or Symbols used as indexes.
That is the reason why in the suggested form of this method (below) the (usually ugly) test for .is_a?(Hash) is used instead of (usually better) .respond_to?(:[]):
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}
end
end
a_hash = {:one => {:two => {:three => "asd"}, :arr => [1,2,3]}}
puts a_hash.get_deep(:one, :two ).inspect # => {:three=>"asd"}
puts a_hash.get_deep(:one, :two, :three ).inspect # => "asd"
puts a_hash.get_deep(:one, :two, :three, :four).inspect # => nil
puts a_hash.get_deep(:one, :arr ).inspect # => [1,2,3]
puts a_hash.get_deep(:one, :arr, :too_deep ).inspect # => nil
The last example would raise an exception: "Symbol as array index (TypeError)" if it was not guarded by this ugly "is_a?(Hash)".
The proper use of try with a hash is #sesion.try(:[], :comments).
#session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')
Update: As of Ruby 2.3 use #dig
Most objects that respond to [] expect an Integer argument, with Hash being an exception that will accept any object (such as strings or symbols).
The following is a slightly more robust version of Arsen7's answer that supports nested Array, Hash, as well as any other objects that expect an Integer passed to [].
It's not fool proof, as someone may have created an object that implements [] and does not accept an Integer argument. However, this solution works great in the common case e.g. pulling nested values from JSON (which has both Hash and Array):
class Hash
def get_deep(*fields)
fields.inject(self) { |acc, e| acc[e] if acc.is_a?(Hash) || (e.is_a?(Integer) && acc.respond_to?(:[])) }
end
end
It can be used the same as Arsen7's solution but also supports arrays e.g.
json = { 'users' => [ { 'name' => { 'first_name' => 'Frank'} }, { 'name' => { 'first_name' => 'Bob' } } ] }
json.get_deep 'users', 1, 'name', 'first_name' # Pulls out 'Bob'
say you want to find params[:user][:email] but it's not sure whether user is there in params or not. Then-
you can try:
params[:user].try(:[], :email)
It will return either nil(if user is not there or email is not there in user) or otherwise the value of email in user.
As of Ruby 2.3 this gets a little easier. Instead of having to nest try statements or define your own method you can now use Hash#dig (documentation).
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
Or in the example above:
session.dig(:comments, #comment.id, "temp_value")
This has the added benefit of being more like try than some of the examples above. If any of the arguments lead to the hash returning nil then it will respond nil.
#myvar = session.fetch(:comments, {}).fetch(#comment.id, {})["temp_value"]
From Ruby 2.0, you can do:
#myvar = session[:comments].to_h[#comment.id].to_h["temp_value"]
From Ruby 2.3, you can do:
#myvar = session.dig(:comments, #comment.id, "temp_value")
Another approach:
#myvar = session[:comments][#comment.id]["temp_value"] rescue nil
This might also be consider a bit dangerous because it can hide too much, personally I like it.
If you want more control, you may consider something like:
def handle # just an example name, use what speaks to you
raise $! unless $!.kind_of? NoMethodError # Do whatever checks or
# reporting you want
end
# then you may use
#myvar = session[:comments][#comment.id]["temp_value"] rescue handle
When you do this:
myhash[:one][:two][:three]
You're just chaining a bunch of calls to a "[]" method, an the error occurs if myhash[:one] returns nil, because nil doesn't have a [] method. So, one simple and rather hacky way is to add a [] method to Niclass, which returns nil: i would set this up in a rails app as follows:
Add the method:
#in lib/ruby_extensions.rb
class NilClass
def [](*args)
nil
end
end
Require the file:
#in config/initializers/app_environment.rb
require 'ruby_extensions'
Now you can call nested hashes without fear: i'm demonstrating in the console here:
>> hash = {:foo => "bar"}
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:doo]
=> nil
>> hash[:doo][:too]
=> nil
Andrew's answer didn't work for me when I tried this again recently. Maybe something has changed?
#myvar = session[:comments].try('[]', #comment.id)
The '[]' is in quotes instead of a symbol :[]
Try to use
#myvar = session[:comments][#comment.id]["temp_value"] if session[:comments]

Resources