diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 6cb2923..33b7c81 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -7,21 +7,33 @@ import JSONAPI +private protocol _Optional {} +extension Optional: _Optional {} + extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { static public var openAPINode: OpenAPI.JSONNode { + // If the RawValue is not required, we actually consider it + // nullable. To be not required is for the Attribute itself + // to be optional. + if !RawValue.openAPINode.required { + return RawValue.openAPINode.requiredNode().nullableNode() + } return RawValue.openAPINode } } extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { static public var openAPINode: OpenAPI.JSONNode { + // If the RawValue is not required, we actually consider it + // nullable. To be not required is for the Attribute itself + // to be optional. + if !RawValue.openAPINode.required { + return RawValue.openAPINode.requiredNode().nullableNode() + } return RawValue.openAPINode } } -private protocol _Optional {} -extension Optional: _Optional {} - extension ToOneRelationship: OpenAPINodeType { // TODO: const for json `type` static public var openAPINode: OpenAPI.JSONNode { diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 2ccb7c6..87271ae 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -12,7 +12,7 @@ public protocol OpenAPINodeType { } public protocol SwiftTyped { - associatedtype SwiftType: Codable + associatedtype SwiftType: Codable, Equatable } public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable { @@ -173,7 +173,7 @@ public extension OpenAPI.JSONTypeFormat { } extension OpenAPI.JSONNode { - public struct Context: JSONNodeContext { + public struct Context: JSONNodeContext, Equatable { public let format: Format public let required: Bool public let nullable: Bool @@ -197,6 +197,7 @@ extension OpenAPI.JSONNode { public func optionalContext() -> Context { return .init(format: format, required: false, + nullable: nullable, allowedValues: allowedValues) } @@ -204,6 +205,15 @@ extension OpenAPI.JSONNode { public func requiredContext() -> Context { return .init(format: format, required: true, + nullable: nullable, + allowedValues: allowedValues) + } + + /// Return the nullable version of this context + public func nullableContext() -> Context { + return .init(format: format, + required: required, + nullable: true, allowedValues: allowedValues) } } @@ -369,4 +379,24 @@ extension OpenAPI.JSONNode { return self } } + + /// Return the nullable version of this JSONNode + public func nullableNode() -> OpenAPI.JSONNode { + switch self { + case .boolean(let context): + return .boolean(context.nullableContext()) + case .object(let contextA, let contextB): + return .object(contextA.nullableContext(), contextB) + case .array(let contextA, let contextB): + return .array(contextA.nullableContext(), contextB) + case .number(let context, let contextB): + return .number(context.nullableContext(), contextB) + case .integer(let context, let contextB): + return .integer(context.nullableContext(), contextB) + case .string(let context, let contextB): + return .string(context.nullableContext(), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self + } + } } diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift index 44d699c..019af16 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift @@ -14,11 +14,221 @@ import JSONAPIOpenAPI class JSONAPIRelationshipsOpenAPITests: XCTestCase { func test_ToOne() { + let node = ToOneRelationship.openAPINode + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case .object(let contextA, let objectContext1) = node else { + XCTFail("Expected object Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext1.additionalProperties) + XCTAssertEqual(Array(objectContext1.properties.keys), ["data"]) + + guard case .object(let contextB, let objectContext2)? = objectContext1.properties["data"] else { + XCTFail("Expected object node within properties") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext2.additionalProperties) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "type"])) + } + + func test_OptionalToOne() { + let node = ToOneRelationship?.openAPINode + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case .object(let contextA, let objectContext1) = node else { + XCTFail("Expected object Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext1.additionalProperties) + XCTAssertEqual(Array(objectContext1.properties.keys), ["data"]) + + guard case .object(let contextB, let objectContext2)? = objectContext1.properties["data"] else { + XCTFail("Expected object node within properties") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext2.additionalProperties) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "type"])) + } + + func test_NullableToOne() { let node = ToOneRelationship.openAPINode - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - print(String(data: (try? encoder.encode(node))!, encoding: .utf8)!) + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case .object(let contextA, let objectContext1) = node else { + XCTFail("Expected object Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext1.additionalProperties) + XCTAssertEqual(Array(objectContext1.properties.keys), ["data"]) + + guard case .object(let contextB, let objectContext2)? = objectContext1.properties["data"] else { + XCTFail("Expected object node within properties") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: true, + allowedValues: nil)) + + XCTAssertNil(objectContext2.additionalProperties) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "type"])) + } + + func test_OptionalNullableToOne() { + let node = ToOneRelationship?.openAPINode + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case .object(let contextA, let objectContext1) = node else { + XCTFail("Expected object Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext1.additionalProperties) + XCTAssertEqual(Array(objectContext1.properties.keys), ["data"]) + + guard case .object(let contextB, let objectContext2)? = objectContext1.properties["data"] else { + XCTFail("Expected object node within properties") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: true, + allowedValues: nil)) + + XCTAssertNil(objectContext2.additionalProperties) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "type"])) + } + + func test_ToMany() { + let node = ToManyRelationship.openAPINode + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case .object(let contextA, let objectContext1) = node else { + XCTFail("Expected object Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext1.additionalProperties) + XCTAssertEqual(Array(objectContext1.properties.keys), ["data"]) + + guard case .array(let contextB, let arrayContext)? = objectContext1.properties["data"] else { + XCTFail("Expected array node within properties") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + guard case .object(let contextC, let objectContext2) = arrayContext.items else { + XCTFail("Expected object node within items") + return + } + + XCTAssertEqual(contextC, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext2.additionalProperties) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "type"])) + } + + func test_OptionalToMany() { + let node = ToManyRelationship?.openAPINode + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) + + guard case .object(let contextA, let objectContext1) = node else { + XCTFail("Expected object Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext1.additionalProperties) + XCTAssertEqual(Array(objectContext1.properties.keys), ["data"]) + + guard case .array(let contextB, let arrayContext)? = objectContext1.properties["data"] else { + XCTFail("Expected array node within properties") + return + } + + XCTAssertEqual(contextB, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + guard case .object(let contextC, let objectContext2) = arrayContext.items else { + XCTFail("Expected object node within items") + return + } + + XCTAssertEqual(contextC, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertNil(objectContext2.additionalProperties) + XCTAssertEqual(Set(objectContext2.properties.keys), Set(["id", "type"])) } }