From 94ae8f6e5e3ccb4a6bf894efb4c8e95eb781b98c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 25 Jul 2020 14:42:14 -0700 Subject: [PATCH 1/4] Add reference to the schema fragment representations. Add some examples from the Swagger.org website to the compatibility suite. --- .../Schema Object/JSONSchemaFragment.swift | 33 ++ .../SwaggerDocSamplesTests.swift | 307 ++++++++++++++++++ .../Schema Object/SchemaFragmentTests.swift | 50 +++ 3 files changed, 390 insertions(+) create mode 100644 Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaFragment.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaFragment.swift index 1e1d9612c..dd49038b9 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaFragment.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaFragment.swift @@ -43,6 +43,8 @@ public enum JSONSchemaFragment: Equatable { GeneralContext, ObjectContext ) + + case reference(JSONReference) } extension JSONSchemaFragment { @@ -223,6 +225,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.format + case .reference: + return nil } } @@ -236,6 +240,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.description + case .reference: + return nil } } @@ -249,6 +255,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.title + case .reference: + return nil } } @@ -262,6 +270,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.nullable + case .reference: + return nil } } @@ -275,6 +285,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.deprecated + case .reference: + return nil } } @@ -288,6 +300,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.externalDocs + case .reference: + return nil } } @@ -301,6 +315,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.allowedValues + case .reference: + return nil } } @@ -314,6 +330,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.example + case .reference: + return nil } } @@ -327,6 +345,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.readOnly + case .reference: + return nil } } @@ -340,6 +360,8 @@ extension JSONSchemaFragment: JSONSchemaFragmentContext { .array(let generalContext, _), .object(let generalContext, _): return generalContext.writeOnly + case .reference: + return nil } } } @@ -428,6 +450,10 @@ extension JSONSchemaFragment: Encodable { try container.encodeIfPresent(objectContext.properties, forKey: .properties) try container.encodeIfPresent(objectContext.additionalProperties, forKey: .additionalProperties) try container.encodeIfPresent(objectContext.required, forKey: .required) + case .reference(let reference): + var container = encoder.singleValueContainer() + + try container.encode(reference) } } } @@ -509,6 +535,13 @@ extension JSONSchemaFragment.ObjectContext: Decodable { extension JSONSchemaFragment: Decodable { public init(from decoder: Decoder) throws { + if let singleValueContainer = try? decoder.singleValueContainer() { + if let ref = try? singleValueContainer.decode(JSONReference.self) { + self = .reference(ref) + return + } + } + let generalContext = try GeneralContext(from: decoder) let generalContainer = try decoder.container(keyedBy: JSONSchemaFragment.GeneralCodingKeys.self) diff --git a/Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift b/Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift new file mode 100644 index 000000000..04c24a37a --- /dev/null +++ b/Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift @@ -0,0 +1,307 @@ +// +// SwaggerDocSamplesTests.swift +// +// +// Created by Mathew Polzin on 7/25/20. +// + +import Foundation +import XCTest +import OpenAPIKit +import Yams + +final class SwaggerDocSamplesTests: XCTestCase { + func test_allOfExample() throws { + let docString = commonBaseDocument + """ +paths: + /pets: + patch: + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + discriminator: + propertyName: pet_type + responses: + '200': + description: Updated +components: + schemas: + Pet: + type: object + required: + - pet_type + properties: #3 + pet_type: + type: string + discriminator: + propertyName: pet_type + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + bark: + type: boolean + breed: + type: string + enum: [Dingo, Husky, Retriever, Shepherd] + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + hunts: + type: boolean + age: + type: integer +""" + + // test decoding + do { + let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) + + // test validating + try doc.validate() + + // test dereferencing and resolving + _ = try doc.locallyDereferenced().resolved() + } catch let error { + let friendlyError = OpenAPI.Error(from: error) + throw friendlyError + } + } + + func test_oneOfExample() throws { + let docString = commonBaseDocument + """ +paths: + /pets: + patch: + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + responses: + '200': + description: Updated +components: + schemas: + Dog: + type: object + properties: + bark: + type: boolean + breed: + type: string + enum: [Dingo, Husky, Retriever, Shepherd] + Cat: + type: object + properties: + hunts: + type: boolean + age: + type: integer +""" + + // test decoding + do { + let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) + + // test validating + try doc.validate() + + // test dereferencing and resolving + _ = try doc.locallyDereferenced().resolved() + } catch let error { + let friendlyError = OpenAPI.Error(from: error) + throw friendlyError + } + } + + func test_anyOfExample() throws { + let docString = commonBaseDocument + """ +paths: + /pets: + patch: + requestBody: + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/PetByAge' + - $ref: '#/components/schemas/PetByType' + responses: + '200': + description: Updated +components: + schemas: + PetByAge: + type: object + properties: + age: + type: integer + nickname: + type: string + required: + - age + + PetByType: + type: object + properties: + pet_type: + type: string + enum: [Cat, Dog] + hunts: + type: boolean + required: + - pet_type +""" + + // test decoding + do { + let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) + + // test validating + try doc.validate() + + // test dereferencing and resolving + _ = try doc.locallyDereferenced().resolved() + } catch let error { + let friendlyError = OpenAPI.Error(from: error) + throw friendlyError + } + } + + func test_notExample() throws { + let docString = commonBaseDocument + """ +paths: + /pets: + patch: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PetByType' + responses: + '200': + description: Updated +components: + schemas: + PetByType: + type: object + properties: + pet_type: + not: + type: integer + required: + - pet_type +""" + + // test decoding + do { + let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) + + // test validating + try doc.validate() + + // test dereferencing and resolving + _ = try doc.locallyDereferenced().resolved() + } catch let error { + let friendlyError = OpenAPI.Error(from: error) + throw friendlyError + } + } + + func test_enumsExample() throws { + let docString = commonBaseDocument + """ +paths: + /items: + get: + parameters: + - in: query + name: sort + description: Sort order + schema: + type: string + enum: [asc, desc] + responses: + '200': + description: OK +""" + + // test decoding + do { + let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) + + // test validating + try doc.validate() + + // test dereferencing and resolving + _ = try doc.locallyDereferenced().resolved() + } catch let error { + let friendlyError = OpenAPI.Error(from: error) + throw friendlyError + } + } + + func test_reusableEnumsExample() throws { + let docString = commonBaseDocument + """ +paths: + /products: + get: + parameters: + - in: query + name: color + required: true + schema: + $ref: '#/components/schemas/Color' + responses: + '200': + description: OK +components: + schemas: + Color: + type: string + enum: + - black + - white + - red + - green + - blue +""" + + // test decoding + do { + let doc = try YAMLDecoder().decode(OpenAPI.Document.self, from: docString) + + // test validating + try doc.validate() + + // test dereferencing and resolving + _ = try doc.locallyDereferenced().resolved() + } catch let error { + let friendlyError = OpenAPI.Error(from: error) + throw friendlyError + } + } +} + +fileprivate let commonBaseDocument = """ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing + +""" diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift index 1f84bae14..5543a286c 100644 --- a/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift @@ -30,6 +30,7 @@ final class SchemaFragmentTests: XCTestCase { assertNoGeneralProperties(JSONSchemaFragment.number(.init(), .init())) assertNoGeneralProperties(JSONSchemaFragment.array(.init(), .init())) assertNoGeneralProperties(JSONSchemaFragment.object(.init(), .init())) + assertNoGeneralProperties(JSONSchemaFragment.reference(.component(named: "test"))) func assertSameGeneralProperties(_ fragment: JSONSchemaFragment, as properties: JSONSchemaFragment.GeneralContext, file: StaticString = #file, line: UInt = #line) { XCTAssertEqual(fragment.allowedValues, properties.allowedValues, file: file, line: line) @@ -173,6 +174,27 @@ extension SchemaFragmentTests { XCTAssertThrowsError(try testDecoder.decode(JSONSchemaFragment.self, from: t)) } + func test_decodeFailsWithInvalidReference() { + let t1 = +""" +{ + "$ref": "not a ref !@#$%%^" +} +""".data(using: .utf8)! + + // should be a schema reference, not a response reference. + let t2 = +""" +{ + "$ref": "#/components/responses/test" +} +""".data(using: .utf8)! + + XCTAssertThrowsError(try testDecoder.decode(JSONSchemaFragment.self, from: t1)) + + XCTAssertThrowsError(try testDecoder.decode(JSONSchemaFragment.self, from: t2)) + } + func test_generalEncode() throws { let t = JSONSchemaFragment.general(.init()) @@ -789,4 +811,32 @@ extension SchemaFragmentTests { XCTAssertEqual(decoded5, JSONSchemaFragment.object(.init(), .init(properties: ["hello": .string(required: false)]))) } + + func test_referenceEncode() throws { + let t1 = JSONSchemaFragment.reference(.component(named: "test")) + + let encoded = try testStringFromEncoding(of: t1) + + assertJSONEquivalent( + encoded, +""" +{ + "$ref" : "#\\/components\\/schemas\\/test" +} +""" + ) + } + + func test_referenceDecode() throws { + let t1 = +""" +{ + "$ref": "#/components/schemas/test" +} +""".data(using: .utf8)! + + let decoded = try testDecoder.decode(JSONSchemaFragment.self, from: t1) + + XCTAssertEqual(decoded, .reference(.component(named: "test"))) + } } From a4cbcdd4b3bac759d1b24fe4ac0c61d104a97ab8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 25 Jul 2020 14:46:05 -0700 Subject: [PATCH 2/4] address changes due to refactored test tooling. --- .../Schema Object/SchemaFragmentTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift index 3fb7c2035..a1fc960d8 100644 --- a/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaFragmentTests.swift @@ -190,9 +190,9 @@ extension SchemaFragmentTests { } """.data(using: .utf8)! - XCTAssertThrowsError(try testDecoder.decode(JSONSchemaFragment.self, from: t1)) + XCTAssertThrowsError(try orderUnstableDecode(JSONSchemaFragment.self, from: t1)) - XCTAssertThrowsError(try testDecoder.decode(JSONSchemaFragment.self, from: t2)) + XCTAssertThrowsError(try orderUnstableDecode(JSONSchemaFragment.self, from: t2)) } func test_generalEncode() throws { @@ -815,7 +815,7 @@ extension SchemaFragmentTests { func test_referenceEncode() throws { let t1 = JSONSchemaFragment.reference(.component(named: "test")) - let encoded = try testStringFromEncoding(of: t1) + let encoded = try orderUnstableTestStringFromEncoding(of: t1) assertJSONEquivalent( encoded, @@ -835,7 +835,7 @@ extension SchemaFragmentTests { } """.data(using: .utf8)! - let decoded = try testDecoder.decode(JSONSchemaFragment.self, from: t1) + let decoded = try orderUnstableDecode(JSONSchemaFragment.self, from: t1) XCTAssertEqual(decoded, .reference(.component(named: "test"))) } From e5f954b05d50e5ab52a0bd9480dc85e2c49c91db Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 25 Jul 2020 15:06:27 -0700 Subject: [PATCH 3/4] test the swagger example that prompted this bug fix a bit more deeply. --- .../SwaggerDocSamplesTests.swift | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift b/Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift index 04c24a37a..e7a7e2a69 100644 --- a/Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift +++ b/Tests/OpenAPIKitCompatibilitySuite/SwaggerDocSamplesTests.swift @@ -67,8 +67,36 @@ components: // test validating try doc.validate() + XCTAssertEqual( + doc.paths["/pets"]?.patch?.requestBody?.requestValue? + .content[.json]?.schema.schemaValue, + JSONSchema.one( + of: .reference(.component(named: "Cat")), + .reference(.component(named: "Dog")), + discriminator: .init(propertyName: "pet_type") + ) + ) + // test dereferencing and resolving - _ = try doc.locallyDereferenced().resolved() + let resolvedDoc = try doc.locallyDereferenced().resolved() + + XCTAssertEqual(resolvedDoc.routes.count, 1) + XCTAssertEqual(resolvedDoc.endpoints.count, 1) + + let dogSchema = doc.components.schemas["Dog"]! + let catSchema = doc.components.schemas["Cat"]! + + XCTAssertEqual( + resolvedDoc.endpoints[0].requestBody? + .content[.json]?.schema.underlyingJSONSchema, + JSONSchema.one( + of: [ + catSchema, + dogSchema + ], + discriminator: .init(propertyName: "pet_type") + ) + ) } catch let error { let friendlyError = OpenAPI.Error(from: error) throw friendlyError From b38aa24478f85a21dd655aed0fa82fcc7f300a62 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 25 Jul 2020 15:20:18 -0700 Subject: [PATCH 4/4] throw reference into the allOf schema tests. --- .../Schema Object/SchemaObjectTests.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift index 95ba6f1a1..d24177eef 100644 --- a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift @@ -3952,6 +3952,12 @@ extension SchemaObjectTests { ], discriminator: .init(propertyName: "hello") ) + let allOfWithReference = JSONSchema.all( + of: [ + .object(.init(), .init()), + .reference(.component(named: "test")) + ] + ) testEncodingPropertyLines(entity: allOf, propertyLines: [ "\"allOf\" : [", @@ -3987,6 +3993,17 @@ extension SchemaObjectTests { " \"propertyName\" : \"hello\"", "}" ]) + + testEncodingPropertyLines(entity: allOfWithReference, propertyLines: [ + "\"allOf\" : [", + " {", + " \"type\" : \"object\"", + " },", + " {", + " \"$ref\" : \"#\\/components\\/schemas\\/test\"", + " }", + "]" + ]) } func test_decodeAll() throws { @@ -4008,9 +4025,18 @@ extension SchemaObjectTests { "discriminator": { "propertyName": "hello" } } """.data(using: .utf8)! + let allWithReferenceData = """ + { + "allOf": [ + { "type": "object" }, + { "$ref": "#/components/schemas/test" } + ] + } + """.data(using: .utf8)! let all = try orderUnstableDecode(JSONSchema.self, from: allData) let allWithDiscriminator = try orderUnstableDecode(JSONSchema.self, from: allWithDiscriminatorData) + let allWithReference = try orderUnstableDecode(JSONSchema.self, from: allWithReferenceData) XCTAssertEqual( all, @@ -4032,6 +4058,16 @@ extension SchemaObjectTests { discriminator: .init(propertyName: "hello") ) ) + + XCTAssertEqual( + allWithReference, + JSONSchema.all( + of: [ + .object(.init(), .init()), + .reference(.component(named: "test")) + ] + ) + ) } func test_encodeOne() {