diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 636184a..6afd065 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -24,8 +24,10 @@ extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { } extension Attribute: RawOpenAPINodeType where RawValue: RawRepresentable, RawValue.RawValue: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { - + static public func rawOpenAPINode() throws -> 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 try !RawValue.RawValue.openAPINode().required { return try RawValue.RawValue.openAPINode().requiredNode().nullableNode() } @@ -33,6 +35,18 @@ extension Attribute: RawOpenAPINodeType where RawValue: RawRepresentable, RawVal } } +extension Attribute: WrappedRawOpenAPIType where RawValue: RawOpenAPINodeType { + public static func wrappedOpenAPINode() throws -> 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 try !RawValue.rawOpenAPINode().required { + return try RawValue.rawOpenAPINode().requiredNode().nullableNode() + } + return try RawValue.rawOpenAPINode() + } +} + extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable, RawValue: Codable { public static var allCases: [AnyCodable] { return (try? allCases(from: Array(RawValue.allCases))) ?? [] diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 5545e36..79bf009 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -23,7 +23,30 @@ public protocol OpenAPINodeType { /// different schema. The "different" conditions have to do /// with Raw Representability, hence the name of this protocol. public protocol RawOpenAPINodeType { - static func openAPINode() throws -> JSONNode + static func rawOpenAPINode() throws -> JSONNode +} + +/// Anything conforming to `RawOpenAPINodeType` can provide an +/// OpenAPI schema representing itself. This third protocol is +/// necessary so that one type can conditionally provide a +/// schema and then (under different conditions) provide a +/// different schema. The "different" conditions have to do +/// with Optionality, hence the name of this protocol. +public protocol WrappedRawOpenAPIType { + static func wrappedOpenAPINode() throws -> JSONNode +} + +/// Anything conforming to `RawOpenAPINodeType` can provide an +/// OpenAPI schema representing itself. This third protocol is +/// necessary so that one type can conditionally provide a +/// schema and then (under different conditions) provide a +/// different schema. The "different" conditions have to do +/// with Optionality, hence the name of this protocol. +public protocol DoubleWrappedRawOpenAPIType { + // NOTE: This is definitely a rabbit hole... hopefully I + // will realize I've been missing something obvious + // and dig my way back out at some point... + static func wrappedOpenAPINode() throws -> JSONNode } /// Anything conforming to `AnyJSONCaseIterable` can provide a @@ -209,7 +232,7 @@ public enum JSONTypeFormat: Equatable { /// A JSON Node is what OpenAPI calls a /// "Schema Object" -public enum JSONNode { +public enum JSONNode: Equatable { case boolean(Context) indirect case object(Context, ObjectContext) indirect case array(Context, ArrayContext) @@ -281,7 +304,7 @@ public enum JSONNode { } } - public struct NumericContext { + public struct NumericContext: Equatable { /// A numeric instance is valid only if division by this keyword's value results in an integer. Defaults to nil. public let multipleOf: Double? public let maximum: Double? @@ -302,7 +325,7 @@ public enum JSONNode { } } - public struct StringContext { + public struct StringContext: Equatable { public let maxLength: Int? public let minLength: Int @@ -318,7 +341,7 @@ public enum JSONNode { } } - public struct ArrayContext { + public struct ArrayContext: Equatable { /// A JSON Type Node that describes /// the type of each element in the array. public let items: JSONNode @@ -346,7 +369,7 @@ public enum JSONNode { } } - public struct ObjectContext { + public struct ObjectContext: Equatable { public let maxProperties: Int? public let minProperties: Int public let properties: [String: JSONNode] diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift index c7fa194..8302ae7 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable.swift @@ -38,7 +38,10 @@ extension Sampleable { return try valType.openAPINode() case let valType as RawOpenAPINodeType.Type: - return try valType.openAPINode() + return try valType.rawOpenAPINode() + + case let valType as WrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() default: return nil diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index 1b09ab8..ae03f6a 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -38,11 +38,23 @@ extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { } extension Optional: RawOpenAPINodeType where Wrapped: RawRepresentable, Wrapped.RawValue: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func rawOpenAPINode() throws -> JSONNode { return try Wrapped.RawValue.openAPINode().optionalNode() } } +extension Optional: WrappedRawOpenAPIType where Wrapped: RawOpenAPINodeType { + static public func wrappedOpenAPINode() throws -> JSONNode { + return try Wrapped.rawOpenAPINode().optionalNode() + } +} + +extension Optional: DoubleWrappedRawOpenAPIType where Wrapped: WrappedRawOpenAPIType { + static public func wrappedOpenAPINode() throws -> JSONNode { + return try Wrapped.wrappedOpenAPINode().optionalNode() + } +} + extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable, Wrapped: Codable { public static var allCases: [AnyCodable] { return (try? allCases(from: Array(Wrapped.allCases))) ?? [] diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift new file mode 100644 index 0000000..726f99a --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift @@ -0,0 +1,513 @@ +// +// JSONAPIAttributeOpenAPITests.swift +// JSONAPIOpenAPITests +// +// Created by Mathew Polzin on 1/20/19. +// + +import XCTest +import JSONAPI +import JSONAPIOpenAPI +import AnyCodable + +class JSONAPIAttributeOpenAPITests: XCTestCase { +} + +// MARK: - Boolean +extension JSONAPIAttributeOpenAPITests { + func test_BooleanAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .boolean(.generic)) + + guard case .boolean(let contextA) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + } + + func test_NullableBooleanAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .boolean(.generic)) + + guard case .boolean(let contextA) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: true, + allowedValues: nil)) + } + + func test_OptionalBooleanAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .boolean(.generic)) + + guard case .boolean(let contextA) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + } + + func test_OptionalNullableBooleanAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .boolean(.generic)) + + guard case .boolean(let contextA) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: true, + allowedValues: nil)) + } +} + +// MARK: - Array of Strings +extension JSONAPIAttributeOpenAPITests { + func test_Arrayttribute() { + let node = try! Attribute<[String]>.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .array(.generic)) + + guard case .array(let contextA, let arrayContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + let stringNode = JSONNode.string(.init(format: .generic, + required: true), + .init()) + + XCTAssertEqual(arrayContext, .init(items: stringNode)) + } + + func test_NullableArrayAttribute() { + let node = try! Attribute<[String]?>.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .array(.generic)) + + guard case .array(let contextA, let arrayContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: true, + allowedValues: nil)) + + let stringNode = JSONNode.string(.init(format: .generic, + required: true), + .init()) + + XCTAssertEqual(arrayContext, .init(items: stringNode)) + } + + func test_OptionalArrayAttribute() { + let node = try! Attribute<[String]>?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .array(.generic)) + + guard case .array(let contextA, let arrayContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + + let stringNode = JSONNode.string(.init(format: .generic, + required: true), + .init()) + + XCTAssertEqual(arrayContext, .init(items: stringNode)) + } + + func test_OptionalNullableArrayAttribute() { + let node = try! Attribute<[String]?>?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .array(.generic)) + + guard case .array(let contextA, let arrayContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: true, + allowedValues: nil)) + + let stringNode = JSONNode.string(.init(format: .generic, + required: true), + .init()) + + XCTAssertEqual(arrayContext, .init(items: stringNode)) + } +} + +// MARK: - Number +extension JSONAPIAttributeOpenAPITests { + func test_NumberAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .number(.double)) + + guard case .number(let contextA, let numberContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .double, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(numberContext, .init()) + } + + func test_NullableNumberAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .number(.double)) + + guard case .number(let contextA, let numberContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .double, + required: true, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(numberContext, .init()) + } + + func test_OptionalNumberAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .number(.double)) + + guard case .number(let contextA, let numberContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .double, + required: false, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(numberContext, .init()) + } + + func test_OptionalNullableNumberAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .number(.double)) + + guard case .number(let contextA, let numberContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .double, + required: false, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(numberContext, .init()) + } + + func test_FloatNumberAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertEqual(node.jsonTypeFormat, .number(.float)) + } +} + +// MARK: - Integer +extension JSONAPIAttributeOpenAPITests { + func test_IntegerAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .integer(.generic)) + + guard case .integer(let contextA, let intContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(intContext, .init()) + } + + func test_NullableIntegerAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .integer(.generic)) + + guard case .integer(let contextA, let intContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(intContext, .init()) + } + + func test_OptionalIntegerAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .integer(.generic)) + + guard case .integer(let contextA, let intContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(intContext, .init()) + } + + func test_OptionalNullableIntegerAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .integer(.generic)) + + guard case .integer(let contextA, let intContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(intContext, .init()) + } +} + +// MARK: - String +extension JSONAPIAttributeOpenAPITests { + func test_StringAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_NullableStringAttribute() { + let node = try! Attribute.openAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_OptionalStringAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_OptionalNullableStringAttribute() { + let node = try! Attribute?.openAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } +} + +// MARK: - Enum +// NOTE: `enum` Attributes only gain the automatic support for allowed values +// (`enum` property in the OpenAPI Spec) at the Entity scope. These attributes +// will all still have `allowedValues: nil` at the attribute scope. +extension JSONAPIAttributeOpenAPITests { + func test_EnumAttribute() { + let node = try! Attribute.rawOpenAPINode() + print(EnumAttribute.allCases) + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_NullableEnumAttribute() { + let node = try! Attribute.wrappedOpenAPINode() + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: true, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_OptionalEnumAttribute() { + let node = try! Attribute?.wrappedOpenAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_OptionalNullableEnumAttribute() { + let node = try! Attribute?.wrappedOpenAPINode() + + XCTAssertFalse(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .generic, + required: false, + nullable: true, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } +} + +// MARK: - Test Types +extension JSONAPIAttributeOpenAPITests { + enum EnumAttribute: String, Codable, CaseIterable { + case one + case two + } +} diff --git a/Tests/JSONAPIOpenAPITests/XCTestManifests.swift b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift new file mode 100644 index 0000000..b05e1af --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift @@ -0,0 +1,59 @@ +import XCTest + +extension JSONAPIAttributeOpenAPITests { + static let __allTests = [ + ("test_Arrayttribute", test_Arrayttribute), + ("test_BooleanAttribute", test_BooleanAttribute), + ("test_EnumAttribute", test_EnumAttribute), + ("test_FloatNumberAttribute", test_FloatNumberAttribute), + ("test_IntegerAttribute", test_IntegerAttribute), + ("test_NullableArrayAttribute", test_NullableArrayAttribute), + ("test_NullableBooleanAttribute", test_NullableBooleanAttribute), + ("test_NullableEnumAttribute", test_NullableEnumAttribute), + ("test_NullableIntegerAttribute", test_NullableIntegerAttribute), + ("test_NullableNumberAttribute", test_NullableNumberAttribute), + ("test_NullableStringAttribute", test_NullableStringAttribute), + ("test_NumberAttribute", test_NumberAttribute), + ("test_OptionalArrayAttribute", test_OptionalArrayAttribute), + ("test_OptionalBooleanAttribute", test_OptionalBooleanAttribute), + ("test_OptionalEnumAttribute", test_OptionalEnumAttribute), + ("test_OptionalIntegerAttribute", test_OptionalIntegerAttribute), + ("test_OptionalNullableArrayAttribute", test_OptionalNullableArrayAttribute), + ("test_OptionalNullableBooleanAttribute", test_OptionalNullableBooleanAttribute), + ("test_OptionalNullableEnumAttribute", test_OptionalNullableEnumAttribute), + ("test_OptionalNullableIntegerAttribute", test_OptionalNullableIntegerAttribute), + ("test_OptionalNullableNumberAttribute", test_OptionalNullableNumberAttribute), + ("test_OptionalNullableStringAttribute", test_OptionalNullableStringAttribute), + ("test_OptionalNumberAttribute", test_OptionalNumberAttribute), + ("test_OptionalStringAttribute", test_OptionalStringAttribute), + ("test_StringAttribute", test_StringAttribute), + ] +} + +extension JSONAPIEntityOpenAPITests { + static let __allTests = [ + ("test_AttributesEntity", test_AttributesEntity), + ("test_EmptyEntity", test_EmptyEntity), + ] +} + +extension JSONAPIRelationshipsOpenAPITests { + static let __allTests = [ + ("test_NullableToOne", test_NullableToOne), + ("test_OptionalNullableToOne", test_OptionalNullableToOne), + ("test_OptionalToMany", test_OptionalToMany), + ("test_OptionalToOne", test_OptionalToOne), + ("test_ToMany", test_ToMany), + ("test_ToOne", test_ToOne), + ] +} + +#if !os(macOS) +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(JSONAPIAttributeOpenAPITests.__allTests), + testCase(JSONAPIEntityOpenAPITests.__allTests), + testCase(JSONAPIRelationshipsOpenAPITests.__allTests), + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index ea07a7d..5741ebc 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,10 +1,12 @@ import XCTest import JSONAPITests -import JSONAPITestLibTests +import JSONAPITestingTests +import JSONAPIOpenAPITests var tests = [XCTestCaseEntry]() tests += JSONAPITests.__allTests() -tests += JSONAPITestLibTests.__allTests() +tests += JSONAPITestingTests.__allTests() +tests += JSONAPIOpenAPITests.__allTests() XCTMain(tests)