Rails, strong parameters, and complex data structures - ruby-on-rails

Good aftern, SO folks
I am strong parameterizing a Rails 3 application that we plan to upgrade to Rails 4. Some of the controllers use the params object to hold not just nested hashes, but hashes within arrays within hashes within arrays etc. Changing the nature of the data structure would be too intense, we want to ideally have it return the same data structure, but strong parameterized
Here's an example as JSON:
"my_example" => {
"options" =>
[{"id" => "1"
"name" => "claire"
"keywords" =>
["foo", "bar"]
},
{"id" => "2",
"name" => "marie",
"keywords =>
["baz"]
}],
"wut" => "I know, right?"
}
But for added fun, the keywords array can contain any string. Which I've read about and which is tricky and supported in other versions of rails but whatever.
Are there any general rules of thumb about making complex data structures with the strong_parameters gem? I know that Rails 4 and 5 handle this better, but I'm curious.

Nested parameters are not really that challenging.
params.require(:my_example)
.permit(:wutz, options: [:id, :name, keywords: []])
This expects that options is an array of resources where the keys :id, :name, and :keywords are to be whitelisted.
:wutz, :id, :name can be any permitted scalar type. keywords: [] permits an array of any scalar type (any string, integer, date, etc). I don't really get why you're fretting here.
The issue is mainly with nested hashes with extremely dynamic contents. In that case which is not quite covered the Rails strong parameters you can use .permit! and unleash the full tools of Ruby hash slicing and dicing which are quite formidable.
The gem pretty much backports the api of ActionController::Parameters in later versions of Rails pretty closely so I would not expect any major hickups when upgrading.
https://github.com/rails/strong_parameters#nested-parameters

Related

Rails Strong Params how to Permit a nested Array

I have the following params:
params={"data"=>
{"type"=>"book",
"id"=>14,
"attributes"=>
{"id"=>14,
"created_at"=>"2022-06-27 21:15:39",
"title"=>"sdfdsf",
"targeting"=> { "release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]],
"days"=>["Monday", "Tuesday", "Wednesday"],
"gender"=>["male", "female"]
}
}
}
When I use this, I can get every value but release_times is always null:
When I use this:
safe_params = params.require(:data).permit( attributes: [:id, :created_at, :title, { targeting: {} }])
How can I extract the release times value?
I tried doing this
safe_params = params.require(:data).permit( attributes: [:id, :created_at, :title, { targeting: [:days, :gender, release_times:[]]}])
But I get the error:
Validation failed: Targeting gender should be a list of values, Targeting days should be a list of values
How can I extract all the values from targeting including the release_times?
As Ruby on Rails API states, when using ActionController::Parameters you want to declare that a parameter should be an array (list) by mapping it to a empty array. Like you did with release_times.
You should permit targeting params with [days: [], gender: []] instead of [:days, :gender]. This should solve the error.
But even them, release_times is an array of arrays, which I believe is not supported at the moment (there is an old issue for it).
One way you could bypass this would be by changing the way you're communicating release_times. Using an arrays of hashes instead of nested arrays.
From this:
"release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]]
To this (or something similar):
"release_times"=>[{"start" => "4:00", "end"=>"5:00"}, {"start" =>"5:00", "end" => "6:00"}]
That way, you could do this:
safe_params = params.require(:data).permit(attributes: [:id, :created_at, :title, { targeting: [days: [], gender: [], release_times: [:start, :end]] }])
Exactly how you would implement that is up to you, but I hope it helps.
**Also, there was a typo with release_times.
You can do some testing yourself. Open rails c and do something like this:
param = ActionController::Parameters.new("targeting"=> { "release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]]})
param.require(:targeting).permit(release_times: []) # > Doesn't return times.
new_param = ActionController::Parameters.new("targeting"=> { "release_times"=>[{"start" => "4:00", "end"=>"5:00"}, {"start" =>"5:00", "end" => "6:00"}] })
new_param.require(:targeting).permit(release_times: [:start, :end]) # > Return times.
Just an observation, using permit! would work. But as strong params doc says:
Extreme care should be taken when using permit! as it will allow all
current and future model attributes to be mass-assigned.
So you could try to slice arguments yourself and them permit! - but I can't tell you that's the way to go.
Learn more about Mass Assignment Vulnerability here.

How to pluck multiple attributes in Rails 3.x?

I have an ActiveRecord model like this:
class Person < ActiveRecord::Base
attr_accessible :name
end
and need to get a hash mapping Person's ids to their names:
{1 => "Paul", 2 => "Aliyah", 3 => ... }
Now, the obvious way would be
Person.all.collect { |p| [p.id, p.name] }.to_h
However, I don't need to instantiate every Person, I just need the hash. In Rails 4, I can .pluck(:id, :name) instead of collect, however in 3.x, pluck takes only one argument. However I found this workaround to get what I want without loading the models:
Person.all.group(:id).minimum(:name)
Question: will I burn in hell? Also, is there a more elegant way to do this, and are there any drawbacks of this hacky approach that I may not be aware of? Thanks!
Here's a pretty good write up of this situation and various tactics for handling it: Plucking Multiple Columns in Rails 3
My preference of suggested solutions there is to make and include a module:
# multi_pluck.rb
require 'active_support/concern'
module MultiPluck
extend ActiveSupport::Concern
included do
def self.pluck_all(relation, *args)
connection.select_all(relation.select(args))
end
end
end
class Person < ActiveRecord::Base
attr_accessible :name
def self.pluck_id_and_name
result = connection.select_all(select(:id, :name))
if result.any?
# if you are using Ruby 2.1+
result.to_h
# Works in 1.9.3+
Hash[result]
end
end
end
Since the result should be an array of arrays we can use nifty trick to get a hash with the first element as keys and the second as values:
Hash[ [ [1, "Joe"], [2, "Jill"] ] ]
# => { 1 => "Joe", 2 => "Jill"}
See:
Convert array of 2-element arrays into a hash, where duplicate keys append additional values
To avoid loading all of the objects you could do this:
hash = Hash.new
ActiveRecord::Base.connection.execute("SELECT id, name FROM persons").each {|person| hash[person['id'].to_s] = person['name'].to_s}
My company used one of the tactics from Plucking Multiple Columns in Rails 3 before.
But we had trouble upgrading from Rails 3 to Rails 4 because it didn't work in Rails 4.
I suggest using pluck_all gem which has high test coverage in Rails 3, 4, 5, so you will not worry about future upgrades.

Rails - Strong Parameters - Nested Objects

I've got a pretty simple question. But haven't found a solution so far.
So here's the JSON string I send to the server:
{
"name" : "abc",
"groundtruth" : {
"type" : "Point",
"coordinates" : [ 2.4, 6 ]
}
}
Using the new permit method, I've got:
params.require(:measurement).permit(:name, :groundtruth)
This throws no errors, but the created database entry contains null instead of the groundtruth value.
If I just set:
params.require(:measurement).permit!
Everything get's saved as expected, but of course, this kills the security provided by strong parameters.
I've found solutions, how to permit arrays, but not a single example using nested objects. This must be possible somehow, since it should be a pretty common use case. So, how does it work?
As odd as it sound when you want to permit nested attributes you do specify the attributes of nested object within an array. In your case it would be
Update as suggested by #RafaelOliveira
params.require(:measurement)
.permit(:name, :groundtruth => [:type, :coordinates => []])
On the other hand if you want nested of multiple objects then you wrap it inside a hash… like this
params.require(:foo).permit(:bar, {:baz => [:x, :y]})
Rails actually have pretty good documentation on this: http://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-permit
For further clarification, you could look at the implementation of permit and strong_parameters itself: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/strong_parameters.rb#L246-L247
I found this suggestion useful in my case:
def product_params
params.require(:product).permit(:name).tap do |whitelisted|
whitelisted[:data] = params[:product][:data]
end
end
Check this link of Xavier's comment on github.
This approach whitelists the entire params[:measurement][:groundtruth] object.
Using the original questions attributes:
def product_params
params.require(:measurement).permit(:name, :groundtruth).tap do |whitelisted|
whitelisted[:groundtruth] = params[:measurement][:groundtruth]
end
end
Permitting a nested object :
params.permit( {:school => [:id , :name]},
{:student => [:id,
:name,
:address,
:city]},
{:records => [:marks, :subject]})
If it is Rails 5, because of new hash notation:
params.permit(:name, groundtruth: [:type, coordinates:[]]) will work fine.

What does :attribute => parameter actually do?

I have a hard time understanding the form :attribute => parameter
Can anyone give me some explanations for it? Is :attribute a field (variable) belonging to the class or something else? Why we can pass this pair as one parameter to methods?
If you're referring to something like this:
some_method(:foo => "bar", :baz => "abc")
then it's just shorthand which causes ruby to convert those things into a Hash. Please note that when using this form, that the hash must be the final argument to the method in order for this to work.
Based on the explanation above, this
some_method(:foo => "bar", :baz => "abc")
is ok, but this
some_method(:foo => "bar", :baz => "abc", moo)
is not.
Though you will see this commonly in Rails, it is not a Rails specific question. It is Ruby.
The answer to your question is that it is key/value pairs in a Hash, generally passed as an argument to a method.
You will see this as well when it is being assigned to a variable directly. But let me show you a sample method, and a sample usage, so that you can put them together:
def some_method(*args, name: 'Joe', amount: 42, **other_params )
puts "#{name}, #{amount}, glob of arguments = #{args.inspect}",
"other params #{other_params}"
end
some_method(:occupation => 'programmer', :phone => '123-456-7890', name: 'Jane')
This is Ruby 2.0.0 specific in the fact that you can provide for that last argument, which provides for unnamed parameters, in practice. Using the 1.9+ syntax for a Hash in the argument list, you are allowed to provide for other unnamed "parameters" which can appear after the hash argument.
Notice that if I had used the older syntax for Hash, namely the :key => 'value' syntax, I would not be allowed (at least as of this writing) to have the **other_params argument at the end of the argument list.
You could also provide the hash using the newer syntax in the calling code, though I left it as the Hash syntax when calling some_method.
The Hash still needs to be the last provided in the calling argument list, the same as indicated in the argument list for the method definition.

Nested attributes in hash representation of a record

I've given up hours of my day trying to accomplish this simple thing in Rails 3.1 with no luck. I've got some models nested 2 levels deep and associated many-to-one with belongs_to/foreign key, like:
TopLevelModel:
MiddleLevelModel:
BottomLevelModel
I am eagerly loading the whole hierarchy in my queries like so:
#model = TopLevelModel.find(1, :include => {:middle_level_children => :bottom_level_children})
The JSON serializer works fine for serializing the nested hierarchy (using the :include option), but this isn't enough for my purposes and I need a (ruby) hash representation of the record's attributes. #model.attributes() would be perfect but it neglects my relations. Is there a way to get a nested hash representation using this method (I read the documentation thoroughly and suspect not, but maybe there's some exotic option I don't know about). To be clear, the representation I am looking for would be:
{
:attribute_1 => 'some attribute', #an attribute of top level model
#...
:middle_level_children: => [{ # type 'MiddleLevelModel'
:attr_1 => 'some attribute of middle level model',
# ...
:bottom_level_children => [{ #type 'BottomLevelModel'
:attr => 'some attribute of bottom level model'
}]
}]
}
This seems like an incredibly simple (and, I would think, common) need, but I've had no luck.
Why can't you iterate through all your child relationships and print all the attributes for each instance of them?
Might be a little hokey but give Hash.from_xml a whirl.
Use the object's to_xml method to serialize with associations and then deserialize with the Hash.from_xml class method.
xml = #model_instance.to_xml(:include=>:middle_level_children)
nested_hash = Hash.from_xml(xml)

Resources