Rails: Accept 2D array of strings with strong parameters - ruby-on-rails

We have a Rails controller that gets the following data:
params = ActionController::Parameters.new({
"requests": [{
"params": {
"facets": ["user.id", "user.type"],
"facetFilters": [
["user.type:Individual"]
]
}
}, {
"params": {
"facets": "user.type"
}
}]
})
We want to use strong parameters to accept this data, but I haven't yet seen a pattern that will let us accept the 2D array in facetFilters. I'm tinkering with the following:
params[:requests].each do |request|
request[:permitted] = true
request[:params].each do |o|
if ['facets', 'facetFilters'].include?(o.first)
begin
o[:permitted] = true
rescue
end
end
end
end
As one can see not all attributes are permitted (the permitted attributes don't get passed to children elements), as this returns:
[<ActionController::Parameters {"params"=><ActionController::Parameters {"facets"=>["user.id", "user.type"], "facetFilters"=>[["user.type:Individual"]]} permitted: false>, "permitted"=>true} permitted: false>, <ActionController::Parameters {"params"=><ActionController::Parameters {"facets"=>"user.type"} permitted: false>, "permitted"=>true} permitted: false>]
And there are lots of permitted: false in there...
Is it possible to accomplish this goal? Any pointers would be super helpful!

The best way I've found to get this done is to convert the 2D array to a 1D array, permit the 1D array, and then convert the array back to 2D.
nested_facet_filters = []
(params[:requests] || []).each_with_index do |r, ridx|
if r[:params].key?(:facetFilters) && r[:params][:facetFilters].kind_of?(Array) && r[:params][:facetFilters].first.kind_of?(Array)
# flatten the facet filters into a 1D array because Rails can't permit a 2D array
nested_facet_filters << ridx
r[:params][:facetFilters] = r[:params][:facetFilters].flatten
end
end
permitted = params.permit({
requests: [
:query,
:params => [
:facets => [],
:facetFilters => [],
]
]
}).to_h
# after permitting the params, we need to convert the facetFilters back into a 2D array
if nested_facet_filters.present?
nested_facet_filters.each do |idx|
# create map from facet key to array of values
d = {}
permitted[:requests][idx][:params][:facetFilters].each do |s|
split = s.split(':')
facet = split.first
value = split[1..-1].join(':')
if d.key?(facet)
d[facet] << s
else
d[facet] = [s]
end
end
# mutate facetFilters back to 2D array for posting to Algolia
permitted[:requests][idx][:params][:facetFilters] = d.values
end
end
There's been a merge request to accept 2D arrays for six years open in the Rails issue tracker [thread]...

Related

Rails 6 - Strong Parameters - allowing array

I am sending this simple hash as JSON to my controller:
{
"cars": [
{ "rego": "ABC123" }
]
}
In the controller, I am trying to allow the array of cars for further processing.
I tried the following:
params.permit(:cars)
params.permit(cars: [])
params.permit(:cars, cars: [])
In every attempt I am not getting anything in my filtered params:
DEBUG -- : Unpermitted parameters: :cars, :car, :user_username, :user_token
=> <ActionController::Parameters {} permitted: true>
I am using RoR 6.0.2.1 with Ruby 2.6.5.
Try params.permit(cars: [:rego])
params.permit(cars: []) allows cars as an array of primitive values
{
"cars": [1, 2, 3, 4]
}
"Strong Parameters" has more information.

Stringify/Parse request data through rspec

I'm submitting requests on the frontend where I stringify my data (array of objects) and then parse it in the backend.
When I run my specs, I'm getting the error no implicit conversion of Array into String
How can I stringify my data in my spec so that it's consistent with what I'm doing in the frontend? Or is there another way where I don't have to stringify/parse my data to handle all of this?
This is how my frontend data structure looks like:
"categories_and_years": JSON.stringify(
[
{"category_id": 1, "year_ids":[1, 2, 3]},
{"category_id": 2, "year_ids":[2, 3]},
]
)
In my controller, I'm validating the data is an array first:
def validate_categories_and_years_array
#cats_and_yrs = JSON.parse(params[:categories_and_years])
return unless #cats_and_yrs
if !#cats_and_yrs.is_a?(Array)
render_response(:unprocessable_entity, { description_detailed: "categories_and_years must be an array of objects"})
end
end
In my specs, I'm setting my params like this:
context "when all categories and years are valid" do
let(:params) do
{
school_id: school.id,
id: standard_group.id,
categories_and_years: [
{ category_id: category_1.id, year_ids: [ year_1.id ] }
]
}
end
it "adds standards from specific categories and years to the school" do
post :add, params: params, as: :json
expect(school.achievement_standards).to contain_exactly( std_1 )
end
end
This post explains the difference between a regular ruby hash which you have in your spec and a HashWithIndifferentAccess.
Can you also try to_json?
Params hash keys as symbols vs strings
params = ActiveSupport::HashWithIndifferentAccess.new()
params['school_id'] = school.id
params['id'] = standard_group.id
params['categories_and_years'] = [
{ category_id: category_1.id, year_ids: [ year_1.id ] }
]
params = params.to_json
let(:params) { params }

Merge multidimensional array of hash based on hash key and value in ruby

I have one array and i want to match value of id key with other array of hash in multidimensional array,
input = [
[ {"id"=>"1","name"=>"a"},
{"id"=>"2","name"=>"b"},
{"id"=>"3","name"=>"c"},
{"id"=>"4","name"=>"d"},
{"id"=>"5","name"=>"e"},
{"id"=>"6","name"=>"f"}
],
[ {"id"=>"3","hoby"=>"AA"},
{"id"=>"3","hoby"=>"BB"},
{"id"=>"1","hoby"=>"CC"},
{"id"=>"1","hoby"=>"DD"},
{"id"=>"4","hoby"=>"EE"}
],
[ {"id"=>"1","language"=>"A"},
{"id"=>"1","language"=>"B"},
{"id"=>"2","language"=>"B"},
{"id"=>"2","language"=>"C"},
{"id"=>"6","language"=>"D"}
]
]
I need array output like,
output = [
{"id"=>"1","name"=>"a","id"=>"1","hoby"=>"CC","id"=>"1","language"=>"A","id"=>"1","language"=>"B"},
{"id"=>"2","name"=>"b","id"=>"2","language"=>"B"},
{"id"=>"3","name"=>"c","id"=>"3","hoby"=>"AA","id"=>"3","hoby"=>"BB"},
{"id"=>"4","name"=>"d","id"=>"4","hoby"=>"EE"},
{"id"=>"5","name"=>"e"},
{"id"=>"6","name"=>"f","id"=>"6","language"=>"D"}
]
I have wrote code for this,
len = input.length - 1
output = []
input[0].each do |value,index|
for i in 1..len
input[i].each do |j|
if value["id"] == j["id"]
output << value.merge(j)
end
end
end
end
But i am getting wrong output array.There might be any number of sub array in multidimensional array.
Thank,
First of all - it is impossible to have two elements in a hash with the same key. When assigning the value to some key will make the next assignment of the same key with new value override the previous one.
Let's consider the example:
hash = {}
hash["id"] = 1
hash["id"] = 3
hash["id"] = 5
What output for hash["id"] would you expect? 1, 3, 5 or maybe [1, 3, 5]? The way the Hash in ruby works it will output 5, because this is the last assignment to unique key.
Having said that, it is impossible to store multiple occurrences in your hash, but you can try processing it with something like:
input.flatten
.group_by { |h| h["id"] }
.map do |k, a|
a.each_with_object({}) { |in_h, out_h| out_h.merge!(in_h) }
end
Which will result with hash like:
[{"id"=>"1", "name"=>"a", "hoby"=>"DD", "language"=>"B"},
{"id"=>"2", "name"=>"b", "language"=>"C"},
{"id"=>"3", "name"=>"c", "hoby"=>"BB"},
{"id"=>"4", "name"=>"d", "hoby"=>"EE"},
{"id"=>"5", "name"=>"e"},
{"id"=>"6", "name"=>"f", "language"=>"D"}]
Well, it is not the hash as you would expect, but at least it might put you in some direction.
Hope that helps!
maybe this can help you.
input = [
[
{"id"=>"1","name"=>"a"},
{"id"=>"2","name"=>"b"},
{"id"=>"3","name"=>"c"},
{"id"=>"4","name"=>"d"},
{"id"=>"5","name"=>"e"},
{"id"=>"6","name"=>"f"}
],
[
{"id"=>"3","hoby"=>"AA"},
{"id"=>"3","hoby"=>"BB"},
{"id"=>"1","hoby"=>"CC"},
{"id"=>"1","hoby"=>"DD"},
{"id"=>"4","hoby"=>"EE"}
],
[
{"id"=>"1","language"=>"A"},
{"id"=>"1","language"=>"B"},
{"id"=>"2","language"=>"B"},
{"id"=>"2","language"=>"C"},
{"id"=>"6","language"=>"D"}
]
]
This way you can make your "sort" results.
output = {}
input.flatten.each do |h|
output[h["id"]] = {} unless output[h["id"]]
output[h["id"]].merge!(h)
end
output.values
# => [
# => {"id"=>"1", "name"=>"a", "hoby"=>"DD", "language"=>"B"},
# => {"id"=>"2", "name"=>"b", "language"=>"C"},
# => {"id"=>"3", "name"=>"c", "hoby"=>"BB"},
# => {"id"=>"4", "name"=>"d", "hoby"=>"EE"},
# => {"id"=>"5", "name"=>"e"},
# => {"id"=>"6", "name"=>"f", "language"=>"D"}
# => ]
But the better way is use Hash in input. You can define input like hash and "id" like key so if you generate the data, you dont have problem to sort it.
Someting like this
{
"1" => {"name" => "a", "hoby" => "DD", "language" => "B"}
}

map array with condition

I have array like
strings = ["by_product[]=1", "by_product[]=2", "page=1", "per_page=10", "select[]=current", "select[]=requested", "select[]=original"]
which is array of params from request
Then there is code that generates hash from array
arrays = strings.map do |segment|
k,v = segment.split("=")
[k, v && CGI.unescape(v)]
Hash[arrays]
CUrrent output -
"by_product[]": "2",
"page":"1",
"per_page":"10",
"select[]":"original"
Expected output -
"by_product[]":"1, 2",
"page":"1",
"per_page":"10",
"select[]":"current, requested, original"
The problem is - after split method there are few by_product[] and the last one just overrides any other params, so in result instead of hash with array as value of these params im getting only last one. And i'm not sure how to fix it. Any ideas? Or at least algorithms
So try this:
hash = {}
arrays = strings.map do |segment|
k,v = segment.split("=")
hash[k]||=[]
hash[k] << v
end
output is
1.9.3-p547 :025 > hash
=> {"by_product[]"=>["1", "2"], "page"=>["1"], "per_page"=>["10"], "select[]"=>["current", "requested", "original"]}
or if you want just strings do
arrays = strings.map do |segment|
k,v = segment.split("=")
hash[k].nil? ? hash[k] = v : hash[k] << ", " + v
end
Don't reinvent the wheel, CGI and Rack can already handle query strings.
Assuming your strings array comes from a single query string:
query = "by_product[]=1&by_product[]=2&page=1&per_page=10&select[]=current&select[]=requested&select[]=original"
you can use CGI::parse: (all values as arrays)
require 'cgi'
CGI.parse(query)
#=> {"by_product[]"=>["1", "2"], "page"=>["1"], "per_page"=>["10"], "select[]"=>["current", "requested", "original"]}
or Rack::Utils.parse_query: (arrays where needed)
require 'rack'
Rack::Utils.parse_nested_query(query)
# => {"by_product[]"=>["1", "2"], "page"=>"1", "per_page"=>"10", "select[]"=>["current", "requested", "original"]}
or Rack::Utils.parse_nested_query: (values without [] suffix)
require 'rack'
Rack::Utils.parse_nested_query(query)
# => {"by_product"=>["1", "2"], "page"=>"1", "per_page"=>"10", "select"=>["current", "requested", "original"]}
And if these are parameters for a Rails controller, you can just use params.
this will also work :
strings.inject({}){ |hash, string|
key, value = string.split('=');
hash[key] = (hash[key]|| []) << value;
hash;
}
output :
{"by_product[]"=>["1", "2"], "page"=>["1"], "per_page"=>["10"], "select[]"=>["current", "requested", "original"]}
As simple as that
array.map { |record| record*3 if condition }
record*3 is the resultant operation you wanna do to the array while mapping

Recursive/Tree like strong parameters?

Is there a way of specifying arbitrarily deep strong parameters for tree structures in Rails 4? For example, how would I specify something as follows:
{
"node": {
"name": "parent",
"nodes": [
{ "name": "child1", "nodes": []},
{ "name": "child2", "nodes": [
{"name": "grandchild", "nodes": []}
]}
]
}
}
To allow each node to have a name attribute, and a nodes attribute?
There may be a cleaner solution for solving this but this is my current work around. The general idea is to count how deep my nesting goes and then auto generate the correct nested hash based on that number. So to follow your example:
def count_levels(node_params)
if !node_params[:nodes].nil?
max = 0
node_params[:node_parameters].each do |child_params|
count = count_levels(child_params[1])
max = count if count > max
end
return max + 1
else
return 0
end
end
def node_params
node_attributes_base = [:id, :name]
nodes = []
(1..count_levels(params[:node])).each do |val|
nodes = node_attributes_base + [node_attributes: nodes]
end
params.require(:node).permit(:id, :name, node_attributes: nodes)
end
(The above example can be cleaned up more since it's based on my code where the top level did not have the same parameters. I left it as I had it since it worked on my system.)
You can solve by depending on the fact that number of allowed level can be more than the levels you actually need, so you can count the occurrence of the recursive key nodes key in your hash and use this count as number of levels.
Note that this count will be more than the levels you actually need, but it's simpler than recursively count number of levels in the hash
So in your controller you can the following:
# your_controller.rb
# include RecursiveParametersBuilder
recursive_nodes_attr = build_recursive_params(
recursive_key: 'nodes',
parameters: params,
permitted_attributes: [:name]
)
params.require(:model_name).permit(:name, nodes: recursive_nodes_attr)
And have the actual strong parameters building code can be like the following
# helper module
module RecursiveParametersBuilder
# recursive_path = [:post]
# recursive_key = :comment_attributes
# recursive_node_permitted_params = [:id, :_destroy, :parameterized_type, :parameterized_id, :name, :value, :is_encrypted, :base_param_id, :parent_param_id]
#
def build_recursive_params(recursive_key:, parameters:, permitted_attributes:)
template = { recursive_key => permitted_attributes }
nested_permit_list = template.deep_dup
current_node = nested_permit_list[recursive_key]
nested_count = parameters.to_s.scan(/#{recursive_key}/).count
(1..nested_count).each do |i|
new_element = template.deep_dup
current_node << new_element
current_node = new_element[recursive_key]
end
nested_permit_list
end
end
This is not possible with strong parameters. You should use plain ruby for that, i.e converting your params to a hash with to_hash and validating the format yourself.

Resources