diff --git a/lib/openapi_parser/errors.rb b/lib/openapi_parser/errors.rb index 95e5785..93621f2 100644 --- a/lib/openapi_parser/errors.rb +++ b/lib/openapi_parser/errors.rb @@ -43,6 +43,29 @@ def message end end + class NotExistDiscriminatorMappingTarget < OpenAPIError + def initialize(key, reference) + super(reference) + @key = key + end + + def message + "discriminator mapping key #{@key} does not exist in #{@reference}" + end + end + + class NotExistDiscriminatorPropertyName < OpenAPIError + def initialize(key, value, reference) + super(reference) + @key = key + @value = value + end + + def message + "discriminator propertyName #{@key} does not exist in value #{@value} in #{@reference}" + end + end + class NotOneOf < OpenAPIError def initialize(value, reference) super(reference) diff --git a/lib/openapi_parser/schema_validators/any_of_validator.rb b/lib/openapi_parser/schema_validators/any_of_validator.rb index 38fa5d2..fc57f4a 100644 --- a/lib/openapi_parser/schema_validators/any_of_validator.rb +++ b/lib/openapi_parser/schema_validators/any_of_validator.rb @@ -3,6 +3,10 @@ class AnyOfValidator < Base # @param [Object] value # @param [OpenAPIParser::Schemas::Schema] schema def coerce_and_validate(value, schema) + if schema.discriminator + return validate_discriminator_schema(schema.discriminator, value) + end + # in all schema return error (=true) not any of data schema.any_of.each do |s| coerced, err = validatable.validate_schema(value, s) diff --git a/lib/openapi_parser/schema_validators/base.rb b/lib/openapi_parser/schema_validators/base.rb index 48c92b1..10017af 100644 --- a/lib/openapi_parser/schema_validators/base.rb +++ b/lib/openapi_parser/schema_validators/base.rb @@ -17,5 +17,23 @@ def initialize(validatable, coerce_value) def coerce_and_validate(_value, _schema) raise 'need implement' end + + def validate_discriminator_schema(discriminator, value) + unless value.key?(discriminator.property_name) + return [nil, OpenAPIParser::NotExistDiscriminatorPropertyName.new(discriminator.property_name, value, discriminator.object_reference)] + end + mapping_key = value[discriminator.property_name] + + # TODO: it's allowed to have discriminator without mapping, then we need to lookup discriminator.property_name + # but the format is not the full path, just model name in the components + mapping_target = discriminator.mapping[mapping_key] + unless mapping_target + return [nil, OpenAPIParser::NotExistDiscriminatorMappingTarget.new(mapping_key, discriminator.object_reference)] + end + + # Find object does O(n) search at worst, then caches the result, so this is ok for repeated search + resolved_schema = discriminator.root.find_object(mapping_target) + validatable.validate_schema(value, resolved_schema) + end end end diff --git a/lib/openapi_parser/schema_validators/one_of_validator.rb b/lib/openapi_parser/schema_validators/one_of_validator.rb index 7bdfc03..8ff88b7 100644 --- a/lib/openapi_parser/schema_validators/one_of_validator.rb +++ b/lib/openapi_parser/schema_validators/one_of_validator.rb @@ -3,6 +3,10 @@ class OneOfValidator < Base # @param [Object] value # @param [OpenAPIParser::Schemas::Schema] schema def coerce_and_validate(value, schema) + if schema.discriminator + return validate_discriminator_schema(schema.discriminator, value) + end + # if multiple schemas are satisfied, it's not valid result = schema.one_of.one? do |s| _coerced, err = validatable.validate_schema(value, s) diff --git a/lib/openapi_parser/schemas.rb b/lib/openapi_parser/schemas.rb index deee177..97a184a 100644 --- a/lib/openapi_parser/schemas.rb +++ b/lib/openapi_parser/schemas.rb @@ -1,6 +1,7 @@ require_relative 'schemas/classes' require_relative 'schemas/base' +require_relative 'schemas/discriminator' require_relative 'schemas/openapi' require_relative 'schemas/paths' require_relative 'schemas/path_item' diff --git a/lib/openapi_parser/schemas/classes.rb b/lib/openapi_parser/schemas/classes.rb index bc0308a..eb385d9 100644 --- a/lib/openapi_parser/schemas/classes.rb +++ b/lib/openapi_parser/schemas/classes.rb @@ -2,6 +2,7 @@ module OpenAPIParser::Schemas class Base; end + class Discriminator < Base; end class OpenAPI < Base; end class Operation < Base; end class Parameter < Base; end diff --git a/lib/openapi_parser/schemas/discriminator.rb b/lib/openapi_parser/schemas/discriminator.rb new file mode 100644 index 0000000..bbea24d --- /dev/null +++ b/lib/openapi_parser/schemas/discriminator.rb @@ -0,0 +1,11 @@ +module OpenAPIParser::Schemas + class Discriminator < Base + # @!attribute [r] property_name + # @return [String, nil] + openapi_attr_value :property_name, schema_key: :propertyName + + # @!attribute [r] mapping + # @return [Hash{String => String] + openapi_attr_value :mapping + end +end diff --git a/lib/openapi_parser/schemas/schema.rb b/lib/openapi_parser/schemas/schema.rb index 5a031c1..a9b834c 100644 --- a/lib/openapi_parser/schemas/schema.rb +++ b/lib/openapi_parser/schemas/schema.rb @@ -1,5 +1,5 @@ # TODO: support 'not' because I need check reference... -# TODO: support 'discriminator', 'xml', 'externalDocs' +# TODO: support 'xml', 'externalDocs' # TODO: support extended property module OpenAPIParser::Schemas @@ -101,6 +101,10 @@ class Schema < Base # @return [Hash{String => Schema}, nil] openapi_attr_hash_object :properties, Schema, reference: true + # @!attribute [r] discriminator + # @return [Discriminator, nil] + openapi_attr_object :discriminator, Discriminator + # @!attribute [r] additional_properties # @return [Boolean, Schema, Reference, nil] openapi_attr_object :additional_properties, Schema, reference: true, allow_data_type: true, schema_key: :additionalProperties diff --git a/spec/data/petstore-with-discriminator.yaml b/spec/data/petstore-with-discriminator.yaml new file mode 100644 index 0000000..8713780 --- /dev/null +++ b/spec/data/petstore-with-discriminator.yaml @@ -0,0 +1,109 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification + termsOfService: http://swagger.io/terms/ + contact: + name: Swagger API Team + email: apiteam@swagger.io + url: http://swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +servers: + - url: http://petstore.swagger.io/api +paths: + /save_the_pets: + post: + description: Creates a new pet in the store. Duplicates are allowed + operationId: addPet + requestBody: + description: Pet to add to the store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetBaskets' + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object +components: + schemas: + PetBaskets: + type: object + properties: + baskets: + type: array + items: + anyOf: + - "$ref": "#/components/schemas/SquirrelBasket" + - "$ref": "#/components/schemas/CatBasket" + discriminator: + propertyName: name + mapping: + cats: "#/components/schemas/CatBasket" + squirrels: "#/components/schemas/SquirrelBasket" + SquirrelBasket: + type: object + required: + - name + properties: + name: + type: string + content: + type: array + items: + "$ref": "#/components/schemas/Squirrel" + Squirrel: + type: object + required: + - name + - nut_stock + properties: + name: + type: string + born_at: + format: date-time + nullable: true + type: string + description: + nullable: true + type: string + nut_stock: + nullable: true + type: integer + CatBasket: + type: object + required: + - name + properties: + name: + type: string + content: + type: array + items: + "$ref": "#/components/schemas/Cat" + Cat: + type: object + required: + - name + - milk_stock + properties: + name: + type: string + born_at: + format: date-time + nullable: true + type: string + description: + nullable: true + type: string + milk_stock: + nullable: true + type: integer + diff --git a/spec/openapi_parser/schemas/discriminator_spec.rb b/spec/openapi_parser/schemas/discriminator_spec.rb new file mode 100644 index 0000000..841fbf5 --- /dev/null +++ b/spec/openapi_parser/schemas/discriminator_spec.rb @@ -0,0 +1,100 @@ +require_relative '../../spec_helper' + +RSpec.describe OpenAPIParser::Schemas::RequestBody do + let(:root) { OpenAPIParser.parse(petstore_with_discriminator_schema, {}) } + + describe 'discriminator' do + let(:content_type) { 'application/json' } + let(:http_method) { :post } + let(:request_path) { '/save_the_pets' } + let(:request_operation) { root.request_operation(http_method, request_path) } + let(:params) { {} } + + it 'picks correct object based on mapping and succeeds' do + body = { + "baskets" => [ + { + "name" => "cats", + "content" => [ + { + "name" => "Mr. Cat", + "born_at" => "2019-05-16T11:37:02.160Z", + "description" => "Cat gentleman", + "milk_stock" => 10 + } + ] + }, + ] + } + + request_operation.validate_request_body(content_type, body) + end + + it 'picks correct object based on mapping and fails' do + body = { + "baskets" => [ + { + "name" => "cats", + "content" => [ + { + "name" => "Mr. Cat", + "born_at" => "2019-05-16T11:37:02.160Z", + "description" => "Cat gentleman", + "nut_stock" => 10 # passing squirrel attribute here, but discriminator still picks cats and fails + } + ] + }, + ] + } + expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotExistRequiredKey)).to eq true + expect(e.message).to match("^required parameters milk_stock not exist.*?$") + end + end + + it "throws error when discriminator mapping is not found" do + body = { + "baskets" => [ + { + "name" => "dogs", + "content" => [ + { + "name" => "Mr. Dog", + "born_at" => "2019-05-16T11:37:02.160Z", + "description" => "Dog bruiser", + "nut_stock" => 10 + } + ] + }, + ] + } + + expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorMappingTarget)).to eq true + expect(e.message).to match("^discriminator mapping key dogs does not exist.*?$") + end + end + + it "throws error if discriminator propertyName is not present on object" do + body = { + "baskets" => [ + { + "content" => [ + { + "name" => "Mr. Dog", + "born_at" => "2019-05-16T11:37:02.160Z", + "description" => "Dog bruiser", + "milk_stock" => 10 + } + ] + }, + ] + } + + expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| + expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorPropertyName)).to eq true + expect(e.message).to match("^discriminator propertyName name does not exist in value.*?$") + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 82bf37d..99b4a61 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,6 +32,10 @@ def petstore_schema YAML.load_file('./spec/data/petstore-expanded.yaml') end +def petstore_with_discriminator_schema + YAML.load_file('./spec/data/petstore-with-discriminator.yaml') +end + def build_validate_test_schema(new_properties) b = YAML.load_file('./spec/data/validate_test.yaml') obj = b['paths']['/validate_test']['post']['requestBody']['content']['application/json']['schema']['properties']