Support `ActionController::Parameters#deep_merge`

When [rails/rails#20868][] changed the `ActionController::Parameters`
ancestory from `HashWithIndifferentAccess` to `Object`, support for
`#deep_merge` and `#deep_merge!` were omitted.

This commit restores support by integrating with
[ActiveSupport::DeepMergeable](./activesupport/lib/active_support/deep_mergeable.rb).

[rails/rails#20868]: https://github.com/rails/rails/pull/20868

Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
This commit is contained in:
Sean Doyle 2022-06-15 18:48:16 +01:00 committed by Jonathan Hefner
parent 5fcc61076d
commit 1d999e681e
3 changed files with 94 additions and 2 deletions

View File

@ -1,3 +1,8 @@
* Add support for `#deep_merge` and `#deep_merge!` to
`ActionController::Parameters`.
*Sean Doyle*
## Rails 7.1.0.beta1 (September 13, 2023) ## ## Rails 7.1.0.beta1 (September 13, 2023) ##
* `AbstractController::Translation.raise_on_missing_translations` removed * `AbstractController::Translation.raise_on_missing_translations` removed

View File

@ -4,6 +4,7 @@ require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/array/wrap" require "active_support/core_ext/array/wrap"
require "active_support/core_ext/string/filters" require "active_support/core_ext/string/filters"
require "active_support/core_ext/object/to_query" require "active_support/core_ext/object/to_query"
require "active_support/deep_mergeable"
require "action_dispatch/http/upload" require "action_dispatch/http/upload"
require "rack/test" require "rack/test"
require "stringio" require "stringio"
@ -137,6 +138,8 @@ module ActionController
# params[:key] # => "value" # params[:key] # => "value"
# params["key"] # => "value" # params["key"] # => "value"
class Parameters class Parameters
include ActiveSupport::DeepMergeable
cattr_accessor :permit_all_parameters, instance_accessor: false, default: false cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
@ -853,13 +856,41 @@ module ActionController
) )
end end
##
# :call-seq: merge!(other_hash)
#
# Returns the current +ActionController::Parameters+ instance with # Returns the current +ActionController::Parameters+ instance with
# +other_hash+ merged into current hash. # +other_hash+ merged into current hash.
def merge!(other_hash) def merge!(other_hash, &block)
@parameters.merge!(other_hash.to_h) @parameters.merge!(other_hash.to_h, &block)
self self
end end
##
# :method: deep_merge
# :call-seq: deep_merge(other_hash, &block)
#
# Returns a new +ActionController::Parameters+ instance with +self+ and +other_hash+ merged recursively.
#
# Like with +Hash#merge+ in the standard library, a block can be provided
# to merge values.
#
#--
# Implemented by ActiveSupport::DeepMergeable#deep_merge.
##
# :method: deep_merge!
# :call-seq: deep_merge!(other_hash, &block)
#
# Same as +#deep_merge+, but modifies +self+.
#
#--
# Implemented by ActiveSupport::DeepMergeable#deep_merge!.
def deep_merge?(other_hash) # :nodoc
other_hash.is_a?(ActiveSupport::DeepMergeable)
end
# Returns a new +ActionController::Parameters+ instance with all keys # Returns a new +ActionController::Parameters+ instance with all keys
# from current hash merged into +other_hash+. # from current hash merged into +other_hash+.
def reverse_merge(other_hash) def reverse_merge(other_hash)

View File

@ -319,6 +319,62 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "32", @params[:person][:age] assert_equal "32", @params[:person][:age]
end end
test "not permitted is sticky beyond deep merges" do
assert_not_predicate @params.deep_merge(a: "b"), :permitted?
end
test "permitted is sticky beyond deep merges" do
@params.permit!
assert_predicate @params.deep_merge(a: "b"), :permitted?
end
test "not permitted is sticky beyond deep_merge!" do
assert_not_predicate @params.deep_merge!(a: "b"), :permitted?
end
test "permitted is sticky beyond deep_merge!" do
@params.permit!
assert_predicate @params.deep_merge!(a: "b"), :permitted?
end
test "deep_merge with other Hash" do
first, last = @params.dig(:person, :name).values_at(:first, :last)
merged_params = @params.deep_merge(person: { name: { last: "A." } })
assert_equal first, merged_params.dig(:person, :name, :first)
assert_not_equal last, merged_params.dig(:person, :name, :last)
assert_equal "A.", merged_params.dig(:person, :name, :last)
end
test "deep_merge! with other Hash" do
first, last = @params.dig(:person, :name).values_at(:first, :last)
@params.deep_merge!(person: { name: { last: "A." } })
assert_equal first, @params.dig(:person, :name, :first)
assert_not_equal last, @params.dig(:person, :name, :last)
assert_equal "A.", @params.dig(:person, :name, :last)
end
test "deep_merge with other Parameters" do
first, last = @params.dig(:person, :name).values_at(:first, :last)
other_params = ActionController::Parameters.new(person: { name: { last: "A." } }).permit!
merged_params = @params.deep_merge(other_params)
assert_equal first, merged_params.dig(:person, :name, :first)
assert_not_equal last, merged_params.dig(:person, :name, :last)
assert_equal "A.", merged_params.dig(:person, :name, :last)
end
test "deep_merge! with other Parameters" do
first, last = @params.dig(:person, :name).values_at(:first, :last)
other_params = ActionController::Parameters.new(person: { name: { last: "A." } }).permit!
@params.deep_merge!(other_params)
assert_equal first, @params.dig(:person, :name, :first)
assert_not_equal last, @params.dig(:person, :name, :last)
assert_equal "A.", @params.dig(:person, :name, :last)
end
test "#reverse_merge with parameters" do test "#reverse_merge with parameters" do
default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
merged_params = @params.reverse_merge(default_params) merged_params = @params.reverse_merge(default_params)