Skip to content

Commit

Permalink
Documentation and implementation for ActionController::Parameters#man…
Browse files Browse the repository at this point in the history
…date
  • Loading branch information
martinemde committed Aug 2, 2024
1 parent a5ffec5 commit 96041b5
Showing 1 changed file with 134 additions and 0 deletions.
134 changes: 134 additions & 0 deletions actionpack/lib/action_controller/metal/strong_parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,140 @@ def require(key)

alias :required :require

# `mandate` is the preferred way to require and permit parameters.
# It is similar to calling `permit` and `require` in sequence, but it avoids some
# potential pitfalls, such as NoMethodErrors that can be triggered by external
# users passing unexpected scalar or non-scalar types.
#
# `mandate` can be thought of as a shortcut for `params.permit(filters).require(filters)`
# with the additional special behavior of requiring `filetrs.keys` if `filters` is a hash.
# This can be convenient for the standard format of params from rails forms.
#
# # given a usual format of params from a form
# params = ActionController::Parameters.new(user: { name: "Martin", age: 40, role: "admin" })
# # instead of this (not recommended)
# permitted = params.require(:user).permit(:name, :age)
# # do this (recommended)
# permitted = params.mandate(user: [:name, :age])
#
# In the example, `mandate` will require the `:user` key and permit the
# `:name` and `:age` keys, and return the params `{ name: "Martin", age: 40 }`.
# It is functionally the same as the following:
#
# permitted = params.permit(user: [:name, :age]).require(:user)
#
# `mandate` aims to correct a potential error that can be triggered when an end user
# alters the params. In the following example, the params are altered by the user in
# order to cause a 500 error.
#
# params = ActionController::Parameters.new(user: "Hax0r")
# params.require(:user).permit(:name, :age) # causes a 500 error
# # => NoMethodError: undefined method `permit' for an instance of String
# params.mandate(user: [:name, :age]) # expected behavior, handled correctly by rails
# # => ActionController::ParameterMissing: param is missing or the value is empty: user
#
# Like `permit`, `mandate` only allows permitted scalars to pass the filter.
# For example,
#
# params.mandate(:name)
#
# If `:name` is a `Hash` or `Array`, ActionController::ParameterMissing is raised.
#
# `:name` is permitted if it is a key of `params` whose associated value is of type
# `String`, `Symbol`, `NilClass`, `Numeric`, `TrueClass`, `FalseClass`, `Date`,
# `Time`, `DateTime`, `StringIO`, `IO`, ActionDispatch::Http::UploadedFile or
# `Rack::Test::UploadedFile`.
#
# You can also `mandate` nested parameters just like permit. The result will
# `require(filters.keys)` on the permitted parameters.
#
# params = ActionController::Parameters.new({
# person: {
# name: "Francesco",
# age: 22,
# pets: [{
# name: "Purplish",
# category: "dogs"
# }]
# }
# })
#
# permitted = params.mandate(person: [ :name, { pets: :name } ])
# permitted.permitted? # => true
# permitted[:name] # => "Francesco"
# permitted[:age] # => nil
# permitted[:pets][0][:name] # => "Purplish"
# permitted[:pets][0][:category] # => nil
#
# You may declare that the parameter should be an array of permitted scalars by
# mapping it to an empty array:
#
# params = ActionController::Parameters.new(tags: ["rails", "parameters"])
# permitted = params.mandate(tags: [])
# permitted.permitted? # => true
# permitted.is_a?(Array) # => true
# permitted.size # => 2
#
# Like `permit`, `mandate` can be used to permit nested parameters.
# Be careful because this opens the door to arbitrary input. In this case,
# `permit` ensures values in the returned structure are permitted scalars and
# filters out anything else. For example:
#
# params = ActionController::Parameters.new(user: { name: "Martin", age: 40, hax: [] })
# permitted = params.mandate(user: {})
# permitted.permitted? # => true
# permitted.has_key?(:name) # => true
# permitted.has_key?(:age) # => true
# permitted.has_key?(:hax) # => false
#
# Note that if you use `mandate` on a key that points to a hash, it will not
# allow the hash. You also need to specify which attributes inside the hash
# should be permitted.
#
# params = ActionController::Parameters.new({
# person: {
# contact: {
# email: "[email protected]",
# phone: "555-1234"
# }
# }
# })
#
# params.mandate(person: :contact) # wrong
# # => ActionController::ParameterMissing: param is missing or the value is empty: person
#
# params.mandate(person: { contact: :phone }) # correct
# # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
#
# params.mandate(person: { contact: [ :email, :phone ] }) # correct
# # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"email"=>"[email protected]", "phone"=>"555-1234"} permitted: true>} permitted: true>
#
# Use `mandate` to require multiple top level scalar parameters.
# The keys must be passed as an array, similar to using `require`.
# Note that this is different than `permit` which allows multiple keys
# to be passed as arguments instead of as an array.
#
# params = ActionController::Parameters.new(name: "Martin", age: 40)
# name, age = params.mandate(:name, :age) # wrong
# # => ArgumentError: wrong number of arguments (given 2, expected 1)
# name, age = params.mandate([:name, :age]) # correct
# name # => "Martin"
# age # => 40
#
# When called with a hash with multiple keys, `mandate` will behave as
# described above, requiring all the keys of the hash and returning the value
# at each key in the order they are given in the filters argument.
#
# params = ActionController::Parameters.new(subject: { name: "Martin" }, object: { pie: "pumpkin" })
# subject, object = params.mandate(subject: [:name], object: [:pie])
# subject # => #<ActionController::Parameters {"name"=>"Martin"} permitted: true>
# object # => #<ActionController::Parameters {"pie"=>"pumpkin"} permitted: true>
#
def mandate(filters)
keys = filters.respond_to?(:keys) ? filters.keys : filters
permit(filters).require(keys)
end

# Returns a new `ActionController::Parameters` instance that includes only the
# given `filters` and sets the `permitted` attribute for the object to `true`.
# This is useful for limiting which attributes should be allowed for mass
Expand Down

0 comments on commit 96041b5

Please sign in to comment.