From 14a0256885dabbb4aee3a0d356c3bd3b4b0cc5c6 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 13 Jan 2019 17:27:10 -0800 Subject: [PATCH 01/22] Add new targets --- Package.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 2797017..c8a1145 100644 --- a/Package.swift +++ b/Package.swift @@ -9,9 +9,12 @@ let package = Package( .library( name: "JSONAPI", targets: ["JSONAPI"]), - .library( - name: "JSONAPITestLib", - targets: ["JSONAPITestLib"]) + .library( + name: "JSONAPITesting", + targets: ["JSONAPITestLib"]), + .library( + name: "JSONAPIOpenAPI", + targets: ["JSONAPIOpenAPI"]) ], dependencies: [ .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")) @@ -23,12 +26,18 @@ let package = Package( .target( name: "JSONAPITestLib", dependencies: ["JSONAPI"]), + .target( + name: "JSONAPIOpenAPI", + dependencies: ["JSONAPI"]), .testTarget( name: "JSONAPITests", - dependencies: ["JSONAPI", "JSONAPITestLib"]), + dependencies: ["JSONAPI", "JSONAPITesting"]), .testTarget( name: "JSONAPITestLibTests", - dependencies: ["JSONAPI", "JSONAPITestLib"]) + dependencies: ["JSONAPI", "JSONAPITesting"]), + .testTarget( + name: "JSONAPIOpenAPITests", + dependencies: ["JSONAPI", "JSONAPIOpenAPI"]) ], swiftLanguageVersions: [.v4_2] ) From 25bda795a7205af6c79102d2a8af6377fd83e042 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 13 Jan 2019 18:28:41 -0800 Subject: [PATCH 02/22] Rename JSONAPITestLib to JSONAPITesting to fit with naming convention I am adopting going forward. Added OpenAPI JSON schema types. Added default schema types for a handful of Swift primitives. --- Package.resolved | 2 +- Package.swift | 6 +- Sources/JSONAPIOpenAPI/OpenAPI.swift | 8 ++ Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 82 +++++++++++++++++++ .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 78 ++++++++++++++++++ .../Attribute+Literal.swift | 0 .../EntityCheck.swift | 0 .../Id+Literal.swift | 0 .../Optional+Literal.swift | 0 .../Relationship+Literal.swift | 0 .../Attribute+LiteralTests.swift | 0 .../EntityCheckTests.swift | 0 .../Id+LiteralTests.swift | 0 .../Relationship+LiteralTests.swift | 0 .../Test Helpers/EntityTestTypes.swift | 0 .../String+CreatableRawIdType.swift | 0 .../XCTestManifests.swift | 0 17 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 Sources/JSONAPIOpenAPI/OpenAPI.swift create mode 100644 Sources/JSONAPIOpenAPI/OpenAPITypes.swift create mode 100644 Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift rename Sources/{JSONAPITestLib => JSONAPITesting}/Attribute+Literal.swift (100%) rename Sources/{JSONAPITestLib => JSONAPITesting}/EntityCheck.swift (100%) rename Sources/{JSONAPITestLib => JSONAPITesting}/Id+Literal.swift (100%) rename Sources/{JSONAPITestLib => JSONAPITesting}/Optional+Literal.swift (100%) rename Sources/{JSONAPITestLib => JSONAPITesting}/Relationship+Literal.swift (100%) rename Tests/{JSONAPITestLibTests => JSONAPITestingTests}/Attribute+LiteralTests.swift (100%) rename Tests/{JSONAPITestLibTests => JSONAPITestingTests}/EntityCheckTests.swift (100%) rename Tests/{JSONAPITestLibTests => JSONAPITestingTests}/Id+LiteralTests.swift (100%) rename Tests/{JSONAPITestLibTests => JSONAPITestingTests}/Relationship+LiteralTests.swift (100%) rename Tests/{JSONAPITestLibTests => JSONAPITestingTests}/Test Helpers/EntityTestTypes.swift (100%) rename Tests/{JSONAPITestLibTests => JSONAPITestingTests}/Test Helpers/String+CreatableRawIdType.swift (100%) rename Tests/{JSONAPITestLibTests => JSONAPITestingTests}/XCTestManifests.swift (100%) diff --git a/Package.resolved b/Package.resolved index e498677..85b6784 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,7 +6,7 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": "master", - "revision": "e03e896e23315525702334cfb552bb947d085ae5", + "revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07", "version": null } } diff --git a/Package.swift b/Package.swift index c8a1145..3278ecf 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( targets: ["JSONAPI"]), .library( name: "JSONAPITesting", - targets: ["JSONAPITestLib"]), + targets: ["JSONAPITesting"]), .library( name: "JSONAPIOpenAPI", targets: ["JSONAPIOpenAPI"]) @@ -24,7 +24,7 @@ let package = Package( name: "JSONAPI", dependencies: ["Poly"]), .target( - name: "JSONAPITestLib", + name: "JSONAPITesting", dependencies: ["JSONAPI"]), .target( name: "JSONAPIOpenAPI", @@ -33,7 +33,7 @@ let package = Package( name: "JSONAPITests", dependencies: ["JSONAPI", "JSONAPITesting"]), .testTarget( - name: "JSONAPITestLibTests", + name: "JSONAPITestingTests", dependencies: ["JSONAPI", "JSONAPITesting"]), .testTarget( name: "JSONAPIOpenAPITests", diff --git a/Sources/JSONAPIOpenAPI/OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI.swift new file mode 100644 index 0000000..7e89f04 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/OpenAPI.swift @@ -0,0 +1,8 @@ +// +// OpenAPI.swift +// JSONAPI +// +// Created by Mathew Polzin on 1/13/19. +// + +public enum OpenAPI {} diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift new file mode 100644 index 0000000..efd8aea --- /dev/null +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -0,0 +1,82 @@ +// +// OpenAPITypes.swift +// JSONAPI +// +// Created by Mathew Polzin on 1/13/19. +// + +public protocol OpenAPITyped { + var openAPIType: OpenAPI.JSONTypeFormat { get } +} + +public extension OpenAPI { + enum JSONType: String { + case boolean = "boolean" + case object = "object" + case array = "array" + case number = "number" + case integer = "integer" + case string = "string" + } + + enum JSONTypeFormat: Equatable { + case boolean(BooleanFormat) + case object(ObjectFormat) + case array(ArrayFormat) + case number(NumberFormat) + case integer(IntegerFormat) + case string(StringFormat) + } +} + +public extension OpenAPI.JSONTypeFormat { + public enum BooleanFormat: String, Equatable, Codable { + case generic = "" + } + + public enum ObjectFormat: String, Equatable, Codable { + case generic = "" + } + + public enum ArrayFormat: String, Equatable, Codable { + case generic = "" + } + + public enum NumberFormat: String, Equatable, Codable { + case generic = "" + case float = "float" + case double = "double" + } + + public enum IntegerFormat: String, Equatable, Codable { + case generic = "" + case int32 = "int32" + case int64 = "int64" + } + + public enum StringFormat: String, Equatable, Codable { + case generic = "" + case byte = "byte" + case binary = "binary" + case date = "date" + case dateTime = "date-time" + case password = "password" + } + + public var type: OpenAPI.JSONType { + switch self { + case .boolean: + return .boolean + case .object: + return .object + case .array: + return .array + case .number: + return .number + case .integer: + return .integer + case .string: + return .string + } + } +} diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift new file mode 100644 index 0000000..2da0222 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -0,0 +1,78 @@ +// +// PrimitiveTypes.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 01/13/19. +// + +/** + +Notable omissions in this library's default offerings: + +Base 64 encoded characters: +.string(.byte) + +Any sequence of octets: +.string(.binary) + +RFC3339 full-date: +.string(.date) + +RFC3339 date-time: +.string(.dateTime) + +A hint to UIs to obscure input: +.string(.password) + +An object: +.object(.generic) + +**/ + +extension String: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .string(.generic) + } +} + +extension Bool: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .boolean(.generic) + } +} + +extension Array: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .array(.generic) + } +} + +extension Double: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .number(.double) + } +} + +extension Float: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .number(.float) + } +} + +extension Int: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .integer(.generic) + } +} + +extension Int32: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .integer(.int32) + } +} + +extension Int64: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .integer(.int64) + } +} diff --git a/Sources/JSONAPITestLib/Attribute+Literal.swift b/Sources/JSONAPITesting/Attribute+Literal.swift similarity index 100% rename from Sources/JSONAPITestLib/Attribute+Literal.swift rename to Sources/JSONAPITesting/Attribute+Literal.swift diff --git a/Sources/JSONAPITestLib/EntityCheck.swift b/Sources/JSONAPITesting/EntityCheck.swift similarity index 100% rename from Sources/JSONAPITestLib/EntityCheck.swift rename to Sources/JSONAPITesting/EntityCheck.swift diff --git a/Sources/JSONAPITestLib/Id+Literal.swift b/Sources/JSONAPITesting/Id+Literal.swift similarity index 100% rename from Sources/JSONAPITestLib/Id+Literal.swift rename to Sources/JSONAPITesting/Id+Literal.swift diff --git a/Sources/JSONAPITestLib/Optional+Literal.swift b/Sources/JSONAPITesting/Optional+Literal.swift similarity index 100% rename from Sources/JSONAPITestLib/Optional+Literal.swift rename to Sources/JSONAPITesting/Optional+Literal.swift diff --git a/Sources/JSONAPITestLib/Relationship+Literal.swift b/Sources/JSONAPITesting/Relationship+Literal.swift similarity index 100% rename from Sources/JSONAPITestLib/Relationship+Literal.swift rename to Sources/JSONAPITesting/Relationship+Literal.swift diff --git a/Tests/JSONAPITestLibTests/Attribute+LiteralTests.swift b/Tests/JSONAPITestingTests/Attribute+LiteralTests.swift similarity index 100% rename from Tests/JSONAPITestLibTests/Attribute+LiteralTests.swift rename to Tests/JSONAPITestingTests/Attribute+LiteralTests.swift diff --git a/Tests/JSONAPITestLibTests/EntityCheckTests.swift b/Tests/JSONAPITestingTests/EntityCheckTests.swift similarity index 100% rename from Tests/JSONAPITestLibTests/EntityCheckTests.swift rename to Tests/JSONAPITestingTests/EntityCheckTests.swift diff --git a/Tests/JSONAPITestLibTests/Id+LiteralTests.swift b/Tests/JSONAPITestingTests/Id+LiteralTests.swift similarity index 100% rename from Tests/JSONAPITestLibTests/Id+LiteralTests.swift rename to Tests/JSONAPITestingTests/Id+LiteralTests.swift diff --git a/Tests/JSONAPITestLibTests/Relationship+LiteralTests.swift b/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift similarity index 100% rename from Tests/JSONAPITestLibTests/Relationship+LiteralTests.swift rename to Tests/JSONAPITestingTests/Relationship+LiteralTests.swift diff --git a/Tests/JSONAPITestLibTests/Test Helpers/EntityTestTypes.swift b/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift similarity index 100% rename from Tests/JSONAPITestLibTests/Test Helpers/EntityTestTypes.swift rename to Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift diff --git a/Tests/JSONAPITestLibTests/Test Helpers/String+CreatableRawIdType.swift b/Tests/JSONAPITestingTests/Test Helpers/String+CreatableRawIdType.swift similarity index 100% rename from Tests/JSONAPITestLibTests/Test Helpers/String+CreatableRawIdType.swift rename to Tests/JSONAPITestingTests/Test Helpers/String+CreatableRawIdType.swift diff --git a/Tests/JSONAPITestLibTests/XCTestManifests.swift b/Tests/JSONAPITestingTests/XCTestManifests.swift similarity index 100% rename from Tests/JSONAPITestLibTests/XCTestManifests.swift rename to Tests/JSONAPITestingTests/XCTestManifests.swift From 8726bcc9fa8be37c73f15fbd32ad4938d476e198 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 13 Jan 2019 18:35:20 -0800 Subject: [PATCH 03/22] Update all references to JSONAPITestLib to reflect renaming --- .../Pages/Test Library.xcplaygroundpage/Contents.swift | 6 +++--- Sources/JSONAPITesting/EntityCheck.swift | 2 +- Sources/JSONAPITesting/Optional+Literal.swift | 2 +- Sources/JSONAPITesting/Relationship+Literal.swift | 2 +- Tests/JSONAPITestingTests/Attribute+LiteralTests.swift | 2 +- Tests/JSONAPITestingTests/EntityCheckTests.swift | 2 +- Tests/JSONAPITestingTests/Id+LiteralTests.swift | 2 +- Tests/JSONAPITestingTests/Relationship+LiteralTests.swift | 2 +- Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift | 2 +- .../Computed Properties/ComputedPropertiesTests.swift | 2 +- .../Custom Attributes Tests/CustomAttributesTests.swift | 2 +- Tests/JSONAPITests/Entity/EntityTests.swift | 2 +- Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift | 2 +- .../Test Helpers/EncodedEntityPropertyTest.swift | 2 +- 14 files changed, 16 insertions(+), 16 deletions(-) diff --git a/JSONAPI.playground/Pages/Test Library.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Test Library.xcplaygroundpage/Contents.swift index a857014..fa71086 100644 --- a/JSONAPI.playground/Pages/Test Library.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Test Library.xcplaygroundpage/Contents.swift @@ -2,7 +2,7 @@ import Foundation import JSONAPI -import JSONAPITestLib +import JSONAPITesting /******* @@ -11,12 +11,12 @@ Please enjoy these examples, but allow me the forced casting and the lack of err ********/ // MARK: - Literal Expressibility -// The JSONAPITestLib provides literal expressibility for key types to +// The JSONAPITesting framework provides literal expressibility for key types to // make creating tests easier let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil), meta: .none, links: .none) // MARK: - JSON API structure checking -// The JSONAPITestLib provides a `check` function for each Entity type +// The JSONAPITesting framework provides a `check` function for each Entity type // that uses reflection to catch mistakes that are not forbidden by // Swift's type system but will result in unexpected results when // encoding/decoding. It is a good idea to add a `check` to each of diff --git a/Sources/JSONAPITesting/EntityCheck.swift b/Sources/JSONAPITesting/EntityCheck.swift index 2a91e31..1b7bd1f 100644 --- a/Sources/JSONAPITesting/EntityCheck.swift +++ b/Sources/JSONAPITesting/EntityCheck.swift @@ -1,6 +1,6 @@ // // EntityCheck.swift -// JSONAPITestLib +// JSONAPITesting // // Created by Mathew Polzin on 11/27/18. // diff --git a/Sources/JSONAPITesting/Optional+Literal.swift b/Sources/JSONAPITesting/Optional+Literal.swift index 7c9087f..e0d81b5 100644 --- a/Sources/JSONAPITesting/Optional+Literal.swift +++ b/Sources/JSONAPITesting/Optional+Literal.swift @@ -1,6 +1,6 @@ // // Optional+Literal.swift -// JSONAPITestLib +// JSONAPITesting // // Created by Mathew Polzin on 11/29/18. // diff --git a/Sources/JSONAPITesting/Relationship+Literal.swift b/Sources/JSONAPITesting/Relationship+Literal.swift index 61c4c92..9af692c 100644 --- a/Sources/JSONAPITesting/Relationship+Literal.swift +++ b/Sources/JSONAPITesting/Relationship+Literal.swift @@ -1,6 +1,6 @@ // // Relationship+Literal.swift -// JSONAPITestLib +// JSONAPITesting // // Created by Mathew Polzin on 11/27/18. // diff --git a/Tests/JSONAPITestingTests/Attribute+LiteralTests.swift b/Tests/JSONAPITestingTests/Attribute+LiteralTests.swift index a213cf7..da1f00a 100644 --- a/Tests/JSONAPITestingTests/Attribute+LiteralTests.swift +++ b/Tests/JSONAPITestingTests/Attribute+LiteralTests.swift @@ -7,7 +7,7 @@ import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting class Attribute_LiteralTests: XCTestCase { diff --git a/Tests/JSONAPITestingTests/EntityCheckTests.swift b/Tests/JSONAPITestingTests/EntityCheckTests.swift index c59ac1d..10d5a48 100644 --- a/Tests/JSONAPITestingTests/EntityCheckTests.swift +++ b/Tests/JSONAPITestingTests/EntityCheckTests.swift @@ -7,7 +7,7 @@ import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting // Successes are fairly well-checked by the EntityTests in the JSONAPITests target. // We will confirm failure cases are working in this file. diff --git a/Tests/JSONAPITestingTests/Id+LiteralTests.swift b/Tests/JSONAPITestingTests/Id+LiteralTests.swift index a6812be..2390fe0 100644 --- a/Tests/JSONAPITestingTests/Id+LiteralTests.swift +++ b/Tests/JSONAPITestingTests/Id+LiteralTests.swift @@ -7,7 +7,7 @@ import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting extension Int: RawIdType {} diff --git a/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift b/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift index 87e7f4f..9a2d7c9 100644 --- a/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift +++ b/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift @@ -7,7 +7,7 @@ import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting class Relationship_LiteralTests: XCTestCase { diff --git a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift index 4274015..a87f72a 100644 --- a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift +++ b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift @@ -7,7 +7,7 @@ import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting class Attribute_FunctorTests: XCTestCase { func test_mapGuaranteed() { diff --git a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift index f80839a..aaf626d 100644 --- a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift +++ b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift @@ -7,7 +7,7 @@ import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting class ComputedPropertiesTests: XCTestCase { func test_DecodeIgnoresComputed() { diff --git a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift index 3282b4e..91c46bd 100644 --- a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift +++ b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import JSONAPI -import JSONAPITestLib +import JSONAPITesting class CustomAttributesTests: XCTestCase { func test_customDecode() { diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index 6142247..ab75f07 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -7,7 +7,7 @@ import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting class EntityTests: XCTestCase { diff --git a/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift b/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift index 55f0c2f..56070a0 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift @@ -8,7 +8,7 @@ import Foundation import XCTest @testable import JSONAPI -import JSONAPITestLib +import JSONAPITesting private struct TransformedWrapper: Codable where Value == Transform.From { let x: TransformedAttribute diff --git a/Tests/JSONAPITests/Test Helpers/EncodedEntityPropertyTest.swift b/Tests/JSONAPITests/Test Helpers/EncodedEntityPropertyTest.swift index d2ce1bc..e7de8b7 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodedEntityPropertyTest.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodedEntityPropertyTest.swift @@ -8,7 +8,7 @@ import Foundation import XCTest import JSONAPI -import JSONAPITestLib +import JSONAPITesting func testEncoded(entity: E) { let encodedEntityData = encoded(value: entity) From 308f168a8c5bc7c2f3ba5e9b9f56ab26465c0cf3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 13 Jan 2019 23:24:02 -0800 Subject: [PATCH 04/22] Trying to map out the Open API Schema Object using Swift enums --- Sources/JSONAPI/Resource/Attribute.swift | 2 +- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 32 ++++ Sources/JSONAPIOpenAPI/OpenAPI.swift | 2 +- .../JSONAPIOpenAPI/OpenAPISchemaNode.swift | 28 ++++ Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 145 +++++++++++++++++- .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 3 - 6 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift create mode 100644 Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index 670dca5..a43c368 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -16,7 +16,7 @@ public protocol AttributeType: Codable { /// A TransformedAttribute takes a Codable type and attempts to turn it into another type. public struct TransformedAttribute: AttributeType where Transformer.From == RawValue { - let rawValue: RawValue + public let rawValue: RawValue public let value: Transformer.To diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift new file mode 100644 index 0000000..8a27709 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -0,0 +1,32 @@ +// +// JSONAPIOpenAPITypes.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/13/19. +// + +import JSONAPI + +extension Attribute: OpenAPITyped where RawValue: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return value.openAPIType + } +} + +extension TransformedAttribute: OpenAPITyped where RawValue: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return rawValue.openAPIType + } +} + +extension ToOneRelationship: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .object(.generic) + } +} + +extension ToManyRelationship: OpenAPITyped { + public var openAPIType: OpenAPI.JSONTypeFormat { + return .object(.generic) + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI.swift index 7e89f04..966d43a 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI.swift @@ -1,6 +1,6 @@ // // OpenAPI.swift -// JSONAPI +// JSONAPIOpenAPI // // Created by Mathew Polzin on 1/13/19. // diff --git a/Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift b/Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift new file mode 100644 index 0000000..70784f4 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift @@ -0,0 +1,28 @@ +// +// OpenAPISchemaNode.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/13/19. +// + +extension OpenAPI { + /// A single node in the schema. This is the + struct SchemaNode { + + /// Indicates where the object is required + /// or optional. + let required: Bool + + /// This can be an empty Dictionary if there + /// are no properties on this node. + let properties: [String: SchemaNode] + + + + let title: String? + } + + enum SchemaNodeType { + case `enum` + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index efd8aea..24c0587 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -1,6 +1,6 @@ // // OpenAPITypes.swift -// JSONAPI +// JSONAPIOpenAPI // // Created by Mathew Polzin on 1/13/19. // @@ -9,6 +9,12 @@ public protocol OpenAPITyped { var openAPIType: OpenAPI.JSONTypeFormat { get } } +public protocol SwiftTyped { + associatedtype SwiftType +} + +public protocol OpenAPIFormat: SwiftTyped {} + public extension OpenAPI { enum JSONType: String { case boolean = "boolean" @@ -27,43 +33,87 @@ public extension OpenAPI { case integer(IntegerFormat) case string(StringFormat) } + + enum JSONTypeNode { + case boolean(Context) + indirect case object(Context, ObjectContext) + indirect case array(Context, ArrayContext) + case number(Context, NumericContext) + case integer(Context, NumericContext) + case string(Context, StringContext) + indirect case allOf([JSONTypeNode]) + indirect case oneOf([JSONTypeNode]) + indirect case anyOf([JSONTypeNode]) + indirect case not(JSONTypeNode) + } +} + +extension OpenAPI.JSONType { + public var swiftType: Any.Type { + switch self { + case .boolean: + return Bool.self + case .object: + return Any.self + case .array: + return [Any].self + case .number: + return Double.self + case .integer: + return Int.self + case .string: + return String.self + } + } } public extension OpenAPI.JSONTypeFormat { - public enum BooleanFormat: String, Equatable, Codable { + public enum BooleanFormat: String, Equatable, Codable, OpenAPIFormat { case generic = "" + + public typealias SwiftType = Bool } - public enum ObjectFormat: String, Equatable, Codable { + public enum ObjectFormat: String, Equatable, Codable, OpenAPIFormat { case generic = "" + + public typealias SwiftType = Any } - public enum ArrayFormat: String, Equatable, Codable { + public enum ArrayFormat: String, Equatable, Codable, OpenAPIFormat { case generic = "" + + public typealias SwiftType = [Any] } - public enum NumberFormat: String, Equatable, Codable { + public enum NumberFormat: String, Equatable, Codable, OpenAPIFormat { case generic = "" case float = "float" case double = "double" + + public typealias SwiftType = Double } - public enum IntegerFormat: String, Equatable, Codable { + public enum IntegerFormat: String, Equatable, Codable, OpenAPIFormat { case generic = "" case int32 = "int32" case int64 = "int64" + + public typealias SwiftType = Int } - public enum StringFormat: String, Equatable, Codable { + public enum StringFormat: String, Equatable, Codable, OpenAPIFormat { case generic = "" case byte = "byte" case binary = "binary" case date = "date" case dateTime = "date-time" case password = "password" + + public typealias SwiftType = String } - public var type: OpenAPI.JSONType { + public var jsonType: OpenAPI.JSONType { switch self { case .boolean: return .boolean @@ -80,3 +130,82 @@ public extension OpenAPI.JSONTypeFormat { } } } + +extension OpenAPI.JSONTypeNode { + public struct Context { + public let format: Format + public let required: Bool + + /// The OpenAPI spec calls this "enum" + /// If not specified, it is assumed that any + /// value of the given format is allowed. + public let allowedValues: [Format.SwiftType]? + + public init(format: Format, + required: Bool, + allowedValues: [Format.SwiftType]? = nil) { + self.format = format + self.required = required + self.allowedValues = allowedValues + } + } + + public struct NumericContext { + public let multipleOf: Double? + public let maximum: Double? + public let exclusiveMaximum: Double? + public let minimum: Double? + public let exclusiveMinimum: Double? + } + + public struct StringContext { + public let maxLength: Int? + public let minLength: Int? + + /// Regular expression + public let pattern: String? + } + + public struct ArrayContext { + /// A JSON Type Node that describes + /// the type of each element in the array. + public let items: OpenAPI.JSONTypeNode + + public let maxItems: Int? + public let minItems: Int? + public let uniqueItems: Bool? + } + + public struct ObjectContext { + public let maxProperties: Int? + public let minProperties: Int? + public let properties: [String: OpenAPI.JSONTypeNode] + public let additionalProperties: [String: OpenAPI.JSONTypeNode] + + /* + // NOTE that an object's required properties + // array is determined by looking at its properties' + // required Bool. + public let required: [String] + */ + } + + public var jsonTypeFormat: OpenAPI.JSONTypeFormat? { + switch self { + case .boolean(let context): + return .boolean(context.format) + case .object(let context, _): + return .object(context.format) + case .array(let context, _): + return .array(context.format) + case .number(let context, _): + return .number(context.format) + case .integer(let context, _): + return .integer(context.format) + case .string(let context, _): + return .string(context.format) + case .allOf, .oneOf, .anyOf, .not: + return nil + } + } +} diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index 2da0222..7c91a49 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -24,9 +24,6 @@ RFC3339 date-time: A hint to UIs to obscure input: .string(.password) -An object: -.object(.generic) - **/ extension String: OpenAPITyped { From 9e6e713ad260e863fb5cbb89efdfdfa3f4c81691 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 14 Jan 2019 21:17:07 -0800 Subject: [PATCH 05/22] Add Codable for the parts of OpenAPI that are built so far. --- Package.resolved | 9 + Package.swift | 5 +- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 59 +++- .../JSONAPIOpenAPI/OpenAPITypes+Codable.swift | 176 ++++++++++++ Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 253 ++++++++++++++---- .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 69 +++-- .../JSONAPIRelationshipsOpenAPITests.swift | 47 ++++ 7 files changed, 534 insertions(+), 84 deletions(-) create mode 100644 Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift create mode 100644 Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift diff --git a/Package.resolved b/Package.resolved index 85b6784..0703061 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "AnyCodable", + "repositoryURL": "https://github.com/Flight-School/AnyCodable.git", + "state": { + "branch": null, + "revision": "396ccc3dba5bdee04c1e742e7fab40582861401e", + "version": "0.1.0" + } + }, { "package": "Poly", "repositoryURL": "https://github.com/mattpolzin/Poly.git", diff --git a/Package.swift b/Package.swift index 3278ecf..d1b92a5 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,8 @@ let package = Package( targets: ["JSONAPIOpenAPI"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")) + .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")), + .package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0") ], targets: [ .target( @@ -28,7 +29,7 @@ let package = Package( dependencies: ["JSONAPI"]), .target( name: "JSONAPIOpenAPI", - dependencies: ["JSONAPI"]), + dependencies: ["JSONAPI", "AnyCodable"]), .testTarget( name: "JSONAPITests", dependencies: ["JSONAPI", "JSONAPITesting"]), diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 8a27709..6cb2923 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -7,26 +7,61 @@ import JSONAPI -extension Attribute: OpenAPITyped where RawValue: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return value.openAPIType +extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return RawValue.openAPINode } } -extension TransformedAttribute: OpenAPITyped where RawValue: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return rawValue.openAPIType +extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return RawValue.openAPINode } } -extension ToOneRelationship: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .object(.generic) +private protocol _Optional {} +extension Optional: _Optional {} + +extension ToOneRelationship: OpenAPINodeType { + // TODO: const for json `type` + static public var openAPINode: OpenAPI.JSONNode { + let nullable = Identifiable.self is _Optional.Type + return .object(.init(format: .generic, + required: true), + .init(properties: [ + "data": .object(.init(format: .generic, + required: true, + nullable: nullable), + .init(properties: [ + "id": .string(.init(format: .generic, + required: true), + .init()), + "type": .string(.init(format: .generic, + required: true), + .init()) + ])) + ])) } } -extension ToManyRelationship: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .object(.generic) +extension ToManyRelationship: OpenAPINodeType { + // TODO: const for json `type` + static public var openAPINode: OpenAPI.JSONNode { + return .object(.init(format: .generic, + required: true), + .init(properties: [ + "data": .array(.init(format: .generic, + required: true), + .init(items: .object(.init(format: .generic, + required: true), + .init(properties: [ + "id": .string(.init(format: .generic, + required: true), + .init()), + "type": .string(.init(format: .generic, + required: true), + .init()) + ])))) + ])) } } diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift new file mode 100644 index 0000000..4212ccc --- /dev/null +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift @@ -0,0 +1,176 @@ +// +// OpenAPITypes+Codable.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/14/19. +// + +extension OpenAPI.JSONNode.Context: Encodable { + + private enum CodingKeys: String, CodingKey { + case type + case format + case allowedValues = "enum" + case nullable + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(format.jsonType, forKey: .type) + + if format != Format.unspecified { + try container.encode(format, forKey: .format) + } + + if allowedValues != nil { + try container.encode(allowedValues, forKey: .allowedValues) + } + + try container.encode(nullable, forKey: .nullable) + } +} + +extension OpenAPI.JSONNode.NumericContext: Encodable { + private enum CodingKeys: String, CodingKey { + case multipleOf + case maximum + case exclusiveMaximum + case minimum + case exclusiveMinimum + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + if multipleOf != nil { + try container.encode(multipleOf, forKey: .multipleOf) + } + + if maximum != nil { + try container.encode(maximum, forKey: .maximum) + } + + if exclusiveMaximum != nil { + try container.encode(exclusiveMaximum, forKey: .exclusiveMaximum) + } + + if minimum != nil { + try container.encode(minimum, forKey: .minimum) + } + + if exclusiveMinimum != nil { + try container.encode(exclusiveMinimum, forKey: .exclusiveMinimum) + } + } +} + +extension OpenAPI.JSONNode.StringContext: Encodable { + private enum CodingKeys: String, CodingKey { + case maxLength + case minLength + case pattern + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + if maxLength != nil { + try container.encode(maxLength, forKey: .maxLength) + } + + try container.encode(minLength, forKey: .minLength) + + if pattern != nil { + try container.encode(pattern, forKey: .pattern) + } + } +} + +extension OpenAPI.JSONNode.ArrayContext: Encodable { + private enum CodingKeys: String, CodingKey { + case items + case maxItems + case minItems + case uniqueItems + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(items, forKey: .items) + + if maxItems != nil { + try container.encode(maxItems, forKey: .maxItems) + } + + try container.encode(minItems, forKey: .minItems) + + try container.encode(uniqueItems, forKey: .uniqueItems) + } +} + +extension OpenAPI.JSONNode.ObjectContext : Encodable{ + private enum CodingKeys: String, CodingKey { + case maxProperties + case minProperties + case properties + case additionalProperties + case required + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + if maxProperties != nil { + try container.encode(maxProperties, forKey: .maxProperties) + } + + try container.encode(properties, forKey: .properties) + + if additionalProperties != nil { + try container.encode(additionalProperties, forKey: .additionalProperties) + } + + let required = properties.filter { (name, node) in + node.required + }.keys + + try container.encode(Array(required), forKey: .required) + + try container.encode(max(minProperties, required.count), forKey: .minProperties) + } +} + +extension OpenAPI.JSONNode: Encodable { + public func encode(to encoder: Encoder) throws { + switch self { + case .boolean(let context): + try context.encode(to: encoder) + + case .object(let contextA as Encodable, let contextB as Encodable), + .array(let contextA as Encodable, let contextB as Encodable), + .number(let contextA as Encodable, let contextB as Encodable), + .integer(let contextA as Encodable, let contextB as Encodable), + .string(let contextA as Encodable, let contextB as Encodable): + try contextA.encode(to: encoder) + try contextB.encode(to: encoder) + + case .allOf(let nodes): + // TODO + print("TODO") + + case .oneOf(let nodes): + // TODO + print("TODO") + + case .anyOf(let nodes): + // TODO + print("TODO") + + case .not(let node): + // TODO + print("TODO") + } + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 24c0587..2ccb7c6 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -5,18 +5,28 @@ // Created by Mathew Polzin on 1/13/19. // -public protocol OpenAPITyped { - var openAPIType: OpenAPI.JSONTypeFormat { get } +import AnyCodable + +public protocol OpenAPINodeType { + static var openAPINode: OpenAPI.JSONNode { get } } public protocol SwiftTyped { - associatedtype SwiftType + associatedtype SwiftType: Codable } -public protocol OpenAPIFormat: SwiftTyped {} +public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable { + static var unspecified: Self { get } + + var jsonType: OpenAPI.JSONType { get } +} + +public protocol JSONNodeContext { + var required: Bool { get } +} public extension OpenAPI { - enum JSONType: String { + enum JSONType: String, Codable { case boolean = "boolean" case object = "object" case array = "array" @@ -34,75 +44,98 @@ public extension OpenAPI { case string(StringFormat) } - enum JSONTypeNode { + /// A JSON Node is what OpenAPI calls a + /// "Schema Object" + enum JSONNode { case boolean(Context) indirect case object(Context, ObjectContext) indirect case array(Context, ArrayContext) case number(Context, NumericContext) case integer(Context, NumericContext) case string(Context, StringContext) - indirect case allOf([JSONTypeNode]) - indirect case oneOf([JSONTypeNode]) - indirect case anyOf([JSONTypeNode]) - indirect case not(JSONTypeNode) - } -} - -extension OpenAPI.JSONType { - public var swiftType: Any.Type { - switch self { - case .boolean: - return Bool.self - case .object: - return Any.self - case .array: - return [Any].self - case .number: - return Double.self - case .integer: - return Int.self - case .string: - return String.self - } + indirect case allOf([JSONNode]) + indirect case oneOf([JSONNode]) + indirect case anyOf([JSONNode]) + indirect case not(JSONNode) } } public extension OpenAPI.JSONTypeFormat { - public enum BooleanFormat: String, Equatable, Codable, OpenAPIFormat { + public enum BooleanFormat: String, Equatable, OpenAPIFormat { case generic = "" public typealias SwiftType = Bool + + public static var unspecified: BooleanFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .boolean + } } - public enum ObjectFormat: String, Equatable, Codable, OpenAPIFormat { + public enum ObjectFormat: String, Equatable, OpenAPIFormat { case generic = "" - public typealias SwiftType = Any + public typealias SwiftType = AnyCodable + + public static var unspecified: ObjectFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .object + } } - public enum ArrayFormat: String, Equatable, Codable, OpenAPIFormat { + public enum ArrayFormat: String, Equatable, OpenAPIFormat { case generic = "" - public typealias SwiftType = [Any] + public typealias SwiftType = [AnyCodable] + + public static var unspecified: ArrayFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .array + } } - public enum NumberFormat: String, Equatable, Codable, OpenAPIFormat { + public enum NumberFormat: String, Equatable, OpenAPIFormat { case generic = "" case float = "float" case double = "double" public typealias SwiftType = Double + + public static var unspecified: NumberFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .number + } } - public enum IntegerFormat: String, Equatable, Codable, OpenAPIFormat { + public enum IntegerFormat: String, Equatable, OpenAPIFormat { case generic = "" case int32 = "int32" case int64 = "int64" public typealias SwiftType = Int + + public static var unspecified: IntegerFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .integer + } } - public enum StringFormat: String, Equatable, Codable, OpenAPIFormat { + public enum StringFormat: String, Equatable, OpenAPIFormat { case generic = "" case byte = "byte" case binary = "binary" @@ -111,6 +144,14 @@ public extension OpenAPI.JSONTypeFormat { case password = "password" public typealias SwiftType = String + + public static var unspecified: StringFormat { + return .generic + } + + public var jsonType: OpenAPI.JSONType { + return .string + } } public var jsonType: OpenAPI.JSONType { @@ -131,10 +172,11 @@ public extension OpenAPI.JSONTypeFormat { } } -extension OpenAPI.JSONTypeNode { - public struct Context { +extension OpenAPI.JSONNode { + public struct Context: JSONNodeContext { public let format: Format public let required: Bool + public let nullable: Bool /// The OpenAPI spec calls this "enum" /// If not specified, it is assumed that any @@ -143,44 +185,99 @@ extension OpenAPI.JSONTypeNode { public init(format: Format, required: Bool, + nullable: Bool = false, allowedValues: [Format.SwiftType]? = nil) { self.format = format self.required = required + self.nullable = nullable self.allowedValues = allowedValues } + + /// Return the optional version of this Context + public func optionalContext() -> Context { + return .init(format: format, + required: false, + allowedValues: allowedValues) + } + + /// Return the required version of this context + public func requiredContext() -> Context { + return .init(format: format, + required: true, + allowedValues: allowedValues) + } } public struct NumericContext { + /// 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? public let exclusiveMaximum: Double? public let minimum: Double? public let exclusiveMinimum: Double? + + public init(multipleOf: Double? = nil, + maximum: Double? = nil, + exclusiveMaximum: Double? = nil, + minimum: Double? = nil, + exclusiveMinimum: Double? = nil) { + self.multipleOf = multipleOf + self.maximum = maximum + self.exclusiveMaximum = exclusiveMaximum + self.minimum = minimum + self.exclusiveMinimum = exclusiveMinimum + } } public struct StringContext { public let maxLength: Int? - public let minLength: Int? + public let minLength: Int /// Regular expression public let pattern: String? + + public init(maxLength: Int? = nil, + minLength: Int = 0, + pattern: String? = nil) { + self.maxLength = maxLength + self.minLength = minLength + self.pattern = pattern + } } public struct ArrayContext { /// A JSON Type Node that describes /// the type of each element in the array. - public let items: OpenAPI.JSONTypeNode + public let items: OpenAPI.JSONNode + /// Maximum number of items in array. public let maxItems: Int? - public let minItems: Int? - public let uniqueItems: Bool? + + /// Minimum number of items in array. + /// Defaults to 0. + public let minItems: Int + + /// Setting to true indicates all + /// elements of the array are expected + /// to be unique. Defaults to false. + public let uniqueItems: Bool + + public init(items: OpenAPI.JSONNode, + maxItems: Int? = nil, + minItems: Int = 0, + uniqueItems: Bool = false) { + self.items = items + self.maxItems = maxItems + self.minItems = minItems + self.uniqueItems = uniqueItems + } } public struct ObjectContext { public let maxProperties: Int? - public let minProperties: Int? - public let properties: [String: OpenAPI.JSONTypeNode] - public let additionalProperties: [String: OpenAPI.JSONTypeNode] + public let minProperties: Int + public let properties: [String: OpenAPI.JSONNode] + public let additionalProperties: [String: OpenAPI.JSONNode]? /* // NOTE that an object's required properties @@ -188,6 +285,16 @@ extension OpenAPI.JSONTypeNode { // required Bool. public let required: [String] */ + + public init(properties: [String: OpenAPI.JSONNode], + additionalProperties: [String: OpenAPI.JSONNode]? = nil, + maxProperties: Int? = nil, + minProperties: Int = 0) { + self.properties = properties + self.additionalProperties = additionalProperties + self.maxProperties = maxProperties + self.minProperties = minProperties + } } public var jsonTypeFormat: OpenAPI.JSONTypeFormat? { @@ -208,4 +315,58 @@ extension OpenAPI.JSONTypeNode { return nil } } + + public var required: Bool { + switch self { + case .boolean(let contextA as JSONNodeContext), + .object(let contextA as JSONNodeContext, _), + .array(let contextA as JSONNodeContext, _), + .number(let contextA as JSONNodeContext, _), + .integer(let contextA as JSONNodeContext, _), + .string(let contextA as JSONNodeContext, _): + return contextA.required + case .allOf, .oneOf, .anyOf, .not: + return true + } + } + + /// Return the optional version of this JSONNode + public func optionalNode() -> OpenAPI.JSONNode { + switch self { + case .boolean(let context): + return .boolean(context.optionalContext()) + case .object(let contextA, let contextB): + return .object(contextA.optionalContext(), contextB) + case .array(let contextA, let contextB): + return .array(contextA.optionalContext(), contextB) + case .number(let context, let contextB): + return .number(context.optionalContext(), contextB) + case .integer(let context, let contextB): + return .integer(context.optionalContext(), contextB) + case .string(let context, let contextB): + return .string(context.optionalContext(), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self + } + } + + /// Return the required version of this JSONNode + public func requiredNode() -> OpenAPI.JSONNode { + switch self { + case .boolean(let context): + return .boolean(context.requiredContext()) + case .object(let contextA, let contextB): + return .object(contextA.requiredContext(), contextB) + case .array(let contextA, let contextB): + return .array(contextA.requiredContext(), contextB) + case .number(let context, let contextB): + return .number(context.requiredContext(), contextB) + case .integer(let context, let contextB): + return .integer(context.requiredContext(), contextB) + case .string(let context, let contextB): + return .string(context.requiredContext(), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self + } + } } diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index 7c91a49..236ef95 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -26,50 +26,71 @@ A hint to UIs to obscure input: **/ -extension String: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .string(.generic) +extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return Wrapped.openAPINode.optionalNode() } } -extension Bool: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .boolean(.generic) +extension String: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .string(.init(format: .generic, + required: true), + .init()) } } -extension Array: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .array(.generic) +extension Bool: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .boolean(.init(format: .generic, + required: true)) } } -extension Double: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .number(.double) +extension Array: OpenAPINodeType where Element: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .array(.init(format: .generic, + required: true), + .init(items: Element.openAPINode)) } } -extension Float: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .number(.float) +extension Double: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .number(.init(format: .double, + required: true), + .init()) } } -extension Int: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .integer(.generic) +extension Float: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .number(.init(format: .float, + required: true), + .init()) } } -extension Int32: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .integer(.int32) +extension Int: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .integer(.init(format: .generic, + required: true), + .init()) } } -extension Int64: OpenAPITyped { - public var openAPIType: OpenAPI.JSONTypeFormat { - return .integer(.int64) +extension Int32: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .integer(.init(format: .int32, + required: true), + .init()) + } +} + +extension Int64: OpenAPINodeType { + static public var openAPINode: OpenAPI.JSONNode { + return .integer(.init(format: .int64, + required: true), + .init()) } } diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift new file mode 100644 index 0000000..44d699c --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift @@ -0,0 +1,47 @@ +// +// JSONAPIRelationshipsOpenAPITests.swift +// JSONAPI +// +// Created by Mathew Polzin on 1/14/19. +// + +import Foundation +import XCTest +import JSONAPI +import JSONAPITesting +import JSONAPIOpenAPI + +class JSONAPIRelationshipsOpenAPITests: XCTestCase { + + func test_ToOne() { + let node = ToOneRelationship.openAPINode + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + print(String(data: (try? encoder.encode(node))!, encoding: .utf8)!) + } +} + +// MARK: Test Types +extension JSONAPIRelationshipsOpenAPITests { + enum TestEntityType1: EntityDescription { + static var jsonType: String { return "test_entities"} + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + typealias TestEntity1 = BasicEntity + + enum TestEntityType2: EntityDescription { + static var jsonType: String { return "second_test_entities"} + + typealias Attributes = NoAttributes + + struct Relationships: JSONAPI.Relationships { + let other: ToOneRelationship + } + } + + typealias TestEntity2 = BasicEntity +} From a9e1e49a79f06f587ca215d1cb7b63d89e177bb0 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 14 Jan 2019 21:18:49 -0800 Subject: [PATCH 06/22] Add test helpers for Open API test target --- .../Test Helpers/EntityTestTypes.swift | 14 ++++++++++++++ .../String+CreatableRawIdType.swift | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 Tests/JSONAPIOpenAPITests/Test Helpers/EntityTestTypes.swift create mode 100644 Tests/JSONAPIOpenAPITests/Test Helpers/String+CreatableRawIdType.swift diff --git a/Tests/JSONAPIOpenAPITests/Test Helpers/EntityTestTypes.swift b/Tests/JSONAPIOpenAPITests/Test Helpers/EntityTestTypes.swift new file mode 100644 index 0000000..23e6b7d --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/Test Helpers/EntityTestTypes.swift @@ -0,0 +1,14 @@ +// +// EntityTestTypes.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/15/18. +// + +import JSONAPI + +public typealias Entity = JSONAPI.Entity + +public typealias BasicEntity = Entity + +public typealias NewEntity = JSONAPI.Entity diff --git a/Tests/JSONAPIOpenAPITests/Test Helpers/String+CreatableRawIdType.swift b/Tests/JSONAPIOpenAPITests/Test Helpers/String+CreatableRawIdType.swift new file mode 100644 index 0000000..dd3c8f7 --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/Test Helpers/String+CreatableRawIdType.swift @@ -0,0 +1,17 @@ +// +// String+CreatableRawIdType.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import JSONAPI + +private var uniqueStringCounter = 0 + +extension String: CreatableRawIdType { + public static func unique() -> String { + uniqueStringCounter += 1 + return String(uniqueStringCounter) + } +} From 3c292445407858a86ef5f291279b702f45110aae Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 14 Jan 2019 23:14:25 -0800 Subject: [PATCH 07/22] fill out some of the relationship tests --- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 18 +- Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 34 ++- .../JSONAPIRelationshipsOpenAPITests.swift | 216 +++++++++++++++++- 3 files changed, 260 insertions(+), 8 deletions(-) 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"])) } } From cb04a01e713074ab270da074455e385a9caaff8d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 14 Jan 2019 23:29:49 -0800 Subject: [PATCH 08/22] Add some todos and delete an old file --- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 2 ++ .../JSONAPIOpenAPI/OpenAPISchemaNode.swift | 28 ------------------- .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 3 ++ 3 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 33b7c81..ecd76db 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -36,6 +36,7 @@ extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType extension ToOneRelationship: OpenAPINodeType { // TODO: const for json `type` + // TODO: metadata & links static public var openAPINode: OpenAPI.JSONNode { let nullable = Identifiable.self is _Optional.Type return .object(.init(format: .generic, @@ -58,6 +59,7 @@ extension ToOneRelationship: OpenAPINodeType { extension ToManyRelationship: OpenAPINodeType { // TODO: const for json `type` + // TODO: metadata & links static public var openAPINode: OpenAPI.JSONNode { return .object(.init(format: .generic, required: true), diff --git a/Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift b/Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift deleted file mode 100644 index 70784f4..0000000 --- a/Sources/JSONAPIOpenAPI/OpenAPISchemaNode.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// OpenAPISchemaNode.swift -// JSONAPIOpenAPI -// -// Created by Mathew Polzin on 1/13/19. -// - -extension OpenAPI { - /// A single node in the schema. This is the - struct SchemaNode { - - /// Indicates where the object is required - /// or optional. - let required: Bool - - /// This can be an empty Dictionary if there - /// are no properties on this node. - let properties: [String: SchemaNode] - - - - let title: String? - } - - enum SchemaNodeType { - case `enum` - } -} diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index 236ef95..c217eb4 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -24,6 +24,9 @@ RFC3339 date-time: A hint to UIs to obscure input: .string(.password) +Any object: +.object(.generic) + **/ extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { From 3a0ede2372f83de55baed7075f3e64b2a94cbb47 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 14 Jan 2019 23:46:39 -0800 Subject: [PATCH 09/22] Get rid of OpenAPI namespace because a compiler bug was making it more hassle than it was worth. --- Package.resolved | 18 ++++ Package.swift | 28 +++-- Sources/JSONAPIArbitrary/Id+Arbitrary.swift | 15 +++ .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 8 +- Sources/JSONAPIOpenAPI/OpenAPI.swift | 8 -- .../JSONAPIOpenAPI/OpenAPITypes+Codable.swift | 12 +-- Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 102 +++++++++--------- .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 18 ++-- 8 files changed, 119 insertions(+), 90 deletions(-) create mode 100644 Sources/JSONAPIArbitrary/Id+Arbitrary.swift delete mode 100644 Sources/JSONAPIOpenAPI/OpenAPI.swift diff --git a/Package.resolved b/Package.resolved index 0703061..69dfbfa 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,15 @@ "version": "0.1.0" } }, + { + "package": "FileCheck", + "repositoryURL": "https://github.com/llvm-swift/FileCheck.git", + "state": { + "branch": null, + "revision": "89b8480055f9adf8ce2f9ad5e2fac7ac1076242e", + "version": "0.0.8" + } + }, { "package": "Poly", "repositoryURL": "https://github.com/mattpolzin/Poly.git", @@ -18,6 +27,15 @@ "revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07", "version": null } + }, + { + "package": "SwiftCheck", + "repositoryURL": "https://github.com/typelift/SwiftCheck.git", + "state": { + "branch": null, + "revision": "cf9958085b2ee1643e541e407c3233d1b76c18ff", + "version": "0.11.0" + } } ] }, diff --git a/Package.swift b/Package.swift index d1b92a5..3622d82 100644 --- a/Package.swift +++ b/Package.swift @@ -9,33 +9,43 @@ let package = Package( .library( name: "JSONAPI", targets: ["JSONAPI"]), - .library( - name: "JSONAPITesting", - targets: ["JSONAPITesting"]), + .library( + name: "JSONAPITesting", + targets: ["JSONAPITesting"]), + .library( + name: "JSONAPIArbitrary", + targets: ["JSONAPIArbitrary"]), .library( name: "JSONAPIOpenAPI", targets: ["JSONAPIOpenAPI"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")), - .package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0") + .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")), + .package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0"), + .package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.11.0") ], targets: [ .target( name: "JSONAPI", dependencies: ["Poly"]), - .target( - name: "JSONAPITesting", - dependencies: ["JSONAPI"]), + .target( + name: "JSONAPITesting", + dependencies: ["JSONAPI"]), + .target( + name: "JSONAPIArbitrary", + dependencies: ["JSONAPI", "SwiftCheck"]), .target( name: "JSONAPIOpenAPI", - dependencies: ["JSONAPI", "AnyCodable"]), + dependencies: ["JSONAPI", "AnyCodable", "JSONAPIArbitrary"]), .testTarget( name: "JSONAPITests", dependencies: ["JSONAPI", "JSONAPITesting"]), .testTarget( name: "JSONAPITestingTests", dependencies: ["JSONAPI", "JSONAPITesting"]), + .testTarget( + name: "JSONAPIArbitraryTests", + dependencies: ["JSONAPI", "SwiftCheck", "JSONAPIArbitrary"]), .testTarget( name: "JSONAPIOpenAPITests", dependencies: ["JSONAPI", "JSONAPIOpenAPI"]) diff --git a/Sources/JSONAPIArbitrary/Id+Arbitrary.swift b/Sources/JSONAPIArbitrary/Id+Arbitrary.swift new file mode 100644 index 0000000..17b7e23 --- /dev/null +++ b/Sources/JSONAPIArbitrary/Id+Arbitrary.swift @@ -0,0 +1,15 @@ +// +// Id+Arbitrary.swift +// JSONAPIArbitrary +// +// Created by Mathew Polzin on 1/14/19. +// + +import SwiftCheck +import JSONAPI + +extension Id: Arbitrary where RawType: Arbitrary { + public static var arbitrary: Gen> { + return RawType.arbitrary.map { Id(rawValue: $0) } + } +} diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index ecd76db..97865df 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -11,7 +11,7 @@ private protocol _Optional {} extension Optional: _Optional {} extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { // If the RawValue is not required, we actually consider it // nullable. To be not required is for the Attribute itself // to be optional. @@ -23,7 +23,7 @@ extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { } extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { // If the RawValue is not required, we actually consider it // nullable. To be not required is for the Attribute itself // to be optional. @@ -37,7 +37,7 @@ extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType extension ToOneRelationship: OpenAPINodeType { // TODO: const for json `type` // TODO: metadata & links - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { let nullable = Identifiable.self is _Optional.Type return .object(.init(format: .generic, required: true), @@ -60,7 +60,7 @@ extension ToOneRelationship: OpenAPINodeType { extension ToManyRelationship: OpenAPINodeType { // TODO: const for json `type` // TODO: metadata & links - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .object(.init(format: .generic, required: true), .init(properties: [ diff --git a/Sources/JSONAPIOpenAPI/OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI.swift deleted file mode 100644 index 966d43a..0000000 --- a/Sources/JSONAPIOpenAPI/OpenAPI.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// OpenAPI.swift -// JSONAPIOpenAPI -// -// Created by Mathew Polzin on 1/13/19. -// - -public enum OpenAPI {} diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift index 4212ccc..7b04957 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 1/14/19. // -extension OpenAPI.JSONNode.Context: Encodable { +extension JSONNode.Context: Encodable { private enum CodingKeys: String, CodingKey { case type @@ -31,7 +31,7 @@ extension OpenAPI.JSONNode.Context: Encodable { } } -extension OpenAPI.JSONNode.NumericContext: Encodable { +extension JSONNode.NumericContext: Encodable { private enum CodingKeys: String, CodingKey { case multipleOf case maximum @@ -65,7 +65,7 @@ extension OpenAPI.JSONNode.NumericContext: Encodable { } } -extension OpenAPI.JSONNode.StringContext: Encodable { +extension JSONNode.StringContext: Encodable { private enum CodingKeys: String, CodingKey { case maxLength case minLength @@ -87,7 +87,7 @@ extension OpenAPI.JSONNode.StringContext: Encodable { } } -extension OpenAPI.JSONNode.ArrayContext: Encodable { +extension JSONNode.ArrayContext: Encodable { private enum CodingKeys: String, CodingKey { case items case maxItems @@ -110,7 +110,7 @@ extension OpenAPI.JSONNode.ArrayContext: Encodable { } } -extension OpenAPI.JSONNode.ObjectContext : Encodable{ +extension JSONNode.ObjectContext : Encodable{ private enum CodingKeys: String, CodingKey { case maxProperties case minProperties @@ -142,7 +142,7 @@ extension OpenAPI.JSONNode.ObjectContext : Encodable{ } } -extension OpenAPI.JSONNode: Encodable { +extension JSONNode: Encodable { public func encode(to encoder: Encoder) throws { switch self { case .boolean(let context): diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 87271ae..c039975 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -8,7 +8,7 @@ import AnyCodable public protocol OpenAPINodeType { - static var openAPINode: OpenAPI.JSONNode { get } + static var openAPINode: JSONNode { get } } public protocol SwiftTyped { @@ -18,49 +18,30 @@ public protocol SwiftTyped { public protocol OpenAPIFormat: SwiftTyped, Codable, Equatable { static var unspecified: Self { get } - var jsonType: OpenAPI.JSONType { get } + var jsonType: JSONType { get } } public protocol JSONNodeContext { var required: Bool { get } } -public extension OpenAPI { - enum JSONType: String, Codable { - case boolean = "boolean" - case object = "object" - case array = "array" - case number = "number" - case integer = "integer" - case string = "string" - } - - enum JSONTypeFormat: Equatable { - case boolean(BooleanFormat) - case object(ObjectFormat) - case array(ArrayFormat) - case number(NumberFormat) - case integer(IntegerFormat) - case string(StringFormat) - } - - /// A JSON Node is what OpenAPI calls a - /// "Schema Object" - enum JSONNode { - case boolean(Context) - indirect case object(Context, ObjectContext) - indirect case array(Context, ArrayContext) - case number(Context, NumericContext) - case integer(Context, NumericContext) - case string(Context, StringContext) - indirect case allOf([JSONNode]) - indirect case oneOf([JSONNode]) - indirect case anyOf([JSONNode]) - indirect case not(JSONNode) - } +public enum JSONType: String, Codable { + case boolean = "boolean" + case object = "object" + case array = "array" + case number = "number" + case integer = "integer" + case string = "string" } -public extension OpenAPI.JSONTypeFormat { +public enum JSONTypeFormat: Equatable { + case boolean(BooleanFormat) + case object(ObjectFormat) + case array(ArrayFormat) + case number(NumberFormat) + case integer(IntegerFormat) + case string(StringFormat) + public enum BooleanFormat: String, Equatable, OpenAPIFormat { case generic = "" @@ -70,7 +51,7 @@ public extension OpenAPI.JSONTypeFormat { return .generic } - public var jsonType: OpenAPI.JSONType { + public var jsonType: JSONType { return .boolean } } @@ -84,7 +65,7 @@ public extension OpenAPI.JSONTypeFormat { return .generic } - public var jsonType: OpenAPI.JSONType { + public var jsonType: JSONType { return .object } } @@ -98,7 +79,7 @@ public extension OpenAPI.JSONTypeFormat { return .generic } - public var jsonType: OpenAPI.JSONType { + public var jsonType: JSONType { return .array } } @@ -114,7 +95,7 @@ public extension OpenAPI.JSONTypeFormat { return .generic } - public var jsonType: OpenAPI.JSONType { + public var jsonType: JSONType { return .number } } @@ -130,7 +111,7 @@ public extension OpenAPI.JSONTypeFormat { return .generic } - public var jsonType: OpenAPI.JSONType { + public var jsonType: JSONType { return .integer } } @@ -149,12 +130,12 @@ public extension OpenAPI.JSONTypeFormat { return .generic } - public var jsonType: OpenAPI.JSONType { + public var jsonType: JSONType { return .string } } - public var jsonType: OpenAPI.JSONType { + public var jsonType: JSONType { switch self { case .boolean: return .boolean @@ -172,7 +153,20 @@ public extension OpenAPI.JSONTypeFormat { } } -extension OpenAPI.JSONNode { +/// A JSON Node is what OpenAPI calls a +/// "Schema Object" +public enum JSONNode { + case boolean(Context) + indirect case object(Context, ObjectContext) + indirect case array(Context, ArrayContext) + case number(Context, NumericContext) + case integer(Context, NumericContext) + case string(Context, StringContext) + indirect case allOf([JSONNode]) + indirect case oneOf([JSONNode]) + indirect case anyOf([JSONNode]) + indirect case not(JSONNode) + public struct Context: JSONNodeContext, Equatable { public let format: Format public let required: Bool @@ -258,7 +252,7 @@ extension OpenAPI.JSONNode { public struct ArrayContext { /// A JSON Type Node that describes /// the type of each element in the array. - public let items: OpenAPI.JSONNode + public let items: JSONNode /// Maximum number of items in array. public let maxItems: Int? @@ -272,7 +266,7 @@ extension OpenAPI.JSONNode { /// to be unique. Defaults to false. public let uniqueItems: Bool - public init(items: OpenAPI.JSONNode, + public init(items: JSONNode, maxItems: Int? = nil, minItems: Int = 0, uniqueItems: Bool = false) { @@ -286,8 +280,8 @@ extension OpenAPI.JSONNode { public struct ObjectContext { public let maxProperties: Int? public let minProperties: Int - public let properties: [String: OpenAPI.JSONNode] - public let additionalProperties: [String: OpenAPI.JSONNode]? + public let properties: [String: JSONNode] + public let additionalProperties: [String: JSONNode]? /* // NOTE that an object's required properties @@ -296,8 +290,8 @@ extension OpenAPI.JSONNode { public let required: [String] */ - public init(properties: [String: OpenAPI.JSONNode], - additionalProperties: [String: OpenAPI.JSONNode]? = nil, + public init(properties: [String: JSONNode], + additionalProperties: [String: JSONNode]? = nil, maxProperties: Int? = nil, minProperties: Int = 0) { self.properties = properties @@ -307,7 +301,7 @@ extension OpenAPI.JSONNode { } } - public var jsonTypeFormat: OpenAPI.JSONTypeFormat? { + public var jsonTypeFormat: JSONTypeFormat? { switch self { case .boolean(let context): return .boolean(context.format) @@ -341,7 +335,7 @@ extension OpenAPI.JSONNode { } /// Return the optional version of this JSONNode - public func optionalNode() -> OpenAPI.JSONNode { + public func optionalNode() -> JSONNode { switch self { case .boolean(let context): return .boolean(context.optionalContext()) @@ -361,7 +355,7 @@ extension OpenAPI.JSONNode { } /// Return the required version of this JSONNode - public func requiredNode() -> OpenAPI.JSONNode { + public func requiredNode() -> JSONNode { switch self { case .boolean(let context): return .boolean(context.requiredContext()) @@ -381,7 +375,7 @@ extension OpenAPI.JSONNode { } /// Return the nullable version of this JSONNode - public func nullableNode() -> OpenAPI.JSONNode { + public func nullableNode() -> JSONNode { switch self { case .boolean(let context): return .boolean(context.nullableContext()) diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index c217eb4..ce7fe6b 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -30,13 +30,13 @@ Any object: **/ extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return Wrapped.openAPINode.optionalNode() } } extension String: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .string(.init(format: .generic, required: true), .init()) @@ -44,14 +44,14 @@ extension String: OpenAPINodeType { } extension Bool: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .boolean(.init(format: .generic, required: true)) } } extension Array: OpenAPINodeType where Element: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .array(.init(format: .generic, required: true), .init(items: Element.openAPINode)) @@ -59,7 +59,7 @@ extension Array: OpenAPINodeType where Element: OpenAPINodeType { } extension Double: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .number(.init(format: .double, required: true), .init()) @@ -67,7 +67,7 @@ extension Double: OpenAPINodeType { } extension Float: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .number(.init(format: .float, required: true), .init()) @@ -75,7 +75,7 @@ extension Float: OpenAPINodeType { } extension Int: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .integer(.init(format: .generic, required: true), .init()) @@ -83,7 +83,7 @@ extension Int: OpenAPINodeType { } extension Int32: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .integer(.init(format: .int32, required: true), .init()) @@ -91,7 +91,7 @@ extension Int32: OpenAPINodeType { } extension Int64: OpenAPINodeType { - static public var openAPINode: OpenAPI.JSONNode { + static public var openAPINode: JSONNode { return .integer(.init(format: .int64, required: true), .init()) From 81b78cd7491e990b4e4a5f2da570e96ae97dad13 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 15 Jan 2019 09:27:09 -0800 Subject: [PATCH 10/22] Add more Arbitrary conformances --- .../Attribute+Arbitrary.swift | 20 +++++++ .../JSONAPIArbitrary/Entity+Arbitrary.swift | 45 ++++++++++++++++ Sources/JSONAPIArbitrary/Id+Arbitrary.swift | 6 +++ .../Relationship+Arbitrary.swift | 52 +++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 Sources/JSONAPIArbitrary/Attribute+Arbitrary.swift create mode 100644 Sources/JSONAPIArbitrary/Entity+Arbitrary.swift create mode 100644 Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift diff --git a/Sources/JSONAPIArbitrary/Attribute+Arbitrary.swift b/Sources/JSONAPIArbitrary/Attribute+Arbitrary.swift new file mode 100644 index 0000000..5c3ddb5 --- /dev/null +++ b/Sources/JSONAPIArbitrary/Attribute+Arbitrary.swift @@ -0,0 +1,20 @@ +// +// Attribute+Arbitrary.swift +// JSONAPIArbitrary +// +// Created by Mathew Polzin on 1/15/19. +// + +import SwiftCheck +import JSONAPI + +extension Attribute: Arbitrary where RawValue: Arbitrary { + public static var arbitrary: Gen> { + return RawValue.arbitrary.map { .init(value: $0) } + } +} + +// Cannot extend TransformedAttribute here +// because there is no way to guarantee that an arbitrary +// RawValue will successfully transform or that an +// arbitrary Value will successfully reverse-transform. diff --git a/Sources/JSONAPIArbitrary/Entity+Arbitrary.swift b/Sources/JSONAPIArbitrary/Entity+Arbitrary.swift new file mode 100644 index 0000000..b686057 --- /dev/null +++ b/Sources/JSONAPIArbitrary/Entity+Arbitrary.swift @@ -0,0 +1,45 @@ +// +// Entity+Arbitrary.swift +// JSONAPIArbitrary +// +// Created by Mathew Polzin on 1/14/19. +// + +import SwiftCheck +import JSONAPI + +extension NoMetadata: Arbitrary { + public static var arbitrary: Gen { + return Gen.pure(.none) + } +} + +extension NoLinks: Arbitrary { + public static var arbitrary: Gen { + return Gen.pure(.none) + } +} + +extension NoAttributes: Arbitrary { + public static var arbitrary: Gen { + return Gen.pure(.none) + } +} + +extension NoRelationships: Arbitrary { + public static var arbitrary: Gen { + return Gen.pure(.none) + } +} + +extension Entity: Arbitrary where MetaType: Arbitrary, LinksType: Arbitrary, Description.Attributes: Arbitrary, Description.Relationships: Arbitrary, EntityRawIdType: Arbitrary { + public static var arbitrary: Gen> { + return Gen.compose { c in + Entity(id: c.generate(), + attributes: c.generate(), + relationships: c.generate(), + meta: c.generate(), + links: c.generate()) + } + } +} diff --git a/Sources/JSONAPIArbitrary/Id+Arbitrary.swift b/Sources/JSONAPIArbitrary/Id+Arbitrary.swift index 17b7e23..fbf6c32 100644 --- a/Sources/JSONAPIArbitrary/Id+Arbitrary.swift +++ b/Sources/JSONAPIArbitrary/Id+Arbitrary.swift @@ -8,6 +8,12 @@ import SwiftCheck import JSONAPI +extension Unidentified: Arbitrary { + public static var arbitrary: Gen { + return Gen.pure(.init()) + } +} + extension Id: Arbitrary where RawType: Arbitrary { public static var arbitrary: Gen> { return RawType.arbitrary.map { Id(rawValue: $0) } diff --git a/Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift b/Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift new file mode 100644 index 0000000..df1dd75 --- /dev/null +++ b/Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift @@ -0,0 +1,52 @@ +// +// Relationship+Arbitrary.swift +// JSONAPIArbitrary +// +// Created by Mathew Polzin on 1/15/19. +// + +import SwiftCheck +import JSONAPI + +extension ToOneRelationship: Arbitrary where Identifiable.Identifier: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary { + public static var arbitrary: Gen> { + return Gen.compose { c in + return .init(id: c.generate(), + meta: c.generate(), + links: c.generate()) + } + } +} + +extension ToOneRelationship where MetaType: Arbitrary, LinksType: Arbitrary { + public static func arbitrary(givenEntities: [E]) -> Gen> where E.Id == Identifiable.Identifier { + + return Gen.compose { c in + let idGen = Gen.fromElements(of: givenEntities).map { $0.id } + return .init(id: c.generate(using: idGen), + meta: c.generate(), + links: c.generate()) + } + } +} + +extension ToManyRelationship: Arbitrary where Relatable.Identifier: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary { + public static var arbitrary: Gen> { + return Gen.compose { c in + return .init(ids: c.generate(), + meta: c.generate(), + links: c.generate()) + } + } +} + +extension ToManyRelationship where MetaType: Arbitrary, LinksType: Arbitrary { + public static func arbitrary(givenEntities: [E]) -> Gen> where E.Id == Relatable.Identifier { + return Gen.compose { c in + let idsGen = Gen.fromElements(of: givenEntities).map { $0.id }.proliferate + return .init(ids: c.generate(using: idsGen), + meta: c.generate(), + links: c.generate()) + } + } +} From 52d2e9819d2ddf70719e493efa0e5634b6a5fdc8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 15 Jan 2019 09:40:49 -0800 Subject: [PATCH 11/22] Update README project status to include OpenAPI and Arbitrary work. --- README.md | 190 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 121 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 6002dfa..5d0ada7 100644 --- a/README.md +++ b/README.md @@ -8,59 +8,60 @@ See the JSON API Spec here: https://jsonapi.org/format/ :warning: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating entities in-code (servers and test suites must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: ## Table of Contents - + -- [Table of Contents](#table-of-contents) -- [Primary Goals](#primary-goals) - - [Caveat](#caveat) -- [Dev Environment](#dev-environment) - - [Prerequisites](#prerequisites) - - [Xcode project](#xcode-project) - - [Running the Playground](#running-the-playground) -- [Project Status](#project-status) - - [Encoding/Decoding](#encodingdecoding) - - [Document](#document) - - [Resource Object](#resource-object) - - [Relationship Object](#relationship-object) - - [Links Object](#links-object) - - [Misc](#misc) +- [JSONAPI](#jsonapi) + - [Table of Contents](#table-of-contents) + - [Primary Goals](#primary-goals) + - [Caveat](#caveat) + - [Dev Environment](#dev-environment) + - [Prerequisites](#prerequisites) + - [Xcode project](#xcode-project) + - [Running the Playground](#running-the-playground) + - [Project Status](#project-status) + - [JSON:API](#jsonapi) + - [Document](#document) + - [Resource Object](#resource-object) + - [Relationship Object](#relationship-object) + - [Links Object](#links-object) + - [Misc](#misc) + - [JSONAPITestLib](#jsonapitestlib) + - [Entity Validator](#entity-validator) + - [Potential Improvements](#potential-improvements) + - [Usage](#usage) + - [`JSONAPI.EntityDescription`](#jsonapientitydescription) + - [`JSONAPI.Entity`](#jsonapientity) + - [`Meta`](#meta) + - [`Links`](#links) + - [`IdType`](#idtype) + - [`MaybeRawId`](#mayberawid) + - [Convenient `typealiases`](#convenient-typealiases) + - [`JSONAPI.Relationships`](#jsonapirelationships) + - [`JSONAPI.Attributes`](#jsonapiattributes) + - [`Transformer`](#transformer) + - [`Validator`](#validator) + - [Computed `Attribute`](#computed-attribute) + - [Copying `Entities`](#copying-entities) + - [`JSONAPI.Document`](#jsonapidocument) + - [`ResourceBody`](#resourcebody) + - [nullable `PrimaryResource`](#nullable-primaryresource) + - [`MetaType`](#metatype) + - [`LinksType`](#linkstype) + - [`IncludeType`](#includetype) + - [`APIDescriptionType`](#apidescriptiontype) + - [`Error`](#error) + - [`JSONAPI.Meta`](#jsonapimeta) + - [`JSONAPI.Links`](#jsonapilinks) + - [`JSONAPI.RawIdType`](#jsonapirawidtype) + - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) + - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) + - [Meta-Attributes](#meta-attributes) + - [Meta-Relationships](#meta-relationships) + - [Example](#example) + - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) + - [Server Pseudo-example](#server-pseudo-example) + - [Client Pseudo-example](#client-pseudo-example) - [JSONAPITestLib](#jsonapitestlib) - - [Entity Validator](#entity-validator) - - [Potential Improvements](#potential-improvements) -- [Usage](#usage) - - [`JSONAPI.EntityDescription`](#jsonapientitydescription) - - [`JSONAPI.Entity`](#jsonapientity) - - [`Meta`](#meta) - - [`Links`](#links) - - [`IdType`](#idtype) - - [`MaybeRawId`](#mayberawid) - - [Convenient `typealiases`](#convenient-typealiases) - - [`JSONAPI.Relationships`](#jsonapirelationships) - - [`JSONAPI.Attributes`](#jsonapiattributes) - - [`Transformer`](#transformer) - - [`Validator`](#validator) - - [Computed `Attribute`](#computed-attribute) - - [Copying `Entities`](#copying-entities) - - [`JSONAPI.Document`](#jsonapidocument) - - [`ResourceBody`](#resourcebody) - - [nullable `PrimaryResource`](#nullable-primaryresource) - - [`MetaType`](#metatype) - - [`LinksType`](#linkstype) - - [`IncludeType`](#includetype) - - [`APIDescriptionType`](#apidescriptiontype) - - [`Error`](#error) - - [`JSONAPI.Meta`](#jsonapimeta) - - [`JSONAPI.Links`](#jsonapilinks) - - [`JSONAPI.RawIdType`](#jsonapirawidtype) - - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - - [Meta-Attributes](#meta-attributes) - - [Meta-Relationships](#meta-relationships) -- [Example](#example) - - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) - - [Server Pseudo-example](#server-pseudo-example) - - [Client Pseudo-example](#client-pseudo-example) -- [JSONAPITestLib](#jsonapitestlib) @@ -92,31 +93,82 @@ Note that Playground support for importing non-system Frameworks is still a bit ## Project Status -### Encoding/Decoding +### JSON:API #### Document -- [x] `data` -- [x] `included` -- [x] `errors` -- [x] `meta` -- [x] `jsonapi` -- [x] `links` +- `data` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `included` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `errors` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `meta` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `jsonapi` (i.e. API Information) + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `links` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI #### Resource Object -- [x] `id` -- [x] `type` -- [x] `attributes` -- [x] `relationships` -- [x] `links` -- [x] `meta` +- `id` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `type` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `attributes` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `relationships` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `links` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `meta` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI #### Relationship Object -- [x] `data` -- [x] `links` -- [x] `meta` +- `data` + - [x] Encoding/Decoding + - [x] Arbitrary + - [x] OpenAPI +- `links` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `meta` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI #### Links Object -- [x] `href` -- [x] `meta` +- `href` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI +- `meta` + - [x] Encoding/Decoding + - [ ] Arbitrary + - [ ] OpenAPI ### Misc - [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) From cf746e182f3fae7a14ecd006e4ba6088b7bb51f9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 19 Jan 2019 15:30:09 -0800 Subject: [PATCH 12/22] currently in a pretty broken state with support for enumerations being turned into allowed values via reflection. I think I am going to have to give up type safety if I want to use reflection and keep things open ended --- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 108 +++++++++++----- Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 101 ++++++++++++++- Sources/JSONAPIOpenAPI/Optional+ZipWith.swift | 10 ++ Sources/JSONAPIOpenAPI/Sampleable.swift | 122 ++++++++++++++++++ .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 34 +++-- .../JSONAPIEntityOpenAPITests.swift | 84 ++++++++++++ .../JSONAPIRelationshipsOpenAPITests.swift | 12 +- 7 files changed, 422 insertions(+), 49 deletions(-) create mode 100644 Sources/JSONAPIOpenAPI/Optional+ZipWith.swift create mode 100644 Sources/JSONAPIOpenAPI/Sampleable.swift create mode 100644 Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 97865df..aa36007 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -11,48 +11,78 @@ private protocol _Optional {} extension Optional: _Optional {} extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() 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 !RawValue.openAPINode.required { - return RawValue.openAPINode.requiredNode().nullableNode() + if try !RawValue.openAPINode().required { + return try RawValue.openAPINode().requiredNode().nullableNode() } - return RawValue.openAPINode + return try RawValue.openAPINode() + } +} + +extension Attribute: RawOpenAPINodeType where RawValue: RawRepresentable, RawValue.RawValue: OpenAPINodeType { + static public func openAPINode() throws -> JSONNode { + + if try !RawValue.RawValue.openAPINode().required { + return try RawValue.RawValue.openAPINode().requiredNode().nullableNode() + } + return try RawValue.RawValue.openAPINode() + } +} + +extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable { + public static var allCases: [Any] { + return Array(RawValue.allCases) + } +} + +extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseIterable { + public static var allCases: [Any] { + return RawValue.allCases } } extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() 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 !RawValue.openAPINode.required { - return RawValue.openAPINode.requiredNode().nullableNode() + if try !RawValue.openAPINode().required { + return try RawValue.openAPINode().requiredNode().nullableNode() } - return RawValue.openAPINode + return try RawValue.openAPINode() + } +} + +extension RelationshipType { + static func relationshipNode(nullable: Bool) -> JSONNode { + let propertiesDict: [String: JSONNode] = [ + "id": .string(.init(format: .generic, + required: true), + .init()), + "type": .string(.init(format: .generic, + required: true), + .init()) + ] + + return .object(.init(format: .generic, + required: true, + nullable: nullable), + .init(properties: propertiesDict)) } } extension ToOneRelationship: OpenAPINodeType { // TODO: const for json `type` // TODO: metadata & links - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { let nullable = Identifiable.self is _Optional.Type return .object(.init(format: .generic, required: true), .init(properties: [ - "data": .object(.init(format: .generic, - required: true, - nullable: nullable), - .init(properties: [ - "id": .string(.init(format: .generic, - required: true), - .init()), - "type": .string(.init(format: .generic, - required: true), - .init()) - ])) + "data": ToOneRelationship.relationshipNode(nullable: nullable) ])) } } @@ -60,22 +90,38 @@ extension ToOneRelationship: OpenAPINodeType { extension ToManyRelationship: OpenAPINodeType { // TODO: const for json `type` // TODO: metadata & links - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .object(.init(format: .generic, required: true), .init(properties: [ "data": .array(.init(format: .generic, required: true), - .init(items: .object(.init(format: .generic, - required: true), - .init(properties: [ - "id": .string(.init(format: .generic, - required: true), - .init()), - "type": .string(.init(format: .generic, - required: true), - .init()) - ])))) + .init(items: ToManyRelationship.relationshipNode(nullable: false))) ])) } } + +extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { + public static func openAPINode() throws -> JSONNode { + let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self + ? nil + : try Description.Attributes.genericObjectOpenAPINode() + + let attributesProperty = attributesNode.map { ("attributes", $0) } + + let relationshipsNode: JSONNode? = Description.Relationships.self == NoRelationships.self + ? nil + : try Description.Relationships.genericObjectOpenAPINode() + + let relationshipsProperty = relationshipsNode.map { ("relationships", $0) } + + let propertiesDict = Dictionary([ + attributesProperty, + relationshipsProperty + ].compactMap { $0 }) { _, value in value } + + return .object(.init(format: .generic, + required: true), + .init(properties: propertiesDict)) + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index c039975..1be08c4 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -7,8 +7,39 @@ import AnyCodable +// MARK: Node (i.e. schema) Protocols + +/// Anything conforming to `OpenAPINodeType` can provide an +/// OpenAPI schema representing itself. public protocol OpenAPINodeType { - static var openAPINode: JSONNode { get } + static func openAPINode() throws -> JSONNode +} + +/// Anything conforming to `RawOpenAPINodeType` can provide an +/// OpenAPI schema representing itself. This second 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 Raw Representability, hence the name of this protocol. +public protocol RawOpenAPINodeType { + static func openAPINode() throws -> JSONNode +} + +/// Anything conforming to `AnyJSONCaseIterable` can provide a +/// list of its possible values. +public protocol AnyJSONCaseIterable { + static var allCases: [Any] { get } +} + +/// Anything conforming to `AnyJSONCaseIterable` can provide a +/// list of its possible values. This second protocol is +/// necessary so that one type can conditionally provide a +/// list of possible values and then (under different conditions) +/// provide a different list of possible values. +/// The "different" conditions have to do +/// with Optionality, hence the name of this protocol. +public protocol AnyWrappedJSONCaseIterable { + static var allCases: [Any] { get } } public protocol SwiftTyped { @@ -210,6 +241,14 @@ public enum JSONNode { nullable: true, allowedValues: allowedValues) } + + /// Return this context with the given list of possible values + public func with(allowedValues: [Format.SwiftType]?) -> Context { + return .init(format: format, + required: required, + nullable: nullable, + allowedValues: allowedValues) + } } public struct NumericContext { @@ -393,4 +432,64 @@ public enum JSONNode { return self } } + + public func with(allowedValues: [T]) throws -> JSONNode where T: RawRepresentable, T.RawValue == String { + return try with(allowedValues: allowedValues.map { $0.rawValue }) + } + + public func with(allowedValues: [JSONTypeFormat.BooleanFormat.SwiftType]) throws -> JSONNode { + guard case let .boolean(contextA) = self else { + throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.BooleanFormat.SwiftType.self) + } + return .boolean(contextA.with(allowedValues: allowedValues)) + } + + public func with(allowedValues: [JSONTypeFormat.ObjectFormat.SwiftType]) throws -> JSONNode { + guard case let .object(contextA, contextB) = self else { + throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.ObjectFormat.SwiftType.self) + } + return .object(contextA.with(allowedValues: allowedValues), contextB) + } + + public func with(allowedValues: [JSONTypeFormat.ArrayFormat.SwiftType]) throws -> JSONNode { + guard case let .array(contextA, contextB) = self else { + throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.ArrayFormat.SwiftType.self) + } + return .array(contextA.with(allowedValues: allowedValues), contextB) + } + + public func with(allowedValues: [JSONTypeFormat.NumberFormat.SwiftType]) throws -> JSONNode { + guard case let .number(contextA, contextB) = self else { + throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.NumberFormat.SwiftType.self) + } + return .number(contextA.with(allowedValues: allowedValues), contextB) + } + + public func with(allowedValues: [JSONTypeFormat.IntegerFormat.SwiftType]) throws -> JSONNode { + guard case let .integer(contextA, contextB) = self else { + throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.IntegerFormat.SwiftType.self) + } + return .integer(contextA.with(allowedValues: allowedValues), contextB) + } + + public func with(allowedValues: [JSONTypeFormat.StringFormat.SwiftType]) throws -> JSONNode { + guard case let .string(contextA, contextB) = self else { + throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.StringFormat.SwiftType.self) + } + return .string(contextA.with(allowedValues: allowedValues), contextB) + } +} + +public struct AllowedValueError: Swift.Error, CustomStringConvertible { + public let expectation: JSONType? + public let receivedType: Any.Type + + public init(expectation: JSONType?, receivedType: Any.Type) { + self.expectation = expectation + self.receivedType = receivedType + } + + public var description: String { + return "Expected type compatible with JSON Type \(String(describing: expectation)) but found \(receivedType)" + } } diff --git a/Sources/JSONAPIOpenAPI/Optional+ZipWith.swift b/Sources/JSONAPIOpenAPI/Optional+ZipWith.swift new file mode 100644 index 0000000..cbe113e --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Optional+ZipWith.swift @@ -0,0 +1,10 @@ +// +// Optional+ZipWith.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/19/19. +// + +func zip(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? { + return left.flatMap { lft in right.map { rght in fn(lft, rght) }} +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift new file mode 100644 index 0000000..5e700a3 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable.swift @@ -0,0 +1,122 @@ +// +// Sampleable.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/15/19. +// + +import JSONAPI +import AnyCodable + +/// A Sampleable type can provide a sample value. +/// This is useful for reflection. +public protocol Sampleable { + static var sample: Self { get } +} + +extension Sampleable { + public static func genericObjectOpenAPINode() throws -> JSONNode { + let mirror = Mirror(reflecting: Self.sample) + let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in + + // see if we can enumerate the possible values + let maybeAllCases: [Any]? = { + switch type(of: child.value) { + case let valType as AnyJSONCaseIterable.Type: + return valType.allCases + case let valType as AnyWrappedJSONCaseIterable.Type: + return valType.allCases + default: + return nil + } + }() + + // try to snag an OpenAPI Node + let maybeOpenAPINode: JSONNode? = try { + switch type(of: child.value) { + case let valType as OpenAPINodeType.Type: + return try valType.openAPINode() + + case let valType as RawOpenAPINodeType.Type: + return try valType.openAPINode() + + default: + return nil + } + }() + + // put it all together + let newNode: JSONNode? + if let allCases = maybeAllCases, + let openAPINode = maybeOpenAPINode { + newNode = try { + if let cases = allCases as? [JSONTypeFormat.BooleanFormat.SwiftType] { + return try openAPINode.with(allowedValues: cases) + + } else if let cases = allCases as? [JSONTypeFormat.ArrayFormat.SwiftType] { + return try openAPINode.with(allowedValues: cases) + + } else if let cases = allCases as? [JSONTypeFormat.ObjectFormat.SwiftType] { + return try openAPINode.with(allowedValues: cases) + + } else if let cases = allCases as? [JSONTypeFormat.NumberFormat.SwiftType] { + return try openAPINode.with(allowedValues: cases) + + } else if let cases = allCases as? [JSONTypeFormat.IntegerFormat.SwiftType] { + return try openAPINode.with(allowedValues: cases) + + } else if let cases = allCases as? [JSONTypeFormat.StringFormat.SwiftType] { + return try openAPINode.with(allowedValues: cases) + + } else if allCases.compactMap({ $0 as? RawStringRepresentable }).count == allCases.count { + return try openAPINode.with(allowedValues: allCases.compactMap { ($0 as? RawStringRepresentable)?.rawValue }) + + } else { + throw SampleableError.allowedValuesNotOfExpectedType(forNode: openAPINode, allowedValues: allCases) + } + }() + } else { + newNode = maybeOpenAPINode + } + + return zip(child.label, newNode) { ($0, $1) } + } + + // There should not be any duplication of keys since these are + // property names, but rather than risk runtime exception, we just + // fail to the newer value arbitrarily + let propertiesDict = Dictionary(properties) { _, value2 in value2 } + + return .object(.init(format: .generic, + required: true), + .init(properties: propertiesDict)) + } +} + +extension NoAttributes: Sampleable { + public static var sample: NoAttributes { + return .none + } +} + +extension NoRelationships: Sampleable { + public static var sample: NoRelationships { + return .none + } +} + +extension NoMetadata: Sampleable { + public static var sample: NoMetadata { + return .none + } +} + +extension NoLinks: Sampleable { + public static var sample: NoLinks { + return .none + } +} + +public enum SampleableError: Swift.Error { + case allowedValuesNotOfExpectedType(forNode: JSONNode, allowedValues: [Any]) +} diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index ce7fe6b..1fe6559 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -30,13 +30,25 @@ Any object: **/ extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { - static public var openAPINode: JSONNode { - return Wrapped.openAPINode.optionalNode() + static public func openAPINode() throws -> JSONNode { + return try Wrapped.openAPINode().optionalNode() + } +} + +extension Optional: RawOpenAPINodeType where Wrapped: RawRepresentable, Wrapped.RawValue: OpenAPINodeType { + static public func openAPINode() throws -> JSONNode { + return try Wrapped.RawValue.openAPINode().optionalNode() + } +} + +extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable { + public static var allCases: [Any] { + return Array(Wrapped.allCases) } } extension String: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .string(.init(format: .generic, required: true), .init()) @@ -44,22 +56,22 @@ extension String: OpenAPINodeType { } extension Bool: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .boolean(.init(format: .generic, required: true)) } } extension Array: OpenAPINodeType where Element: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .array(.init(format: .generic, required: true), - .init(items: Element.openAPINode)) + .init(items: try Element.openAPINode())) } } extension Double: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .number(.init(format: .double, required: true), .init()) @@ -67,7 +79,7 @@ extension Double: OpenAPINodeType { } extension Float: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .number(.init(format: .float, required: true), .init()) @@ -75,7 +87,7 @@ extension Float: OpenAPINodeType { } extension Int: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .integer(.init(format: .generic, required: true), .init()) @@ -83,7 +95,7 @@ extension Int: OpenAPINodeType { } extension Int32: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .integer(.init(format: .int32, required: true), .init()) @@ -91,7 +103,7 @@ extension Int32: OpenAPINodeType { } extension Int64: OpenAPINodeType { - static public var openAPINode: JSONNode { + static public func openAPINode() throws -> JSONNode { return .integer(.init(format: .int64, required: true), .init()) diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift new file mode 100644 index 0000000..c7d325f --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -0,0 +1,84 @@ +// +// JSONAPIEntityOpenAPITests.swift +// JSONAPIOpenAPITests +// +// Created by Mathew Polzin on 1/15/19. +// + +import XCTest +import JSONAPI +import JSONAPIOpenAPI + +class JSONAPIEntityOpenAPITests: XCTestCase { + func test_EmptyEntity() { + let node = try! TestType1.openAPINode() + + // TODO: Write test + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let string = String(data: try! encoder.encode(node), encoding: .utf8)! + print(string) + } + + func test_AttributesEntity() { + + let tmp = ["hello"] as [Any] + let tmp2 = tmp as! [String] + let tmp3 = tmp as? RawStringArrayRepresentable + let tmp4 = tmp2 as? RawStringArrayRepresentable + + let y = TestType2Description.EnumType.one + let z = y as Any + + let x = [y as? TestType2Description.EnumType] + + let node = try! TestType2.openAPINode() + + // TODO: Write test + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let string = String(data: try! encoder.encode(node), encoding: .utf8)! + print(string) + } +} + +// MARK: Test Types +extension JSONAPIEntityOpenAPITests { + enum TestType1Description: EntityDescription { + public static var jsonType: String { return "test1" } + + public typealias Attributes = NoAttributes + + public typealias Relationships = NoRelationships + } + + typealias TestType1 = BasicEntity + + enum TestType2Description: EntityDescription { + public static var jsonType: String { return "test1" } + + public enum EnumType: String, CaseIterable, Codable, Equatable { + case one + case two + } + + public struct Attributes: JSONAPI.Attributes, Sampleable { + let stringProperty: Attribute + let enumProperty: Attribute + var computedProperty: Attribute { + return enumProperty + } + + public static var sample: Attributes { + return Attributes(stringProperty: .init(value: "hello"), + enumProperty: .init(value: .one)) + } + } + + public typealias Relationships = NoRelationships + } + + typealias TestType2 = BasicEntity +} diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift index 019af16..ca51f8d 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIRelationshipsOpenAPITests.swift @@ -14,7 +14,7 @@ import JSONAPIOpenAPI class JSONAPIRelationshipsOpenAPITests: XCTestCase { func test_ToOne() { - let node = ToOneRelationship.openAPINode + let node = try! ToOneRelationship.openAPINode() XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -47,7 +47,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase { } func test_OptionalToOne() { - let node = ToOneRelationship?.openAPINode + let node = try! ToOneRelationship?.openAPINode() XCTAssertFalse(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -80,7 +80,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase { } func test_NullableToOne() { - let node = ToOneRelationship.openAPINode + let node = try! ToOneRelationship.openAPINode() XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -113,7 +113,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase { } func test_OptionalNullableToOne() { - let node = ToOneRelationship?.openAPINode + let node = try! ToOneRelationship?.openAPINode() XCTAssertFalse(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -146,7 +146,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase { } func test_ToMany() { - let node = ToManyRelationship.openAPINode + let node = try! ToManyRelationship.openAPINode() XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -189,7 +189,7 @@ class JSONAPIRelationshipsOpenAPITests: XCTestCase { } func test_OptionalToMany() { - let node = ToManyRelationship?.openAPINode + let node = try! ToManyRelationship?.openAPINode() XCTAssertFalse(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) From dc42ec27fc1950ad389ff6344691633b6ec111e3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 15:39:54 -0800 Subject: [PATCH 13/22] Not crazy about how I got there, but now it is relatively easy to print arbitrary enum's allCases as the list of possible values in the format specced out by OpenAPI. --- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 9 +- Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 87 +++++++++---------- Sources/JSONAPIOpenAPI/Sampleable.swift | 54 ++++++------ .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 8 +- .../JSONAPIEntityOpenAPITests.swift | 11 --- 5 files changed, 77 insertions(+), 92 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index aa36007..5b2e24c 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -6,6 +6,7 @@ // import JSONAPI +import AnyCodable private protocol _Optional {} extension Optional: _Optional {} @@ -32,14 +33,14 @@ extension Attribute: RawOpenAPINodeType where RawValue: RawRepresentable, RawVal } } -extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable { - public static var allCases: [Any] { - return Array(RawValue.allCases) +extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable, RawValue: Codable { + public static var allCases: [AnyCodable] { + return (try? allCases(from: Array(RawValue.allCases))) ?? [] } } extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseIterable { - public static var allCases: [Any] { + public static var allCases: [AnyCodable] { return RawValue.allCases } } diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 1be08c4..c4b410d 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -6,6 +6,7 @@ // import AnyCodable +import Foundation // MARK: Node (i.e. schema) Protocols @@ -28,7 +29,20 @@ public protocol RawOpenAPINodeType { /// Anything conforming to `AnyJSONCaseIterable` can provide a /// list of its possible values. public protocol AnyJSONCaseIterable { - static var allCases: [Any] { get } + static var allCases: [AnyCodable] { get } +} + +extension AnyJSONCaseIterable { + static func allCases(from input: [T]) throws -> [AnyCodable] { + if let alreadyGoodToGo = input as? [AnyCodable] { + return alreadyGoodToGo + } + + guard let arrayOfCodables = try JSONSerialization.jsonObject(with: JSONEncoder().encode(input), options: []) as? [Any] else { + throw CodableError.allCasesArrayNotCodable + } + return arrayOfCodables.map(AnyCodable.init) + } } /// Anything conforming to `AnyJSONCaseIterable` can provide a @@ -39,7 +53,7 @@ public protocol AnyJSONCaseIterable { /// The "different" conditions have to do /// with Optionality, hence the name of this protocol. public protocol AnyWrappedJSONCaseIterable { - static var allCases: [Any] { get } + static var allCases: [AnyCodable] { get } } public protocol SwiftTyped { @@ -206,12 +220,12 @@ public enum JSONNode { /// The OpenAPI spec calls this "enum" /// If not specified, it is assumed that any /// value of the given format is allowed. - public let allowedValues: [Format.SwiftType]? + public let allowedValues: [AnyCodable]? public init(format: Format, required: Bool, nullable: Bool = false, - allowedValues: [Format.SwiftType]? = nil) { + allowedValues: [AnyCodable]? = nil) { self.format = format self.required = required self.nullable = nullable @@ -243,7 +257,7 @@ public enum JSONNode { } /// Return this context with the given list of possible values - public func with(allowedValues: [Format.SwiftType]?) -> Context { + public func with(allowedValues: [AnyCodable]?) -> Context { return .init(format: format, required: required, nullable: nullable, @@ -433,50 +447,24 @@ public enum JSONNode { } } - public func with(allowedValues: [T]) throws -> JSONNode where T: RawRepresentable, T.RawValue == String { - return try with(allowedValues: allowedValues.map { $0.rawValue }) - } + public func with(allowedValues: [AnyCodable]) throws -> JSONNode { - public func with(allowedValues: [JSONTypeFormat.BooleanFormat.SwiftType]) throws -> JSONNode { - guard case let .boolean(contextA) = self else { - throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.BooleanFormat.SwiftType.self) + switch self { + case .boolean(let context): + return .boolean(context.with(allowedValues: allowedValues)) + case .object(let contextA, let contextB): + return .object(contextA.with(allowedValues: allowedValues), contextB) + case .array(let contextA, let contextB): + return .array(contextA.with(allowedValues: allowedValues), contextB) + case .number(let context, let contextB): + return .number(context.with(allowedValues: allowedValues), contextB) + case .integer(let context, let contextB): + return .integer(context.with(allowedValues: allowedValues), contextB) + case .string(let context, let contextB): + return .string(context.with(allowedValues: allowedValues), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self } - return .boolean(contextA.with(allowedValues: allowedValues)) - } - - public func with(allowedValues: [JSONTypeFormat.ObjectFormat.SwiftType]) throws -> JSONNode { - guard case let .object(contextA, contextB) = self else { - throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.ObjectFormat.SwiftType.self) - } - return .object(contextA.with(allowedValues: allowedValues), contextB) - } - - public func with(allowedValues: [JSONTypeFormat.ArrayFormat.SwiftType]) throws -> JSONNode { - guard case let .array(contextA, contextB) = self else { - throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.ArrayFormat.SwiftType.self) - } - return .array(contextA.with(allowedValues: allowedValues), contextB) - } - - public func with(allowedValues: [JSONTypeFormat.NumberFormat.SwiftType]) throws -> JSONNode { - guard case let .number(contextA, contextB) = self else { - throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.NumberFormat.SwiftType.self) - } - return .number(contextA.with(allowedValues: allowedValues), contextB) - } - - public func with(allowedValues: [JSONTypeFormat.IntegerFormat.SwiftType]) throws -> JSONNode { - guard case let .integer(contextA, contextB) = self else { - throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.IntegerFormat.SwiftType.self) - } - return .integer(contextA.with(allowedValues: allowedValues), contextB) - } - - public func with(allowedValues: [JSONTypeFormat.StringFormat.SwiftType]) throws -> JSONNode { - guard case let .string(contextA, contextB) = self else { - throw AllowedValueError(expectation: jsonTypeFormat?.jsonType, receivedType: JSONTypeFormat.StringFormat.SwiftType.self) - } - return .string(contextA.with(allowedValues: allowedValues), contextB) } } @@ -493,3 +481,8 @@ public struct AllowedValueError: Swift.Error, CustomStringConvertible { return "Expected type compatible with JSON Type \(String(describing: expectation)) but found \(receivedType)" } } + +public enum CodableError: Swift.Error { + case codableNotAnyCodable(Any.Type) + case allCasesArrayNotCodable +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift index 5e700a3..437ccfb 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable.swift @@ -20,7 +20,7 @@ extension Sampleable { let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in // see if we can enumerate the possible values - let maybeAllCases: [Any]? = { + let maybeAllCases: [AnyCodable]? = { switch type(of: child.value) { case let valType as AnyJSONCaseIterable.Type: return valType.allCases @@ -49,32 +49,32 @@ extension Sampleable { let newNode: JSONNode? if let allCases = maybeAllCases, let openAPINode = maybeOpenAPINode { - newNode = try { - if let cases = allCases as? [JSONTypeFormat.BooleanFormat.SwiftType] { - return try openAPINode.with(allowedValues: cases) - - } else if let cases = allCases as? [JSONTypeFormat.ArrayFormat.SwiftType] { - return try openAPINode.with(allowedValues: cases) - - } else if let cases = allCases as? [JSONTypeFormat.ObjectFormat.SwiftType] { - return try openAPINode.with(allowedValues: cases) - - } else if let cases = allCases as? [JSONTypeFormat.NumberFormat.SwiftType] { - return try openAPINode.with(allowedValues: cases) - - } else if let cases = allCases as? [JSONTypeFormat.IntegerFormat.SwiftType] { - return try openAPINode.with(allowedValues: cases) - - } else if let cases = allCases as? [JSONTypeFormat.StringFormat.SwiftType] { - return try openAPINode.with(allowedValues: cases) - - } else if allCases.compactMap({ $0 as? RawStringRepresentable }).count == allCases.count { - return try openAPINode.with(allowedValues: allCases.compactMap { ($0 as? RawStringRepresentable)?.rawValue }) - - } else { - throw SampleableError.allowedValuesNotOfExpectedType(forNode: openAPINode, allowedValues: allCases) - } - }() + newNode = try openAPINode.with(allowedValues: allCases) // try { +// if let cases = allCases as? [JSONTypeFormat.BooleanFormat.SwiftType] { +// return try openAPINode.with(allowedValues: cases) +// +// } else if let cases = allCases as? [JSONTypeFormat.ArrayFormat.SwiftType] { +// return try openAPINode.with(allowedValues: cases) +// +// } else if let cases = allCases as? [JSONTypeFormat.ObjectFormat.SwiftType] { +// return try openAPINode.with(allowedValues: cases) +// +// } else if let cases = allCases as? [JSONTypeFormat.NumberFormat.SwiftType] { +// return try openAPINode.with(allowedValues: cases) +// +// } else if let cases = allCases as? [JSONTypeFormat.IntegerFormat.SwiftType] { +// return try openAPINode.with(allowedValues: cases) +// +// } else if let cases = allCases as? [JSONTypeFormat.StringFormat.SwiftType] { +// return try openAPINode.with(allowedValues: cases) +// +// } else if allCases.compactMap({ $0 as? RawStringRepresentable }).count == allCases.count { +// return try openAPINode.with(allowedValues: allCases.compactMap { ($0 as? RawStringRepresentable)?.rawValue }) +// +// } else { +// throw SampleableError.allowedValuesNotOfExpectedType(forNode: openAPINode, allowedValues: allCases) +// } +// }() } else { newNode = maybeOpenAPINode } diff --git a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift index 1fe6559..1b09ab8 100644 --- a/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift +++ b/Sources/JSONAPIOpenAPI/SwiftPrimitiveTypes.swift @@ -5,6 +5,8 @@ // Created by Mathew Polzin on 01/13/19. // +import AnyCodable + /** Notable omissions in this library's default offerings: @@ -41,9 +43,9 @@ extension Optional: RawOpenAPINodeType where Wrapped: RawRepresentable, Wrapped. } } -extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable { - public static var allCases: [Any] { - return Array(Wrapped.allCases) +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/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift index c7d325f..298100c 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -22,17 +22,6 @@ class JSONAPIEntityOpenAPITests: XCTestCase { } func test_AttributesEntity() { - - let tmp = ["hello"] as [Any] - let tmp2 = tmp as! [String] - let tmp3 = tmp as? RawStringArrayRepresentable - let tmp4 = tmp2 as? RawStringArrayRepresentable - - let y = TestType2Description.EnumType.one - let z = y as Any - - let x = [y as? TestType2Description.EnumType] - let node = try! TestType2.openAPINode() // TODO: Write test From 531a7f35bd10cb9142f1885ccc666f12000e0608 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 15:50:46 -0800 Subject: [PATCH 14/22] Cleanup and a bit of documentation --- Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 35 ++++++++++++----------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index c4b410d..5545e36 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -33,13 +33,22 @@ public protocol AnyJSONCaseIterable { } extension AnyJSONCaseIterable { + /// Given an array of Codable values, retrieve an array of AnyCodables. static func allCases(from input: [T]) throws -> [AnyCodable] { if let alreadyGoodToGo = input as? [AnyCodable] { return alreadyGoodToGo } + // The following is messy, but it does get us the intended result: + // Given any array of things that can be encoded, we want + // to map to an array of AnyCodable so we can store later. We need to + // muck with JSONSerialization because something like an `enum` may + // very well be encoded as a string, and therefore representable + // by AnyCodable, but AnyCodable wants it to actually BE a String + // upon initialization. + guard let arrayOfCodables = try JSONSerialization.jsonObject(with: JSONEncoder().encode(input), options: []) as? [Any] else { - throw CodableError.allCasesArrayNotCodable + throw OpenAPICodableError.allCasesArrayNotCodable } return arrayOfCodables.map(AnyCodable.init) } @@ -220,6 +229,13 @@ public enum JSONNode { /// The OpenAPI spec calls this "enum" /// If not specified, it is assumed that any /// value of the given format is allowed. + /// NOTE: I would like the array of allowed + /// values to have the type `Format.SwiftType` + /// but this is not tractable because I also + /// want to be able to automatically turn any + /// Swift type that will get _encoded as + /// something compatible with_ `Format.SwiftType` + /// into an allowed value. public let allowedValues: [AnyCodable]? public init(format: Format, @@ -468,21 +484,6 @@ public enum JSONNode { } } -public struct AllowedValueError: Swift.Error, CustomStringConvertible { - public let expectation: JSONType? - public let receivedType: Any.Type - - public init(expectation: JSONType?, receivedType: Any.Type) { - self.expectation = expectation - self.receivedType = receivedType - } - - public var description: String { - return "Expected type compatible with JSON Type \(String(describing: expectation)) but found \(receivedType)" - } -} - -public enum CodableError: Swift.Error { - case codableNotAnyCodable(Any.Type) +public enum OpenAPICodableError: Swift.Error { case allCasesArrayNotCodable } From 1ed34b00dc519dd2c838ee3431f3ec193ee5f89c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 16:10:31 -0800 Subject: [PATCH 15/22] Add todos for Entity Id and type. Add example usage of OpenAPI entity schema support in Playground --- .../Contents.swift | 13 +++++++++ .../Sources/OpenAPISupport.swift | 16 +++++++++++ JSONAPI.playground/contents.xcplayground | 3 +++ .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 1 + Sources/JSONAPIOpenAPI/Sampleable.swift | 27 +------------------ 5 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift create mode 100644 JSONAPI.playground/Sources/OpenAPISupport.swift diff --git a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..c1b6cc9 --- /dev/null +++ b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift @@ -0,0 +1,13 @@ +//: [Previous](@previous) + +import Foundation +import JSONAPI +import JSONAPIOpenAPI + +// print Entity Schema +let encoder = JSONEncoder() +encoder.outputFormatting = .prettyPrinted + +let personSchemaData = try? encoder.encode(Person.openAPINode()) + +print(personSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed") diff --git a/JSONAPI.playground/Sources/OpenAPISupport.swift b/JSONAPI.playground/Sources/OpenAPISupport.swift new file mode 100644 index 0000000..5b7e895 --- /dev/null +++ b/JSONAPI.playground/Sources/OpenAPISupport.swift @@ -0,0 +1,16 @@ +import Foundation +import JSONAPI +import JSONAPITesting // for the convenience of literal initialization +import JSONAPIOpenAPI + +extension PersonDescription.Attributes: Sampleable { + public static var sample: PersonDescription.Attributes { + return .init(name: ["Abbie", "Eibba"], favoriteColor: "Blue") + } +} + +extension PersonDescription.Relationships: Sampleable { + public static var sample: PersonDescription.Relationships { + return .init(friends: ["1", "2"], dogs: ["2"], home: "1") + } +} diff --git a/JSONAPI.playground/contents.xcplayground b/JSONAPI.playground/contents.xcplayground index 8df8fa3..a1ec109 100644 --- a/JSONAPI.playground/contents.xcplayground +++ b/JSONAPI.playground/contents.xcplayground @@ -3,5 +3,8 @@ + + + \ No newline at end of file diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 5b2e24c..01702b6 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -104,6 +104,7 @@ extension ToManyRelationship: OpenAPINodeType { extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { public static func openAPINode() throws -> JSONNode { + // TODO: Id, type, metadata, links let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self ? nil : try Description.Attributes.genericObjectOpenAPINode() diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift index 437ccfb..c7fa194 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable.swift @@ -49,32 +49,7 @@ extension Sampleable { let newNode: JSONNode? if let allCases = maybeAllCases, let openAPINode = maybeOpenAPINode { - newNode = try openAPINode.with(allowedValues: allCases) // try { -// if let cases = allCases as? [JSONTypeFormat.BooleanFormat.SwiftType] { -// return try openAPINode.with(allowedValues: cases) -// -// } else if let cases = allCases as? [JSONTypeFormat.ArrayFormat.SwiftType] { -// return try openAPINode.with(allowedValues: cases) -// -// } else if let cases = allCases as? [JSONTypeFormat.ObjectFormat.SwiftType] { -// return try openAPINode.with(allowedValues: cases) -// -// } else if let cases = allCases as? [JSONTypeFormat.NumberFormat.SwiftType] { -// return try openAPINode.with(allowedValues: cases) -// -// } else if let cases = allCases as? [JSONTypeFormat.IntegerFormat.SwiftType] { -// return try openAPINode.with(allowedValues: cases) -// -// } else if let cases = allCases as? [JSONTypeFormat.StringFormat.SwiftType] { -// return try openAPINode.with(allowedValues: cases) -// -// } else if allCases.compactMap({ $0 as? RawStringRepresentable }).count == allCases.count { -// return try openAPINode.with(allowedValues: allCases.compactMap { ($0 as? RawStringRepresentable)?.rawValue }) -// -// } else { -// throw SampleableError.allowedValuesNotOfExpectedType(forNode: openAPINode, allowedValues: allCases) -// } -// }() + newNode = try openAPINode.with(allowedValues: allCases) } else { newNode = maybeOpenAPINode } From 875d938b95ae92382e0957698bce5dab438da1d7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 16:19:26 -0800 Subject: [PATCH 16/22] Add id and type properties to Entity OpenAPI Schema --- Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift index 01702b6..636184a 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift @@ -104,7 +104,18 @@ extension ToManyRelationship: OpenAPINodeType { extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { public static func openAPINode() throws -> JSONNode { - // TODO: Id, type, metadata, links + // TODO: metadata, links + + let idNode = JSONNode.string(.init(format: .generic, + required: true), + .init()) + let idProperty = ("id", idNode) + + let typeNode = JSONNode.string(.init(format: .generic, + required: true), + .init()) + let typeProperty = ("type", typeNode) + let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self ? nil : try Description.Attributes.genericObjectOpenAPINode() @@ -118,6 +129,8 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc let relationshipsProperty = relationshipsNode.map { ("relationships", $0) } let propertiesDict = Dictionary([ + idProperty, + typeProperty, attributesProperty, relationshipsProperty ].compactMap { $0 }) { _, value in value } From 2de6580519eef70d2beec05aa6706dfa0ef37925 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 18:18:35 -0800 Subject: [PATCH 17/22] Add some tests around Attribute OpenAPI descriptions --- .../JSONAPIOpenAPI/JSONAPIOpenAPITypes.swift | 18 +- Sources/JSONAPIOpenAPI/OpenAPITypes.swift | 35 +- Sources/JSONAPIOpenAPI/Sampleable.swift | 5 +- .../JSONAPIOpenAPI/SwiftPrimitiveTypes.swift | 14 +- .../JSONAPIAttributeOpenAPITests.swift | 513 ++++++++++++++++++ .../JSONAPIOpenAPITests/XCTestManifests.swift | 59 ++ Tests/LinuxMain.swift | 6 +- 7 files changed, 638 insertions(+), 12 deletions(-) create mode 100644 Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift create mode 100644 Tests/JSONAPIOpenAPITests/XCTestManifests.swift 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) From e3b005b89a319d64e087062642f9a38847a8c659 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 18:25:28 -0800 Subject: [PATCH 18/22] Update README --- README.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5d0ada7..151b691 100644 --- a/README.md +++ b/README.md @@ -123,20 +123,17 @@ Note that Playground support for importing non-system Frameworks is still a bit #### Resource Object - `id` - [x] Encoding/Decoding - - [ ] Arbitrary - - [ ] OpenAPI + - [x] Arbitrary + - [x] OpenAPI - `type` - [x] Encoding/Decoding - - [ ] Arbitrary - - [ ] OpenAPI + - [x] OpenAPI - `attributes` - [x] Encoding/Decoding - - [ ] Arbitrary - - [ ] OpenAPI + - [x] OpenAPI - `relationships` - [x] Encoding/Decoding - - [ ] Arbitrary - - [ ] OpenAPI + - [x] OpenAPI - `links` - [x] Encoding/Decoding - [ ] Arbitrary From 8939d637d49980fa64f176e07eacb798b9de5a1b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 18:34:24 -0800 Subject: [PATCH 19/22] Add placeholder test to arbitrary test target. --- .../PlaceholderTests.swift | 15 ++++++++++++++ .../XCTestManifests.swift | 15 ++++++++++++++ .../JSONAPIOpenAPITests/XCTestManifests.swift | 20 +++++++++++++++++++ Tests/LinuxMain.swift | 8 ++++++-- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 Tests/JSONAPIArbitraryTests/PlaceholderTests.swift create mode 100644 Tests/JSONAPIArbitraryTests/XCTestManifests.swift create mode 100644 Tests/JSONAPIOpenAPITests/XCTestManifests.swift diff --git a/Tests/JSONAPIArbitraryTests/PlaceholderTests.swift b/Tests/JSONAPIArbitraryTests/PlaceholderTests.swift new file mode 100644 index 0000000..61ceaa0 --- /dev/null +++ b/Tests/JSONAPIArbitraryTests/PlaceholderTests.swift @@ -0,0 +1,15 @@ +// +// PlaceholderTests.swift +// JSONAPIArbitraryTests +// +// Created by Mathew Polzin on 12/7/18. +// + +import XCTest +import JSONAPIArbitrary + +class PlaceholderTests: XCTestCase { + func test_Placeholder() { + + } +} diff --git a/Tests/JSONAPIArbitraryTests/XCTestManifests.swift b/Tests/JSONAPIArbitraryTests/XCTestManifests.swift new file mode 100644 index 0000000..acb4b36 --- /dev/null +++ b/Tests/JSONAPIArbitraryTests/XCTestManifests.swift @@ -0,0 +1,15 @@ +import XCTest + +extension PlaceholderTests { + static let __allTests = [ + ("test_Placeholder", test_Placeholder), + ] +} + +#if !os(macOS) +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(PlaceholderTests.__allTests), + ] +} +#endif diff --git a/Tests/JSONAPIOpenAPITests/XCTestManifests.swift b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift new file mode 100644 index 0000000..fe77ce5 --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/XCTestManifests.swift @@ -0,0 +1,20 @@ +import XCTest + +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(JSONAPIRelationshipsOpenAPITests.__allTests), + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index ea07a7d..791d328 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,10 +1,14 @@ import XCTest import JSONAPITests -import JSONAPITestLibTests +import JSONAPITestingTests +import JSONAPIOpenAPITests +import JSONAPIArbitraryTests var tests = [XCTestCaseEntry]() tests += JSONAPITests.__allTests() -tests += JSONAPITestLibTests.__allTests() +tests += JSONAPITestingTests.__allTests() +tests += JSONAPIOpenAPITests.__allTests() +tests += JSONAPIArbitraryTests.__allTests() XCTMain(tests) From 1d8ceef317bc33241c25cb62c25a51ac3d788a24 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 18:52:21 -0800 Subject: [PATCH 20/22] Update README, add a bit more code documentation --- README.md | 4 ++-- Sources/JSONAPIArbitrary/Entity+Arbitrary.swift | 5 +++++ Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift | 8 ++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 151b691..1b35e86 100644 --- a/README.md +++ b/README.md @@ -136,11 +136,11 @@ Note that Playground support for importing non-system Frameworks is still a bit - [x] OpenAPI - `links` - [x] Encoding/Decoding - - [ ] Arbitrary + - [x] Arbitrary - [ ] OpenAPI - `meta` - [x] Encoding/Decoding - - [ ] Arbitrary + - [x] Arbitrary - [ ] OpenAPI #### Relationship Object diff --git a/Sources/JSONAPIArbitrary/Entity+Arbitrary.swift b/Sources/JSONAPIArbitrary/Entity+Arbitrary.swift index b686057..ac22b5f 100644 --- a/Sources/JSONAPIArbitrary/Entity+Arbitrary.swift +++ b/Sources/JSONAPIArbitrary/Entity+Arbitrary.swift @@ -32,6 +32,11 @@ extension NoRelationships: Arbitrary { } } +// NOTE: Arbitrary conformance for MetaType, LinksType, Description.Attributes, +// and Description.Relationships must all be provided BY YOU for Entity to +// gain Arbitrary conformance (with the exception of NoMetadata, NoLinks, +// NoAttributes, and NoRelationships which all have Arbitrary conformance +// out of the box). extension Entity: Arbitrary where MetaType: Arbitrary, LinksType: Arbitrary, Description.Attributes: Arbitrary, Description.Relationships: Arbitrary, EntityRawIdType: Arbitrary { public static var arbitrary: Gen> { return Gen.compose { c in diff --git a/Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift b/Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift index df1dd75..a1a8255 100644 --- a/Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift +++ b/Sources/JSONAPIArbitrary/Relationship+Arbitrary.swift @@ -19,6 +19,10 @@ extension ToOneRelationship: Arbitrary where Identifiable.Identifier: Arbitrary, } extension ToOneRelationship where MetaType: Arbitrary, LinksType: Arbitrary { + /// Create a generator of arbitrary ToOneRelationships that will all + /// point to one of the given entities. This allows you to create + /// arbitrary relationships that make sense in a broader context where + /// the relationship must actually point to another entity. public static func arbitrary(givenEntities: [E]) -> Gen> where E.Id == Identifiable.Identifier { return Gen.compose { c in @@ -41,6 +45,10 @@ extension ToManyRelationship: Arbitrary where Relatable.Identifier: Arbitrary, M } extension ToManyRelationship where MetaType: Arbitrary, LinksType: Arbitrary { + /// Create a generator of arbitrary ToManyRelationships that will all + /// point to some number of the given entities. This allows you to create + /// arbitrary relationships that make sense in a broader context where + /// the relationship must actually point to other existing entities. public static func arbitrary(givenEntities: [E]) -> Gen> where E.Id == Relatable.Identifier { return Gen.compose { c in let idsGen = Gen.fromElements(of: givenEntities).map { $0.id }.proliferate From 15e13e2cc77a377a23a60c2aa2a12bf84626179c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 18:54:37 -0800 Subject: [PATCH 21/22] Remove unused error and add a bit of code documentation --- Sources/JSONAPIOpenAPI/Sampleable.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift index 8302ae7..bfcf491 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable.swift @@ -11,6 +11,9 @@ import AnyCodable /// A Sampleable type can provide a sample value. /// This is useful for reflection. public protocol Sampleable { + /// Get a sample value of type Self. This can be the + /// same value every time, or it can be an arbitrarily random + /// value each time. static var sample: Self { get } } @@ -94,7 +97,3 @@ extension NoLinks: Sampleable { return .none } } - -public enum SampleableError: Swift.Error { - case allowedValuesNotOfExpectedType(forNode: JSONNode, allowedValues: [Any]) -} From 1ec4b21c34db29a54e0295c5c772e17755dc68b1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Jan 2019 19:18:10 -0800 Subject: [PATCH 22/22] Add missing case in Sampleable generic OpenAPINode implementation. Add info on new auxiliary frameworks to the README. --- README.md | 22 +++++++++++++++++----- Sources/JSONAPIOpenAPI/Sampleable.swift | 3 +++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1b35e86..a27f196 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) - - [JSONAPITestLib](#jsonapitestlib) + - [JSONAPI+Testing](#jsonapitesting) - [Entity Validator](#entity-validator) - [Potential Improvements](#potential-improvements) - [Usage](#usage) @@ -61,7 +61,9 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) - [Server Pseudo-example](#server-pseudo-example) - [Client Pseudo-example](#client-pseudo-example) - - [JSONAPITestLib](#jsonapitestlib) +- [JSONAPI+Testing](#jsonapitesting) +- [JSONAPI+Arbitrary](#jsonapiarbitrary) +- [JSONAPI+OpenAPI](#jsonapiopenapi) @@ -173,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. -### JSONAPITestLib +### JSONAPI+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. @@ -830,5 +832,15 @@ print(response.article) print(response.author) ``` -## JSONAPITestLib -The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITestLib` in action in the Playground included with the `JSONAPI` repository. +# JSONAPI+Testing +The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. + +# JSONAPI+Arbitrary +The `JSONAPIArbitrary` framework adds `Arbitrary` support via `SwiftCheck`. With a little extra work on your part, this framework will allow you to create "arbitrary" (i.e. randomly generated) instances of your JSONAPI entities, includes, documents, etc. + +This library does not offer full support of all `JSONAPI` types yet. The documentation will grow as the framework becomes more complete. + +# JSONAPI+OpenAPI +The `JSONAPIOpenAPI` framework adds the ability to generate OpenAPI compliant JSON documentation of a JSONAPI Document. + +This library is in its infancy. The documentation will grow as the framework becomes more complete. diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable.swift index bfcf491..328080e 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable.swift @@ -46,6 +46,9 @@ extension Sampleable { case let valType as WrappedRawOpenAPIType.Type: return try valType.wrappedOpenAPINode() + case let valType as DoubleWrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() + default: return nil }