From 4afe4cfb312101c99e024a67fa1d5553f6afddf3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 11:52:02 -0800 Subject: [PATCH 1/9] Rename first JSONAPI+Testing header to Testing to remove conflict in GitHub Markdown links. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a27f196..2a72de6 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Relationship Object](#relationship-object) - [Links Object](#links-object) - [Misc](#misc) - - [JSONAPI+Testing](#jsonapitesting) + - [Testing](#testing) - [Entity Validator](#entity-validator) - [Potential Improvements](#potential-improvements) - [Usage](#usage) @@ -175,7 +175,7 @@ Note that Playground support for importing non-system Frameworks is still a bit - [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields. - [ ] Create more descriptive errors that are easier to use for troubleshooting. -### JSONAPI+Testing +### Testing #### Entity Validator - [x] Disallow optional array in `Attribute` (should be empty array, not `null`). - [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded. From 845d0854555117ff9802cf0ad0a027915e55963b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 15:32:38 -0800 Subject: [PATCH 2/9] Fill out a fair bit of OpenAPI Entity testing --- .../JSONAPIOpenAPI/OpenAPITypes+Codable.swift | 10 +- Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 14 +- .../JSONAPIAttributeOpenAPITests.swift | 2 +- .../JSONAPIEntityOpenAPITests.swift | 260 +++++++++++++++++- .../JSONAPIOpenAPITests/XCTestManifests.swift | 2 + 5 files changed, 271 insertions(+), 17 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift index 7b04957..6d0aced 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift @@ -110,7 +110,7 @@ extension JSONNode.ArrayContext: Encodable { } } -extension JSONNode.ObjectContext : Encodable{ +extension JSONNode.ObjectContext : Encodable { private enum CodingKeys: String, CodingKey { case maxProperties case minProperties @@ -132,13 +132,9 @@ extension JSONNode.ObjectContext : Encodable{ try container.encode(additionalProperties, forKey: .additionalProperties) } - let required = properties.filter { (name, node) in - node.required - }.keys + try container.encode(requiredProperties, forKey: .required) - try container.encode(Array(required), forKey: .required) - - try container.encode(max(minProperties, required.count), forKey: .minProperties) + try container.encode(minProperties, forKey: .minProperties) } } diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 79bf009..033a3bd 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -371,7 +371,7 @@ public enum JSONNode: Equatable { public struct ObjectContext: Equatable { public let maxProperties: Int? - public let minProperties: Int + let _minProperties: Int public let properties: [String: JSONNode] public let additionalProperties: [String: JSONNode]? @@ -379,8 +379,16 @@ public enum JSONNode: Equatable { // NOTE that an object's required properties // array is determined by looking at its properties' // required Bool. - public let required: [String] */ + public var requiredProperties: [String] { + return Array(properties.filter { (name, node) in + node.required + }.keys) + } + + public var minProperties: Int { + return max(_minProperties, requiredProperties.count) + } public init(properties: [String: JSONNode], additionalProperties: [String: JSONNode]? = nil, @@ -389,7 +397,7 @@ public enum JSONNode: Equatable { self.properties = properties self.additionalProperties = additionalProperties self.maxProperties = maxProperties - self.minProperties = minProperties + self._minProperties = minProperties } } diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift index 726f99a..642b95e 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift @@ -429,7 +429,7 @@ extension JSONAPIAttributeOpenAPITests { extension JSONAPIAttributeOpenAPITests { func test_EnumAttribute() { let node = try! Attribute.rawOpenAPINode() - print(EnumAttribute.allCases) + XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift index 298100c..329e0fb 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -8,12 +8,115 @@ import XCTest import JSONAPI import JSONAPIOpenAPI +import AnyCodable class JSONAPIEntityOpenAPITests: XCTestCase { func test_EmptyEntity() { let node = try! TestType1.openAPINode() - // TODO: Write test + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case let .object(contextA, objectContext1) = node else { + XCTFail("Expected Object node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(objectContext1.minProperties, 2) + XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["id", "type"])) + XCTAssertEqual(Set(objectContext1.properties.keys), Set(["id", "type"])) + XCTAssertEqual(objectContext1.properties["id"], .string(.init(format: .generic, + required: true), + .init())) + XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic, + required: true), + .init())) + } + + func test_AttributesEntity() { + let node = try! TestType2.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case let .object(contextA, objectContext1) = node else { + XCTFail("Expected Object node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(objectContext1.minProperties, 3) + XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["id", "type", "attributes"])) + XCTAssertEqual(Set(objectContext1.properties.keys), Set(["id", "type", "attributes"])) + + XCTAssertEqual(objectContext1.properties["id"], .string(.init(format: .generic, + required: true), + .init())) + XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic, + required: true), + .init())) + + let attributesNode = objectContext1.properties["attributes"] + + XCTAssertNotNil(attributesNode) + XCTAssertTrue(attributesNode?.required ?? false) + XCTAssertEqual(attributesNode?.jsonTypeFormat, .object(.generic)) + + guard case let .object(contextB, attributesContext)? = attributesNode else { + XCTFail("Expected Object node for attributes") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(attributesContext.minProperties, 3) + XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "nullableProperty"])) + XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"])) + + XCTAssertEqual(attributesContext.properties["stringProperty"], + .string(.init(format: .generic, + required: true), + .init())) + + XCTAssertEqual(attributesContext.properties["enumProperty"], + .string(.init(format: .generic, + required: true, + nullable: false, + allowedValues: ["one", "two"].map(AnyCodable.init)), + .init())) + + XCTAssertEqual(attributesContext.properties["optionalProperty"], + .string(.init(format: .generic, + required: false, + nullable: false, + allowedValues: nil), + .init())) + + XCTAssertEqual(attributesContext.properties["nullableProperty"], + .string(.init(format: .generic, + required: true, + nullable: true, + allowedValues: nil), + .init())) + + XCTAssertEqual(attributesContext.properties["nullableOptionalProperty"], + .string(.init(format: .generic, + required: false, + nullable: true, + allowedValues: nil), + .init())) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted @@ -21,15 +124,125 @@ class JSONAPIEntityOpenAPITests: XCTestCase { print(string) } - func test_AttributesEntity() { - let node = try! TestType2.openAPINode() + func test_RelationshipsEntity() { + let node = try! TestType3.openAPINode() - // TODO: Write test + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case let .object(contextA, objectContext1) = node else { + XCTFail("Expected Object node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(objectContext1.minProperties, 3) + XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["id", "type", "relationships"])) + XCTAssertEqual(Set(objectContext1.properties.keys), Set(["id", "type", "relationships"])) + + XCTAssertEqual(objectContext1.properties["id"], .string(.init(format: .generic, + required: true), + .init())) + XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic, + required: true), + .init())) + + let relationshipsNode = objectContext1.properties["relationships"] + + XCTAssertNotNil(relationshipsNode) + XCTAssertTrue(relationshipsNode?.required ?? false) + XCTAssertEqual(relationshipsNode?.jsonTypeFormat, .object(.generic)) + + guard case let .object(contextB, relationshipsContext)? = relationshipsNode else { + XCTFail("Expected Object node for relationships") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(relationshipsContext.minProperties, 3) + XCTAssertEqual(Set(relationshipsContext.requiredProperties), Set(["toOne", "nullableToOne", "toMany"])) + XCTAssertEqual(Set(relationshipsContext.properties.keys), Set(["toOne", "optionalTooOne", "nullableToOne", "nullableOptionalToOne", "toMany", "optionalToMany"])) + + let pointerDataContext = JSONNode.ObjectContext(properties: ["id": .string(.init(format: .generic, + required: true), + .init()), + "type": .string(.init(format: .generic, + required: true), + .init())]) + + let pointerContext = JSONNode.ObjectContext(properties: ["data": .object(.init(format: .generic, + required: true), + pointerDataContext)]) + + let nullablePointerContext = JSONNode.ObjectContext(properties: ["data": .object(.init(format: .generic, + required: true, + nullable: true), + pointerDataContext)]) + + let manyPointerContext = JSONNode.ObjectContext(properties: ["data": .array(.init(format: .generic, + required: true), + .init(items: .object(.init(format: .generic, + required: true), + pointerDataContext)))]) + + XCTAssertEqual(relationshipsContext.properties["toOne"], + .object(.init(format: .generic, + required: true), + pointerContext)) + + XCTAssertEqual(relationshipsContext.properties["optionalTooOne"], + .object(.init(format: .generic, + required: false, + nullable: false, + allowedValues: nil), + pointerContext)) + + XCTAssertEqual(relationshipsContext.properties["nullableToOne"], + .object(.init(format: .generic, + required: true, + nullable: false, + allowedValues: nil), + nullablePointerContext)) + + XCTAssertEqual(relationshipsContext.properties["nullableOptionalToOne"], + .object(.init(format: .generic, + required: false, + nullable: false, + allowedValues: nil), + nullablePointerContext)) + + XCTAssertEqual(relationshipsContext.properties["toMany"], + .object(.init(format: .generic, + required: true), + manyPointerContext)) + + XCTAssertEqual(relationshipsContext.properties["optionalToMany"], + .object(.init(format: .generic, + required: false, + nullable: false, + allowedValues: nil), + manyPointerContext)) + } + + func test_AttributesAndRelationshipsEntity() { + // TODO: write test + + /* let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let string = String(data: try! encoder.encode(node), encoding: .utf8)! print(string) + + */ } } @@ -46,7 +259,7 @@ extension JSONAPIEntityOpenAPITests { typealias TestType1 = BasicEntity enum TestType2Description: EntityDescription { - public static var jsonType: String { return "test1" } + public static var jsonType: String { return "test2" } public enum EnumType: String, CaseIterable, Codable, Equatable { case one @@ -56,13 +269,19 @@ extension JSONAPIEntityOpenAPITests { public struct Attributes: JSONAPI.Attributes, Sampleable { let stringProperty: Attribute let enumProperty: Attribute + let optionalProperty: Attribute? + let nullableProperty: Attribute + let nullableOptionalProperty: Attribute? var computedProperty: Attribute { return enumProperty } public static var sample: Attributes { return Attributes(stringProperty: .init(value: "hello"), - enumProperty: .init(value: .one)) + enumProperty: .init(value: .one), + optionalProperty: nil, + nullableProperty: .init(value: nil), + nullableOptionalProperty: nil) } } @@ -70,4 +289,33 @@ extension JSONAPIEntityOpenAPITests { } typealias TestType2 = BasicEntity + + enum TestType3Description: EntityDescription { + public static var jsonType: String { return "test3" } + + public typealias Attributes = NoAttributes + + public struct Relationships: JSONAPI.Relationships, Sampleable { + public let toOne: ToOneRelationship + public let optionalTooOne: ToOneRelationship? + public let nullableToOne: ToOneRelationship + public let nullableOptionalToOne: ToOneRelationship? + + public let toMany: ToManyRelationship + public let optionalToMany: ToManyRelationship? + // Note there is no such thing as nullable to-many relationships (Just use + // an empty array) + + public static var sample: Relationships { + return Relationships(toOne: .init(id: .init(rawValue: "1")), + optionalTooOne: nil, + nullableToOne: .init(id: nil), + nullableOptionalToOne: nil, + toMany: .init(ids: [.init(rawValue: "1")]), + optionalToMany: nil) + } + } + } + + typealias TestType3 = BasicEntity } diff --git a/Tests/JSONAPIOpenAPITests/XCTestManifests.swift b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift index b05e1af..a2dcf48 100644 --- a/Tests/JSONAPIOpenAPITests/XCTestManifests.swift +++ b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift @@ -32,8 +32,10 @@ extension JSONAPIAttributeOpenAPITests { extension JSONAPIEntityOpenAPITests { static let __allTests = [ + ("test_AttributesAndRelationshipsEntity", test_AttributesAndRelationshipsEntity), ("test_AttributesEntity", test_AttributesEntity), ("test_EmptyEntity", test_EmptyEntity), + ("test_RelationshipsEntity", test_RelationshipsEntity), ] } From 4dc63167d610a5fecac6cb818de4b8961975a1bd Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 15:35:54 -0800 Subject: [PATCH 3/9] Rename a couple of files --- .../{JSONAPIOpenAPITypes.swift => JSONAPITypes+OpenAPI.swift} | 0 ...wiftPrimitiveTypes.swift => SwiftPrimitiveTypes+OpenAPI.swift} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Sources/JSONAPIOpenAPI/{JSONAPIOpenAPITypes.swift => JSONAPITypes+OpenAPI.swift} (100%) rename Sources/JSONAPIOpenAPI/{SwiftPrimitiveTypes.swift => SwiftPrimitiveTypes+OpenAPI.swift} (100%) diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift similarity index 100% rename from Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift rename to Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes+OpenAPI.swift similarity index 100% rename from Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift rename to Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes+OpenAPI.swift From 9972d13a4ece8b85c515f02d818baae9351f4249 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 15:37:41 -0800 Subject: [PATCH 4/9] Document zip(with:) on optionals --- Sources/JSONAPIOpenAPI/Optional+ZipWith.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/JSONAPIOpenAPI/Optional+ZipWith.swift b/Sources/JSONAPIOpenAPI/Optional+ZipWith.swift index cbe113e..b04b7c2 100644 --- a/Sources/JSONAPIOpenAPI/Optional+ZipWith.swift +++ b/Sources/JSONAPIOpenAPI/Optional+ZipWith.swift @@ -5,6 +5,9 @@ // Created by Mathew Polzin on 1/19/19. // +/// Zip two optionals together with the given operation performed on +/// the unwrapped contents. If either optional is nil, the zip +/// yields nil. func zip(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? { return left.flatMap { lft in right.map { rght in fn(lft, rght) }} } From d6911f170cfdf8fe9d4bf55125eeda895a90a1b2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 20:57:54 -0800 Subject: [PATCH 5/9] Add example to JSONNode --- JSONAPI.playground/contents.xcplayground | 1 - .../JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift | 36 ++++++++++++- .../JSONAPIOpenAPI/OpenAPITypes+Codable.swift | 5 ++ Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 50 ++++++++++++++++++- .../JSONAPIDocumentOpenAPITests.swift | 42 ++++++++++++++++ 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift diff --git a/JSONAPI.playground/contents.xcplayground b/JSONAPI.playground/contents.xcplayground index a1ec109..e240eff 100644 --- a/JSONAPI.playground/contents.xcplayground +++ b/JSONAPI.playground/contents.xcplayground @@ -5,6 +5,5 @@ - \ No newline at end of file diff --git a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift index 6afd065..35fd347 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift @@ -118,6 +118,7 @@ extension ToManyRelationship: OpenAPINodeType { extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { public static func openAPINode() throws -> JSONNode { + // TODO: const for json `type` // TODO: metadata, links let idNode = JSONNode.string(.init(format: .generic, @@ -147,7 +148,40 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc typeProperty, attributesProperty, relationshipsProperty - ].compactMap { $0 }) { _, value in value } + ].compactMap { $0 }) { _, value in value } + + return .object(.init(format: .generic, + required: true), + .init(properties: propertiesDict)) + } +} + +extension SingleResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { + public static func openAPINode() throws -> JSONNode { + return try Entity.openAPINode() + } +} + +extension ManyResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { + public static func openAPINode() throws -> JSONNode { + return .array(.init(format: .generic, + required: true), + .init(items: try Entity.openAPINode())) + } +} + +extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType { + public static func openAPINode() throws -> JSONNode { + // TODO: metadata, links, api description, includes, errors + // TODO: represent data and errors as the two distinct possible outcomes + + let primaryDataNode: JSONNode? = try PrimaryResourceBody.openAPINode() + + let primaryDataProperty = primaryDataNode.map { ("data", $0) } + + let propertiesDict = Dictionary([ + primaryDataProperty + ].compactMap { $0 }) { _, value in value } return .object(.init(format: .generic, required: true), diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift index 6d0aced..9d4d2cc 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift @@ -12,6 +12,7 @@ extension JSONNode.Context: Encodable { case format case allowedValues = "enum" case nullable + case example } public func encode(to encoder: Encoder) throws { @@ -28,6 +29,10 @@ extension JSONNode.Context: Encodable { } try container.encode(nullable, forKey: .nullable) + + if example != nil { + try container.encode(example, forKey: .example) + } } } diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 033a3bd..65ec92b 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -16,6 +16,12 @@ public protocol OpenAPINodeType { static func openAPINode() throws -> JSONNode } +extension OpenAPINodeType where Self: Sampleable, Self: Encodable { + public static func openAPINodeWithExample() throws -> JSONNode { + return try openAPINode().with(example: Self.sample) + } +} + /// Anything conforming to `RawOpenAPINodeType` can provide an /// OpenAPI schema representing itself. This second protocol is /// necessary so that one type can conditionally provide a @@ -261,14 +267,18 @@ public enum JSONNode: Equatable { /// into an allowed value. public let allowedValues: [AnyCodable]? + public let example: AnyCodable? + public init(format: Format, required: Bool, nullable: Bool = false, - allowedValues: [AnyCodable]? = nil) { + allowedValues: [AnyCodable]? = nil, + example: AnyCodable? = nil) { self.format = format self.required = required self.nullable = nullable self.allowedValues = allowedValues + self.example = example } /// Return the optional version of this Context @@ -296,12 +306,21 @@ public enum JSONNode: Equatable { } /// Return this context with the given list of possible values - public func with(allowedValues: [AnyCodable]?) -> Context { + public func with(allowedValues: [AnyCodable]) -> Context { return .init(format: format, required: required, nullable: nullable, allowedValues: allowedValues) } + + /// Return this context with the given example + public func with(example: AnyCodable) -> Context { + return .init(format: format, + required: required, + nullable: nullable, + allowedValues: allowedValues, + example: example) + } } public struct NumericContext: Equatable { @@ -513,8 +532,35 @@ public enum JSONNode: Equatable { return self } } + + public func with(example codableExample: T) throws -> JSONNode { + let example: AnyCodable + if let goodToGo = codableExample as? AnyCodable { + example = goodToGo + } else { + example = AnyCodable(try JSONSerialization.jsonObject(with: JSONEncoder().encode(codableExample), options: [])) + } + + switch self { + case .boolean(let context): + return .boolean(context.with(example: example)) + case .object(let contextA, let contextB): + return .object(contextA.with(example: example), contextB) + case .array(let contextA, let contextB): + return .array(contextA.with(example: example), contextB) + case .number(let context, let contextB): + return .number(context.with(example: example), contextB) + case .integer(let context, let contextB): + return .integer(context.with(example: example), contextB) + case .string(let context, let contextB): + return .string(context.with(example: example), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self + } + } } public enum OpenAPICodableError: Swift.Error { case allCasesArrayNotCodable + case exampleNotCodable } diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift new file mode 100644 index 0000000..f709f17 --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift @@ -0,0 +1,42 @@ +// +// JSONAPIDocumentOpenAPITests.swift +// JSONAPIOpenAPITests +// +// Created by Mathew Polzin on 1/21/19. +// + +import XCTest +import JSONAPI +import JSONAPIOpenAPI + +class JSONAPIDocumentOpenAPITests: XCTestCase { + func test_SingleResourceDocument() { + let node = try! SingleEntityDocument.openAPINode() + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + print(String(data: try! encoder.encode(node), encoding: .utf8)!) + } +} + +// MARK: - Test Types +extension JSONAPIDocumentOpenAPITests { + enum TestEntityDescription: EntityDescription { + static var jsonType: String { return "test" } + + struct Attributes: JSONAPI.Attributes, Sampleable { + let name: Attribute + + static var sample: Attributes { + return .init(name: "hello world") + } + } + + typealias Relationships = NoRelationships + } + + typealias TestEntity = BasicEntity + + typealias SingleEntityDocument = Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +} From 95f9d8084d0ab232a33006ce90d35091d20e2d9a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 21:56:33 -0800 Subject: [PATCH 6/9] Add Arbitrary conformance for UnknownJSONAPIError. Fix weird encoding exception that I cannot quite figure out or explain at the moment. --- .../Contents.swift | 10 ++++++ .../Usage.xcplaygroundpage/Contents.swift | 4 --- JSONAPI.playground/Sources/Entities.swift | 2 ++ .../Sources/OpenAPISupport.swift | 32 +++++++++++++++++++ .../JSONAPIArbitrary/Error+Arbitrary.swift | 15 +++++++++ Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 8 ++++- .../JSONAPIEntityOpenAPITests.swift | 7 ++-- 7 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 Sources/JSONAPIArbitrary/Error+Arbitrary.swift diff --git a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift index c1b6cc9..e9c018a 100644 --- a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift @@ -10,4 +10,14 @@ encoder.outputFormatting = .prettyPrinted let personSchemaData = try? encoder.encode(Person.openAPINode()) +print("Person Schema") +print("====") print(personSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed") +print("====") + +let dogDocumentSchemaData = try? encoder.encode(SingleDogDocument.openAPINodeWithExample()) + +print("Dog Document Schema") +print("====") +print(dogDocumentSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed") +print("====") diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index bd94b45..25b0235 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -11,8 +11,6 @@ Please enjoy these examples, but allow me the forced casting and the lack of err // MARK: - Create a request or response body with one Dog in it let dogFromCode = try! Dog(name: "Buddy", owner: nil) -typealias SingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> - let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none) let singleDogData = try! JSONEncoder().encode(singleDogDocument) @@ -34,8 +32,6 @@ let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)] let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])] -typealias BatchPeopleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError> - let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) } let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none) let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument) diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index 48559cf..c340862 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -139,4 +139,6 @@ public enum HouseDescription: EntityDescription { public typealias House = ExampleEntity +public typealias SingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +public typealias BatchPeopleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError> diff --git a/JSONAPI.playground/Sources/OpenAPISupport.swift b/JSONAPI.playground/Sources/OpenAPISupport.swift index 5b7e895..9a1bb9a 100644 --- a/JSONAPI.playground/Sources/OpenAPISupport.swift +++ b/JSONAPI.playground/Sources/OpenAPISupport.swift @@ -2,6 +2,8 @@ import Foundation import JSONAPI import JSONAPITesting // for the convenience of literal initialization import JSONAPIOpenAPI +import SwiftCheck +import JSONAPIArbitrary extension PersonDescription.Attributes: Sampleable { public static var sample: PersonDescription.Attributes { @@ -14,3 +16,33 @@ extension PersonDescription.Relationships: Sampleable { return .init(friends: ["1", "2"], dogs: ["2"], home: "1") } } + +extension DogDescription.Attributes: Arbitrary, Sampleable { + public static var arbitrary: Gen { + return Gen.compose { c in + return DogDescription.Attributes(name: c.generate()) + } + } + + public static var sample: DogDescription.Attributes { + return DogDescription.Attributes.arbitrary.generate + } +} + +extension DogDescription.Relationships: Arbitrary, Sampleable { + public static var arbitrary: Gen { + return Gen.compose { c in + return DogDescription.Relationships(owner: c.generate()) + } + } + + public static var sample: DogDescription.Relationships { + return DogDescription.Relationships.arbitrary.generate + } +} + +extension Document: Sampleable where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary, Error: Arbitrary, APIDescription: Arbitrary { + public static var sample: Document { + return Document.arbitrary.generate + } +} diff --git a/Sources/JSONAPIArbitrary/Error+Arbitrary.swift b/Sources/JSONAPIArbitrary/Error+Arbitrary.swift new file mode 100644 index 0000000..0369c69 --- /dev/null +++ b/Sources/JSONAPIArbitrary/Error+Arbitrary.swift @@ -0,0 +1,15 @@ +// +// Error+Arbitrary.swift +// JSONAPIArbitrary +// +// Created by Mathew Polzin on 1/21/19. +// + +import SwiftCheck +import JSONAPI + +extension UnknownJSONAPIError: Arbitrary { + public static var arbitrary: Gen { + return Gen.pure(.unknownError) + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 65ec92b..30a5338 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -267,7 +267,11 @@ public enum JSONNode: Equatable { /// into an allowed value. public let allowedValues: [AnyCodable]? - public let example: AnyCodable? + // I wanted example to be AnyCodable, but alas that causes + // runtime problems when encoding in a very strange way. + // For now, a String (which is OK by the OpenAPI spec) will + // have to do. + public let example: String? public init(format: Format, required: Bool, @@ -279,6 +283,8 @@ public enum JSONNode: Equatable { self.nullable = nullable self.allowedValues = allowedValues self.example = example + .flatMap { try? JSONEncoder().encode($0)} + .flatMap { String(data: $0, encoding: .utf8) } } /// Return the optional version of this Context diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift index 329e0fb..1fc31ae 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -117,11 +117,6 @@ class JSONAPIEntityOpenAPITests: XCTestCase { nullable: true, allowedValues: nil), .init())) - - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - let string = String(data: try! encoder.encode(node), encoding: .utf8)! - print(string) } func test_RelationshipsEntity() { @@ -230,6 +225,8 @@ class JSONAPIEntityOpenAPITests: XCTestCase { nullable: false, allowedValues: nil), manyPointerContext)) + + let tmpData = try! JSONEncoder().encode(node) } func test_AttributesAndRelationshipsEntity() { From 5433dddc81327f6cc5fb8353b7ef675845be555f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 22:47:03 -0800 Subject: [PATCH 7/9] Add support for 'const' via OpenAPI 3.0 workaround suggested by others: Use 'enum' with one value. Add Sampleable support for requesting samples representing 'success' and 'failure' if available. --- JSONAPI.playground/Sources/OpenAPISupport.swift | 8 ++++++++ .../JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift | 16 ++++++++++------ .../JSONAPIOpenAPI/OpenAPITypes+Codable.swift | 5 +++++ Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 13 ++++++++++++- Sources/JSONAPIOpenAPI/Sampleable.swift | 14 ++++++++++++++ .../JSONAPIEntityOpenAPITests.swift | 14 ++++++++------ 6 files changed, 57 insertions(+), 13 deletions(-) diff --git a/JSONAPI.playground/Sources/OpenAPISupport.swift b/JSONAPI.playground/Sources/OpenAPISupport.swift index 9a1bb9a..ac267dd 100644 --- a/JSONAPI.playground/Sources/OpenAPISupport.swift +++ b/JSONAPI.playground/Sources/OpenAPISupport.swift @@ -45,4 +45,12 @@ extension Document: Sampleable where PrimaryResourceBody: Arbitrary, IncludeType public static var sample: Document { return Document.arbitrary.generate } + + public static var successSample: Document? { + return Document.arbitraryData.generate + } + + public static var failureSample: Document? { + return Document.arbitraryErrors.generate + } } diff --git a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift index 35fd347..11934be 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift @@ -72,13 +72,14 @@ extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType } extension RelationshipType { - static func relationshipNode(nullable: Bool) -> JSONNode { + static func relationshipNode(nullable: Bool, jsonType: String) -> JSONNode { let propertiesDict: [String: JSONNode] = [ "id": .string(.init(format: .generic, required: true), .init()), "type": .string(.init(format: .generic, - required: true), + required: true, + allowedValues: [.init(jsonType)]), .init()) ] @@ -97,7 +98,7 @@ extension ToOneRelationship: OpenAPINodeType { return .object(.init(format: .generic, required: true), .init(properties: [ - "data": ToOneRelationship.relationshipNode(nullable: nullable) + "data": ToOneRelationship.relationshipNode(nullable: nullable, jsonType: Identifiable.jsonType) ])) } } @@ -111,14 +112,16 @@ extension ToManyRelationship: OpenAPINodeType { .init(properties: [ "data": .array(.init(format: .generic, required: true), - .init(items: ToManyRelationship.relationshipNode(nullable: false))) + .init(items: ToManyRelationship.relationshipNode(nullable: false, jsonType: Relatable.jsonType))) ])) } } extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { public static func openAPINode() throws -> JSONNode { - // TODO: const for json `type` + // NOTE: const for json `type` not supported by OpenAPI 3.0 + // Will use "enum" with one possible value for now. + // TODO: metadata, links let idNode = JSONNode.string(.init(format: .generic, @@ -127,7 +130,8 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc let idProperty = ("id", idNode) let typeNode = JSONNode.string(.init(format: .generic, - required: true), + required: true, + allowedValues: [.init(Entity.jsonType)]), .init()) let typeProperty = ("type", typeNode) diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift index 9d4d2cc..fa6d93b 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift @@ -13,6 +13,7 @@ extension JSONNode.Context: Encodable { case allowedValues = "enum" case nullable case example +// case constantValue = "const" } public func encode(to encoder: Encoder) throws { @@ -28,6 +29,10 @@ extension JSONNode.Context: Encodable { try container.encode(allowedValues, forKey: .allowedValues) } +// if constantValue != nil { +// try container.encode(constantValue, forKey: .constantValue) +// } + try container.encode(nullable, forKey: .nullable) if example != nil { diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 30a5338..2a65d9e 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -18,7 +18,7 @@ public protocol OpenAPINodeType { extension OpenAPINodeType where Self: Sampleable, Self: Encodable { public static func openAPINodeWithExample() throws -> JSONNode { - return try openAPINode().with(example: Self.sample) + return try openAPINode().with(example: Self.successSample ?? Self.sample) } } @@ -255,6 +255,10 @@ public enum JSONNode: Equatable { public let required: Bool public let nullable: Bool + // NOTE: "const" is supported by the newest JSON Schema spec but not + // yet by OpenAPI. Instead, will use "enum" with one possible value for now. +// public let constantValue: Format.SwiftType? + /// The OpenAPI spec calls this "enum" /// If not specified, it is assumed that any /// value of the given format is allowed. @@ -276,11 +280,13 @@ public enum JSONNode: Equatable { public init(format: Format, required: Bool, nullable: Bool = false, +// constantValue: Format.SwiftType? = nil, allowedValues: [AnyCodable]? = nil, example: AnyCodable? = nil) { self.format = format self.required = required self.nullable = nullable +// self.constantValue = constantValue self.allowedValues = allowedValues self.example = example .flatMap { try? JSONEncoder().encode($0)} @@ -292,6 +298,7 @@ public enum JSONNode: Equatable { return .init(format: format, required: false, nullable: nullable, +// constantValue: constantValue, allowedValues: allowedValues) } @@ -300,6 +307,7 @@ public enum JSONNode: Equatable { return .init(format: format, required: true, nullable: nullable, +// constantValue: constantValue, allowedValues: allowedValues) } @@ -308,6 +316,7 @@ public enum JSONNode: Equatable { return .init(format: format, required: required, nullable: true, +// constantValue: constantValue, allowedValues: allowedValues) } @@ -316,6 +325,7 @@ public enum JSONNode: Equatable { return .init(format: format, required: required, nullable: nullable, +// constantValue: constantValue, allowedValues: allowedValues) } @@ -324,6 +334,7 @@ public enum JSONNode: Equatable { return .init(format: format, required: required, nullable: nullable, +// constantValue: constantValue, allowedValues: allowedValues, example: example) } diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift index 328080e..c730791 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable.swift @@ -15,6 +15,20 @@ public protocol Sampleable { /// same value every time, or it can be an arbitrarily random /// value each time. static var sample: Self { get } + + /// Get an example of success, if that is meaningful and + /// available. If not, will be nil. + static var successSample: Self? { get } + + /// Get an example of failure, if that is meaningful and + /// available. If not, will be nil. + static var failureSample: Self? { get } +} + +public extension Sampleable { + public static var successSample: Self? { return nil } + + public static var failureSample: Self? { return nil } } extension Sampleable { diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift index 1fc31ae..abaf9d1 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -34,7 +34,8 @@ class JSONAPIEntityOpenAPITests: XCTestCase { required: true), .init())) XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic, - required: true), + required: true, + allowedValues: [.init(TestType1.jsonType)]), .init())) } @@ -62,7 +63,8 @@ class JSONAPIEntityOpenAPITests: XCTestCase { required: true), .init())) XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic, - required: true), + required: true, + allowedValues: [.init(TestType2.jsonType)]), .init())) let attributesNode = objectContext1.properties["attributes"] @@ -143,7 +145,8 @@ class JSONAPIEntityOpenAPITests: XCTestCase { required: true), .init())) XCTAssertEqual(objectContext1.properties["type"], .string(.init(format: .generic, - required: true), + required: true, + allowedValues: [.init(TestType3.jsonType)]), .init())) let relationshipsNode = objectContext1.properties["relationships"] @@ -170,7 +173,8 @@ class JSONAPIEntityOpenAPITests: XCTestCase { required: true), .init()), "type": .string(.init(format: .generic, - required: true), + required: true, + allowedValues: [.init(TestType1.jsonType)]), .init())]) let pointerContext = JSONNode.ObjectContext(properties: ["data": .object(.init(format: .generic, @@ -225,8 +229,6 @@ class JSONAPIEntityOpenAPITests: XCTestCase { nullable: false, allowedValues: nil), manyPointerContext)) - - let tmpData = try! JSONEncoder().encode(node) } func test_AttributesAndRelationshipsEntity() { From 5da4a963d440a76300faa0095e3ddf6e2ad48410 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 22:50:19 -0800 Subject: [PATCH 8/9] Update README and code documentation slightly --- README.md | 2 +- Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cba483a..6ffe3e1 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Note that Playground support for importing non-system Frameworks is still a bit - `data` - [x] Encoding/Decoding - [x] Arbitrary - - [ ] OpenAPI + - [x] OpenAPI - `included` - [x] Encoding/Decoding - [x] Arbitrary diff --git a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift index 11934be..619e296 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift @@ -91,7 +91,9 @@ extension RelationshipType { } extension ToOneRelationship: OpenAPINodeType { - // TODO: const for json `type` + // NOTE: const for json `type` not supported by OpenAPI 3.0 + // Will use "enum" with one possible value for now. + // TODO: metadata & links static public func openAPINode() throws -> JSONNode { let nullable = Identifiable.self is _Optional.Type @@ -104,7 +106,9 @@ extension ToOneRelationship: OpenAPINodeType { } extension ToManyRelationship: OpenAPINodeType { - // TODO: const for json `type` + // NOTE: const for json `type` not supported by OpenAPI 3.0 + // Will use "enum" with one possible value for now. + // TODO: metadata & links static public func openAPINode() throws -> JSONNode { return .object(.init(format: .generic, From 744e08acf4aabc803db849ba055fe8feb597e65a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Jan 2019 23:00:13 -0800 Subject: [PATCH 9/9] Update linuxmain --- Tests/JSONAPIOpenAPITests/XCTestManifests.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/JSONAPIOpenAPITests/XCTestManifests.swift b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift index a2dcf48..8f7c32b 100644 --- a/Tests/JSONAPIOpenAPITests/XCTestManifests.swift +++ b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift @@ -30,6 +30,12 @@ extension JSONAPIAttributeOpenAPITests { ] } +extension JSONAPIDocumentOpenAPITests { + static let __allTests = [ + ("test_SingleResourceDocument", test_SingleResourceDocument), + ] +} + extension JSONAPIEntityOpenAPITests { static let __allTests = [ ("test_AttributesAndRelationshipsEntity", test_AttributesAndRelationshipsEntity), @@ -54,6 +60,7 @@ extension JSONAPIRelationshipsOpenAPITests { public func __allTests() -> [XCTestCaseEntry] { return [ testCase(JSONAPIAttributeOpenAPITests.__allTests), + testCase(JSONAPIDocumentOpenAPITests.__allTests), testCase(JSONAPIEntityOpenAPITests.__allTests), testCase(JSONAPIRelationshipsOpenAPITests.__allTests), ]