Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Implements require with multiple parameters

Normal use is unchanged.

When used with multiple parameters the error raised in case a
parameter is missing lists all the missing parameters.

Returns the params hash if no required parameters are missing.
  • Loading branch information...
commit 7758a3446879f27b1932eef81adb116659266218 1 parent c789f57
Jonas Schubert Erlandsson authored
View
10 README.md
@@ -70,6 +70,16 @@ This declaration whitelists the `name`, `emails` and `friends` attributes. It is
Thanks to Nick Kallen for the permit idea!
+## Requiring multiple values
+
+In some cases you might want to require several parameters to be present before proceeding, such as for an API where the params might be a flat hash. You can use `require` to do this:
+
+``` ruby
+params.require(:username, :password).permit(:version)
@ajsharp
ajsharp added a note

My understanding of the require / permit syntax was that, if you do something like params.require(:post).permit(:title, :body), that the :title and :body params were permitted only within the :post key, e.g. {post: {title: 'Title', body: 'Body'}. It seems like your example here assumes that the version param will be passed along as a top-level param, along with username and password. Am I misunderstanding the API? Thanks.

@dhh Owner
dhh added a note

@ajsharp, this is for when you don't have a root level like post. So imagine params: { username: x, password: y }. Instead of { post: { title: x, body: y } }.

@fxn Owner
fxn added a note

Is the example plausible? In which situation do you get username and password parameters and want them to be filtered out?

@ajsharp
ajsharp added a note

@ajsharp, this is for when you don't have a root level like post. So imagine params: { username: x, password: y }. Instead of { post: { title: x, body: y } }.

@dhh Right. My question was about how the permit call works in a case where you're requiring multiple root-level params. In the post example, permit seems to whitelist the title and body attributes nested within the post key. So first question: Is that indeed how the library behaves, where, if you do require(:key).permit(*keys), it will only whitelist those params on the required key, and not at the top-level?

And my second question is, in the example here -- params.require(:username, :password).permit(:version) -- the resulting behavior is that version is whitelisted at the top-level, along with username and password, right?

@dhh Owner
dhh added a note

You're right, that actually is confusing. It doesn't know which element to go off.

@d-pixie, any thoughts here? Otherwise this doesn't add up for me either.

@fxn Owner
fxn added a note

@ajsharp The singular require returns the hash expected to be associated with the required key. The plural version implemented in this patch just returns self.

The method permit whitelists its receiver. So, the nested hash in the singular case, and params itself in the multiple case. Thus, in the example above :username and :password are filtered out. You'll get a hash whose only key is :version.

@ajsharp
ajsharp added a note

@fxn Thanks much for the explanation. IMO that API behavior is a bit tricky, so it might be worth it to include a note in the readme about this.

Also, not to get too off topic, but IMO this particular quirk of the API fails the test of "least surprise". When I look at the public API, the first thought I have is "wait, how does that work"? Unfortunately, I don't have any ideas for how to improve it ;) Hopefully this feedback is helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+```
+
+When used with multiple inputs `require` raises a `ActionController::MissingParameter` error with *all* the missing attributes specified in the message. If no values are missing the params hash is returned unmodified.
+
## Handling of Unpermitted Keys
By default parameter keys that are not explicitly permitted will be logged in the development and test environment. In other environments these parameters will simply be filtered out and ignored.
View
29 lib/action_controller/parameters.rb
@@ -22,7 +22,7 @@ class UnpermittedParameters < IndexError
def initialize(params)
@params = params
- super("found unpermitted parameters: #{params.join(", ")}")
+ super("found unpermitted parameter(s): #{params.join(", ")}")
end
end
@@ -51,8 +51,12 @@ def permit!
self
end
- def require(key)
- self[key].presence || raise(ActionController::ParameterMissing.new(key))
+ def require(*keys)
+ if keys.many?
+ require_multiple(keys)
+ else
+ require_single(keys)
+ end
end
alias :required :require
@@ -98,6 +102,20 @@ def dup
end
protected
+ def require_single(keys)
+ key = keys.first
+ self[key].presence || raise(ActionController::ParameterMissing.new(key))
+ end
+
+ def require_multiple(keys)
+ missing_keys = keys.select do |key|
+ self[key].blank?
+ end
+
+ raise(ActionController::ParameterMissing.new(missing_keys.join(', '))) if missing_keys.compact.any?
+ self
+ end
+
def convert_value(value)
if value.class == Hash
self.class.new_from_hash_copying_default(value)
@@ -109,7 +127,6 @@ def convert_value(value)
end
private
-
def convert_hashes_to_parameters(key, value)
if value.is_a?(Parameters) || !value.is_a?(Hash)
value
@@ -217,7 +234,7 @@ def unpermitted_parameters!(params)
unpermitted_keys = unpermitted_keys(params)
if unpermitted_keys.any?
- case self.class.action_on_unpermitted_parameters
+ case self.class.action_on_unpermitted_parameters
when :log
name = "unpermitted_parameters.action_controller"
ActiveSupport::Notifications.instrument(name, :keys => unpermitted_keys)
@@ -237,7 +254,7 @@ module StrongParameters
included do
rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception|
- render :text => "Required parameter missing: #{parameter_missing_exception.param}", :status => :bad_request
+ render :text => "Required parameter(s) missing: #{parameter_missing_exception.param}", :status => :bad_request
end
end
View
2  test/action_controller_required_params_test.rb
@@ -25,6 +25,6 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase
test "missing parameters will be mentioned in the return" do
post :create, { :magazine => { :name => "Mjallo!" } }
- assert_equal "Required parameter missing: book", response.body
+ assert_match "book", response.body
end
end
View
4 test/log_on_unpermitted_params_test.rb
@@ -16,7 +16,7 @@ def teardown
:fishing => "Turnips"
})
- assert_logged("Unpermitted parameters: fishing") do
+ assert_logged("Unpermitted parameter(s): fishing") do
params.permit(:book => [:pages])
end
end
@@ -26,7 +26,7 @@ def teardown
:book => { :pages => 65, :title => "Green Cats and where to find then." }
})
- assert_logged("Unpermitted parameters: title") do
+ assert_logged("Unpermitted parameter(s): title") do
params.permit(:book => [:pages])
end
end
View
25 test/parameters_require_test.rb
@@ -7,4 +7,29 @@ class ParametersRequireTest < ActiveSupport::TestCase
ActionController::Parameters.new(:person => {}).require(:person)
end
end
+
+ test "permit multiple required parameters" do
+ params = ActionController::Parameters.new(:username => 'user', :password => '<3<3<3<3')
+ assert_nothing_raised(ActionController::ParameterMissing) do
+ params.require(:username, :password)
+ end
+
+ assert params.has_key?(:username)
+ assert params.has_key?(:password)
+ end
+
+ test "multiple required parameters must be present not merely not nil" do
+ params = ActionController::Parameters.new(:username => '', :password => nil)
+ assert_raises(ActionController::ParameterMissing) do
+ params.require(:username, :password)
+ end
+ end
+
+ test "all parameters are returned after required with multiple parameters" do
+ params = ActionController::Parameters.new(:username => 'user', :password => '<3<3<3<3', :version => 1)
+
+ params.require(:username, :password)
+
+ assert params.has_key?(:version)
@fxn Owner
fxn added a note

I think this test should check the value returned by the call to require.

@d-Pixie
d-Pixie added a note
@fxn Owner
fxn added a note

I mean, the interface of require is its return value:

params.require(...).permit(...)

The suite has to check that the hash returned by require satisfies such and such. Granted, the implementation uses self, but if tomorrow you return self.dup the suite should still pass because the contract with the user has not changed.

@d-Pixie
d-Pixie added a note
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
end
Please sign in to comment.
Something went wrong with that request. Please try again.