Integrate strong_parameters in Rails 4
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -214,6 +214,7 @@ def self.without_modules(*modules) | ||
| Caching, | ||
| MimeResponds, | ||
| ImplicitRender, | ||
| StrongParameters, | ||
|
|
||
| Cookies, | ||
| Flash, | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| require 'active_support/concern' | ||
| require 'active_support/core_ext/hash/indifferent_access' | ||
| require 'active_support/rescuable' | ||
|
|
||
| module ActionController | ||
| class ParameterMissing < KeyError | ||
| attr_reader :param | ||
|
|
||
| def initialize(param) | ||
| @param = param | ||
| super("key not found: #{param}") | ||
| end | ||
| end | ||
|
|
||
| class Parameters < ActiveSupport::HashWithIndifferentAccess | ||
| cattr_accessor :permit_all_parameters, instance_accessor: false | ||
| attr_accessor :permitted | ||
| alias :permitted? :permitted | ||
|
|
||
| def initialize(attributes = nil) | ||
| super(attributes) | ||
| @permitted = self.class.permit_all_parameters | ||
| end | ||
|
|
||
| def permit! | ||
| @permitted = true | ||
| self | ||
| end | ||
|
|
||
| def require(key) | ||
| self[key].presence || raise(ParameterMissing.new(key)) | ||
| end | ||
|
|
||
| alias :required :require | ||
|
|
||
| def permit(*filters) | ||
| params = self.class.new | ||
|
|
||
| filters.each do |filter| | ||
| case filter | ||
| when Symbol, String then | ||
| params[filter] = self[filter] if has_key?(filter) | ||
| when Hash then | ||
| self.slice(*filter.keys).each do |key, value| | ||
| return unless value | ||
|
|
||
| key = key.to_sym | ||
|
|
||
| params[key] = each_element(value) do |value| | ||
| # filters are a Hash, so we expect value to be a Hash too | ||
| next if filter.is_a?(Hash) && !value.is_a?(Hash) | ||
|
|
||
| value = self.class.new(value) if !value.respond_to?(:permit) | ||
|
|
||
| value.permit(*Array.wrap(filter[key])) | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| params.permit! | ||
| end | ||
|
|
||
| def [](key) | ||
| convert_hashes_to_parameters(key, super) | ||
| end | ||
|
|
||
| def fetch(key, *args) | ||
| convert_hashes_to_parameters(key, super) | ||
| rescue KeyError | ||
| raise ActionController::ParameterMissing.new(key) | ||
| end | ||
|
|
||
| def slice(*keys) | ||
| self.class.new(super) | ||
| end | ||
|
|
||
| def dup | ||
| super.tap do |duplicate| | ||
| duplicate.instance_variable_set :@permitted, @permitted | ||
| end | ||
| end | ||
|
|
||
| private | ||
| def convert_hashes_to_parameters(key, value) | ||
| if value.is_a?(Parameters) || !value.is_a?(Hash) | ||
| value | ||
| else | ||
| # Convert to Parameters on first access | ||
| self[key] = self.class.new(value) | ||
| end | ||
| end | ||
|
|
||
| def each_element(object) | ||
| if object.is_a?(Array) | ||
| object.map { |el| yield el }.compact | ||
| elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ } | ||
| hash = object.class.new | ||
| object.each { |k,v| hash[k] = yield v } | ||
| hash | ||
| else | ||
| yield object | ||
| end | ||
| end | ||
| end | ||
|
|
||
| module StrongParameters | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
| extend ActiveSupport::Concern | ||
| include ActiveSupport::Rescuable | ||
|
|
||
| included do | ||
| rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception| | ||
| render text: "Required parameter missing: #{parameter_missing_exception.param}", status: :bad_request | ||
| end | ||
| end | ||
|
|
||
| def params | ||
| @_params ||= Parameters.new(request.parameters) | ||
| end | ||
|
|
||
| def params=(val) | ||
| @_params = val.is_a?(Hash) ? Parameters.new(val) : val | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| require 'abstract_unit' | ||
| require 'action_controller/metal/strong_parameters' | ||
|
|
||
| class NestedParametersTest < ActiveSupport::TestCase | ||
| test "permitted nested parameters" do | ||
| params = ActionController::Parameters.new({ | ||
| book: { | ||
| title: "Romeo and Juliet", | ||
| authors: [{ | ||
| name: "William Shakespeare", | ||
| born: "1564-04-26" | ||
| }, { | ||
| name: "Christopher Marlowe" | ||
| }], | ||
| details: { | ||
| pages: 200, | ||
| genre: "Tragedy" | ||
| } | ||
| }, | ||
| magazine: "Mjallo!" | ||
| }) | ||
|
|
||
| permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages } ] | ||
|
|
||
| assert permitted.permitted? | ||
| assert_equal "Romeo and Juliet", permitted[:book][:title] | ||
| assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] | ||
| assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] | ||
| assert_equal 200, permitted[:book][:details][:pages] | ||
| assert_nil permitted[:book][:details][:genre] | ||
| assert_nil permitted[:book][:authors][1][:born] | ||
This comment has been minimized.
Sorry, something went wrong.
RKushnir
Contributor
|
||
| assert_nil permitted[:magazine] | ||
| end | ||
|
|
||
| test "nested arrays with strings" do | ||
| params = ActionController::Parameters.new({ | ||
| :book => { | ||
| :genres => ["Tragedy"] | ||
| } | ||
| }) | ||
|
|
||
| permitted = params.permit :book => :genres | ||
| assert_equal ["Tragedy"], permitted[:book][:genres] | ||
| end | ||
|
|
||
| test "permit may specify symbols or strings" do | ||
| params = ActionController::Parameters.new({ | ||
| :book => { | ||
| :title => "Romeo and Juliet", | ||
| :author => "William Shakespeare" | ||
| }, | ||
| :magazine => "Shakespeare Today" | ||
| }) | ||
|
|
||
| permitted = params.permit({:book => ["title", :author]}, "magazine") | ||
| assert_equal "Romeo and Juliet", permitted[:book][:title] | ||
| assert_equal "William Shakespeare", permitted[:book][:author] | ||
| assert_equal "Shakespeare Today", permitted[:magazine] | ||
| end | ||
|
|
||
| test "nested array with strings that should be hashes" do | ||
| params = ActionController::Parameters.new({ | ||
| book: { | ||
| genres: ["Tragedy"] | ||
| } | ||
| }) | ||
|
|
||
| permitted = params.permit book: { genres: :type } | ||
| assert_empty permitted[:book][:genres] | ||
| end | ||
|
|
||
| test "nested array with strings that should be hashes and additional values" do | ||
| params = ActionController::Parameters.new({ | ||
| book: { | ||
| title: "Romeo and Juliet", | ||
| genres: ["Tragedy"] | ||
| } | ||
| }) | ||
|
|
||
| permitted = params.permit book: [ :title, { genres: :type } ] | ||
| assert_equal "Romeo and Juliet", permitted[:book][:title] | ||
| assert_empty permitted[:book][:genres] | ||
| end | ||
|
|
||
| test "nested string that should be a hash" do | ||
| params = ActionController::Parameters.new({ | ||
| book: { | ||
| genre: "Tragedy" | ||
| } | ||
| }) | ||
|
|
||
| permitted = params.permit book: { genre: :type } | ||
| assert_nil permitted[:book][:genre] | ||
| end | ||
|
|
||
| test "fields_for-style nested params" do | ||
| params = ActionController::Parameters.new({ | ||
| book: { | ||
| authors_attributes: { | ||
| :'0' => { name: 'William Shakespeare', age_of_death: '52' }, | ||
| :'-1' => { name: 'Unattributed Assistant' } | ||
| } | ||
| } | ||
| }) | ||
| permitted = params.permit book: { authors_attributes: [ :name ] } | ||
|
|
||
| assert_not_nil permitted[:book][:authors_attributes]['0'] | ||
| assert_not_nil permitted[:book][:authors_attributes]['-1'] | ||
| assert_nil permitted[:book][:authors_attributes]['0'][:age_of_death] | ||
| assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['0'][:name] | ||
| assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['-1'][:name] | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| require 'abstract_unit' | ||
| require 'action_controller/metal/strong_parameters' | ||
|
|
||
| class ParametersPermitTest < ActiveSupport::TestCase | ||
| setup do | ||
| @params = ActionController::Parameters.new({ person: { | ||
| age: "32", name: { first: "David", last: "Heinemeier Hansson" } | ||
| }}) | ||
| end | ||
|
|
||
| test "fetch raises ParameterMissing exception" do | ||
| e = assert_raises(ActionController::ParameterMissing) do | ||
| @params.fetch :foo | ||
| end | ||
| assert_equal :foo, e.param | ||
| end | ||
|
|
||
| test "fetch doesnt raise ParameterMissing exception if there is a default" do | ||
| assert_equal "monkey", @params.fetch(:foo, "monkey") | ||
| assert_equal "monkey", @params.fetch(:foo) { "monkey" } | ||
| end | ||
|
|
||
| test "permitted is sticky on accessors" do | ||
| assert !@params.slice(:person).permitted? | ||
| assert !@params[:person][:name].permitted? | ||
|
|
||
| @params.each { |key, value| assert(value.permitted?) if key == :person } | ||
|
|
||
| assert !@params.fetch(:person).permitted? | ||
|
|
||
| assert !@params.values_at(:person).first.permitted? | ||
| end | ||
|
|
||
| test "permitted is sticky on mutators" do | ||
| assert !@params.delete_if { |k| k == :person }.permitted? | ||
| assert !@params.keep_if { |k,v| k == :person }.permitted? | ||
| end | ||
|
|
||
| test "permitted is sticky beyond merges" do | ||
| assert !@params.merge(a: "b").permitted? | ||
| end | ||
|
|
||
| test "modifying the parameters" do | ||
| @params[:person][:hometown] = "Chicago" | ||
| @params[:person][:family] = { brother: "Jonas" } | ||
|
|
||
| assert_equal "Chicago", @params[:person][:hometown] | ||
| assert_equal "Jonas", @params[:person][:family][:brother] | ||
| end | ||
|
|
||
| test "permitting parameters that are not there should not include the keys" do | ||
| assert !@params.permit(:person, :funky).has_key?(:funky) | ||
| end | ||
|
|
||
| test "permit state is kept on a dup" do | ||
| @params.permit! | ||
| assert_equal @params.permitted?, @params.dup.permitted? | ||
| end | ||
|
|
||
| test "permitted takes a default value when Parameters.permit_all_parameters is set" do | ||
| begin | ||
| ActionController::Parameters.permit_all_parameters = true | ||
| params = ActionController::Parameters.new({ person: { | ||
| age: "32", name: { first: "David", last: "Heinemeier Hansson" } | ||
| }}) | ||
|
|
||
| assert params.slice(:person).permitted? | ||
| assert params[:person][:name].permitted? | ||
| ensure | ||
| ActionController::Parameters.permit_all_parameters = false | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| require 'abstract_unit' | ||
| require 'action_controller/metal/strong_parameters' | ||
|
|
||
| class ParametersRequireTest < ActiveSupport::TestCase | ||
| test "required parameters must be present not merely not nil" do | ||
| assert_raises(ActionController::ParameterMissing) do | ||
| ActionController::Parameters.new(person: {}).require(:person) | ||
| end | ||
| end | ||
| end |
7 comments
on commit c49d959
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RIP attr_accessible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good work!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
Can we pretend attr_accessible never happened?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, thanks @guilleiguaran and @dhh !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will miss you, attr_accessible...NOT
Can we add some docs for
StrongParametersmodule andParametersclass? Seems trivial, but when I find myself code spelunking in a new file while trying to add a bugfix or feature having the intent of classes in comments in the file makes the experience much easier.