diff --git a/lib/openapi_parser/errors.rb b/lib/openapi_parser/errors.rb index 8ffddd8..db2eb06 100644 --- a/lib/openapi_parser/errors.rb +++ b/lib/openapi_parser/errors.rb @@ -105,6 +105,17 @@ def message end end + class NotNot < OpenAPIError + def initialize(value, reference) + super(reference) + @value = value + end + + def message + "#{@value.inspect} isn't 'not' of #{@reference}" + end + end + class NotEnumInclude < OpenAPIError def initialize(value, reference) super(reference) diff --git a/lib/openapi_parser/schema_validator.rb b/lib/openapi_parser/schema_validator.rb index 90c7b1b..a4c3773 100644 --- a/lib/openapi_parser/schema_validator.rb +++ b/lib/openapi_parser/schema_validator.rb @@ -11,6 +11,7 @@ require_relative 'schema_validators/any_of_validator' require_relative 'schema_validators/all_of_validator' require_relative 'schema_validators/one_of_validator' +require_relative 'schema_validators/not_validator' require_relative 'schema_validators/nil_validator' require_relative 'schema_validators/unspecified_type_validator' @@ -98,6 +99,7 @@ def validator(value, schema) return any_of_validator if schema.any_of return all_of_validator if schema.all_of return one_of_validator if schema.one_of + return not_validator if schema.not return nil_validator if value.nil? case schema.type @@ -154,6 +156,10 @@ def one_of_validator @one_of_validator ||= OpenAPIParser::SchemaValidator::OneOfValidator.new(self, @coerce_value) end + def not_validator + @not_validator ||= OpenAPIParser::SchemaValidator::NotValidator.new(self, @coerce_value) + end + def nil_validator @nil_validator ||= OpenAPIParser::SchemaValidator::NilValidator.new(self, @coerce_value) end diff --git a/lib/openapi_parser/schema_validators/not_validator.rb b/lib/openapi_parser/schema_validators/not_validator.rb new file mode 100644 index 0000000..9c03f61 --- /dev/null +++ b/lib/openapi_parser/schema_validators/not_validator.rb @@ -0,0 +1,15 @@ +class OpenAPIParser::SchemaValidator + class NotValidator < Base + # @param [Object] value + # @param [OpenAPIParser::Schemas::Schema] schema + def coerce_and_validate(value, schema, **_keyword_args) + coerced, err = validatable.validate_schema(value, schema.not) + + if err + [coerced, nil] + else + [nil, OpenAPIParser::NotNot.new(value, schema.object_reference)] + end + end + end +end diff --git a/lib/openapi_parser/schemas/schema.rb b/lib/openapi_parser/schemas/schema.rb index a516d70..779c55e 100644 --- a/lib/openapi_parser/schemas/schema.rb +++ b/lib/openapi_parser/schemas/schema.rb @@ -93,6 +93,10 @@ class Schema < Base # @return [Array, nil] openapi_attr_list_object :any_of, Schema, reference: true, schema_key: :anyOf + # @!attribute [r] not + # @return [Schema, nil] + openapi_attr_object :not, Schema, reference: true + # @!attribute [r] items # @return [Schema, nil] openapi_attr_object :items, Schema, reference: true diff --git a/spec/data/normal.yml b/spec/data/normal.yml index 66d1437..20fb9c0 100644 --- a/spec/data/normal.yml +++ b/spec/data/normal.yml @@ -334,6 +334,22 @@ paths: type: boolean enum: - true + not_integer: + not: + type: integer + # The idea of null value comes from + # https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-487314681 + null_value: + not: + anyOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + # Array schema must specify items, so we'll use an + # empty object to mean arrays of any type are included. + items: {} responses: '200': diff --git a/spec/openapi_parser/schema_validator_spec.rb b/spec/openapi_parser/schema_validator_spec.rb index 360dac3..33f0e9d 100644 --- a/spec/openapi_parser/schema_validator_spec.rb +++ b/spec/openapi_parser/schema_validator_spec.rb @@ -427,6 +427,59 @@ end end + describe 'not' do + context 'not_integer' do + it do + expect(request_operation.validate_request_body(content_type, { 'not_integer' => "This is string" })). + to eq({ 'not_integer' => "This is string" }) + end + + it 'invalid' do + expect { request_operation.validate_request_body(content_type, { 'not_integer' => 1 }) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotNot)).to eq true + expect(e.message.start_with?("1 isn't 'not' of")).to eq true + end + end + end + + context 'null_value' do + it 'invalid' do + expect { request_operation.validate_request_body(content_type, { 'null_value' => 'abc' }) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotNot)).to eq true + expect(e.message.start_with?("\"abc\" isn't 'not' of")).to eq true + end + end + + it 'invalid' do + expect { request_operation.validate_request_body(content_type, { 'null_value' => 1 }) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotNot)).to eq true + expect(e.message.start_with?("1 isn't 'not' of")).to eq true + end + end + + it 'invalid' do + expect { request_operation.validate_request_body(content_type, { 'null_value' => true }) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotNot)).to eq true + expect(e.message.start_with?("true isn't 'not' of")).to eq true + end + end + + it 'invalid' do + expect { request_operation.validate_request_body(content_type, { 'null_value' => {} }) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotNot)).to eq true + expect(e.message.start_with?("{} isn't 'not' of")).to eq true + end + end + + it 'invalid' do + expect { request_operation.validate_request_body(content_type, { 'null_value' => [1] }) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotNot)).to eq true + expect(e.message.start_with?("[1] isn't 'not' of")).to eq true + end + end + end + end + it 'unknown param' do expect { request_operation.validate_request_body(content_type, { 'unknown' => 1 }) }.to raise_error do |e| expect(e).to be_kind_of(OpenAPIParser::NotExistPropertyDefinition)