diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift index 6f7f966..c7afff0 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift @@ -8,9 +8,9 @@ import JSONAPI import Foundation -extension Includes: OpenAPINodeType where I: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { - let includeNode = try I.openAPINode() +extension Includes: OpenAPIEncodedNodeType where I: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + let includeNode = try I.openAPINode(using: encoder) return .array(.init(format: .generic, required: true), @@ -19,114 +19,114 @@ extension Includes: OpenAPINodeType where I: OpenAPINodeType { } } -extension Include0: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include0: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { throw OpenAPITypeError.invalidNode } } -extension Include1: OpenAPINodeType where A: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { - return try .one(of: [A.openAPINode()]) +extension Include1: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try A.openAPINode(using: encoder) } } -extension Include2: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include2: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder) ]) } } -extension Include3: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include3: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder) ]) } } -extension Include4: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include4: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder) ]) } } -extension Include5: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include5: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder) ]) } } -extension Include6: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include6: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder) ]) } } -extension Include7: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include7: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode(), - G.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder), + G.openAPINode(using: encoder) ]) } } -extension Include8: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include8: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType, H: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode(), - G.openAPINode(), - H.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder), + G.openAPINode(using: encoder), + H.openAPINode(using: encoder) ]) } } -extension Include9: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType, I: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { +extension Include9: OpenAPIEncodedNodeType where A: OpenAPIEncodedNodeType, B: OpenAPIEncodedNodeType, C: OpenAPIEncodedNodeType, D: OpenAPIEncodedNodeType, E: OpenAPIEncodedNodeType, F: OpenAPIEncodedNodeType, G: OpenAPIEncodedNodeType, H: OpenAPIEncodedNodeType, I: OpenAPIEncodedNodeType { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode(), - G.openAPINode(), - H.openAPINode(), - I.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder), + G.openAPINode(using: encoder), + H.openAPINode(using: encoder), + I.openAPINode(using: encoder) ]) } } diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index f9c411a..538a7e6 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -213,7 +213,7 @@ extension ManyResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedN } } -extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType { +extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPIEncodedNodeType { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { // TODO: metadata, links, api description, errors // TODO: represent data and errors as the two distinct possible outcomes @@ -224,7 +224,7 @@ extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEnc let includeNode: JSONNode? do { - includeNode = try Includes.openAPINode() + includeNode = try Includes.openAPINode(using: encoder) } catch let err as OpenAPITypeError { guard case .invalidNode = err else { throw err diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift index b65d8e5..f961fc5 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift @@ -24,8 +24,6 @@ class JSONAPIDocumentOpenAPITests: XCTestCase { let node = try! SingleEntityDocument.openAPINodeWithExample(using: encoder) - print(String(data: try! encoder.encode(node), encoding: .utf8)!) - XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -76,8 +74,6 @@ class JSONAPIDocumentOpenAPITests: XCTestCase { let node = try! ManyEntityDocument.openAPINodeWithExample(using: encoder) - print(String(data: try! encoder.encode(node), encoding: .utf8)!) - XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -126,6 +122,192 @@ class JSONAPIDocumentOpenAPITests: XCTestCase { allowedValues: [.init("test")]), .init())) } + + func test_DocumentWithOneIncludeType() { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! DocumentWithIncludes.openAPINodeWithExample(using: encoder) + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case let .object(contextA, objectContext1) = node else { + XCTFail("Expected JSON Document to be an Object Node") + return + } + + XCTAssertNotNil(contextA.example) + XCTAssertFalse(contextA.nullable) + XCTAssertEqual(contextA.format, .generic) + XCTAssertTrue(contextA.required) + + XCTAssertEqual(objectContext1.minProperties, 2) + XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data", "included"])) + XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data", "included"])) + + guard case let .object(contextB, objectContext2)? = objectContext1.properties["data"] else { + XCTFail("Expected Data field of JSON Document to be an Object Node") + return + } + + XCTAssertFalse(contextB.nullable) + XCTAssertEqual(contextB.format, .generic) + XCTAssertTrue(contextB.required) + + XCTAssertEqual(objectContext2.minProperties, 3) + XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"])) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"])) + + XCTAssertEqual(objectContext2.properties["type"], + JSONNode.string(.init(format: .generic, + required: true, + allowedValues: [.init("test")]), + .init())) + + guard case let .array(contextC, arrayContext)? = objectContext1.properties["included"] else { + XCTFail("Expected Includes field of JSON Document to be an Array Node") + return + } + + XCTAssertFalse(contextC.nullable) + XCTAssertEqual(contextC.format, .generic) + XCTAssertTrue(contextC.required) + + XCTAssertTrue(arrayContext.uniqueItems) + XCTAssertEqual(arrayContext.minItems, 0) + + guard case let .object(contextD, objectContext3) = arrayContext.items else { + XCTFail("Expected Items of Array under Data to be an Object Node") + return + } + + XCTAssertFalse(contextD.nullable) + XCTAssertEqual(contextD.format, .generic) + XCTAssertTrue(contextD.required) + + XCTAssertEqual(objectContext3.minProperties, 3) + XCTAssertEqual(Set(objectContext3.requiredProperties), Set(["id", "attributes", "type"])) + XCTAssertEqual(Set(objectContext3.properties.keys), Set(["id", "attributes", "type"])) + + XCTAssertEqual(objectContext3.properties["type"], + JSONNode.string(.init(format: .generic, + required: true, + allowedValues: [.init("test")]), + .init())) + } + + func test_DocumentWithTwoIncludeTypes() { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! DocumentWithMultipleTypesOfIncludes.openAPINodeWithExample(using: encoder) + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case let .object(contextA, objectContext1) = node else { + XCTFail("Expected JSON Document to be an Object Node") + return + } + + XCTAssertNotNil(contextA.example) + XCTAssertFalse(contextA.nullable) + XCTAssertEqual(contextA.format, .generic) + XCTAssertTrue(contextA.required) + + XCTAssertEqual(objectContext1.minProperties, 2) + XCTAssertEqual(Set(objectContext1.requiredProperties), Set(["data", "included"])) + XCTAssertEqual(Set(objectContext1.properties.keys), Set(["data", "included"])) + + guard case let .object(contextB, objectContext2)? = objectContext1.properties["data"] else { + XCTFail("Expected Data field of JSON Document to be an Object Node") + return + } + + XCTAssertFalse(contextB.nullable) + XCTAssertEqual(contextB.format, .generic) + XCTAssertTrue(contextB.required) + + XCTAssertEqual(objectContext2.minProperties, 3) + XCTAssertEqual(Set(objectContext2.requiredProperties), Set(["id", "attributes", "type"])) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "attributes", "type"])) + + XCTAssertEqual(objectContext2.properties["type"], + JSONNode.string(.init(format: .generic, + required: true, + allowedValues: [.init("test")]), + .init())) + + guard case let .array(contextC, arrayContext)? = objectContext1.properties["included"] else { + XCTFail("Expected Includes field of JSON Document to be an Array Node") + return + } + + XCTAssertFalse(contextC.nullable) + XCTAssertEqual(contextC.format, .generic) + XCTAssertTrue(contextC.required) + + XCTAssertTrue(arrayContext.uniqueItems) + XCTAssertEqual(arrayContext.minItems, 0) + + guard case let .one(of: includeNodes) = arrayContext.items else { + XCTFail("Expected Included to contain multiple types of items.") + return + } + + XCTAssertEqual(includeNodes.count, 2) + + guard case let .object(contextD, objectContext3) = includeNodes[0] else { + XCTFail("Expected Items of OneOf under Array under Data to be an Object Node") + return + } + + XCTAssertFalse(contextD.nullable) + XCTAssertEqual(contextD.format, .generic) + XCTAssertTrue(contextD.required) + + XCTAssertEqual(objectContext3.minProperties, 3) + XCTAssertEqual(Set(objectContext3.requiredProperties), Set(["id", "attributes", "type"])) + XCTAssertEqual(Set(objectContext3.properties.keys), Set(["id", "attributes", "type"])) + + XCTAssertEqual(objectContext3.properties["type"], + JSONNode.string(.init(format: .generic, + required: true, + allowedValues: [.init("test")]), + .init())) + + guard case let .object(contextE, objectContext4) = includeNodes[1] else { + XCTFail("Expected Items of OneOf under Array under Data to be an Object Node") + return + } + + XCTAssertFalse(contextE.nullable) + XCTAssertEqual(contextE.format, .generic) + XCTAssertTrue(contextE.required) + + XCTAssertEqual(objectContext4.minProperties, 2) + XCTAssertEqual(Set(objectContext4.requiredProperties), Set(["id", "type"])) + XCTAssertEqual(Set(objectContext4.properties.keys), Set(["id", "type"])) + + XCTAssertEqual(objectContext4.properties["type"], + JSONNode.string(.init(format: .generic, + required: true, + allowedValues: [.init("test2")]), + .init())) + } } // MARK: - Test Types @@ -151,6 +333,20 @@ extension JSONAPIDocumentOpenAPITests { typealias SingleEntityDocument = Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> typealias ManyEntityDocument = Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> + + typealias DocumentWithIncludes = Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + enum TestEntityDescription2: EntityDescription { + static var jsonType: String { return "test2" } + + typealias Attributes = NoAttributes + + typealias Relationships = NoRelationships + } + + typealias TestEntity2 = BasicEntity + + typealias DocumentWithMultipleTypesOfIncludes = Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError> } extension Id: Sampleable where RawType == String {