Skip to content

Commit e4b704c

Browse files
authored
Merge pull request #1622 from jlfaber/except_values
Feature #1616 - split out except_values validator
2 parents fc5300d + 82f2bb5 commit e4b704c

File tree

10 files changed

+344
-52
lines changed

10 files changed

+344
-52
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#### Features
44

55
* [#1594](https://github.com/ruby-grape/grape/pull/1594): Replace `Hashie::Mash` parameters with `ActiveSupport::HashWithIndifferentAccess` - [@james2m](https://github.com/james2m), [@dblock](https://github.com/dblock).
6+
* [#1622](https://github.com/ruby-grape/grape/pull/1622): Add `except_values` validator to replace `except` option of `values` validator - [@jlfaber](https://github.com/jlfaber).
67
* Your contribution here.
78

89
#### Fixes

README.md

+22-24
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,10 @@ end
807807
Note that default values will be passed through to any validation options specified.
808808
The following example will always fail if `:color` is not explicitly provided.
809809

810+
Default values are eagerly evaluated. Above `:non_random_number` will evaluate to the same
811+
number for each call to the endpoint of this `params` block. To have the default evaluate
812+
lazily with each request use a lambda, like `:random_number` above.
813+
810814
```ruby
811815
params do
812816
optional :color, type: String, default: 'blue', values: ['red', 'green']
@@ -1114,9 +1118,6 @@ end
11141118

11151119
Parameters can be restricted to a specific set of values with the `:values` option.
11161120

1117-
Default values are eagerly evaluated. Above `:non_random_number` will evaluate to the same
1118-
number for each call to the endpoint of this `params` block. To have the default evaluate
1119-
lazily with each request use a lambda, like `:random_number` above.
11201121

11211122
```ruby
11221123
params do
@@ -1135,7 +1136,7 @@ params do
11351136
end
11361137
```
11371138

1138-
Note that *both* range endpoints have to be a `#kind_of?` your `:type` option (if you don't supplied the `:type` option, it will be guessed to be equal to the class of the range's first endpoint). So the following is invalid:
1139+
Note that *both* range endpoints have to be a `#kind_of?` your `:type` option (if you don't supply the `:type` option, it will be guessed to be equal to the class of the range's first endpoint). So the following is invalid:
11391140

11401141
```ruby
11411142
params do
@@ -1145,6 +1146,9 @@ end
11451146
```
11461147

11471148
The `:values` option can also be supplied with a `Proc`, evaluated lazily with each request.
1149+
If the Proc has arity zero (i.e. it takes no arguments) it is expected to return either a list
1150+
or a range which will then be used to validate the parameter.
1151+
11481152
For example, given a status model you may want to restrict by hashtags that you have
11491153
previously defined in the `HashTag` model.
11501154

@@ -1154,40 +1158,34 @@ params do
11541158
end
11551159
```
11561160

1157-
The values validator can also validate that the value is explicitly not within a specific
1158-
set of values by passing ```except```. ```except``` accepts the same types of parameters as
1159-
values (Procs, ranges, etc.).
1161+
Alternatively, a Proc with arity one (i.e. taking one argument) can be used to explicitly validate
1162+
each parameter value. In that case, the Proc is expected to return a truthy value if the parameter
1163+
value is valid.
11601164

11611165
```ruby
11621166
params do
1163-
requires :browsers, values: { except: [ 'ie6', 'ie7', 'ie8' ] }
1167+
requires :number, type: Integer, values: ->(v) { v.even? && v < 25 }
11641168
end
11651169
```
11661170

1167-
Values and except can be combined to define a range of accepted values while not allowing
1168-
certain values within the set. Custom error messages can be defined for both when the parameter
1169-
passed falls within the ```except``` list or when it falls entirely outside the ```value``` list.
1171+
While Procs are convenient for single cases, consider using [Custom Validators](#custom-validators) in cases where a validation is used more than once.
11701172

1171-
```ruby
1172-
params do
1173-
requires :number, type: Integer, values: { value: 1..20, except: [4, 13], except_message: 'includes unsafe numbers', message: 'is outside the range of numbers allowed' }
1174-
end
1175-
```
1173+
#### `except_values`
11761174

1177-
Finally, for even greater control, an explicit validation Proc may be supplied using ```proc```.
1178-
It will be called with a single argument (the input value), and should return
1179-
a truthy value if the value passes validation. If the input is an array, the Proc will be called
1180-
multiple times, once for each element in the array.
1175+
Parameters can be restricted from having a specific set of values with the `:except_values` option.
1176+
1177+
The `except_values` validator behaves similarly to the `values` validator in that it accepts either
1178+
an Array, a Range, or a Proc. Unlike the `values` validator, however, `except_values` only accepts
1179+
Procs with arity zero.
11811180

11821181
```ruby
11831182
params do
1184-
requires :number, type: Integer, values: { proc: ->(v) { v.even? && v < 25 }, message: 'is odd or greater than 25' }
1183+
requires :browser, except_values: [ 'ie6', 'ie7', 'ie8' ]
1184+
requires :port, except_values: { value: 0..1024, message: 'is not allowed' }
1185+
requires :hashtag, except_values: -> { Hashtag.FORBIDDEN_LIST }
11851186
end
11861187
```
11871188

1188-
While ```proc``` is convenient for single cases, consider using [Custom Validators](#custom-validators) in cases where a validation is used more than once.
1189-
1190-
11911189
#### `regexp`
11921190

11931191
Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value

UPGRADING.md

+28
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,34 @@ end
5252

5353
See [#1610](https://github.com/ruby-grape/grape/pull/1610) for more information.
5454

55+
#### The `except`, `except_message`, and `proc` options of the `values` validator are deprecated.
56+
57+
The new `except_values` validator should be used in place of the `except` and `except_message` options of
58+
the `values` validator.
59+
60+
Arity one Procs may now be used directly as the `values` option to explicitly test param values.
61+
62+
**Deprecated**
63+
```ruby
64+
params do
65+
requires :a, values: { value: 0..99, except: [3] }
66+
requires :b, values: { value: 0..99, except: [3], except_message: 'not allowed' }
67+
requires :c, values: { except: ['admin'] }
68+
requires :d, values: { proc: -> (v) { v.even? } }
69+
end
70+
```
71+
**New**
72+
```ruby
73+
params do
74+
requires :a, values: 0..99, except_values: [3]
75+
requires :b, values: 0..99, except_values: { value: [3], message: 'not allowed' }
76+
requires :c, except_values: ['admin']
77+
requires :d, values: -> (v) { v.even? }
78+
end
79+
```
80+
81+
See [#1616](https://github.com/ruby-grape/grape/pull/1616) for more information.
82+
5583
### Upgrading to >= 0.19.1
5684

5785
#### DELETE now defaults to status code 200 for responses with a body, or 204 otherwise

lib/grape.rb

+1
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ module ServeFile
206206
require 'grape/validations/validators/presence'
207207
require 'grape/validations/validators/regexp'
208208
require 'grape/validations/validators/values'
209+
require 'grape/validations/validators/except_values'
209210
require 'grape/validations/params_scope'
210211
require 'grape/validations/validators/all_or_none'
211212
require 'grape/validations/types'

lib/grape/locale/en.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ en:
88
regexp: 'is invalid'
99
blank: 'is empty'
1010
values: 'does not have a valid value'
11-
except: 'has a value not allowed'
11+
except_values: 'has a value not allowed'
1212
missing_vendor_option:
1313
problem: 'missing :vendor option.'
1414
summary: 'when version using header, you must specify :vendor option. '

lib/grape/validations/params_scope.rb

+39-22
Original file line numberDiff line numberDiff line change
@@ -234,26 +234,26 @@ def validates(attrs, validations)
234234

235235
if (values_hash = validations[:values]).is_a? Hash
236236
values = values_hash[:value]
237+
# NB: excepts is deprecated
237238
excepts = values_hash[:except]
238239
else
239240
values = validations[:values]
240241
end
241242
doc_attrs[:values] = values if values
242243

244+
except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]
245+
243246
# NB. values and excepts should be nil, Proc, Array, or Range.
244247
# Specifically, values should NOT be a Hash
245248

246249
# use values or excepts to guess coerce type when stated type is Array
247-
coerce_type = guess_coerce_type(coerce_type, values)
248-
coerce_type = guess_coerce_type(coerce_type, excepts)
250+
coerce_type = guess_coerce_type(coerce_type, values, except_values, excepts)
249251

250252
# default value should be present in values array, if both exist and are not procs
251-
check_incompatible_option_values(values, default)
253+
check_incompatible_option_values(default, values, except_values, excepts)
252254

253255
# type should be compatible with values array, if both exist
254-
validate_value_coercion(coerce_type, values)
255-
# type should be compatible with excepts array, if both exist
256-
validate_value_coercion(coerce_type, excepts)
256+
validate_value_coercion(coerce_type, values, except_values, excepts)
257257

258258
doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation)
259259

@@ -358,17 +358,31 @@ def coerce_type(validations, attrs, doc_attrs, opts)
358358
validations.delete(:coerce_message)
359359
end
360360

361-
def guess_coerce_type(coerce_type, values)
362-
return coerce_type if !values || values.is_a?(Proc)
363-
return values.first.class if coerce_type == Array && (values.is_a?(Range) || !values.empty?)
361+
def guess_coerce_type(coerce_type, *values_list)
362+
return coerce_type unless coerce_type == Array
363+
values_list.each do |values|
364+
next if !values || values.is_a?(Proc)
365+
return values.first.class if values.is_a?(Range) || !values.empty?
366+
end
364367
coerce_type
365368
end
366369

367-
def check_incompatible_option_values(values, default)
368-
return unless values && default
369-
return if values.is_a?(Proc) || default.is_a?(Proc)
370-
return if values.include?(default) || (Array(default) - Array(values)).empty?
371-
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values)
370+
def check_incompatible_option_values(default, values, except_values, excepts)
371+
return unless default && !default.is_a?(Proc)
372+
373+
if values && !values.is_a?(Proc)
374+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) \
375+
unless Array(default).all? { |def_val| values.include?(def_val) }
376+
end
377+
378+
if except_values && !except_values.is_a?(Proc)
379+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \
380+
unless Array(default).none? { |def_val| except_values.include?(def_val) }
381+
end
382+
383+
return unless excepts && !excepts.is_a?(Proc)
384+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, excepts) \
385+
unless Array(default).none? { |def_val| excepts.include?(def_val) }
372386
end
373387

374388
def validate(type, options, attrs, doc_attrs, opts)
@@ -380,16 +394,19 @@ def validate(type, options, attrs, doc_attrs, opts)
380394
@api.namespace_stackable(:validations, value)
381395
end
382396

383-
def validate_value_coercion(coerce_type, values)
384-
return unless coerce_type && values
385-
return if values.is_a?(Proc)
397+
def validate_value_coercion(coerce_type, *values_list)
398+
return unless coerce_type
386399
coerce_type = coerce_type.first if coerce_type.is_a?(Array)
387-
value_types = values.is_a?(Range) ? [values.begin, values.end] : values
388-
if coerce_type == Virtus::Attribute::Boolean
389-
value_types = value_types.map { |type| Virtus::Attribute.build(type) }
400+
values_list.each do |values|
401+
next if !values || values.is_a?(Proc)
402+
value_types = values.is_a?(Range) ? [values.begin, values.end] : values
403+
if coerce_type == Virtus::Attribute::Boolean
404+
value_types = value_types.map { |type| Virtus::Attribute.build(type) }
405+
end
406+
unless value_types.all? { |v| v.is_a? coerce_type }
407+
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
408+
end
390409
end
391-
return unless value_types.any? { |v| !v.is_a?(coerce_type) }
392-
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
393410
end
394411

395412
def extract_message_option(attrs)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Grape
2+
module Validations
3+
class ExceptValuesValidator < Base
4+
def initialize(attrs, options, required, scope, opts = {})
5+
@except = options.is_a?(Hash) ? options[:value] : options
6+
super
7+
end
8+
9+
def validate_param!(attr_name, params)
10+
return unless params.respond_to?(:key?) && params.key?(attr_name)
11+
12+
excepts = @except.is_a?(Proc) ? @except.call : @except
13+
return if excepts.nil?
14+
15+
param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
16+
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:except_values) if param_array.any? { |param| excepts.include?(param) }
17+
end
18+
end
19+
end
20+
end

lib/grape/validations/validators/values.rb

+22-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ def initialize(attrs, options, required, scope, opts = {})
66
@excepts = options[:except]
77
@values = options[:value]
88
@proc = options[:proc]
9+
10+
warn '[DEPRECATION] The values validator except option is deprecated. ' \
11+
'Use the except validator instead.' if @excepts
12+
913
raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc)
14+
warn '[DEPRECATION] The values validator proc option is deprecated. ' \
15+
'The lambda expression can now be assigned directly to values.' if @proc
1016
else
1117
@values = options
1218
end
@@ -17,25 +23,36 @@ def validate_param!(attr_name, params)
1723
return unless params.is_a?(Hash)
1824
return unless params[attr_name] || required_for_root_scope?
1925

20-
values = @values.is_a?(Proc) ? @values.call : @values
21-
excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts
2226
param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
2327

2428
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: except_message \
25-
if !excepts.nil? && param_array.any? { |param| excepts.include?(param) }
29+
unless check_excepts(param_array)
2630

2731
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values) \
28-
if !values.nil? && !param_array.all? { |param| values.include?(param) }
32+
unless check_values(param_array)
2933

3034
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values) \
3135
if @proc && !param_array.all? { |param| @proc.call(param) }
3236
end
3337

3438
private
3539

40+
def check_values(param_array)
41+
values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values
42+
return true if values.nil?
43+
return param_array.all? { |param| values.call(param) } if values.is_a? Proc
44+
param_array.all? { |param| values.include?(param) }
45+
end
46+
47+
def check_excepts(param_array)
48+
excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts
49+
return true if excepts.nil?
50+
param_array.none? { |param| excepts.include?(param) }
51+
end
52+
3653
def except_message
3754
options = instance_variable_get(:@option)
38-
options_key?(:except_message) ? options[:except_message] : message(:except)
55+
options_key?(:except_message) ? options[:except_message] : message(:except_values)
3956
end
4057

4158
def required_for_root_scope?

0 commit comments

Comments
 (0)