Ruby 2 Keyword Arguments and ActionController::Parameters - ruby-on-rails

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

Related

How it works find_by_* in rails

May be its weird for some people about the question. By looking at the syntax its identifiable as class method.
Model.find_by_*
So if its class method it should be defined either in model we created or in
ActiveRecord::Base
So my question is how rails manages to add these methods and makes us available.
Examples like
Model.find_by_id
Model.find_by_name
Model.find_by_status
and etc.
You need to look at ActiveRecord::FinderMethods. Here you can find more details.
Internally, it fires a WHERE query based on attributes present in find_by_attributes. It returns the first matching object.
def find_by_attributes(match, attributes, *args)
conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
result = where(conditions).send(match.finder)
if match.bang? && result.nil?
raise RecordNotFound, "Couldn't find #{#klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
else
yield(result) if block_given?
result
end
end
There is also find_all_by_attributes that returns all matching records.
Rails are using ruby metaprogramming method_missing for that. The method find_by_name is not in a model, instead of this rails are taking name as first argument and it calls it like find_by(name: ?) which is calling where(name: ?).take

Preferred Workflow for Ruby-YAML interaction

I have just started working on a project where there is a lot of interaction between Ruby and 5-6 levels deep YAML files. Since Ruby will respond with NoMethodError: undefined method '[]' for nil:NilClass when you are trying to access a key that doesn't exist there are lots of methods with the following setup:
def retrieve_som_data(key1, key2)
results = []
if data(key1, key2)
if data_set_2(key, key2)["my_key"]
results = data_set_2(key, key2)["my_other_key"]
end
end
return results.clone
end
This looks horrible, so I am looking at a way to refactor it. I have tried working on a version where I would replace a method like this:
def data(key1, key2)
if data = names_data(key1)
return data[key2]
end
end
with this instead:
def data(key1, key2)
names_data(key1).fetch(key2)
end
This raises a more specific error KeyError which can than be rescued and acted on in any method calling .data(), but this also doesn't seem like a good solution readability wise.
I'd love to get some input on how you are handling situations where you are trying to access YAML_DATA[key][key1][key2][key3][key4] and take into account that any of the provided keys could hit something thats nil.
What are your preferred workflows for this?
If you're using rails, they've added a method, try to Object. For nil objects this will always return nil, rather than throwing so you could do something along the lines of this:
def get_yaml_obj(yaml_data, key1, key2, key3)
yaml_data.try(:[], key1).try(:[], key2).try(:[], key3)
end
Or if you have an arbitary number of keys:
def get_data(yaml_data, keys)
keys.each do |key|
yaml_data = yaml_data.try(:[], key)
end
yaml_data
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

How does params[:id] not throw an exception even if we do not explicitly whitelist :id?

In rails 4.x, strong_parameters require parameters to be explicitly permitted. Yet, in the following example, I do NOT get a ForbiddenAttributesError - why does :id not throw when in the show action even though it is not explicitly permitted?
def FooController
...
def show
#foo = Foo.find(params[:id]) # why no exception here?
end
private
def foo_params
params.require(:foo).permit(:name, :address) # note: No :id here
end
end
See: http://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters
"With strong parameters, Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been whitelisted."
Doing a find is completely valid, and is, in fact, shown in the example in the documentation linked to, above.
Strong parameters are used only for assignment of attributes. You can freely search and perform other operations with any param, just not mass assignment.
You can see more in-depth explanation and examples in Rails Guides
For Rails, params[:id] outside from default params.
Query string:
www.example.com/foo/123?bar=1&baz=2
Request path:
www.example.com/foo/123 where 123 is params[:id]
Paramerts:
bar=1&baz=2 this can be permitted
If you pass 123 to parameters then you need permitted :id.
There is no need of explicitly permitting the :id unless you want to.Rails will do it implicitly.If want to check whether the :id is whitelisted or not,you can do puts params[:foo] after it is created or you can just see the log.you will see something like this
{id=>some_id, "name"=>"some_name", "adddress"=>"some_address"}
So,defining a Foo object like this
#foo = Foo.find(params[:id])
will not throw an exception.
Hope it helped!

How to transform a string into a variable/field?

I'm new to Ruby and I would like to find out what the best way of doing things is.
Assume the following scenario:
I have a text field where the user can input strings. Based on what the user inputs (after validation) I would like to access different fields of an instance variable.
Example: #zoo is an instance variable. The user inputs "monkey" and I would like to access #zoo.monkey. How can I do that in Ruby?
One idea that crossed my mind is to have a hash:
zoo_hash = { "monkey" => #zoo.monkey, ... }
but I was wondering if there is a better way to do this?
Thanks!
#zoo.attributes gives you a hash of the object attributes. So you can access them like
#zoo.attributes['monkey']
This will give nil if the attribute is not present. Calling a method which doesn't exist will throw NoMethodError
In your controller you could use the public_send (or even send) method like this:
def your_action
#zoo.public_send(params[:your_field])
end
Obviously this is no good, since someone can post somehing like delete_all as the method name, so you must sanitize the value you get from the form. As a simple example:
ALLOWED_METHODS = [:monkey, :tiger]
def your_action
raise unless ALLOWED_METHODS.include?(params[:your_field])
#zoo.public_send(params[:your_field])
end
There is much better way to do this - you should use Object#send or (even better, because it raises error if you try to call private or protected method) Object#public_send, like this:
message = 'monkey'
#zoo.public_send( message )
You could implement method_missing in your class and have it interrogate #zoo for a matching method. Documentation: http://ruby-doc.org/core-1.9.3/BasicObject.html#method-i-method_missing
require 'ostruct' # only necessary for my example
class ZooKeeper
def initialize
#zoo = OpenStruct.new(monkey: 'chimp')
end
def method_missing(method, *args)
if #zoo.respond_to?(method)
return #zoo.send(method)
else
super
end
end
end
keeper = ZooKeeper.new
keeper.monkey #=> "chimp"
keeper.lion #=> NoMethodError: undefined method `lion'

Resources