Skip to content

Commit 5c7a785

Browse files
committed
Allow permitting numeric params
When specifying numeric parameters, strong params lets you permit them all using the same permitted params for each. For example params like, ```ruby book: { authors_attributes: { '0': { name: "William Shakespeare", age_of_death: "52" }, '1': { name: "Unattributed Assistant" }, '2': "Not a hash", 'new_record': { name: "Some name" } } } ``` can be permitted with, ``` permit book: { authors_attributes: [ :name ] } ``` This returns the name keys for each of the numeric keyed params that have a name field, ```ruby { "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare" }, "1" => { "name" => "Unattributed Assistant" } } } } ``` This is exactly what you want most of the time. Rarely you might need to specify different keys for particular numeric attributes. This allows another strong params syntax for those cases where you can specify the keys allowed for each individual numerically keys attributes hash. After this change using the same params above, you can permit the name and age for only the `0` key and only the name for the `1` key, ```ruby permit book: { authors_attributes: { '1': [ :name ], '0': [ :name, :age_of_death ] } } ``` This returns exactly the parameters that you specify, ```ruby { "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare", "age_of_death" => "52" }, "1" => { "name" => "Unattributed Assistant" } } } } ``` Sidenote: this allows `permit` to do the equivalent to ```ruby params.require(:book).permit(authors_attributes: { '1': [:name]}) ``` without raising when `book: ... ` is not present. The simpler syntax should be preferred, but in cases where you need more control, this is a nice option to have.
1 parent f1c9de7 commit 5c7a785

File tree

2 files changed

+113
-3
lines changed

2 files changed

+113
-3
lines changed

actionpack/lib/action_controller/metal/strong_parameters.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,41 @@ def require(key)
592592
#
593593
# params.require(:person).permit(contact: [ :email, :phone ])
594594
# # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"[email protected]", "phone"=>"555-1234"} permitted: true>} permitted: true>
595+
#
596+
# If your parameters specify multiple parameters indexed by a number,
597+
# you can permit each set of parameters under the numeric key to be the same using the same syntax as permitting a single item.
598+
#
599+
# params = ActionController::Parameters.new({
600+
# person: {
601+
# '0': {
602+
# email: "[email protected]",
603+
# phone: "555-1234"
604+
# },
605+
# '1': {
606+
# email: "[email protected]",
607+
# phone: "555-6789"
608+
# },
609+
# }
610+
# })
611+
# params.permit(person: [:email]).to_h
612+
# # => {"person"=>{"0"=>{"email"=>"[email protected]"}, "1"=>{"email"=>"[email protected]"}}}
613+
#
614+
# If you want to specify what keys you want from each numeric key, you can instead specify each one individually
615+
#
616+
# params = ActionController::Parameters.new({
617+
# person: {
618+
# '0': {
619+
# email: "[email protected]",
620+
# phone: "555-1234"
621+
# },
622+
# '1': {
623+
# email: "[email protected]",
624+
# phone: "555-6789"
625+
# },
626+
# }
627+
# })
628+
# params.permit(person: { '0': [:email], '1': [:phone]}).to_h
629+
# # => {"person"=>{"0"=>{"email"=>"[email protected]"}, "1"=>{"phone"=>"555-6789"}}}
595630
def permit(*filters)
596631
params = self.class.new
597632

@@ -952,12 +987,18 @@ def convert_value_to_parameters(value)
952987
end
953988
end
954989

955-
def each_element(object, &block)
990+
def specify_numeric_keys?(filter)
991+
if filter.respond_to?(:keys)
992+
filter.keys.any? { |key| /\A-?\d+\z/.match?(key) }
993+
end
994+
end
995+
996+
def each_element(object, filter, &block)
956997
case object
957998
when Array
958999
object.grep(Parameters).filter_map { |el| yield el }
9591000
when Parameters
960-
if object.nested_attributes?
1001+
if object.nested_attributes? && !specify_numeric_keys?(filter)
9611002
object.each_nested_attribute(&block)
9621003
else
9631004
yield object
@@ -1070,7 +1111,7 @@ def hash_filter(params, filter)
10701111
end
10711112
elsif non_scalar?(value)
10721113
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
1073-
params[key] = each_element(value) do |element|
1114+
params[key] = each_element(value, filter[key]) do |element|
10741115
element.permit(*Array.wrap(filter[key]))
10751116
end
10761117
end

actionpack/test/controller/parameters/nested_parameters_permit_test.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,75 @@ def assert_filtered_out(params, key)
194194
assert_filtered_out permitted[:book][:authors_attributes]["-1"], :age_of_death
195195
end
196196

197+
test "nested params with numeric keys addressing individual numeric keys" do
198+
params = ActionController::Parameters.new(
199+
book: {
200+
authors_attributes: {
201+
'0': { name: "William Shakespeare", age_of_death: "52" },
202+
'1': { name: "Unattributed Assistant" },
203+
'2': { name: %w(injected names) }
204+
}
205+
})
206+
permitted = params.permit book: { authors_attributes: { '1': [ :name ], '0': [ :name, :age_of_death ] } }
207+
208+
assert_equal(
209+
{ "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare", "age_of_death" => "52" }, "1" => { "name" => "Unattributed Assistant" } } } },
210+
permitted.to_h
211+
)
212+
end
213+
214+
test "nested params with numeric keys addressing individual numeric keys using require first" do
215+
params = ActionController::Parameters.new(
216+
book: {
217+
authors_attributes: {
218+
'0': { name: "William Shakespeare", age_of_death: "52" },
219+
'1': { name: "Unattributed Assistant" },
220+
'2': { name: %w(injected names) }
221+
}
222+
})
223+
224+
permitted = params.require(:book).permit(authors_attributes: { '1': [:name] })
225+
226+
assert_equal(
227+
{ "authors_attributes" => { "1" => { "name" => "Unattributed Assistant" } } },
228+
permitted.to_h
229+
)
230+
end
231+
232+
test "nested params with numeric keys addressing individual numeric keys to arrays" do
233+
params = ActionController::Parameters.new(
234+
book: {
235+
authors_attributes: {
236+
'0': ["draft 1", "draft 2", "draft 3"],
237+
'1': ["final draft"],
238+
'2': { name: %w(injected names) }
239+
}
240+
})
241+
permitted = params.permit book: { authors_attributes: { '2': [ :name ], '0': [] } }
242+
243+
assert_equal(
244+
{ "book" => { "authors_attributes" => { "2" => {}, "0" => ["draft 1", "draft 2", "draft 3"] } } },
245+
permitted.to_h
246+
)
247+
end
248+
249+
test "nested params with numeric keys addressing individual numeric keys to more nested params" do
250+
params = ActionController::Parameters.new(
251+
book: {
252+
authors_attributes: {
253+
'0': ["draft 1", "draft 2", "draft 3"],
254+
'1': ["final draft"],
255+
'2': { name: { "projects" => [ "hamlet", "Othello" ] } }
256+
}
257+
})
258+
permitted = params.permit book: { authors_attributes: { '2': { name: { projects: [] } }, '0': [] } }
259+
260+
assert_equal(
261+
{ "book" => { "authors_attributes" => { "2" => { "name" => { "projects" => ["hamlet", "Othello"] } }, "0" => ["draft 1", "draft 2", "draft 3"] } } },
262+
permitted.to_h
263+
)
264+
end
265+
197266
test "nested number as key" do
198267
params = ActionController::Parameters.new(
199268
product: {

0 commit comments

Comments
 (0)