Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate params only if another param is present #1047

Merged
merged 1 commit into from
Jun 29, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Next Release
#### Features

* [#1039](https://github.com/intridea/grape/pull/1039): Added support for custom parameter types - [@rnubel](https://github.com/rnubel).
* [#1047](https://github.com/intridea/grape/pull/1047): Adds `given` to DSL::Parameters, allowing for dependent params - [@rnubel](https://github.com/rnubel).
* Your contribution here!

#### Fixes
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [Parameter Validation and Coercion](#parameter-validation-and-coercion)
- [Supported Parameter Types](#supported-parameter-types)
- [Custom Types](#custom-types)
- [Dependent Parameters](#dependent-parameters)
- [Built-in Validators](#built-in-validators)
- [Namespace Validation and Coercion](#namespace-validation-and-coercion)
- [Custom Validators](#custom-validators)
Expand Down Expand Up @@ -803,6 +804,21 @@ params do
end
```

#### Dependent Parameters

Suppose some of your parameters are only relevant if another parameter is given;
Grape allows you to express this relationship through the `given` method in your
parameters block, like so:

```ruby
params do
optional :shelf_id, type: Integer
given :shelf_id do
requires :bin_id, type: Integer
end
end
```

### Built-in Validators

#### `allow_blank`
Expand Down
1 change: 1 addition & 0 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ module Exceptions
autoload :InvalidVersionerOption
autoload :UnknownValidator
autoload :UnknownOptions
autoload :UnknownParameter
autoload :InvalidWithOptionForRepresent
autoload :IncompatibleOptionValues
autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type'
Expand Down
21 changes: 21 additions & 0 deletions lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,27 @@ def all_or_none_of(*attrs)
validates(attrs, all_or_none_of: true)
end

# Define a block of validations which should be applied if and only if
# the given parameter is present. The parameters are not nested.
# @param attr [Symbol] the parameter which, if present, triggers the
# validations
# @throws Grape::Exceptions::UnknownParameter if `attr` has not been
# defined in this scope yet
# @yield a parameter definition DSL
def given(attr, &block)
fail Grape::Exceptions::UnknownParameter.new(attr) unless declared_param?(attr)
new_lateral_scope(dependent_on: attr, &block)
end

# Test for whether a certain parameter has been defined in this params
# block yet.
# @returns [Boolean] whether the parameter has been defined
def declared_param?(param)
# @declared_params also includes hashes of options and such, but those
# won't be flattened out.
@declared_params.flatten.include?(param)
end

alias_method :group, :requires

# @param params [Hash] initial hash of parameters
Expand Down
10 changes: 10 additions & 0 deletions lib/grape/exceptions/unknown_parameter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# encoding: utf-8
module Grape
module Exceptions
class UnknownParameter < Base
def initialize(param)
super(message: compose_message('unknown_parameter', param: param))
end
end
end
end
1 change: 1 addition & 0 deletions lib/grape/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ en:
resolution: 'available strategy for :using is :path, :header, :param'
unknown_validator: 'unknown validator: %{validator_type}'
unknown_options: 'unknown options: %{options}'
unknown_parameter: 'unknown parameter: %{param}'
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
mutual_exclusion: 'are mutually exclusive'
at_least_one: 'are missing, at least one parameter must be provided'
Expand Down
72 changes: 62 additions & 10 deletions lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ class ParamsScope
# @option opts :optional [Boolean] whether or not this scope needs to have
# any parameters set or not
# @option opts :type [Class] a type meant to govern this scope (deprecated)
# @option opts :dependent_on [Symbol] if present, this scope should only
# validate if this param is present in the parent scope
# @yield the instance context, open for parameter definitions
def initialize(opts, &block)
@element = opts[:element]
@parent = opts[:parent]
@api = opts[:api]
@optional = opts[:optional] || false
@type = opts[:type]
@element = opts[:element]
@parent = opts[:parent]
@api = opts[:api]
@optional = opts[:optional] || false
@type = opts[:type]
@dependent_on = opts[:dependent_on]
@declared_params = []

instance_eval(&block) if block_given?
Expand All @@ -33,21 +36,45 @@ def initialize(opts, &block)
# validated
def should_validate?(parameters)
return false if @optional && params(parameters).respond_to?(:all?) && params(parameters).all?(&:blank?)
return false if @dependent_on && params(parameters).try(:[], @dependent_on).blank?
return true if parent.nil?
parent.should_validate?(parameters)
end

# @return [String] the proper attribute name, with nesting considered.
def full_name(name)
return "#{@parent.full_name(@element)}[#{name}]" if @parent
name.to_s
case
when nested?
# Find our containing element's name, and append ours.
"#{@parent.full_name(@element)}[#{name}]"
when lateral?
# Find the name of the element as if it was at the
# same nesting level as our parent.
@parent.full_name(name)
else
# We must be the root scope, so no prefix needed.
name.to_s
end
end

# @return [Boolean] whether or not this scope is the root-level scope
def root?
!@parent
end

# A nested scope is contained in one of its parent's elements.
# @return [Boolean] whether or not this scope is nested
def nested?
@parent && @element
end

# A lateral scope is subordinate to its parent, but its keys are at the
# same level as its parent and thus is not contained within an element.
# @return [Boolean] whether or not this scope is lateral
def lateral?
@parent && !@element
end

# @return [Boolean] whether or not this scope needs to be present, or can
# be blank
def required?
Expand All @@ -57,7 +84,7 @@ def required?
protected

# Adds a parameter declaration to our list of validations.
# @param attrs [Array] (see Grape::DSL::Parameters#required)
# @param attrs [Array] (see Grape::DSL::Parameters#requires)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing the comment here.

def push_declared_params(attrs)
@declared_params.concat attrs
end
Expand Down Expand Up @@ -98,6 +125,13 @@ def validate_attributes(attrs, opts, &block)
validates(attrs, validations)
end

# Returns a new parameter scope, subordinate to the current one and nested
# under the parameter corresponding to `attrs.first`.
# @param attrs [Array] the attributes passed to the `requires` or
# `optional` invocation that opened this scope.
# @param optional [Boolean] whether the parameter this are nested under
# is optional or not (and hence, whether this block's params will be).
# @yield parameter scope
def new_scope(attrs, optional = false, &block)
# if required params are grouped and no type or unsupported type is provided, raise an error
type = attrs[1] ? attrs[1][:type] : nil
Expand All @@ -107,12 +141,30 @@ def new_scope(attrs, optional = false, &block)
end

opts = attrs[1] || { type: Array }
ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
self.class.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
end

# Returns a new parameter scope, not nested under any current-level param
# but instead at the same level as the current scope.
# @param options [Hash] options to control how this new scope behaves
# @option options :dependent_on [Symbol] if given, specifies that this
# scope should only validate if this parameter from the above scope is
# present
# @yield parameter scope
def new_lateral_scope(options, &block)
self.class.new(
api: @api,
element: nil,
parent: self,
options: @optional,
type: Hash,
dependent_on: options[:dependent_on],
&block)
end

# Pushes declared params to parent or settings
def configure_declared_params
if @parent
if nested?
@parent.push_declared_params [element => @declared_params]
else
@api.namespace_stackable(:declared_params, @declared_params)
Expand Down
55 changes: 55 additions & 0 deletions spec/grape/validations/params_scope_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,59 @@ def initialize(value)
end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError
end
end

context 'when validations are dependent on a parameter' do
before do
subject.params do
optional :a
given :a do
requires :b
end
end
subject.get('/test') { declared(params).to_json }
end

it 'applies the validations only if the parameter is present' do
get '/test'
expect(last_response.status).to eq(200)

get '/test', a: true
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('b is missing')

get '/test', a: true, b: true
expect(last_response.status).to eq(200)
end

it 'raises an error if the dependent parameter was never specified' do
expect do
subject.params do
given :c do
end
end
end.to raise_error(Grape::Exceptions::UnknownParameter)
end

it 'includes the parameter within #declared(params)' do
get '/test', a: true, b: true

expect(JSON.parse(last_response.body)).to eq('a' => 'true', 'b' => 'true')
end

it 'returns a sensible error message within a nested context' do
subject.params do
requires :bar, type: Hash do
optional :a
given :a do
requires :b
end
end
end
subject.get('/nested') { 'worked' }

get '/nested', bar: { a: true }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('bar[b] is missing')
end
end
end