From 8a3eaefcc2aee194f2f336f3fb655c451db254d7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 17 Feb 2020 21:53:08 -0800 Subject: [PATCH 1/2] Add more tests for new stuff. make extended format options more accessible. --- .../Schema Object/TypesAndFormats.swift | 20 ++- .../Schema Object/SchemaObjectTests.swift | 122 ++++++++++++++---- 2 files changed, 110 insertions(+), 32 deletions(-) diff --git a/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift b/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift index f49cd8995..a06f5ba03 100644 --- a/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift +++ b/Sources/OpenAPIKit/Schema Object/TypesAndFormats.swift @@ -282,15 +282,23 @@ extension JSONTypeFormat { } extension JSONTypeFormat.StringFormat { - public enum Extended { - public static let uuid: JSONTypeFormat.StringFormat = .other("uuid") - public static let email: JSONTypeFormat.StringFormat = .other("email") + public enum Extended: String, Equatable { + case uuid = "uuid" + case email = "email" + } + + public static func extended(_ format: Extended) -> Self { + return .other(format.rawValue) } } extension JSONTypeFormat.IntegerFormat { - public enum Extended { - public static let uint32: JSONTypeFormat.IntegerFormat = .other("uint32") - public static let uint64: JSONTypeFormat.IntegerFormat = .other("uint64") + public enum Extended: String, Equatable { + case uint32 = "uint32" + case uint64 = "uint64" + } + + public static func extended(_ format: Extended) -> Self { + return .other(format.rawValue) } } diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift index 471c69117..598e19e2d 100644 --- a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift @@ -33,6 +33,7 @@ final class SchemaObjectTests: XCTestCase { let oneOf = JSONSchema.one(of: [boolean]) let not = JSONSchema.not(boolean) let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + let undefined = JSONSchema.undefined(description: "hello world") // JSONTypeFormat XCTAssertEqual(boolean.jsonTypeFormat, .boolean(.unspecified)) @@ -55,6 +56,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertNil(oneOf.jsonTypeFormat) XCTAssertNil(not.jsonTypeFormat) XCTAssertNil(reference.jsonTypeFormat) + XCTAssertNil(undefined.jsonTypeFormat) // JSONType XCTAssertEqual(boolean.jsonTypeFormat?.jsonType, .boolean) @@ -104,6 +106,7 @@ final class SchemaObjectTests: XCTestCase { let not = JSONSchema.not(boolean) let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + XCTAssertTrue(boolean.required) XCTAssertTrue(object.required) XCTAssertTrue(array.required) @@ -124,6 +127,7 @@ final class SchemaObjectTests: XCTestCase { let number = JSONSchema.number(.init(format: .unspecified, required: false), .init()) let integer = JSONSchema.integer(.init(format: .unspecified, required: false), .init()) let string = JSONSchema.string(.init(format: .unspecified, required: false), .init()) + let undefined = JSONSchema.undefined(description: nil) XCTAssertFalse(boolean.required) XCTAssertFalse(object.required) @@ -131,6 +135,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertFalse(number.required) XCTAssertFalse(integer.required) XCTAssertFalse(string.required) + XCTAssertFalse(undefined.required) } func test_nullable() { @@ -147,6 +152,35 @@ final class SchemaObjectTests: XCTestCase { XCTAssertTrue(number.nullable) XCTAssertTrue(integer.nullable) XCTAssertTrue(string.nullable) + + } + + func test_notNullable() { + let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true)) + let object = JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) + let array = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: true)))) + let number = JSONSchema.number(.init(format: .unspecified, required: true), .init()) + let integer = JSONSchema.integer(.init(format: .unspecified, required: true), .init()) + let string = JSONSchema.string(.init(format: .unspecified, required: true), .init()) + let allOf = JSONSchema.all(of: [boolean]) + let anyOf = JSONSchema.any(of: [boolean]) + let oneOf = JSONSchema.one(of: [boolean]) + let not = JSONSchema.not(boolean) + let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + let undefined = JSONSchema.undefined(description: nil) + + XCTAssertFalse(boolean.nullable) + XCTAssertFalse(object.nullable) + XCTAssertFalse(array.nullable) + XCTAssertFalse(number.nullable) + XCTAssertFalse(integer.nullable) + XCTAssertFalse(string.nullable) + XCTAssertFalse(allOf.nullable) + XCTAssertFalse(anyOf.nullable) + XCTAssertFalse(oneOf.nullable) + XCTAssertFalse(not.nullable) + XCTAssertFalse(reference.nullable) + XCTAssertFalse(undefined.nullable) } func test_readableAndWritable() { @@ -161,6 +195,7 @@ final class SchemaObjectTests: XCTestCase { let oneOf = JSONSchema.one(of: [boolean]) let not = JSONSchema.not(boolean) let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + let undefined = JSONSchema.undefined(description: nil) XCTAssertFalse(boolean.readOnly) XCTAssertFalse(boolean.writeOnly) @@ -185,6 +220,8 @@ final class SchemaObjectTests: XCTestCase { XCTAssertFalse(not.writeOnly) XCTAssertFalse(reference.readOnly) XCTAssertFalse(reference.writeOnly) + XCTAssertFalse(undefined.readOnly) + XCTAssertFalse(undefined.writeOnly) } func test_readOnly() { @@ -243,6 +280,7 @@ final class SchemaObjectTests: XCTestCase { let oneOf = JSONSchema.one(of: [boolean]) let not = JSONSchema.not(boolean) let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + let undefined = JSONSchema.undefined(description: nil) XCTAssertFalse(boolean.deprecated) XCTAssertFalse(object.deprecated) @@ -256,6 +294,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertFalse(oneOf.deprecated) XCTAssertFalse(not.deprecated) XCTAssertFalse(reference.deprecated) + XCTAssertFalse(undefined.deprecated) } func test_deprecated() { @@ -274,32 +313,6 @@ final class SchemaObjectTests: XCTestCase { XCTAssertTrue(string.deprecated) } - func test_notNullable() { - let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true)) - let object = JSONSchema.object(.init(format: .unspecified, required: true), .init(properties: [:])) - let array = JSONSchema.array(.init(format: .unspecified, required: true), .init(items: .boolean(.init(format: .unspecified, required: true)))) - let number = JSONSchema.number(.init(format: .unspecified, required: true), .init()) - let integer = JSONSchema.integer(.init(format: .unspecified, required: true), .init()) - let string = JSONSchema.string(.init(format: .unspecified, required: true), .init()) - let allOf = JSONSchema.all(of: [boolean]) - let anyOf = JSONSchema.any(of: [boolean]) - let oneOf = JSONSchema.one(of: [boolean]) - let not = JSONSchema.not(boolean) - let reference = JSONSchema.reference(.external("hello/world.json#/hello")) - - XCTAssertFalse(boolean.nullable) - XCTAssertFalse(object.nullable) - XCTAssertFalse(array.nullable) - XCTAssertFalse(number.nullable) - XCTAssertFalse(integer.nullable) - XCTAssertFalse(string.nullable) - XCTAssertFalse(allOf.nullable) - XCTAssertFalse(anyOf.nullable) - XCTAssertFalse(oneOf.nullable) - XCTAssertFalse(not.nullable) - XCTAssertFalse(reference.nullable) - } - func test_title() { let boolean = JSONSchema.boolean(.init(format: .unspecified, required: true, title: "hello")) let object = JSONSchema.object(.init(format: .unspecified, required: true, title: "hello"), .init(properties: [:])) @@ -313,6 +326,7 @@ final class SchemaObjectTests: XCTestCase { let oneOf = JSONSchema.one(of: [boolean]) let not = JSONSchema.not(boolean) let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + let undefined = JSONSchema.undefined(description: nil) XCTAssertEqual(boolean.title, "hello") XCTAssertEqual(object.title, "hello") @@ -326,6 +340,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertNil(oneOf.title) XCTAssertNil(not.title) XCTAssertNil(reference.title) + XCTAssertNil(undefined.title) } func test_description() { @@ -341,6 +356,8 @@ final class SchemaObjectTests: XCTestCase { let oneOf = JSONSchema.one(of: [boolean]) let not = JSONSchema.not(boolean) let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + let undefined = JSONSchema.undefined(description: nil) + let undefinedWithDescription = JSONSchema.undefined(description: "hello") XCTAssertEqual(boolean.description, "hello") XCTAssertEqual(object.description, "hello") @@ -348,12 +365,14 @@ final class SchemaObjectTests: XCTestCase { XCTAssertEqual(number.description, "hello") XCTAssertEqual(integer.description, "hello") XCTAssertEqual(string.description, "hello") + XCTAssertEqual(undefinedWithDescription.description, "hello") XCTAssertNil(allOf.description) XCTAssertNil(anyOf.description) XCTAssertNil(oneOf.description) XCTAssertNil(not.description) XCTAssertNil(reference.description) + XCTAssertNil(undefined.description) } func test_externalDocs() { @@ -369,6 +388,7 @@ final class SchemaObjectTests: XCTestCase { let oneOf = JSONSchema.one(of: [boolean]) let not = JSONSchema.not(boolean) let reference = JSONSchema.reference(.external("hello/world.json#/hello")) + let undefined = JSONSchema.undefined(description: nil) XCTAssertEqual(boolean.externalDocs, .init(url: URL(string: "http://google.com")!)) XCTAssertEqual(object.externalDocs, .init(url: URL(string: "http://google.com")!)) @@ -382,6 +402,7 @@ final class SchemaObjectTests: XCTestCase { XCTAssertNil(oneOf.externalDocs) XCTAssertNil(not.externalDocs) XCTAssertNil(reference.externalDocs) + XCTAssertNil(undefined.externalDocs) } func test_requiredToOptional() { @@ -3751,6 +3772,12 @@ extension SchemaObjectTests { false ] ) + + let t1 = JSONSchema.boolean(format: .generic) + XCTAssertEqual(t1, JSONSchema.boolean(format: .init(rawValue: ""))) + + let t2 = JSONSchema.boolean(format: .other("integer")) + XCTAssertEqual(t2, JSONSchema.boolean(format: .init(rawValue: "integer"))) } func test_number() { @@ -3777,6 +3804,12 @@ extension SchemaObjectTests { format: .double, allowedValues: 5.5 ) + + let t3 = JSONSchema.number(format: .generic) + XCTAssertEqual(t3, JSONSchema.number(format: .init(rawValue: ""))) + + let t4 = JSONSchema.number(format: .other("Float80")) + XCTAssertEqual(t4, JSONSchema.number(format: .init(rawValue: "Float80"))) } func test_integer() { @@ -3799,6 +3832,17 @@ extension SchemaObjectTests { required: true, allowedValues: 1, 2, 3 ) + + let t1 = JSONSchema.integer(format: .extended(.uint32)) + XCTAssertEqual(t1, JSONSchema.integer(format: .other("uint32"))) + XCTAssertEqual(t1, JSONSchema.integer(format: .init(rawValue: "uint32"))) + + let t2 = JSONSchema.integer(format: .extended(.uint64)) + XCTAssertEqual(t2, JSONSchema.integer(format: .other("uint64"))) + XCTAssertEqual(t2, JSONSchema.integer(format: .init(rawValue: "uint64"))) + + let t3 = JSONSchema.integer(format: .generic) + XCTAssertEqual(t3, JSONSchema.integer(format: .init(rawValue: ""))) } func test_string() { @@ -3815,6 +3859,17 @@ extension SchemaObjectTests { let _ = JSONSchema.string( allowedValues: "hello", "world" ) + + let t1 = JSONSchema.string(format: .extended(.uuid)) + XCTAssertEqual(t1, JSONSchema.string(format: .other("uuid"))) + XCTAssertEqual(t1, JSONSchema.string(format: .init(rawValue: "uuid"))) + + let t2 = JSONSchema.string(format: .extended(.email)) + XCTAssertEqual(t2, JSONSchema.string(format: .other("email"))) + XCTAssertEqual(t2, JSONSchema.string(format: .init(rawValue: "email"))) + + let t3 = JSONSchema.string(format: .generic) + XCTAssertEqual(t3, JSONSchema.string(format: .init(rawValue: ""))) } func test_object() { @@ -3856,6 +3911,21 @@ extension SchemaObjectTests { "created_at": .string(format: .dateTime), "bio": .string(nullable: true) ])])]) + + let t1 = JSONSchema.object(format: .generic) + XCTAssertEqual(t1, JSONSchema.object(format: .init(rawValue: ""))) + + let t2 = JSONSchema.object(format: .other("weird")) + XCTAssertEqual(t2, JSONSchema.object(format: .init(rawValue: "weird"))) + } + + func test_array() { + + let t1 = JSONSchema.array(format: .generic) + XCTAssertEqual(t1, JSONSchema.array(format: .init(rawValue: ""))) + + let t2 = JSONSchema.array(format: .other("weird")) + XCTAssertEqual(t2, JSONSchema.array(format: .init(rawValue: "weird"))) } func test_allOf() { From 3ab8709e6d2f35c1460606de889424602f3e33c5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 17 Feb 2020 22:39:07 -0800 Subject: [PATCH 2/2] Add TomTom search API to compatibility suite. Fix bug with parsing examples --- .../Schema Object/SchemaObjectContext.swift | 10 +- .../GoogleBooksAPI.swift | 13 -- .../TomTomAPI.swift | 113 ++++++++++++++++++ 3 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 Tests/OpenAPIKitCompatibilitySuite/TomTomAPI.swift diff --git a/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift b/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift index e4470d608..d67f30a29 100644 --- a/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift +++ b/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift @@ -429,7 +429,15 @@ extension JSONSchema.Context: Decodable { deprecated = try container.decodeIfPresent(Bool.self, forKey: .deprecated) ?? false - example = try container.decodeIfPresent(String.self, forKey: .example) + if let decodedExample = try container.decodeIfPresent(AnyCodable.self, forKey: .example) { + if let fragment = decodedExample.value as? String { + example = fragment + } else { + example = try String(data: JSONEncoder().encode(decodedExample), encoding: .utf8)! + } + } else { + example = nil + } } } diff --git a/Tests/OpenAPIKitCompatibilitySuite/GoogleBooksAPI.swift b/Tests/OpenAPIKitCompatibilitySuite/GoogleBooksAPI.swift index fe2205592..50c2cf375 100644 --- a/Tests/OpenAPIKitCompatibilitySuite/GoogleBooksAPI.swift +++ b/Tests/OpenAPIKitCompatibilitySuite/GoogleBooksAPI.swift @@ -13,19 +13,6 @@ import Foundation import FoundationNetworking #endif -/* - - Currently failing to parse because OpenAPIKit lacks support for JSON references. - - Is it worth failing to parse this? sometimes, yeah, it would be good to know if you were accidentally publishing - an empty server URL when you meant to publish one. - - failed - The data couldn’t be read because it isn’t in the correct format. - coding path: servers -> Index 0 -> url - debug description: Invalid URL string. - - */ - final class GoogleBooksAPICampatibilityTests: XCTestCase { var booksAPI: Result? = nil var apiDoc: OpenAPI.Document? { diff --git a/Tests/OpenAPIKitCompatibilitySuite/TomTomAPI.swift b/Tests/OpenAPIKitCompatibilitySuite/TomTomAPI.swift new file mode 100644 index 000000000..4e573f426 --- /dev/null +++ b/Tests/OpenAPIKitCompatibilitySuite/TomTomAPI.swift @@ -0,0 +1,113 @@ +// +// TomTomAPI.swift +// +// +// Created by Mathew Polzin on 2/17/20. +// + +import XCTest +import OpenAPIKit +import Yams +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +final class TomTomAPICampatibilityTests: XCTestCase { + var tomtomAPI: Result? = nil + var apiDoc: OpenAPI.Document? { + guard case .success(let document) = tomtomAPI else { return nil } + return document + } + + override func setUp() { + if tomtomAPI == nil { + tomtomAPI = Result { + try YAMLDecoder().decode( + OpenAPI.Document.self, + from: String(contentsOf: URL(string: "https://raw.githubusercontent.com/APIs-guru/openapi-directory/c9190db19e5cb151592d44f0d4482839e1e5a8e0/APIs/tomtom.com/search/1.0.0/openapi.yaml")!) + ) + } + } + } + + func test_successfullyParsedDocument() { + switch tomtomAPI { + case nil: + XCTFail("Did not attempt to pull Google Books API documentation like expected.") + case .failure(let error as DecodingError): + let codingPath: String + let debugDetails: String + let underlyingError: String + switch error { + case .dataCorrupted(let context), .keyNotFound(_, let context), .typeMismatch(_, let context), .valueNotFound(_, let context): + codingPath = context.codingPath.map { $0.stringValue }.joined(separator: " -> ") + debugDetails = context.debugDescription + underlyingError = context.underlyingError.map { "\n underlying error: " + String(describing: $0) } ?? "" + @unknown default: + codingPath = "" + debugDetails = "" + underlyingError = "" + } + XCTFail(error.failureReason ?? error.errorDescription ?? error.localizedDescription + "\n coding path: " + codingPath + "\n debug description: " + debugDetails + underlyingError) + case .failure(let error): + XCTFail(error.localizedDescription) + case .success: + break + } + } + + func test_successfullyParsedBasicMetadata() { + guard let apiDoc = apiDoc else { return } + + // title is Search + XCTAssertEqual(apiDoc.info.title, "Search") + + // description is set + XCTAssertFalse(apiDoc.info.description?.isEmpty ?? true) + + // contact name is "Contact Us" + XCTAssertEqual(apiDoc.info.contact?.name, "Contact Us") + + // no contact email is provided + XCTAssert(apiDoc.info.contact?.email?.isEmpty ?? true) + + // server is specified + XCTAssertNotNil(apiDoc.servers.first) + } + + func test_successfullyParsedRoutes() { + guard let apiDoc = apiDoc else { return } + + // just check for a few of the known paths + XCTAssert(apiDoc.paths.contains(key: "/search/{versionNumber}/additionalData.{ext}")) + XCTAssert(apiDoc.paths.contains(key: "/search/{versionNumber}/cS/{category}.{ext}")) + XCTAssert(apiDoc.paths.contains(key: "/search/{versionNumber}/categorySearch/{query}.{ext}")) + XCTAssert(apiDoc.paths.contains(key: "/search/{versionNumber}/geocode/{query}.{ext}")) + XCTAssert(apiDoc.paths.contains(key: "/search/{versionNumber}/geometryFilter.{ext}")) + XCTAssert(apiDoc.paths.contains(key: "/search/{versionNumber}/geometrySearch/{query}.{ext}")) + + // check for a known POST response + XCTAssertNotNil(apiDoc.paths["/search/{versionNumber}/geometrySearch/{query}.{ext}"]?.post?.responses[200 as OpenAPI.Response.StatusCode]) + + // and a known GET response + XCTAssertNotNil(apiDoc.paths["/search/{versionNumber}/geometrySearch/{query}.{ext}"]?.get?.responses[200 as OpenAPI.Response.StatusCode]) + + // check for parameters + XCTAssertFalse(apiDoc.paths["/search/{versionNumber}/geometrySearch/{query}.{ext}"]?.get?.parameters.isEmpty ?? true) + } + + func test_successfullyParsedComponents() { + guard let apiDoc = apiDoc else { return } + + // check for a known parameter + XCTAssertNotNil(apiDoc.components.parameters["btmRight"]) + XCTAssertTrue(apiDoc.components.parameters["btmRight"]?.parameterLocation.inQuery ?? false) + + // check for known response + XCTAssertNotNil(apiDoc.components.responses["200"]) + + // check for known security scheme + XCTAssertNotNil(apiDoc.components.securitySchemes["api_key"]) + } +}