Skip to content

Commit

Permalink
implemented except in values validator (#1486)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonmchan authored and dblock committed Sep 9, 2016
1 parent 7d1eb5c commit 3540ea2
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
==================

* [#1480](https://github.com/ruby-grape/grape/pull/1480): Use the ruby-grape-danger gem for PR linting - [@dblock](https://github.com/dblock).
* [#1486](https://github.com/ruby-grape/grape/pull/1486): Implemented except in values validator - [@jonmchan](https://github.com/jonmchan).
* Your contribution here.

#### Fixes
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,26 @@ params do
end
```

The values validator can also validate that the value is explicitly not within a specific
set of values by passing ```except```. ```except``` accepts the same types of parameters as
values (Procs, ranges, etc.).

```ruby
params do
requires :browsers, values: { except: [ 'ie6', 'ie7', 'ie8' ] }
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.

```ruby
params do
requires :number, type: Integer, values: { value: 1..20 except: [4,13], except_message: 'includes unsafe numbers', message: 'is outside the range of numbers allowed' }
end
```

#### `regexp`

Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value
Expand Down
1 change: 1 addition & 0 deletions lib/grape/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ en:
regexp: 'is invalid'
blank: 'is empty'
values: 'does not have a valid value'
except: 'has a value not allowed'
missing_vendor_option:
problem: 'missing :vendor option.'
summary: 'when version using header, you must specify :vendor option. '
Expand Down
18 changes: 16 additions & 2 deletions lib/grape/validations/validators/values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module Grape
module Validations
class ValuesValidator < Base
def initialize(attrs, options, required, scope)
@values = (options_key?(:value, options) ? options[:value] : options)
@excepts = (options_key?(:except, options) ? options[:except] : [])
@values = (options_key?(:value, options) ? options[:value] : [])

@values = options if @excepts == [] && @values == []
super
end

Expand All @@ -11,13 +14,24 @@ def validate_param!(attr_name, params)
return unless params[attr_name] || required_for_root_scope?

values = @values.is_a?(Proc) ? @values.call : @values
excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts
param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
return if param_array.all? { |param| values.include?(param) }

if param_array.all? { |param| excepts.include?(param) }
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: except_message
end

return if (values.is_a?(Array) && values.empty?) || param_array.all? { |param| values.include?(param) }
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values)
end

private

def except_message
options = instance_variable_get(:@option)
options_key?(:except_message) ? options[:except_message] : message(:except)
end

def required_for_root_scope?
@required && @scope.root?
end
Expand Down
89 changes: 89 additions & 0 deletions spec/grape/validations/validators/values_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
module ValidationsSpec
class ValuesModel
DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3'].freeze
DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze
class << self
def values
@values ||= []
Expand All @@ -14,6 +15,16 @@ def add_value(value)
@values ||= []
@values << value
end

def excepts
@excepts ||= []
[DEFAULT_EXCEPTS + @excepts].flatten.uniq
end

def add_except(except)
@excepts ||= []
@excepts << except
end
end
end

Expand All @@ -35,6 +46,20 @@ class API < Grape::API
get '/lambda' do
{ type: params[:type] }
end

params do
requires :type, values: { except: ValuesModel.excepts, except_message: 'value is on exclusions list', message: 'default exclude message' }
end
get '/exclude/exclude_message' do
{ type: params[:type] }
end

params do
requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' }
end
get '/exclude/fallback_message' do
{ type: params[:type] }
end
end

params do
Expand Down Expand Up @@ -99,6 +124,20 @@ class API < Grape::API
end
end
get '/optional_with_required_values'

params do
requires :type, values: { except: ValuesModel.excepts }
end
get '/except/exclusive' do
{ type: params[:type] }
end

params do
requires :type, type: Integer, values: { value: 1..5, except: [3] }
end
get '/mixed/value/except' do
{ type: params[:type] }
end
end
end
end
Expand Down Expand Up @@ -135,6 +174,22 @@ def app
end
end

context 'with a custom exclude validation message' do
it 'does not allow an invalid value for a parameter' do
get('/custom_message/exclude/exclude_message', type: 'invalid-type1')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type value is on exclusions list' }.to_json)
end
end

context 'exclude with a standard custom validation message' do
it 'does not allow an invalid value for a parameter' do
get('/custom_message/exclude/fallback_message', type: 'invalid-type1')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type default exclude message' }.to_json)
end
end

it 'allows a valid value for a parameter' do
get('/', type: 'valid-type1')
expect(last_response.status).to eq 200
Expand Down Expand Up @@ -321,4 +376,38 @@ def app
expect(last_response.body).to eq('values does not have a valid value')
end
end

context 'exclusive excepts' do
it 'allows any other value outside excepts' do
get '/except/exclusive', type: 'value'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'value' }.to_json)
end

it 'rejects values that matches except' do
get '/except/exclusive', type: 'invalid-type1'
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
end
end

context 'with mixed values and excepts' do
it 'allows value, but not in except' do
get '/mixed/value/except', type: 2
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 2 }.to_json)
end

it 'rejects except' do
get '/mixed/value/except', type: 3
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
end

it 'rejects outside except and outside value' do
get '/mixed/value/except', type: 10
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
end
end

0 comments on commit 3540ea2

Please sign in to comment.