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

Feature: add group attributes for parameter definitions #1507

Merged
merged 2 commits into from
Oct 12, 2016
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
Next Release
============

* Your contribution here.
* [#1503](https://github.com/ruby-grape/grape/pull/1503): Allow to use regexp validator with arrays - [@akoltun](https://github.com/akoltun).
* [#1507](https://github.com/ruby-grape/grape/pull/1507): Add group attributes for parameter definitions - [@304](https://github.com/304).
* Your contribution here.

0.18.0 (10/7/2016)
==================
Expand Down
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- [Multiple Allowed Types](#multiple-allowed-types)
- [Validation of Nested Parameters](#validation-of-nested-parameters)
- [Dependent Parameters](#dependent-parameters)
- [Group Options](#group-options)
- [Built-in Validators](#built-in-validators)
- [Namespace Validation and Coercion](#namespace-validation-and-coercion)
- [Custom Validators](#custom-validators)
Expand Down Expand Up @@ -1001,6 +1002,35 @@ params do
end
```


### Group Options

Parameters options can be grouped. It can be useful if you want to extract
common validation or types for several parameters. The example below presents a
typical case when parameters share common options.

```ruby
params do
requires :first_name, type: String, regexp: /w+/, desc: 'First name'
requires :middle_name, type: String, regexp: /w+/, desc: 'Middle name'
requires :last_name, type: String, regexp: /w+/, desc: 'Last name'
end
```

Grape allows you to present the same logic through the `with` method in your
parameters block, like so:

```ruby
params do
with(type: String, regexp: /w+/) do
requires :first_name, desc: 'First name'
requires :middle_name, desc: 'Middle name'
requires :last_name, desc: 'Last name'
end
end
```


### Built-in Validators

#### `allow_blank`
Expand Down Expand Up @@ -1076,8 +1106,8 @@ end
```

Values and except can be combined to define a range of accepted values while not allowing
certain values within the set. Custom error messages can be defined for both when the parameter
passed falls within the ```except``` list or when it falls entirely outside the ```value``` list.
certain values within the set. Custom error messages can be defined for both when the parameter
passed falls within the ```except``` list or when it falls entirely outside the ```value``` list.

```ruby
params do
Expand Down
9 changes: 9 additions & 0 deletions lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def requires(*attrs, &block)

opts = attrs.extract_options!.clone
opts[:presence] = { value: true, message: opts[:message] }
opts = @group.merge(opts) if @group

if opts[:using]
require_required_and_optional_fields(attrs.first, opts)
Expand All @@ -118,6 +119,7 @@ def optional(*attrs, &block)

opts = attrs.extract_options!.clone
type = opts[:type]
opts = @group.merge(opts) if @group

# check type for optional parameter group
if attrs && block_given?
Expand All @@ -134,6 +136,13 @@ def optional(*attrs, &block)
end
end

# Define common settings for one or more parameters
# @param (see #requires)
# @option (see #requires)
def with(*attrs, &block)
new_group_scope(attrs.clone, &block)
end

# Disallow the given parameters to be present in the same request.
# @param attrs [*Symbol] parameters to validate
def mutually_exclusive(*attrs)
Expand Down
15 changes: 15 additions & 0 deletions lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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 :type [Hash] group options for this scope
# @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
Expand All @@ -25,6 +26,7 @@ def initialize(opts, &block)
@api = opts[:api]
@optional = opts[:optional] || false
@type = opts[:type]
@group = opts[:group] || {}
@dependent_on = opts[:dependent_on]
@declared_params = []

Expand Down Expand Up @@ -189,6 +191,19 @@ def new_lateral_scope(options, &block)
&block)
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.
# @yield parameter scope
def new_group_scope(attrs, &block)
self.class.new(
api: @api,
parent: self,
group: attrs.first,
&block)
end

# Pushes declared params to parent or settings
def configure_declared_params
if nested?
Expand Down
14 changes: 14 additions & 0 deletions spec/grape/dsl/parameters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def validates_reader
@validates
end

def new_group_scope(args)
@group = args.clone.first
yield
end

def extract_message_option(attrs)
return nil unless attrs.is_a?(Array)
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
Expand Down Expand Up @@ -92,6 +97,15 @@ def extract_message_option(attrs)
end
end

describe '#with' do
it 'creates a scope with group attributes' do
subject.with(type: Integer) { subject.optional :id, desc: 'Identity.' }

expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }])
expect(subject.push_declared_params_reader).to eq([[:id]])
end
end

describe '#mutually_exclusive' do
it 'adds an mutally exclusive parameter validation' do
subject.mutually_exclusive :media, :audio
Expand Down
163 changes: 163 additions & 0 deletions spec/grape/validations/params_scope_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -624,4 +624,167 @@ def initialize(value)
end
end
end

context 'when params have group attributes' do
context 'with validations' do
before do
subject.params do
with(allow_blank: false) do
requires :id
optional :name
optional :address, allow_blank: true
end
end
subject.get('test')
end

context 'when data is invalid' do
before do
get 'test', id: '', name: ''
end

it 'returns a validation error' do
expect(last_response.status).to eq(400)
end

it 'applies group validations for every parameter' do
expect(last_response.body).to eq('id is empty, name is empty')
end
end

context 'when parameter has the same validator as a group' do
before do
get 'test', id: 'id', address: ''
end

it 'returns a successful response' do
expect(last_response.status).to eq(200)
end

it 'prioritizes parameter validation over group validation' do
expect(last_response.body).to_not include('address is empty')
end
end
end

context 'with types' do
before do
subject.params do
with(type: Date) do
requires :created_at
end
end
subject.get('test') { params[:created_at] }
end

context 'when invalid date provided' do
before do
get 'test', created_at: 'not_a_date'
end

it 'responds with HTTP error' do
expect(last_response.status).to eq(400)
end

it 'returns a validation error' do
expect(last_response.body).to eq('created_at is invalid')
end
end

context 'when created_at receives a valid date' do
before do
get 'test', created_at: '2016-01-01'
end

it 'returns a successful response' do
expect(last_response.status).to eq(200)
end

it 'returns a date' do
expect(last_response.body).to eq('2016-01-01')
end
end
end

context 'with several group attributes' do
before do
subject.params do
with(values: [1]) do
requires :id, type: Integer
end

with(allow_blank: false) do
optional :address, type: String
end

requires :name
end
subject.get('test')
end

context 'when data is invalid' do
before do
get 'test', id: 2, address: ''
end

it 'responds with HTTP error' do
expect(last_response.status).to eq(400)
end

it 'returns a validation error' do
expect(last_response.body).to eq('id does not have a valid value, address is empty, name is missing')
end
end

context 'when correct data is provided' do
before do
get 'test', id: 1, address: 'Some street', name: 'John'
end

it 'returns a successful response' do
expect(last_response.status).to eq(200)
end
end
end

context 'with nested groups' do
before do
subject.params do
with(type: Integer) do
requires :id

with(type: Date) do
requires :created_at
optional :updated_at
end
end
end
subject.get('test')
end

context 'when data is invalid' do
before do
get 'test', id: 'wrong', created_at: 'not_a_date', updated_at: '2016-01-01'
end

it 'responds with HTTP error' do
expect(last_response.status).to eq(400)
end

it 'returns a validation error' do
expect(last_response.body).to eq('id is invalid, created_at is invalid')
end
end

context 'when correct data is provided' do
before do
get 'test', id: 1, created_at: '2016-01-01'
end

it 'returns a successful response' do
expect(last_response.status).to eq(200)
end
end
end
end
end