Ruby Hash.merge with specified keys only - ruby-on-rails

I'm pretty sure I saw on a Rails related site something along the lines of:
def my_function(*opts)
opts.require_keys(:first, :second, :third)
end
And if one of the keys in require_keys weren't specified, or if there were keys that weren't specified, an exception was raised. I've been looking through ActiveSupport and I guess I might be looking for something like the inverse of except.
I like to try and use as much of the framework as possible compared to writing my own code, that's the reason I'm asking when I know how to make the same functionality on my own. :)
At the moment I'm doing it through the normal merge routine and making sure that I have what I need with some IFs.

I think the method you're thinking of is assert_valid_keys (documentation here) but this only raises an exception if any unexpected keys exist in the hash, not if any of the specified keys are missing. i.e. if a hash is being used to pass options to a method it can be used to check for invalid options not for required options.

You can do this yourself relatively easily. As was stated in an earlier answer, half your work is done for you in assert_valid_keys. You can roll your own method to do the rest.
def my_function( *opts )
opts.require_and_assert_keys( :first, :second, :third )
end
create lib/hash_extensions.rb with the following:
class Hash
def require_and_assert_keys( *required_keys )
assert_valid_keys( keys )
missing_keys = required_keys.inject(missing=[]) do |missing, key|
has_key?( key ) ? missing : missing.push( key )
end
raise( ArgumentError, "Missing key(s): #{missing_keys.join( ", ")}" ) unless missing_keys.empty?
end
end
finally, in config/environment.rb, add this to make it work:
require 'hash_extensions'

Related

What is the use of ! in rails

What is the use of ! in rails?
Especially in this line: From HArtl tutorial
users = User.order(:created_at).take(6)
50.times do
content = Faker::Lorem.sentence(5)
user.each { |user| user.microposts.create!( content: content )}
end
Basically this is creating tweets/microposts for 6 users.
I am really wondering why need to use !
The important thing to remember is that in Ruby a trailing ! or ? are allowed on method names and become part of the method name, not a modifier added on. x and x! and x? are three completely different methods.
In Ruby the convention is to add ! to methods that make in-place modifications, that is they modify the object in fundamental ways. An example of this is String#gsub which returns a copy, and String#gsub! which modifies the string in-place.
In Rails this has been ported over to mean that as well as situations where the method will raise an exception on failure instead of returning nil. This is best illustrated here:
Record.find_by(id: 10) # => Can return nil if not found
Record.find_by!(id: 10) # => Can raise ActiveRecord::RecordNotFound
Note that this is not always the case, as methods like find will raise exceptions even without the !. It's purely an informational component built into the method name and does not guarantee that it will or won't raise exceptions.
Update:
The reason for using exceptions is to make flow-control easier. If you're constantly testing for nil, you end up with highly paranoid code that looks like this:
def update
if (user.save)
if (purchase.save)
if (email.sent?)
redirect_to(success_path)
else
render(template: 'invalid_email')
end
else
render(template: 'edit')
end
else
render(template: 'edit')
end
end
In other words, you always need to be looking over your shoulder to be sure nothing bad is happening. With exceptions it looks like this:
def update
user.save!
purchase.save!
email.send!
redirect_to(success_path)
rescue ActiveRecord::RecordNotFound
render(template: 'edit')
rescue SomeMailer::EmailNotSent
render(template: 'invalid_email')
end
Where you can see the flow is a lot easier to understand. It describes "exceptional situations" as being less likely to occur so they don't clutter up the main code.

Set Parameter if blank

I need to set the id parameter to a value if it is wasn't submitted with the form.
Is it ok to do something like this in Rails or does this violate any standards or cause possible issues?
if params[:cart][:cart_addresses_attributes]["0"][:id].blank?
params[:cart][:cart_addresses_attributes]["0"][:id] = 1234 #default id
end
My implementation works with this logic, but I am not sure if this is the proper way to handle the issue.
There's a chance [:record_type] is nil which will lead to an undefined method error when you attempt to call [:id] on nil. Additionally, I'd find it a bit weird to directly mutate params, even though you technically can do that. I'd consider using Strong Parameter processing methods like so (added a full action, which isn't in your sample, to give more context on how this would be used):
def create
#record_type = RecordType.new(record_type_params)
if record_type.save
redirect_to #record_type
else
render :new
end
end
def record_type_params
params.require(:record_type).permit(:id).reverse_merge(id: 1234)
end
The reverse_merge call is a way to merge the user-supplied parameters into your defaults. This accomplishes what you're after in what I would consider a more conventional way and doesn't mutate params.
def cart_params
params.require(:cart).permit(:cart_addresses_attributes => [:id]).tap do |p|
p[:cart_addresses_attributes]["0"][:id] ||= 1234
end
end
if params[:record_type][:id].nil? # or replace ".nil?" with "== nil"
params[:record_type][:id] = 1234
end
personally, this is the way I prefer to do it. Some ways are more efficient than others, but if that works for you I'd roll with it.

In Rails, how can a mass-assignment be made to an existing model object without warnings?

In a Rails application it's possible to assign some values from params to an existing model object, like this:
model.attributes = params
This would obviously return a ForbiddenAttributesError but that can be avoided like this:
model.attributes = params.permit(:a, :b, :c)
However, although that works, it still outputs a message to the console if params contains keys that are not mentioned in the call to permit:
Unpermitted parameters: d, e, f
The warning is pointless because it's already known that params contains additional keys and permit is being used to select the required subset. It can be avoided by doing
model.attributes = params.slice(:a, :b, :c).permit!
Is there a more appropriate way to do this assignment that does not require having to slice the hash first?
The warning is pointless because it's already known that params contains additional keys and permit is being used to select the required subset.
You mistake the purpose of this warning. It is very valuable when you debug why your form doesn't update this new field you added just today. With the warning, looking at the server log you can quickly find out that you forgot to update strong_params filtering.
Also, the warning will only be shown in dev/test environments. It won't appear in production.
But suppose you really hate that warning. In which case you can do this in your app config:
# possible values: :raise, :log
config.action_controller.action_on_unpermitted_parameters = false
BTW, your code is a bit unorthodox. The common way is to keep the filtering code in one method and call it from both create/update (and whereever else you need it)
def update
#product = ...
if #product.update_attributes(product_params)
...
else
...
end
end
private
def product_params
params.require(:product).permit(:a, :b, :c)
end

Nested arrays in Strong Parameters in Rails

Faced a strange problem or bug.
i have a route with strong parameters method:
def st_params
params.permit([:id, logs: [], metric_ids: []])
end
Then i call it with, for example, rspec:
post some_path(format:json, metric_ids:[ [1,0,1], [5,1,4] ])
And when i call a st_params method there is no metric_ids param and i have got message in logs: Unpermitted parameters: metric_ids
But if i change st_params method like this:
def st_params
p = params.permit([:id, logs: [], metric_ids: []])
p[:metric_ids] = params[:metric_ids]
# or just p = params.permit!
p
end
Now, everything works fine in browser, but it looks strange.
But in rspec i have received nil value for metric_ids in any case :( I have not found any information about the problem. Maybe someone here may help me.
Thanks in advance.
2 things that may be causing your trouble here:
1) The bang method permit! is for accepting an entire hash of parameters and it does not take any arguments. I also am unsure if permit! can be called on the entire params hash, the documentation is unclear on this.
2) Your use of array notation may be causeing the params hash some confusion. (It also may be acceptable, but looks a little unorthodox compared to what I'm used to seeing.) I would write your st_params as
def st_params
params.permit(:id, :log, :metric_ids)
end
Also, a permit argument like metric_ids: [] as you have written will whitelist any parameters nested inside of params[:metric_ids], based on the way you are passing data to this hash above, I do not think this is your intended usage. It appears that you are storing an array at this level of the hash and there is no further complexity, so no need to whitelist params beyond this scope.
Hope this helps!
I was facing same problem. Appending as: :json to post solved the problem for me.
Ref: https://github.com/rspec/rspec-rails/issues/1700#issuecomment-304411233

Ruby 2 Keyword Arguments and ActionController::Parameters

I have a rails 4 application that is running on ruby 2.1. I have a User model that looks something like
class User < ActiveModel::Base
def self.search(query: false, active: true, **extra)
# ...
end
end
As you can see in the search method I am attempting to use the new keyword arguments feature of ruby 2.
The problem is that when I call this code from in my controller all values get dumped into query.
params
{"action"=>"search", "controller"=>"users", query: "foobar" }
Please note that this is a ActionController::Parameters object and not a hash as it looks
UsersController
def search
#users = User.search(params)
end
I feel that this is because params is a ActionController::Parameters object and not a hash. However even calling to_h on params when passing it in dumps everything into query instead of the expected behavior. I think this is because the keys are now strings instead of symbols.
I know that I could build a new hash w/ symbols as the keys but this seems to be more trouble than it's worth. Ideas? Suggestions?
Keywords arguments must be passed as hash with symbols, not strings:
class Something
def initialize(one: nil)
end
end
irb(main):019:0> Something.new("one" => 1)
ArgumentError: wrong number of arguments (1 for 0)
ActionController::Parameters inherits from ActiveSupport::HashWithIndifferentAccess which defaults to string keys:
a = HashWithIndifferentAccess.new(one: 1)
=> {"one"=>1}
To make it symbols you can call symbolize_keys method. In your case: User.search(params.symbolize_keys)
I agree with Morgoth, however, with rails ~5 you will get a Deprecation Warning because ActionController::Parameters no longer inherits from hash. So instead you can do:
params.to_unsafe_hash.symbolize_keys
or if you have nested params as is often the case when building api endpoints:
params.to_unsafe_hash.deep_symbolize_keys
You might add a method to ApplicationController that looks something like this:
def unsafe_keyworded_params
#_unsafe_keyworded_params ||= params.to_unsafe_hash.deep_symbolized_keys
end
You most likely do need them to be symbols. Try this:
def search
#users = User.search(params.inject({}){|para,(k,v)| para[k.to_sym] = v; para}
end
I know it's not the ideal solution, but it is a one liner.
In this particular instance I think you're better off passing the params object and treating it as such rather than trying to be clever with the new functionality in Ruby 2.
For one thing, reading this is a lot clearer about where the variables are coming from and why they might be missing/incorrect/whatever:
def search(params)
raise ArgumentError, 'Required arguments are missing' unless params[:query].present?
# ... do stuff ...
end
What you're trying to do (in my opinion) only clouds the issue and confuses things when trying to debug problems:
def self.search(query: false, active: true, **extra)
# ...
end
# Method explicitly asks for particular arguments, but then you call it like this:
User.search(params)
Personally, I think that code is a bit smelly.
However ... personal opinion aside, how I would fix it would be to monkey-patch the ActionController::Parameters class and add a #to_h method which structured the data as you need it to pass to a method like this.
Using to_unsafe_hash is unsafe because it includes params that are not permitted. (See ActionController::Parameters#permit) A better approach is to use to_hash:
params.to_hash.symbolize_keys
or if you have nested params:
params.to_hash.deep_symbolize_keys
Reference: https://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-to_hash

Resources