Rails permit nested hash attributes - ruby-on-rails

The controller receives JSON object
{
user: {
name: "string",
details: {
info1: "string",
info2: []
}
}
}
During permission controller knows that can permit some defined fields - as name - and hash field details with all nested attributes - also with arrays. What is the correct solution for this situation?
BAD SOLUTIONS
permit cannot be used, because I must select user permitted fields
tap do |whitelisted| cannot be used, because it doesn't make that fields "permit"
case below cannot be user, because with arrays doesn't work
details_keys = params[:user][:details].keys
params.require(:user).permit(:name, details: details_keys)

If you want to permit a key having an array of permitted scalar values, then simply map the key to an empty array:
params.permit(key: [])
The permitted scalar types are String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO, ActionDispatch::Http::UploadedFile, and Rack::Test::UploadedFile
So when an array contains some non-scalar values like a hash, then you have to go further by permitting the nested keys too in the array.
Say, you have the following structure:
{
key: [
{
attr1: 'string',
attr2: 10
},
{
attr1: 'another string',
attr2: 100
}
]
}
then the permission goes in this way:
params.permit(key: [:attr1, :attr2])
Now let's assume your case looks like:
{
user: {
name: "sting",
details: {
info1: "string",
info2: [1, true, :sym] // assume it contains only permitted scalar values
}
}
}
the permission will be:
params.require(:user).permit(:name, details: [:info1, info2: []])
To automate this, lets assume details has 5 attributes with permitted scalar values and 3 more array attributes that also have only scalar values.
First pluck the 5 non-array keys of details:
non_array_keys = params[:user][:details].reject { |_, v| v.class == Array }.keys
Next the 3 array keys inside details:
array_keys = params[:user][:details].select { |_, v| v.class == Array }.keys.map { |k| { k => [] } }
Now the details_keys will be ready by:
details_keys = non_array_keys << array_keys
details_keys.flatten!
Final permission will look like:
params.require(:user).permit(:name, details: details_keys)
If the nested arrays would contain non-scalar values, then I guess you have got enough idea by this point on how to adapt to your changes!
DISCLAIMER: this automation is not appreciated because, instead of doing all these, a simple invocation of params.require(:user).permit! would suffice. But this marks the :user parameters hash and any sub-hash of it as permitted and does not check for permitted scalars, anything is accepted. Extreme care should be taken when using permit!, as it will allow all current and future model attributes to be mass-assigned.
For details, I would strongly suggest to look at the Rails official guide covering Strong Parameters in details.

Adding empty array might work. You can switch off validation for nested attributes?! Are you creating dynamic random input fields which you do not control?

Do you want to pass dynamic fields?
If that is the case, the following may work
Configuring strong parameters for dynamic keys

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.

Is there a more efficient way of returning a hash value given array of keys if hash key/value is present?

I have a method which accepts a hash, and I have an array of keys (ordered by preference) which I want to check the hash for and return the value of the first matching key that's found which is not blank?. So far I have the following, but since I utilize this method heavily, I'm wondering if there is a more efficient way of going about it?
result = [:title, :name, :identifier, :slug].each do |key|
if my_hash[key].present?
return my_hash[key]
break
end
end
So given the following hash:
{
id: 10,
title: "",
name: "Foo",
slug: "foo"
}
I would expect result to be: "Foo"
Your way is probably close to the best from the point of view of efficiency. But if you want it to be more elegant, retaining the efficiency, then:
my_hash[%i[title name identifier slug].find{|key| my_hash[key].present?}]
You can do the following:
whitelisted_keys = [:title, :name, :identifier, :slug]
filtered_hash = your_hash.slice(*whitelisted_keys) # will only get pair matching your whitelisted_keys
# return all `.present?` key/values pairs:
filtered_hash.select{ |k,v| v.present? }
# return an array of the first key/value pair present:
filtered_hash.find{ |k,v| v.present? } # append .last to get the value
As Sawa pointed out in the comment, this is not the most efficient way but might be more "readable"

A route for Grape: either a specific string or an of array of string

I want to create a route for the Grape gem so that a route accepts either an array of strings or a single string with a specific pre-defined value. From the documentation it's not clear how to do that.
Your suggestions?
UPDATE:
I want status to be passed either a single value status1 or as a array where values can be arbitrary and unknown. I combine these?
params do
requires :status, type: Symbol, values: [:status1]
requires :status, type: Array[String]
end
A parameter must be declared only once in the params block. If you declare it twice then only one will be used by Grape. In your case, there're two options to solve your problem.
First option: declare two parameters and define them as mutually exclusive. It means that the user will be able to inform only one of them.
params do
requires :status1, type: Symbol, values: [:status1]
requires :status2, type: Array[String]
mutually_exclusive :status1, :status2
end
Second option: declare only one parameter and set its type to Object. In the method's body, check if it's an Array or a String. If it's a String, verify if it has the correct values.
params do
requires :status, type: Object
end
get 'testing' do
if params[:status].class.name.eql? "Array" then
elsif params[:status].class.name.eql? "String" then
end
end

Rails - permit a param of unknown type (string, hash, array, or fixnum)

My model has a custom_fields column that serializes an array of hashes. Each of these hashes has a value attribute, which can be a hash, array, string, or fixnum. What could I do to permit this value attribute regardless of its type?
My current permitted params line looks something like:
params.require(:model_name).permit([
:field_one,
:field_two,
custom_fields: [:value]
])
Is there any way I can modify this to accept when value is an unknown type?
What you want can probably be done, but will take some work. Your best bet is this post: http://blog.trackets.com/2013/08/17/strong-parameters-by-example.html
This is not my work, but I have used the technique they outline in an app I wrote. The part you are looking for is at the end:
params = ActionController::Parameters.new(user: { username: "john", data: { foo: "bar" } })
# let's assume we can't do this because the data hash can contain any kind of data
params.require(:user).permit(:username, data: [ :foo ])
# we need to use the power of ruby to do this "by hand"
params.require(:user).permit(:username).tap do |whitelisted|
whitelisted[:data] = params[:user][:data]
end
# Unpermitted parameters: data
# => { "username" => "john", "data" => {"foo"=>"bar"} }
That blog post helped me understand params and I still refer to it when I need to brush up on the details.

Allow an array of hashes with a dynamic hash (hstore) inside

I'm stuck with strong_parameters and this array of hashes with a dynamic hash (hstore) inside.
The structure is the following:
{ contact_sources: [
{ id: 1, filled_fields: { randomstuff: 'randomdata', dunno: 123 } },
{ id: 2, filled_fields: { blah: 'blabla', dunno: 9043 } }
] }
So, my main attempt is the following:
params.permit(contact_sources: [{:filled_fields => []}, 'id'])
Which doesn't return filled_fields. Any suggestion on how to deal with it?
Update 1:
I have the following model:
class ContactSource < ActiveRecord::Base
# Fields: id:integer, filled_fields:hstore
end
In my action, I'm submitting multiple records at once (mass update), so I have an array of contact_source, but actually they don't belong to anything, it's just a mass update.
Looks like it's not possible to do it with "plain" strong_parameters syntax. The only option you have is to actually, after filtering, re-add those values with a loop. I know it's terrible but it's the only way right now. I submitted a bug to Rails actually.

Resources