Testing Parameter Coercion and Validation in ruby-grape/grape
This test suite validates the coercion and validation functionality of Grape’s CoerceValidator, focusing on parameter type conversion and validation. It covers various data types including primitives, arrays, custom types, and JSON handling, while ensuring proper error handling and i18n support.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
ruby-grape/grape
spec/grape/validations/validators/coerce_validator_spec.rb
# frozen_string_literal: true
describe Grape::Validations::Validators::CoerceValidator do
subject { Class.new(Grape::API) }
let(:app) { subject }
describe 'coerce' do
let(:secure_uri_only) do
Class.new do
def self.parse(value)
URI.parse(value)
end
def self.parsed?(value)
value.is_a? URI::HTTPS
end
end
end
context 'i18n' do
after do
I18n.available_locales = %i[en]
I18n.locale = :en
I18n.default_locale = :en
end
it 'i18n error on malformed input' do
I18n.available_locales = %i[en zh-CN]
I18n.load_path << File.expand_path('zh-CN.yml', __dir__)
I18n.reload!
I18n.locale = :'zh-CN'
subject.params do
requires :age, type: Integer
end
subject.get '/single' do
'int works'
end
get '/single', age: '43a'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('年龄格式不正确')
end
it 'gives an english fallback error when default locale message is blank' do
I18n.available_locales = %i[en pt-BR]
I18n.locale = :'pt-BR'
subject.params do
requires :age, type: Integer
end
subject.get '/single' do
'int works'
end
get '/single', age: '43a'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('age is invalid')
end
end
context 'with a custom validation message' do
it 'errors on malformed input' do
subject.params do
requires :int, type: { value: Integer, message: 'type cast is invalid' }
end
subject.get '/single' do
'int works'
end
get '/single', int: '43a'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('int type cast is invalid')
get '/single', int: '43'
expect(last_response).to be_successful
expect(last_response.body).to eq('int works')
end
context 'on custom coercion rules' do
before do
subject.params do
requires :a, types: { value: [Grape::API::Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val|
case val
when 'yup'
true
when 'false'
0
else
val
end
end)
end
subject.get '/' do
params[:a].class.to_s
end
end
it 'respects :coerce_with' do
get '/', a: 'yup'
expect(last_response).to be_successful
expect(last_response.body).to eq('TrueClass')
end
it 'still validates type' do
get '/', a: 'false'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('a type cast is invalid')
end
it 'performs no additional coercion' do
get '/', a: 'true'
expect(last_response).to be_successful
expect(last_response.body).to eq('String')
end
end
end
it 'error on malformed input' do
subject.params do
requires :int, type: Integer
end
subject.get '/single' do
'int works'
end
get '/single', int: '43a'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('int is invalid')
get '/single', int: '43'
expect(last_response).to be_successful
expect(last_response.body).to eq('int works')
end
it 'error on malformed input (Array)' do
subject.params do
requires :ids, type: Array[Integer]
end
subject.get '/array' do
'array int works'
end
get 'array', ids: %w[1 2 az]
expect(last_response).to be_bad_request
expect(last_response.body).to eq('ids is invalid')
get 'array', ids: %w[1 2 890]
expect(last_response).to be_successful
expect(last_response.body).to eq('array int works')
end
context 'coerces' do
context 'json' do
let(:headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } }
it 'BigDecimal' do
subject.params do
requires :bigdecimal, type: BigDecimal
end
subject.post '/bigdecimal' do
"#{params[:bigdecimal].class} #{params[:bigdecimal].to_f}"
end
post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers
expect(last_response).to be_created
expect(last_response.body).to eq('BigDecimal 45.1')
end
it 'Grape::API::Boolean' do
subject.params do
requires :boolean, type: Grape::API::Boolean
end
subject.post '/boolean' do
params[:boolean]
end
post '/boolean', { boolean: 'true' }.to_json, headers
expect(last_response).to be_created
expect(last_response.body).to eq('true')
end
end
it 'BigDecimal' do
subject.params do
requires :bigdecimal, coerce: BigDecimal
end
subject.get '/bigdecimal' do
params[:bigdecimal].class
end
get '/bigdecimal', bigdecimal: '45'
expect(last_response).to be_successful
expect(last_response.body).to eq('BigDecimal')
end
it 'Integer' do
subject.params do
requires :int, coerce: Integer
end
subject.get '/int' do
params[:int].class
end
get '/int', int: '45'
expect(last_response).to be_successful
expect(last_response.body).to eq(integer_class_name)
end
it 'String' do
subject.params do
requires :string, coerce: String
end
subject.get '/string' do
params[:string].class
end
get '/string', string: 45
expect(last_response).to be_successful
expect(last_response.body).to eq('String')
get '/string', string: nil
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
context 'a custom type' do
it 'coerces the given value' do
context = self
subject.params do
requires :uri, coerce: context.secure_uri_only
end
subject.get '/secure_uri' do
params[:uri].class
end
get 'secure_uri', uri: 'https://www.example.com'
expect(last_response).to be_successful
expect(last_response.body).to eq('URI::HTTPS')
get 'secure_uri', uri: 'http://www.example.com'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('uri is invalid')
end
context 'returning the InvalidValue instance when invalid' do
let(:custom_type) do
Class.new do
def self.parse(_val)
Grape::Types::InvalidValue.new('must be unique')
end
end
end
it 'uses a custom message added to the invalid value' do
type = custom_type
subject.params do
requires :name, type: type
end
subject.get '/whatever' do
params[:name].class
end
get 'whatever', name: 'Bob'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('name must be unique')
end
end
end
context 'Array' do
it 'Array of Integers' do
subject.params do
requires :arry, coerce: Array[Integer]
end
subject.get '/array' do
params[:arry][0].class
end
get '/array', arry: %w[1 2 3]
expect(last_response).to be_successful
expect(last_response.body).to eq(integer_class_name)
end
it 'Array of Bools' do
subject.params do
requires :arry, coerce: Array[Grape::API::Boolean]
end
subject.get '/array' do
params[:arry][0].class
end
get 'array', arry: [1, 0]
expect(last_response).to be_successful
expect(last_response.body).to eq('TrueClass')
end
it 'Array of type implementing parse' do
subject.params do
requires :uri, type: Array[URI]
end
subject.get '/uri_array' do
params[:uri][0].class
end
get 'uri_array', uri: ['http://www.google.com']
expect(last_response).to be_successful
expect(last_response.body).to eq('URI::HTTP')
end
it 'Set of type implementing parse' do
subject.params do
requires :uri, type: Set[URI]
end
subject.get '/uri_array' do
"#{params[:uri].class},#{params[:uri].first.class},#{params[:uri].size}"
end
get 'uri_array', uri: Array.new(2) { 'http://www.example.com' }
expect(last_response).to be_successful
expect(last_response.body).to eq('Set,URI::HTTP,1')
end
it 'Array of a custom type' do
context = self
subject.params do
requires :uri, type: Array[context.secure_uri_only]
end
subject.get '/secure_uris' do
params[:uri].first.class
end
get 'secure_uris', uri: ['https://www.example.com']
expect(last_response).to be_successful
expect(last_response.body).to eq('URI::HTTPS')
get 'secure_uris', uri: ['https://www.example.com', 'http://www.example.com']
expect(last_response).to be_bad_request
expect(last_response.body).to eq('uri is invalid')
end
end
context 'Set' do
it 'Set of Integers' do
subject.params do
requires :set, coerce: Set[Integer]
end
subject.get '/set' do
params[:set].first.class
end
get '/set', set: Set.new([1, 2, 3, 4]).to_a
expect(last_response).to be_successful
expect(last_response.body).to eq(integer_class_name)
end
it 'Set of Bools' do
subject.params do
requires :set, coerce: Set[Grape::API::Boolean]
end
subject.get '/set' do
params[:set].first.class
end
get '/set', set: Set.new([1, 0]).to_a
expect(last_response).to be_successful
expect(last_response.body).to eq('TrueClass')
end
end
it 'Grape::API::Boolean' do
subject.params do
requires :boolean, type: Grape::API::Boolean
end
subject.get '/boolean' do
params[:boolean].class
end
get '/boolean', boolean: 1
expect(last_response).to be_successful
expect(last_response.body).to eq('TrueClass')
end
context 'File' do
let(:file) { Rack::Test::UploadedFile.new(__FILE__) }
let(:filename) { File.basename(__FILE__).to_s }
it 'Rack::Multipart::UploadedFile' do
subject.params do
requires :file, type: Rack::Multipart::UploadedFile
end
subject.post '/upload' do
params[:file][:filename]
end
post '/upload', file: file
expect(last_response).to be_created
expect(last_response.body).to eq(filename)
post '/upload', file: 'not a file'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('file is invalid')
end
it 'File' do
subject.params do
requires :file, coerce: File
end
subject.post '/upload' do
params[:file][:filename]
end
post '/upload', file: file
expect(last_response).to be_created
expect(last_response.body).to eq(filename)
post '/upload', file: 'not a file'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('file is invalid')
post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
expect(last_response).to be_bad_request
expect(last_response.body).to eq('file is invalid')
end
it 'collection' do
subject.params do
requires :files, type: Array[File]
end
subject.post '/upload' do
params[:files].first[:filename]
end
post '/upload', files: [file]
expect(last_response).to be_created
expect(last_response.body).to eq(filename)
end
end
it 'Nests integers' do
subject.params do
requires :integers, type: Hash do
requires :int, coerce: Integer
end
end
subject.get '/int' do
params[:integers][:int].class
end
get '/int', integers: { int: '45' }
expect(last_response).to be_successful
expect(last_response.body).to eq(integer_class_name)
end
context 'nil values' do
context 'primitive types' do
Grape::Validations::Types::PRIMITIVES.each do |type|
it 'respects the nil value' do
subject.params do
requires :param, type: type
end
subject.get '/nil_value' do
params[:param].class
end
get '/nil_value', param: nil
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
end
context 'structures types' do
Grape::Validations::Types::STRUCTURES.each do |type|
it 'respects the nil value' do
subject.params do
requires :param, type: type
end
subject.get '/nil_value' do
params[:param].class
end
get '/nil_value', param: nil
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
end
context 'special types' do
Grape::Validations::Types::SPECIAL.each_key do |type|
it 'respects the nil value' do
subject.params do
requires :param, type: type
end
subject.get '/nil_value' do
params[:param].class
end
get '/nil_value', param: nil
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
context 'variant-member-type collections' do
[
Array[Integer, String],
[Integer, String, Array[Integer, String]]
].each do |type|
it 'respects the nil value' do
subject.params do
requires :param, type: type
end
subject.get '/nil_value' do
params[:param].class
end
get '/nil_value', param: nil
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
end
end
end
context 'empty string' do
context 'primitive types' do
(Grape::Validations::Types::PRIMITIVES - [String]).each do |type|
it "is coerced to nil for type #{type}" do
subject.params do
requires :param, type: type
end
subject.get '/empty_string' do
params[:param].class
end
get '/empty_string', param: ''
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
it 'is not coerced to nil for type String' do
subject.params do
requires :param, type: String
end
subject.get '/empty_string' do
params[:param].class
end
get '/empty_string', param: ''
expect(last_response).to be_successful
expect(last_response.body).to eq('String')
end
end
context 'structures types' do
(Grape::Validations::Types::STRUCTURES - [Hash]).each do |type|
it "is coerced to nil for type #{type}" do
subject.params do
requires :param, type: type
end
subject.get '/empty_string' do
params[:param].class
end
get '/empty_string', param: ''
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
end
context 'special types' do
(Grape::Validations::Types::SPECIAL.keys - [File, Rack::Multipart::UploadedFile]).each do |type|
it "is coerced to nil for type #{type}" do
subject.params do
requires :param, type: type
end
subject.get '/empty_string' do
params[:param].class
end
get '/empty_string', param: ''
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
context 'variant-member-type collections' do
[
Array[Integer, String],
[Integer, String, Array[Integer, String]]
].each do |type|
it "is coerced to nil for type #{type}" do
subject.params do
requires :param, type: type
end
subject.get '/empty_string' do
params[:param].class
end
get '/empty_string', param: ''
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
end
end
end
end
end
context 'using coerce_with' do
it 'parses parameters with Array type' do
subject.params do
requires :values, type: Array, coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
end
subject.get '/ints' do
params[:values]
end
get '/ints', values: '1 2 3 4'
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4])
get '/ints', values: 'a b c d'
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0])
end
it 'parses parameters with Array[String] type' do
subject.params do
requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/) }
end
subject.get '/strings' do
params[:values]
end
get '/strings', values: '1 2 3 4'
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq(%w[1 2 3 4])
get '/strings', values: 'a b c d'
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq(%w[a b c d])
end
it 'parses parameters with Array[Array[String]] type and coerce_with' do
subject.params do
requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(',').map(&:strip)] : val }
end
subject.post '/coerce_nested_strings' do
params[:values]
end
post '/coerce_nested_strings', Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json'
expect(last_response).to be_created
expect(JSON.parse(last_response.body)).to eq([%w[a b c d]])
post '/coerce_nested_strings', Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json'
expect(last_response).to be_created
expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]])
post '/coerce_nested_strings', Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json'
expect(last_response).to be_created
expect(JSON.parse(last_response.body)).to eq([[]])
post '/coerce_nested_strings', Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json'
expect(last_response).to be_bad_request
end
it 'parses parameters with Array[Integer] type' do
subject.params do
requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
end
subject.get '/ints' do
params[:values]
end
get '/ints', values: '1 2 3 4'
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4])
get '/ints', values: 'a b c d'
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0])
end
it 'parses parameters even if type is valid' do
subject.params do
requires :values, type: Array, coerce_with: ->(array) { array.map { |val| val.to_i + 1 } }
end
subject.get '/ints' do
params[:values]
end
get '/ints', values: [1, 2, 3, 4]
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq([2, 3, 4, 5])
get '/ints', values: %w[a b c d]
expect(last_response).to be_successful
expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1])
end
context 'Array type and coerce_with should' do
before do
subject.params do
optional :arr, type: Array, coerce_with: (lambda do |val|
if val.nil?
[]
else
val
end
end)
end
subject.get '/' do
params[:arr].class.to_s
end
end
it 'coerce nil value to array' do
get '/', arr: nil
expect(last_response).to be_successful
expect(last_response.body).to eq('Array')
end
it 'not coerce missing field' do
get '/'
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
it 'coerce array as array' do
get '/', arr: []
expect(last_response).to be_successful
expect(last_response.body).to eq('Array')
end
end
it 'uses parse where available' do
subject.params do
requires :ints, type: Array, coerce_with: JSON do
requires :i, type: Integer
requires :j
end
end
subject.get '/ints' do
ints = params[:ints].first
'coercion works' if ints[:i] == 1 && ints[:j] == '2'
end
get '/ints', ints: [{ i: 1, j: '2' }]
expect(last_response).to be_bad_request
expect(last_response.body).to eq('ints is invalid')
get '/ints', ints: '{"i":1,"j":"2"}'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('ints is invalid')
get '/ints', ints: '[{"i":"1","j":"2"}]'
expect(last_response).to be_successful
expect(last_response.body).to eq('coercion works')
end
it 'accepts any callable' do
subject.params do
requires :ints, type: Hash, coerce_with: JSON.method(:parse) do
requires :int, type: Integer, coerce_with: ->(val) { val == 'three' ? 3 : val }
end
end
subject.get '/ints' do
params[:ints][:int]
end
get '/ints', ints: '{"int":"3"}'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('ints[int] is invalid')
get '/ints', ints: '{"int":"three"}'
expect(last_response).to be_successful
expect(last_response.body).to eq('3')
get '/ints', ints: '{"int":3}'
expect(last_response).to be_successful
expect(last_response.body).to eq('3')
end
context 'Integer type and coerce_with should' do
before do
subject.params do
optional :int, type: Integer, coerce_with: (lambda do |val|
if val.nil?
0
else
val.to_i
end
end)
end
subject.get '/' do
params[:int].class.to_s
end
end
it 'coerce nil value to integer' do
get '/', int: nil
expect(last_response).to be_successful
expect(last_response.body).to eq('Integer')
end
it 'not coerce missing field' do
get '/'
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
it 'coerce integer as integer' do
get '/', int: 1
expect(last_response).to be_successful
expect(last_response.body).to eq('Integer')
end
end
context 'Integer type and coerce_with potentially returning nil' do
before do
subject.params do
requires :int, type: Integer, coerce_with: (lambda do |val|
case val
when '0'
nil
when /^-?\d+$/
val.to_i
else
val
end
end)
end
subject.get '/' do
params[:int].class.to_s
end
end
it 'accepts value that coerces to nil' do
get '/', int: '0'
expect(last_response).to be_successful
expect(last_response.body).to eq('NilClass')
end
it 'coerces to Integer' do
get '/', int: '1'
expect(last_response).to be_successful
expect(last_response.body).to eq('Integer')
end
it 'returns invalid value if coercion returns a wrong type' do
get '/', int: 'lol'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('int is invalid')
end
end
it 'must be supplied with :type or :coerce' do
expect do
subject.params do
requires :ints, coerce_with: JSON
end
end.to raise_error(ArgumentError)
end
end
context 'first-class JSON' do
it 'parses objects, hashes, and arrays' do
subject.params do
requires :splines, type: JSON do
requires :x, type: Integer, values: [1, 2, 3]
optional :ints, type: Array[Integer]
optional :obj, type: Hash do
optional :y
end
end
end
subject.get '/' do
if params[:splines].is_a? Hash
params[:splines][:obj][:y]
elsif params[:splines].any? { |s| s.key? :obj }
'arrays work'
end
end
get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}'
expect(last_response).to be_successful
expect(last_response.body).to eq('woof')
get '/', splines: { x: 1, ints: [1, 2, 3], obj: { y: 'woof' } }
expect(last_response).to be_successful
expect(last_response.body).to eq('woof')
get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
expect(last_response).to be_successful
expect(last_response.body).to eq('arrays work')
get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
expect(last_response).to be_successful
expect(last_response.body).to eq('arrays work')
get '/', splines: '{"x":4,"ints":[2]}'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: { x: 4, ints: [2] }
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }]
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
end
it 'works when declared optional' do
subject.params do
optional :splines, type: JSON do
requires :x, type: Integer, values: [1, 2, 3]
optional :ints, type: Array[Integer]
optional :obj, type: Hash do
optional :y
end
end
end
subject.get '/' do
if params[:splines].is_a? Hash
params[:splines][:obj][:y]
elsif params[:splines].any? { |s| s.key? :obj }
'arrays work'
end
end
get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}'
expect(last_response).to be_successful
expect(last_response.body).to eq('woof')
get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
expect(last_response).to be_successful
expect(last_response.body).to eq('arrays work')
get '/', splines: '{"x":4,"ints":[2]}'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
end
it 'accepts Array[JSON] shorthand' do
subject.params do
requires :splines, type: Array[JSON] do
requires :x, type: Integer, values: [1, 2, 3]
requires :y
end
end
subject.get '/' do
params[:splines].first[:y].class.to_s
spline = params[:splines].first
"#{spline[:x].class}.#{spline[:y].class}"
end
get '/', splines: '{"x":"1","y":"woof"}'
expect(last_response).to be_successful
expect(last_response.body).to eq("#{integer_class_name}.String")
get '/', splines: '[{"x":1,"y":2},{"x":1,"y":"quack"}]'
expect(last_response).to be_successful
expect(last_response.body).to eq("#{integer_class_name}.#{integer_class_name}")
get '/', splines: '{"x":"4","y":"woof"}'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: '[{"x":"4","y":"woof"}]'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('splines[x] does not have a valid value')
end
it "doesn't make sense using coerce_with" do
expect do
subject.params do
requires :bad, type: JSON, coerce_with: JSON do
requires :x
end
end
end.to raise_error(ArgumentError)
expect do
subject.params do
requires :bad, type: Array[JSON], coerce_with: JSON do
requires :x
end
end
end.to raise_error(ArgumentError)
end
end
context 'multiple types' do
it 'coerces to first possible type' do
subject.params do
requires :a, types: [Grape::API::Boolean, Integer, String]
end
subject.get '/' do
params[:a].class.to_s
end
get '/', a: 'true'
expect(last_response).to be_successful
expect(last_response.body).to eq('TrueClass')
get '/', a: '5'
expect(last_response).to be_successful
expect(last_response.body).to eq(integer_class_name)
get '/', a: 'anything else'
expect(last_response).to be_successful
expect(last_response.body).to eq('String')
end
it 'fails when no coercion is possible' do
subject.params do
requires :a, types: [Grape::API::Boolean, Integer]
end
subject.get '/' do
params[:a].class.to_s
end
get '/', a: true
expect(last_response).to be_successful
expect(last_response.body).to eq('TrueClass')
get '/', a: 'not good'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('a is invalid')
end
context 'for primitive collections' do
before do
subject.params do
optional :a, types: [String, Array[String]]
optional :b, types: [Array[Integer], Array[String]]
optional :c, type: Array[Integer, String]
optional :d, types: [Integer, String, Set[Integer, String]]
end
subject.get '/' do
(
params[:a] ||
params[:b] ||
params[:c] ||
params[:d]
).inspect
end
end
it 'allows singular form declaration' do
get '/', a: 'one way'
expect(last_response).to be_successful
expect(last_response.body).to eq('"one way"')
get '/', a: %w[the other]
expect(last_response).to be_successful
expect(last_response.body).to eq('["the", "other"]')
get '/', a: { a: 1, b: 2 }
expect(last_response).to be_bad_request
expect(last_response.body).to eq('a is invalid')
get '/', a: [1, 2, 3]
expect(last_response).to be_successful
expect(last_response.body).to eq('["1", "2", "3"]')
end
it 'allows multiple collection types' do
get '/', b: [1, 2, 3]
expect(last_response).to be_successful
expect(last_response.body).to eq('[1, 2, 3]')
get '/', b: %w[1 2 3]
expect(last_response).to be_successful
expect(last_response.body).to eq('[1, 2, 3]')
get '/', b: [1, true, 'three']
expect(last_response).to be_successful
expect(last_response.body).to eq('["1", "true", "three"]')
end
it 'allows collections with multiple types' do
get '/', c: [1, '2', true, 'three']
expect(last_response).to be_successful
expect(last_response.body).to eq('[1, 2, "true", "three"]')
get '/', d: '1'
expect(last_response).to be_successful
expect(last_response.body).to eq('1')
get '/', d: 'one'
expect(last_response).to be_successful
expect(last_response.body).to eq('"one"')
get '/', d: %w[1 two]
expect(last_response).to be_successful
expect(last_response.body).to eq('#<Set: {1, "two"}>')
end
end
context 'custom coercion rules' do
before do
subject.params do
requires :a, types: [Grape::API::Boolean, String], coerce_with: (lambda do |val|
case val
when 'yup'
true
when 'false'
0
else
val
end
end)
end
subject.get '/' do
params[:a].class.to_s
end
end
it 'respects :coerce_with' do
get '/', a: 'yup'
expect(last_response).to be_successful
expect(last_response.body).to eq('TrueClass')
end
it 'still validates type' do
get '/', a: 'false'
expect(last_response).to be_bad_request
expect(last_response.body).to eq('a is invalid')
end
it 'performs no additional coercion' do
get '/', a: 'true'
expect(last_response).to be_successful
expect(last_response.body).to eq('String')
end
end
it 'may not be supplied together with a single type' do
expect do
subject.params do
requires :a, type: Integer, types: [Integer, String]
end
end.to raise_exception ArgumentError
end
end
context 'converter' do
it 'does not build a coercer multiple times' do
subject.params do
requires :something, type: Array[String]
end
subject.get do
end
expect(Grape::Validations::Types::ArrayCoercer).to(
receive(:new).at_most(:once).and_call_original
)
10.times { get '/' }
end
end
end
end