Back to Repositories

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

The test suite provides comprehensive coverage of parameter coercion and validation in Grape:
  • Basic type coercion for primitives (Integer, String, Boolean)
  • Complex type handling (Arrays, Sets, JSON objects)
  • Custom type validation and coercion
  • Internationalization (i18n) support for error messages
  • Edge cases like nil values and empty strings

Implementation Analysis

The testing approach uses RSpec to validate Grape’s parameter handling:
  • Isolated unit tests for each coercion type
  • Mocked HTTP requests using Rack::Test
  • Custom validator implementations
  • Multiple coercion strategies including lambda functions

Technical Details

Key technical components tested:
  • Grape::Validations::Validators::CoerceValidator
  • RSpec testing framework
  • Rack::Test for HTTP simulation
  • Custom type classes and coercion rules
  • I18n integration for localized messages

Best Practices Demonstrated

The test suite showcases several testing best practices:
  • Comprehensive edge case coverage
  • Isolated test contexts
  • Clear test descriptions
  • Proper setup and teardown
  • Consistent validation patterns

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