diff --git a/CHANGELOG.md b/CHANGELOG.md index cb63034fe8..4d045dd388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#1555](https://github.com/ruby-grape/grape/pull/1555): Added code coverage w/Coveralls - [@dblock](https://github.com/dblock). +* [#1568](https://github.com/ruby-grape/grape/pull/1568): Add `proc` option to `values` validator to allow custom checks - [@jlfaber](https://github.com/jlfaber). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index 7e7337197d..0fdd2fd610 100644 --- a/README.md +++ b/README.md @@ -1152,6 +1152,20 @@ params do end ``` +Finally, for even greater control, an explicit validation Proc may be supplied using ```proc```. +It will be called with a single argument (the input value), and should return +a truthy value if the value passes validation. If the input is an array, the Proc will be called +multiple times, once for each element in the array. + +```ruby +params do + requires :number, type: Integer, values: { proc: ->(v) { v.even? && v < 25 }, message: 'is odd or greater than 25' } +end +``` + +While ```proc``` is convenient for single cases, consider using [Custom Validators](#custom-validators) in cases where a validation is used more than once. + + #### `regexp` Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value diff --git a/lib/grape/validations/validators/values.rb b/lib/grape/validations/validators/values.rb index 9dc626c2d9..1926bc9020 100644 --- a/lib/grape/validations/validators/values.rb +++ b/lib/grape/validations/validators/values.rb @@ -5,6 +5,8 @@ def initialize(attrs, options, required, scope, opts = {}) if options.is_a?(Hash) @excepts = options[:except] @values = options[:value] + @proc = options[:proc] + raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) else @values = options end @@ -24,6 +26,9 @@ def validate_param!(attr_name, params) raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values) \ if !values.nil? && !param_array.all? { |param| values.include?(param) } + + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values) \ + if @proc && !param_array.all? { |param| @proc.call(param) } end private diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index e102e83fa0..47d75b8026 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -50,23 +50,17 @@ class API < Grape::API 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 + get '/exclude/exclude_message' params do requires :type, values: { except: -> { ValuesModel.excepts }, except_message: 'value is on exclusions list' } end - get '/exclude/lambda/exclude_message' do - { type: params[:type] } - end + get '/exclude/lambda/exclude_message' params do requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' } end - get '/exclude/fallback_message' do - { type: params[:type] } - end + get '/exclude/fallback_message' end params do @@ -174,6 +168,18 @@ class API < Grape::API optional :optional, type: Array[String], values: %w(a b c) end put '/optional_with_array_of_string_values' + + params do + requires :type, values: { proc: ->(v) { ValuesModel.values.include? v } } + end + get '/proc' do + { type: params[:type] } + end + + params do + requires :type, values: { proc: ->(v) { ValuesModel.values.include? v }, message: 'failed check' } + end + get '/proc/message' end end end @@ -505,4 +511,36 @@ def app expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end + + context 'custom validation using proc' do + it 'accepts a single valid value' do + get '/proc', type: 'valid-type1' + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) + end + + it 'accepts multiple valid values' do + get '/proc', type: ['valid-type1', 'valid-type3'] + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: ['valid-type1', 'valid-type3'] }.to_json) + end + + it 'rejects a single invalid value' do + get '/proc', type: 'invalid-type1' + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end + + it 'rejects an invalid value among valid ones' do + get '/proc', type: ['valid-type1', 'invalid-type1', 'valid-type3'] + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end + + it 'uses supplied message' do + get '/proc/message', type: 'invalid-type1' + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type failed check' }.to_json) + end + end end