From a88844fe5edf7904d42debd350b227b07aece759 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 23 Jan 2019 00:34:14 -0800 Subject: [PATCH 01/10] Switch to Poly 1.0.0 (same commit as previously used but by version rather than branch) --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 69dfbfa..d2b976b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,9 +23,9 @@ "package": "Poly", "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { - "branch": "master", + "branch": null, "revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07", - "version": null + "version": "1.0.0" } }, { diff --git a/Package.swift b/Package.swift index 3622d82..9359a14 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( targets: ["JSONAPIOpenAPI"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")), + .package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"), .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") ], From 57df6b147e0c9949799aa2234f3a9de536583285 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 23 Jan 2019 11:52:16 -0800 Subject: [PATCH 02/10] Add Sampleable conformance to UnknownJSONAPIError. Add Sampleable conformance to all Include types and the Includes type. --- .../Sampleable/Include+Sampleable.swift | 184 ++++++++++++++++++ .../{ => Sampleable}/Sampleable.swift | 22 +++ 2 files changed, 206 insertions(+) create mode 100644 Sources/JSONAPIOpenAPI/Sampleable/Include+Sampleable.swift rename Sources/JSONAPIOpenAPI/{ => Sampleable}/Sampleable.swift (84%) diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Include+Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Include+Sampleable.swift new file mode 100644 index 0000000..895e561 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/Include+Sampleable.swift @@ -0,0 +1,184 @@ +// +// Include+Sampleable.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/23/19. +// + +import JSONAPI + +extension Includes: Sampleable where I: Sampleable { + public static var sample: Includes { + guard I.self != NoIncludes.self else { + return .none + } + + return .init(values: I.samples) + } +} + +extension NoIncludes: Sampleable { + public static var sample: NoIncludes { + return NoIncludes() + } +} + +extension Include1: Sampleable where A: Sampleable { + public static var sample: Include1 { + return .init(A.sample) + } + + public static var samples: [Include1] { + return A.samples.map(Include1.init) + } +} + +extension Include2: Sampleable where A: Sampleable, B: Sampleable { + public static var sample: Include2 { + let randomChoice = Int.random(in: 0..] { + return A.samples.map(Include2.init) + + B.samples.map(Include2.init) + } +} + +extension Include3: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable { + public static var sample: Include3 { + let randomChoice = Int.random(in: 0..] { + return A.samples.map(Include3.init) + + B.samples.map(Include3.init) + + C.samples.map(Include3.init) + } +} + +extension Include4: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable { + public static var sample: Include4 { + let randomChoice = Int.random(in: 0..] { + return A.samples.map(Include4.init) + + B.samples.map(Include4.init) + + C.samples.map(Include4.init) + + D.samples.map(Include4.init) + } +} + +extension Include5: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable { + public static var sample: Include5 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include5] = A.samples.map(Include5.init) + + B.samples.map(Include5.init) + + C.samples.map(Include5.init) + + let set2: [Include5] = D.samples.map(Include5.init) + + E.samples.map(Include5.init) + + return set1 + set2 + } +} + +extension Include6: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable { + public static var sample: Include6 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include6] = A.samples.map(Include6.init) + + B.samples.map(Include6.init) + + C.samples.map(Include6.init) + + let set2: [Include6] = D.samples.map(Include6.init) + + E.samples.map(Include6.init) + + F.samples.map(Include6.init) + + return set1 + set2 + } +} + +extension Include7: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable { + public static var sample: Include7 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include7] = A.samples.map(Include7.init) + + B.samples.map(Include7.init) + + C.samples.map(Include7.init) + + let set2: [Include7] = D.samples.map(Include7.init) + + E.samples.map(Include7.init) + + F.samples.map(Include7.init) + + let set3: [Include7] = G.samples.map(Include7.init) + + return set1 + set2 + set3 + } +} + +extension Include8: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable, H: Sampleable { + public static var sample: Include8 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include8] = A.samples.map(Include8.init) + + B.samples.map(Include8.init) + + C.samples.map(Include8.init) + + let set2: [Include8] = D.samples.map(Include8.init) + + E.samples.map(Include8.init) + + F.samples.map(Include8.init) + + let set3: [Include8] = G.samples.map(Include8.init) + + H.samples.map(Include8.init) + + return set1 + set2 + set3 + } +} + +extension Include9: Sampleable where A: Sampleable, B: Sampleable, C: Sampleable, D: Sampleable, E: Sampleable, F: Sampleable, G: Sampleable, H: Sampleable, I: Sampleable { + public static var sample: Include9 { + let randomChoice = Int.random(in: 0..] { + let set1: [Include9] = A.samples.map(Include9.init) + + B.samples.map(Include9.init) + + C.samples.map(Include9.init) + + let set2: [Include9] = D.samples.map(Include9.init) + + E.samples.map(Include9.init) + + F.samples.map(Include9.init) + + let set3: [Include9] = G.samples.map(Include9.init) + + H.samples.map(Include9.init) + + I.samples.map(Include9.init) + + return set1 + set2 + set3 + } +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift similarity index 84% rename from Sources/JSONAPIOpenAPI/Sampleable.swift rename to Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift index c730791..0e8fe58 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift @@ -18,17 +18,33 @@ public protocol Sampleable { /// Get an example of success, if that is meaningful and /// available. If not, will be nil. + /// + /// The default implementation returns `nil`. static var successSample: Self? { get } /// Get an example of failure, if that is meaningful and /// available. If not, will be nil. + /// + /// The default implementation returns `nil`. static var failureSample: Self? { get } + + /// An array of samples of this Type. + /// + /// The default implementation returns + /// an array with just the result of + /// `Self.sample` in it. + static var samples: [Self] { get } } public extension Sampleable { + // default implementation: public static var successSample: Self? { return nil } + // default implementation: public static var failureSample: Self? { return nil } + + // default implementation: + public static var samples: [Self] { return [Self.sample] } } extension Sampleable { @@ -114,3 +130,9 @@ extension NoLinks: Sampleable { return .none } } + +extension UnknownJSONAPIError: Sampleable { + public static var sample: UnknownJSONAPIError { + return .unknownError + } +} From 951c04ad4481f62ffc0bebb34e085f26a9347a19 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 23 Jan 2019 22:21:27 -0800 Subject: [PATCH 03/10] Add Sampleable conformances. Make blanket JSONEncoder change I am not happy with; will try to walk back requirement that encoder gets passed to all functions creating OpenAPI Nodes --- .../JSONAPI/JSONAPIInclude+OpenAPI.swift | 115 +++++++++--------- .../JSONAPI/JSONAPITypes+OpenAPI.swift | 65 +++++----- .../JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift | 30 ++--- .../OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift | 39 +++--- .../Sampleable/Sampleable.swift | 88 ++++++++++++-- .../JSONAPIEntityOpenAPITests.swift | 6 +- 6 files changed, 211 insertions(+), 132 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift index 9b9e456..948cd0e 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift @@ -6,10 +6,11 @@ // import JSONAPI +import Foundation extension Includes: OpenAPINodeType where I: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { - let includeNode = try I.openAPINode() + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + let includeNode = try I.openAPINode(using: encoder) return .array(.init(format: .generic, required: true), @@ -19,113 +20,113 @@ extension Includes: OpenAPINodeType where I: OpenAPINodeType { } extension Include0: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { throw OpenAPITypeError.invalidNode } } extension Include1: OpenAPINodeType where A: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { - return try .one(of: [A.openAPINode()]) + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try .one(of: [A.openAPINode(using: encoder)]) } } extension Include2: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder) ]) } } extension Include3: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder) ]) } } extension Include4: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder) ]) } } extension Include5: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder) ]) } } extension Include6: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder) ]) } } extension Include7: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode(), - G.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder), + G.openAPINode(using: encoder) ]) } } extension Include8: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode(), - G.openAPINode(), - H.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder), + G.openAPINode(using: encoder), + H.openAPINode(using: encoder) ]) } } extension Include9: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType, I: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try .one(of: [ - A.openAPINode(), - B.openAPINode(), - C.openAPINode(), - D.openAPINode(), - E.openAPINode(), - F.openAPINode(), - G.openAPINode(), - H.openAPINode(), - I.openAPINode() + A.openAPINode(using: encoder), + B.openAPINode(using: encoder), + C.openAPINode(using: encoder), + D.openAPINode(using: encoder), + E.openAPINode(using: encoder), + F.openAPINode(using: encoder), + G.openAPINode(using: encoder), + H.openAPINode(using: encoder), + I.openAPINode(using: encoder) ]) } } diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index cc5efde..5e0baac 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -6,68 +6,69 @@ // import JSONAPI +import Foundation import AnyCodable private protocol _Optional {} extension Optional: _Optional {} extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) 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.openAPINode().required { - return try RawValue.openAPINode().requiredNode().nullableNode() + if try !RawValue.openAPINode(using: encoder).required { + return try RawValue.openAPINode(using: encoder).requiredNode().nullableNode() } - return try RawValue.openAPINode() + return try RawValue.openAPINode(using: encoder) } } extension Attribute: RawOpenAPINodeType where RawValue: RawRepresentable, RawValue.RawValue: OpenAPINodeType { - static public func rawOpenAPINode() throws -> JSONNode { + static public func rawOpenAPINode(using encoder: JSONEncoder) 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() + if try !RawValue.RawValue.openAPINode(using: encoder).required { + return try RawValue.RawValue.openAPINode(using: encoder).requiredNode().nullableNode() } - return try RawValue.RawValue.openAPINode() + return try RawValue.RawValue.openAPINode(using: encoder) } } extension Attribute: WrappedRawOpenAPIType where RawValue: RawOpenAPINodeType { - public static func wrappedOpenAPINode() throws -> JSONNode { + public static func wrappedOpenAPINode(using encoder: JSONEncoder) 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() + if try !RawValue.rawOpenAPINode(using: encoder).required { + return try RawValue.rawOpenAPINode(using: encoder).requiredNode().nullableNode() } - return try RawValue.rawOpenAPINode() + return try RawValue.rawOpenAPINode(using: encoder) } } extension Attribute: AnyJSONCaseIterable where RawValue: CaseIterable, RawValue: Codable { - public static var allCases: [AnyCodable] { - return (try? allCases(from: Array(RawValue.allCases))) ?? [] + public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { + return (try? allCases(from: Array(RawValue.allCases), using: encoder)) ?? [] } } extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseIterable { - public static var allCases: [AnyCodable] { - return RawValue.allCases + public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { + return RawValue.allCases(using: encoder) } } extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) 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.openAPINode().required { - return try RawValue.openAPINode().requiredNode().nullableNode() + if try !RawValue.openAPINode(using: encoder).required { + return try RawValue.openAPINode(using: encoder).requiredNode().nullableNode() } - return try RawValue.openAPINode() + return try RawValue.openAPINode(using: encoder) } } @@ -95,7 +96,7 @@ extension ToOneRelationship: OpenAPINodeType { // Will use "enum" with one possible value for now. // TODO: metadata & links - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { let nullable = Identifiable.self is _Optional.Type return .object(.init(format: .generic, required: true), @@ -110,7 +111,7 @@ extension ToManyRelationship: OpenAPINodeType { // Will use "enum" with one possible value for now. // TODO: metadata & links - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .object(.init(format: .generic, required: true), .init(properties: [ @@ -122,7 +123,7 @@ extension ToManyRelationship: OpenAPINodeType { } extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { // NOTE: const for json `type` not supported by OpenAPI 3.0 // Will use "enum" with one possible value for now. @@ -141,13 +142,13 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self ? nil - : try Description.Attributes.genericObjectOpenAPINode() + : try Description.Attributes.genericObjectOpenAPINode(using: encoder) let attributesProperty = attributesNode.map { ("attributes", $0) } let relationshipsNode: JSONNode? = Description.Relationships.self == NoRelationships.self ? nil - : try Description.Relationships.genericObjectOpenAPINode() + : try Description.Relationships.genericObjectOpenAPINode(using: encoder) let relationshipsProperty = relationshipsNode.map { ("relationships", $0) } @@ -165,31 +166,31 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc } extension SingleResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { - return try Entity.openAPINode() + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try Entity.openAPINode(using: encoder) } } extension ManyResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .array(.init(format: .generic, required: true), - .init(items: try Entity.openAPINode())) + .init(items: try Entity.openAPINode(using: encoder))) } } extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType, IncludeType: OpenAPINodeType { - public static func openAPINode() throws -> JSONNode { + public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { // TODO: metadata, links, api description, errors // TODO: represent data and errors as the two distinct possible outcomes - let primaryDataNode: JSONNode? = try PrimaryResourceBody.openAPINode() + let primaryDataNode: JSONNode? = try PrimaryResourceBody.openAPINode(using: encoder) let primaryDataProperty = primaryDataNode.map { ("data", $0) } let includeNode: JSONNode? do { - includeNode = try Includes.openAPINode() + includeNode = try Includes.openAPINode(using: encoder) } catch let err as OpenAPITypeError { guard err == .invalidNode else { throw err diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index c5ac131..d477ae6 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -13,12 +13,12 @@ import Foundation /// Anything conforming to `OpenAPINodeType` can provide an /// OpenAPI schema representing itself. public protocol OpenAPINodeType { - static func openAPINode() throws -> JSONNode + static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode } extension OpenAPINodeType where Self: Sampleable, Self: Encodable { - public static func openAPINodeWithExample() throws -> JSONNode { - return try openAPINode().with(example: Self.successSample ?? Self.sample) + public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode { + return try openAPINode(using: encoder).with(example: Self.successSample ?? Self.sample, using: encoder) } } @@ -29,7 +29,7 @@ extension OpenAPINodeType where Self: Sampleable, Self: Encodable { /// different schema. The "different" conditions have to do /// with Raw Representability, hence the name of this protocol. public protocol RawOpenAPINodeType { - static func rawOpenAPINode() throws -> JSONNode + static func rawOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode } /// Anything conforming to `RawOpenAPINodeType` can provide an @@ -39,7 +39,7 @@ public protocol RawOpenAPINodeType { /// different schema. The "different" conditions have to do /// with Optionality, hence the name of this protocol. public protocol WrappedRawOpenAPIType { - static func wrappedOpenAPINode() throws -> JSONNode + static func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode } /// Anything conforming to `RawOpenAPINodeType` can provide an @@ -52,18 +52,18 @@ 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 + static func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode } /// Anything conforming to `AnyJSONCaseIterable` can provide a /// list of its possible values. public protocol AnyJSONCaseIterable { - static var allCases: [AnyCodable] { get } + static func allCases(using encoder: JSONEncoder) -> [AnyCodable] } extension AnyJSONCaseIterable { /// Given an array of Codable values, retrieve an array of AnyCodables. - static func allCases(from input: [T]) throws -> [AnyCodable] { + static func allCases(from input: [T], using encoder: JSONEncoder) throws -> [AnyCodable] { if let alreadyGoodToGo = input as? [AnyCodable] { return alreadyGoodToGo } @@ -76,7 +76,7 @@ extension AnyJSONCaseIterable { // 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 { + guard let arrayOfCodables = try JSONSerialization.jsonObject(with: encoder.encode(input), options: []) as? [Any] else { throw OpenAPICodableError.allCasesArrayNotCodable } return arrayOfCodables.map(AnyCodable.init) @@ -91,7 +91,7 @@ extension AnyJSONCaseIterable { /// The "different" conditions have to do /// with Optionality, hence the name of this protocol. public protocol AnyWrappedJSONCaseIterable { - static var allCases: [AnyCodable] { get } + static func allCases(using encoder: JSONEncoder) -> [AnyCodable] } public protocol SwiftTyped { @@ -282,14 +282,15 @@ public enum JSONNode: Equatable { nullable: Bool = false, // constantValue: Format.SwiftType? = nil, allowedValues: [AnyCodable]? = nil, - example: AnyCodable? = nil) { + example: AnyCodable? = nil, + using encoder: JSONEncoder = JSONEncoder()) { self.format = format self.required = required self.nullable = nullable // self.constantValue = constantValue self.allowedValues = allowedValues self.example = example - .flatMap { try? JSONEncoder().encode($0)} + .flatMap { try? encoder.encode($0)} .flatMap { String(data: $0, encoding: .utf8) } } @@ -550,12 +551,13 @@ public enum JSONNode: Equatable { } } - public func with(example codableExample: T) throws -> JSONNode { + public func with(example codableExample: T, + using encoder: JSONEncoder) throws -> JSONNode { let example: AnyCodable if let goodToGo = codableExample as? AnyCodable { example = goodToGo } else { - example = AnyCodable(try JSONSerialization.jsonObject(with: JSONEncoder().encode(codableExample), options: [])) + example = AnyCodable(try JSONSerialization.jsonObject(with: encoder.encode(codableExample), options: [])) } switch self { diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift index ae03f6a..90a6be9 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift @@ -6,6 +6,7 @@ // import AnyCodable +import Foundation /** @@ -32,37 +33,37 @@ Any object: **/ extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { - return try Wrapped.openAPINode().optionalNode() + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try Wrapped.openAPINode(using: encoder).optionalNode() } } extension Optional: RawOpenAPINodeType where Wrapped: RawRepresentable, Wrapped.RawValue: OpenAPINodeType { - static public func rawOpenAPINode() throws -> JSONNode { - return try Wrapped.RawValue.openAPINode().optionalNode() + static public func rawOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try Wrapped.RawValue.openAPINode(using: encoder).optionalNode() } } extension Optional: WrappedRawOpenAPIType where Wrapped: RawOpenAPINodeType { - static public func wrappedOpenAPINode() throws -> JSONNode { - return try Wrapped.rawOpenAPINode().optionalNode() + static public func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try Wrapped.rawOpenAPINode(using: encoder).optionalNode() } } extension Optional: DoubleWrappedRawOpenAPIType where Wrapped: WrappedRawOpenAPIType { - static public func wrappedOpenAPINode() throws -> JSONNode { - return try Wrapped.wrappedOpenAPINode().optionalNode() + static public func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try Wrapped.wrappedOpenAPINode(using: encoder).optionalNode() } } extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable, Wrapped: Codable { - public static var allCases: [AnyCodable] { - return (try? allCases(from: Array(Wrapped.allCases))) ?? [] + public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { + return (try? allCases(from: Array(Wrapped.allCases), using: encoder)) ?? [] } } extension String: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .string(.init(format: .generic, required: true), .init()) @@ -70,22 +71,22 @@ extension String: OpenAPINodeType { } extension Bool: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .boolean(.init(format: .generic, required: true)) } } extension Array: OpenAPINodeType where Element: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .array(.init(format: .generic, required: true), - .init(items: try Element.openAPINode())) + .init(items: try Element.openAPINode(using: encoder))) } } extension Double: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .number(.init(format: .double, required: true), .init()) @@ -93,7 +94,7 @@ extension Double: OpenAPINodeType { } extension Float: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .number(.init(format: .float, required: true), .init()) @@ -101,7 +102,7 @@ extension Float: OpenAPINodeType { } extension Int: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .integer(.init(format: .generic, required: true), .init()) @@ -109,7 +110,7 @@ extension Int: OpenAPINodeType { } extension Int32: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .integer(.init(format: .int32, required: true), .init()) @@ -117,7 +118,7 @@ extension Int32: OpenAPINodeType { } extension Int64: OpenAPINodeType { - static public func openAPINode() throws -> JSONNode { + static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .integer(.init(format: .int64, required: true), .init()) diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift index 0e8fe58..bbf778b 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift @@ -6,6 +6,7 @@ // import JSONAPI +import Foundation import AnyCodable /// A Sampleable type can provide a sample value. @@ -48,7 +49,7 @@ public extension Sampleable { } extension Sampleable { - public static func genericObjectOpenAPINode() throws -> JSONNode { + public static func genericObjectOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { let mirror = Mirror(reflecting: Self.sample) let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in @@ -56,9 +57,9 @@ extension Sampleable { let maybeAllCases: [AnyCodable]? = { switch type(of: child.value) { case let valType as AnyJSONCaseIterable.Type: - return valType.allCases + return valType.allCases(using: encoder) case let valType as AnyWrappedJSONCaseIterable.Type: - return valType.allCases + return valType.allCases(using: encoder) default: return nil } @@ -68,16 +69,16 @@ extension Sampleable { let maybeOpenAPINode: JSONNode? = try { switch type(of: child.value) { case let valType as OpenAPINodeType.Type: - return try valType.openAPINode() + return try valType.openAPINode(using: encoder) case let valType as RawOpenAPINodeType.Type: - return try valType.rawOpenAPINode() + return try valType.rawOpenAPINode(using: encoder) case let valType as WrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode() + return try valType.wrappedOpenAPINode(using: encoder) case let valType as DoubleWrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode() + return try valType.wrappedOpenAPINode(using: encoder) default: return nil @@ -107,6 +108,61 @@ extension Sampleable { } } +extension Sampleable { + public static func samples(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] { + return S1.samples.map(constructor) + } + + public static func samples(using s1: S1.Type, _ s2: S2.Type, with constructor: (S1, S2) -> Self) -> [Self] { + return zip(S1.samples, S2.samples).map(constructor) + } + + public static func samples(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, with constructor: (S1, S2, S3) -> Self) -> [Self] { + return zip3(S1.samples, S2.samples, S3.samples).map(constructor) + } + + public static func samples(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, with constructor: (S1, S2, S3, S4) -> Self) -> [Self] { + return zip4(S1.samples, S2.samples, S3.samples, S4.samples).map(constructor) + } + + public static func samples(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, with constructor: (S1, S2, S3, S4, S5) -> Self) -> [Self] { + return zip5(S1.samples, S2.samples, S3.samples, S4.samples, S5.samples).map(constructor) + } + + public static func samples(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, with constructor: (S1, S2, S3, S4, S5, S6) -> Self) -> [Self] { + // the compiler craps out at zip6. breaking it down makes the difference. + let firstZip = zip3(S1.samples, S2.samples, S3.samples) + let secondZip = zip3(S4.samples, S5.samples, S6.samples) + return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.1.0, arg.1.1, arg.1.2) }.map(constructor) + } + + public static func samples(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, _ s7: S7.Type, with constructor: (S1, S2, S3, S4, S5, S6, S7) -> Self) -> [Self] { + // the compiler craps out at zip6. breaking it down makes the difference. + let firstZip = zip3(S1.samples, S2.samples, S3.samples) + let secondZip = zip4(S4.samples, S5.samples, S6.samples, S7.samples) + return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.1.0, arg.1.1, arg.1.2, arg.1.3) }.map(constructor) + } + + public static func samples(using s1: S1.Type, _ s2: S2.Type, _ s3: S3.Type, _ s4: S4.Type, _ s5: S5.Type, _ s6: S6.Type, _ s7: S7.Type, _ s8: S8.Type, with constructor: (S1, S2, S3, S4, S5, S6, S7, S8) -> Self) -> [Self] { + // the compiler craps out at zip6. breaking it down makes the difference. + let firstZip = zip4(S1.samples, S2.samples, S3.samples, S4.samples) + let secondZip = zip4(S5.samples, S6.samples, S7.samples, S8.samples) + return zip(firstZip, secondZip).map { arg in (arg.0.0, arg.0.1, arg.0.2, arg.0.3, arg.1.0, arg.1.1, arg.1.2, arg.1.3) }.map(constructor) + } + + @inlinable static func zip3(_ a: A, _ b: B, _ c: C) -> [(A.Element, B.Element, C.Element)] { + return zip(a, zip(b, c)).map { arg in (arg.0, arg.1.0, arg.1.1) } + } + + @inlinable static func zip4(_ a: A, _ b: B, _ c: C, _ d: D) -> [(A.Element, B.Element, C.Element, D.Element)] { + return zip(a, zip(b, zip(c, d))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1) } + } + + @inlinable static func zip5(_ a: A, _ b: B, _ c: C, _ d: D, _ e: E) -> [(A.Element, B.Element, C.Element, D.Element, E.Element)] { + return zip(a, zip(b, zip(c, zip(d, e)))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1.0, arg.1.1.1.1) } + } +} + extension NoAttributes: Sampleable { public static var sample: NoAttributes { return .none @@ -131,8 +187,26 @@ extension NoLinks: Sampleable { } } +extension NoAPIDescription: Sampleable { + public static var sample: NoAPIDescription { + return .none + } +} + extension UnknownJSONAPIError: Sampleable { public static var sample: UnknownJSONAPIError { return .unknownError } } + +extension SingleResourceBody: Sampleable where Entity: Sampleable { + public static var sample: SingleResourceBody { + return .init(entity: Entity.sample) + } +} + +extension ManyResourceBody: Sampleable where Entity: Sampleable { + public static var sample: ManyResourceBody { + return .init(entities: Entity.samples) + } +} diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift index abaf9d1..e2289ba 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -12,7 +12,7 @@ import AnyCodable class JSONAPIEntityOpenAPITests: XCTestCase { func test_EmptyEntity() { - let node = try! TestType1.openAPINode() + let node = try! TestType1.openAPINode(using: JSONEncoder()) XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -40,7 +40,7 @@ class JSONAPIEntityOpenAPITests: XCTestCase { } func test_AttributesEntity() { - let node = try! TestType2.openAPINode() + let node = try! TestType2.openAPINode(using: JSONEncoder()) XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -122,7 +122,7 @@ class JSONAPIEntityOpenAPITests: XCTestCase { } func test_RelationshipsEntity() { - let node = try! TestType3.openAPINode() + let node = try! TestType3.openAPINode(using: JSONEncoder()) XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) From 952fe8ba7ecbeca7d0f7e39ca793de54ebe7dc5b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 23 Jan 2019 23:21:16 -0800 Subject: [PATCH 04/10] Remove encoder requirement to almost all Open API Node constructors. Made a new protocol for the few places where an encoder did need to be passed in. --- .../JSONAPI/JSONAPIInclude+OpenAPI.swift | 114 +++++++++--------- .../JSONAPI/JSONAPITypes+OpenAPI.swift | 46 +++---- .../JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift | 44 ++++--- .../OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift | 34 +++--- .../Sampleable/Sampleable.swift | 8 +- .../JSONAPIDocumentOpenAPITests.swift | 40 +++++- 6 files changed, 167 insertions(+), 119 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift index 948cd0e..6f7f966 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPIInclude+OpenAPI.swift @@ -9,8 +9,8 @@ import JSONAPI import Foundation extension Includes: OpenAPINodeType where I: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { - let includeNode = try I.openAPINode(using: encoder) + public static func openAPINode() throws -> JSONNode { + let includeNode = try I.openAPINode() return .array(.init(format: .generic, required: true), @@ -20,113 +20,113 @@ extension Includes: OpenAPINodeType where I: OpenAPINodeType { } extension Include0: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { throw OpenAPITypeError.invalidNode } } extension Include1: OpenAPINodeType where A: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { - return try .one(of: [A.openAPINode(using: encoder)]) + public static func openAPINode() throws -> JSONNode { + return try .one(of: [A.openAPINode()]) } } extension Include2: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode() ]) } } extension Include3: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder), - C.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode(), + C.openAPINode() ]) } } extension Include4: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder), - C.openAPINode(using: encoder), - D.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode(), + C.openAPINode(), + D.openAPINode() ]) } } extension Include5: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder), - C.openAPINode(using: encoder), - D.openAPINode(using: encoder), - E.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode(), + C.openAPINode(), + D.openAPINode(), + E.openAPINode() ]) } } extension Include6: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder), - C.openAPINode(using: encoder), - D.openAPINode(using: encoder), - E.openAPINode(using: encoder), - F.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode(), + C.openAPINode(), + D.openAPINode(), + E.openAPINode(), + F.openAPINode() ]) } } extension Include7: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder), - C.openAPINode(using: encoder), - D.openAPINode(using: encoder), - E.openAPINode(using: encoder), - F.openAPINode(using: encoder), - G.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode(), + C.openAPINode(), + D.openAPINode(), + E.openAPINode(), + F.openAPINode(), + G.openAPINode() ]) } } extension Include8: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder), - C.openAPINode(using: encoder), - D.openAPINode(using: encoder), - E.openAPINode(using: encoder), - F.openAPINode(using: encoder), - G.openAPINode(using: encoder), - H.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode(), + C.openAPINode(), + D.openAPINode(), + E.openAPINode(), + F.openAPINode(), + G.openAPINode(), + H.openAPINode() ]) } } extension Include9: OpenAPINodeType where A: OpenAPINodeType, B: OpenAPINodeType, C: OpenAPINodeType, D: OpenAPINodeType, E: OpenAPINodeType, F: OpenAPINodeType, G: OpenAPINodeType, H: OpenAPINodeType, I: OpenAPINodeType { - public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + public static func openAPINode() throws -> JSONNode { return try .one(of: [ - A.openAPINode(using: encoder), - B.openAPINode(using: encoder), - C.openAPINode(using: encoder), - D.openAPINode(using: encoder), - E.openAPINode(using: encoder), - F.openAPINode(using: encoder), - G.openAPINode(using: encoder), - H.openAPINode(using: encoder), - I.openAPINode(using: encoder) + A.openAPINode(), + B.openAPINode(), + C.openAPINode(), + D.openAPINode(), + E.openAPINode(), + F.openAPINode(), + G.openAPINode(), + H.openAPINode(), + I.openAPINode() ]) } } diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index 5e0baac..91c6068 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -13,38 +13,38 @@ private protocol _Optional {} extension Optional: _Optional {} extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> 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 try !RawValue.openAPINode(using: encoder).required { - return try RawValue.openAPINode(using: encoder).requiredNode().nullableNode() + if try !RawValue.openAPINode().required { + return try RawValue.openAPINode().requiredNode().nullableNode() } - return try RawValue.openAPINode(using: encoder) + return try RawValue.openAPINode() } } extension Attribute: RawOpenAPINodeType where RawValue: RawRepresentable, RawValue.RawValue: OpenAPINodeType { - static public func rawOpenAPINode(using encoder: JSONEncoder) 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(using: encoder).required { - return try RawValue.RawValue.openAPINode(using: encoder).requiredNode().nullableNode() + if try !RawValue.RawValue.openAPINode().required { + return try RawValue.RawValue.openAPINode().requiredNode().nullableNode() } - return try RawValue.RawValue.openAPINode(using: encoder) + return try RawValue.RawValue.openAPINode() } } extension Attribute: WrappedRawOpenAPIType where RawValue: RawOpenAPINodeType { - public static func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { + 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(using: encoder).required { - return try RawValue.rawOpenAPINode(using: encoder).requiredNode().nullableNode() + if try !RawValue.rawOpenAPINode().required { + return try RawValue.rawOpenAPINode().requiredNode().nullableNode() } - return try RawValue.rawOpenAPINode(using: encoder) + return try RawValue.rawOpenAPINode() } } @@ -61,14 +61,14 @@ extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseItera } extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> 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 try !RawValue.openAPINode(using: encoder).required { - return try RawValue.openAPINode(using: encoder).requiredNode().nullableNode() + if try !RawValue.openAPINode().required { + return try RawValue.openAPINode().requiredNode().nullableNode() } - return try RawValue.openAPINode(using: encoder) + return try RawValue.openAPINode() } } @@ -96,7 +96,7 @@ extension ToOneRelationship: OpenAPINodeType { // Will use "enum" with one possible value for now. // TODO: metadata & links - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { let nullable = Identifiable.self is _Optional.Type return .object(.init(format: .generic, required: true), @@ -111,7 +111,7 @@ extension ToManyRelationship: OpenAPINodeType { // Will use "enum" with one possible value for now. // TODO: metadata & links - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .object(.init(format: .generic, required: true), .init(properties: [ @@ -122,7 +122,7 @@ extension ToManyRelationship: OpenAPINodeType { } } -extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { +extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { // NOTE: const for json `type` not supported by OpenAPI 3.0 // Will use "enum" with one possible value for now. @@ -165,13 +165,13 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc } } -extension SingleResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { +extension SingleResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try Entity.openAPINode(using: encoder) } } -extension ManyResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { +extension ManyResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .array(.init(format: .generic, required: true), @@ -179,7 +179,7 @@ extension ManyResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { } } -extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType, IncludeType: OpenAPINodeType { +extension Document: OpenAPIEncodedNodeType, OpenAPINodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { // TODO: metadata, links, api description, errors // TODO: represent data and errors as the two distinct possible outcomes @@ -190,7 +190,7 @@ extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType, let includeNode: JSONNode? do { - includeNode = try Includes.openAPINode(using: encoder) + includeNode = try Includes.openAPINode() } catch let err as OpenAPITypeError { guard err == .invalidNode else { throw err diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index d477ae6..a7723ec 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -13,12 +13,25 @@ import Foundation /// Anything conforming to `OpenAPINodeType` can provide an /// OpenAPI schema representing itself. public protocol OpenAPINodeType { - static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode + static func openAPINode() throws -> JSONNode } extension OpenAPINodeType where Self: Sampleable, Self: Encodable { public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode { - return try openAPINode(using: encoder).with(example: Self.successSample ?? Self.sample, using: encoder) + return try openAPINode().with(example: Self.successSample ?? Self.sample, using: encoder) + } +} + +/// Anything conforming to `OpenAPIEncodedNodeType` can provide an +/// OpenAPI schema representing itself but it may need an Encoder +/// to do its job. +public protocol OpenAPIEncodedNodeType: OpenAPINodeType { + static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode +} + +extension OpenAPIEncodedNodeType { + public static func openAPINode() throws -> JSONNode { + return try openAPINode(using: JSONEncoder()) } } @@ -29,7 +42,7 @@ extension OpenAPINodeType where Self: Sampleable, Self: Encodable { /// different schema. The "different" conditions have to do /// with Raw Representability, hence the name of this protocol. public protocol RawOpenAPINodeType { - static func rawOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode + static func rawOpenAPINode() throws -> JSONNode } /// Anything conforming to `RawOpenAPINodeType` can provide an @@ -39,7 +52,7 @@ public protocol RawOpenAPINodeType { /// different schema. The "different" conditions have to do /// with Optionality, hence the name of this protocol. public protocol WrappedRawOpenAPIType { - static func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode + static func wrappedOpenAPINode() throws -> JSONNode } /// Anything conforming to `RawOpenAPINodeType` can provide an @@ -52,7 +65,7 @@ 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(using encoder: JSONEncoder) throws -> JSONNode + static func wrappedOpenAPINode() throws -> JSONNode } /// Anything conforming to `AnyJSONCaseIterable` can provide a @@ -282,15 +295,14 @@ public enum JSONNode: Equatable { nullable: Bool = false, // constantValue: Format.SwiftType? = nil, allowedValues: [AnyCodable]? = nil, - example: AnyCodable? = nil, - using encoder: JSONEncoder = JSONEncoder()) { + example: (codable: AnyCodable, encoder: JSONEncoder)? = nil) { self.format = format self.required = required self.nullable = nullable // self.constantValue = constantValue self.allowedValues = allowedValues self.example = example - .flatMap { try? encoder.encode($0)} + .flatMap { try? $0.encoder.encode($0.codable)} .flatMap { String(data: $0, encoding: .utf8) } } @@ -331,13 +343,13 @@ public enum JSONNode: Equatable { } /// Return this context with the given example - public func with(example: AnyCodable) -> Context { + public func with(example: AnyCodable, using encoder: JSONEncoder) -> Context { return .init(format: format, required: required, nullable: nullable, // constantValue: constantValue, allowedValues: allowedValues, - example: example) + example: (codable: example, encoder: encoder)) } } @@ -562,17 +574,17 @@ public enum JSONNode: Equatable { switch self { case .boolean(let context): - return .boolean(context.with(example: example)) + return .boolean(context.with(example: example, using: encoder)) case .object(let contextA, let contextB): - return .object(contextA.with(example: example), contextB) + return .object(contextA.with(example: example, using: encoder), contextB) case .array(let contextA, let contextB): - return .array(contextA.with(example: example), contextB) + return .array(contextA.with(example: example, using: encoder), contextB) case .number(let context, let contextB): - return .number(context.with(example: example), contextB) + return .number(context.with(example: example, using: encoder), contextB) case .integer(let context, let contextB): - return .integer(context.with(example: example), contextB) + return .integer(context.with(example: example, using: encoder), contextB) case .string(let context, let contextB): - return .string(context.with(example: example), contextB) + return .string(context.with(example: example, using: encoder), contextB) case .all, .one, .any, .not: return self } diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift index 90a6be9..19ee4da 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/SwiftPrimitiveTypes+OpenAPI.swift @@ -33,26 +33,26 @@ Any object: **/ extension Optional: OpenAPINodeType where Wrapped: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { - return try Wrapped.openAPINode(using: encoder).optionalNode() + static public func openAPINode() throws -> JSONNode { + return try Wrapped.openAPINode().optionalNode() } } extension Optional: RawOpenAPINodeType where Wrapped: RawRepresentable, Wrapped.RawValue: OpenAPINodeType { - static public func rawOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { - return try Wrapped.RawValue.openAPINode(using: encoder).optionalNode() + static public func rawOpenAPINode() throws -> JSONNode { + return try Wrapped.RawValue.openAPINode().optionalNode() } } extension Optional: WrappedRawOpenAPIType where Wrapped: RawOpenAPINodeType { - static public func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { - return try Wrapped.rawOpenAPINode(using: encoder).optionalNode() + static public func wrappedOpenAPINode() throws -> JSONNode { + return try Wrapped.rawOpenAPINode().optionalNode() } } extension Optional: DoubleWrappedRawOpenAPIType where Wrapped: WrappedRawOpenAPIType { - static public func wrappedOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { - return try Wrapped.wrappedOpenAPINode(using: encoder).optionalNode() + static public func wrappedOpenAPINode() throws -> JSONNode { + return try Wrapped.wrappedOpenAPINode().optionalNode() } } @@ -63,7 +63,7 @@ extension Optional: AnyJSONCaseIterable where Wrapped: CaseIterable, Wrapped: Co } extension String: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .string(.init(format: .generic, required: true), .init()) @@ -71,22 +71,22 @@ extension String: OpenAPINodeType { } extension Bool: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .boolean(.init(format: .generic, required: true)) } } extension Array: OpenAPINodeType where Element: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .array(.init(format: .generic, required: true), - .init(items: try Element.openAPINode(using: encoder))) + .init(items: try Element.openAPINode())) } } extension Double: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .number(.init(format: .double, required: true), .init()) @@ -94,7 +94,7 @@ extension Double: OpenAPINodeType { } extension Float: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .number(.init(format: .float, required: true), .init()) @@ -102,7 +102,7 @@ extension Float: OpenAPINodeType { } extension Int: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .integer(.init(format: .generic, required: true), .init()) @@ -110,7 +110,7 @@ extension Int: OpenAPINodeType { } extension Int32: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .integer(.init(format: .int32, required: true), .init()) @@ -118,7 +118,7 @@ extension Int32: OpenAPINodeType { } extension Int64: OpenAPINodeType { - static public func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { + static public func openAPINode() throws -> JSONNode { return .integer(.init(format: .int64, required: true), .init()) diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift index bbf778b..e186222 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift @@ -69,16 +69,16 @@ extension Sampleable { let maybeOpenAPINode: JSONNode? = try { switch type(of: child.value) { case let valType as OpenAPINodeType.Type: - return try valType.openAPINode(using: encoder) + return try valType.openAPINode() case let valType as RawOpenAPINodeType.Type: - return try valType.rawOpenAPINode(using: encoder) + return try valType.rawOpenAPINode() case let valType as WrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode(using: encoder) + return try valType.wrappedOpenAPINode() case let valType as DoubleWrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode(using: encoder) + return try valType.wrappedOpenAPINode() default: return nil diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift index f709f17..e82ca34 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift @@ -6,15 +6,23 @@ // import XCTest +import SwiftCheck import JSONAPI import JSONAPIOpenAPI class JSONAPIDocumentOpenAPITests: XCTestCase { func test_SingleResourceDocument() { - let node = try! SingleEntityDocument.openAPINode() + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + dateFormatter.locale = Locale(identifier: "en_US") let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! SingleEntityDocument.openAPINodeWithExample(using: encoder) print(String(data: try! encoder.encode(node), encoding: .utf8)!) } @@ -27,9 +35,11 @@ extension JSONAPIDocumentOpenAPITests { struct Attributes: JSONAPI.Attributes, Sampleable { let name: Attribute + let date: Attribute static var sample: Attributes { - return .init(name: "hello world") + return .init(name: "hello world", + date: .init(value: Date())) } } @@ -40,3 +50,29 @@ extension JSONAPIDocumentOpenAPITests { typealias SingleEntityDocument = Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> } + +extension Id: Sampleable where RawType == String { + public static var sample: Id { + return .init(rawValue: String.arbitrary.generate) + } +} + +extension JSONAPI.Entity: Sampleable where Description.Attributes: Sampleable, Description.Relationships: Sampleable, MetaType: Sampleable, LinksType: Sampleable, EntityRawIdType == String { + public static var sample: JSONAPI.Entity { + return JSONAPI.Entity(id: .sample, + attributes: .sample, + relationships: .sample, + meta: .sample, + links: .sample) + } +} + +extension Document: Sampleable where PrimaryResourceBody: Sampleable, MetaType: Sampleable, LinksType: Sampleable, IncludeType: Sampleable, APIDescription: Sampleable, Error: Sampleable { + public static var sample: Document { + return Document(apiDescription: .sample, + body: .sample, + includes: .sample, + meta: .sample, + links: .sample) + } +} From 70453737080ab46492d5d4da69d175752c619488 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 23 Jan 2019 23:24:53 -0800 Subject: [PATCH 05/10] update and make sure all Playground pages run --- .../Contents.swift | 1 + .../OpenAPI Documentation.xcplaygroundpage/Contents.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index abf3668..27ef739 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -1,5 +1,6 @@ import Foundation import JSONAPI +import Poly // MARK: - Preamble (setup) diff --git a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift index 01cff69..a0691d7 100644 --- a/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/OpenAPI Documentation.xcplaygroundpage/Contents.swift @@ -15,14 +15,14 @@ print("====") print(personSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed") print("====") -let dogDocumentSchemaData = try? encoder.encode(SingleDogDocument.openAPINodeWithExample()) +let dogDocumentSchemaData = try? encoder.encode(SingleDogDocument.openAPINodeWithExample(using: encoder)) print("Dog Document Schema") print("====") print(dogDocumentSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed") print("====") -let batchPersonSchemaData = try? encoder.encode(BatchPeopleDocument.openAPINodeWithExample()) +let batchPersonSchemaData = try? encoder.encode(BatchPeopleDocument.openAPINodeWithExample(using: encoder)) print("Batch Person Document Schema") print("====") From dc30cb3b9e3dde70b04b274febc7aed1a23a2b0d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 24 Jan 2019 00:47:24 -0800 Subject: [PATCH 06/10] Make Attribute Sampleable where its RawValue is Sampleable. Allow Sampleable things to provide a best guess for their node type based on the result of encoding and then deserializing them. --- .../JSONAPI/JSONAPITypes+OpenAPI.swift | 11 +- .../JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift | 7 + .../Sampleable/Sampleable.swift | 87 +++++++++++- .../JSONAPIAttributeOpenAPITests.swift | 125 ++++++++++++++++++ 4 files changed, 226 insertions(+), 4 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index 91c6068..6c07525 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -12,6 +12,11 @@ import AnyCodable private protocol _Optional {} extension Optional: _Optional {} +private protocol Wrapper { + associatedtype Wrapped +} +extension Optional: Wrapper {} + extension Attribute: OpenAPINodeType where RawValue: OpenAPINodeType { static public func openAPINode() throws -> JSONNode { // If the RawValue is not required, we actually consider it @@ -72,6 +77,8 @@ extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType } } +// TODO: conform TransformedAttribute to all of the above protocols that Attribute conforms to. + extension RelationshipType { static func relationshipNode(nullable: Bool, jsonType: String) -> JSONNode { let propertiesDict: [String: JSONNode] = [ @@ -142,13 +149,13 @@ extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attr let attributesNode: JSONNode? = Description.Attributes.self == NoAttributes.self ? nil - : try Description.Attributes.genericObjectOpenAPINode(using: encoder) + : try Description.Attributes.genericOpenAPINode(using: encoder) let attributesProperty = attributesNode.map { ("attributes", $0) } let relationshipsNode: JSONNode? = Description.Relationships.self == NoRelationships.self ? nil - : try Description.Relationships.genericObjectOpenAPINode(using: encoder) + : try Description.Relationships.genericOpenAPINode(using: encoder) let relationshipsProperty = relationshipsNode.map { ("relationships", $0) } diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index a7723ec..14a3a24 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -68,6 +68,12 @@ public protocol DoubleWrappedRawOpenAPIType { static func wrappedOpenAPINode() throws -> JSONNode } +/// A GenericOpenAPINodeType can take a stab at +/// determining its OpenAPINode because it is sampleable. +public protocol GenericOpenAPINodeType { + static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode +} + /// Anything conforming to `AnyJSONCaseIterable` can provide a /// list of its possible values. public protocol AnyJSONCaseIterable { @@ -594,6 +600,7 @@ public enum JSONNode: Equatable { public enum OpenAPICodableError: Swift.Error, Equatable { case allCasesArrayNotCodable case exampleNotCodable + case primitiveGuessFailed } public enum OpenAPITypeError: Swift.Error, Equatable { diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift index e186222..ae7062b 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift @@ -37,6 +37,8 @@ public protocol Sampleable { static var samples: [Self] { get } } +public typealias SampleableOpenAPIType = Sampleable & GenericOpenAPINodeType + public extension Sampleable { // default implementation: public static var successSample: Self? { return nil } @@ -48,8 +50,8 @@ public extension Sampleable { public static var samples: [Self] { return [Self.sample] } } -extension Sampleable { - public static func genericObjectOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { +extension Sampleable where Self: Encodable { + public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { let mirror = Mirror(reflecting: Self.sample) let properties: [(String, JSONNode)] = try mirror.children.compactMap { child in @@ -80,6 +82,9 @@ extension Sampleable { case let valType as DoubleWrappedRawOpenAPIType.Type: return try valType.wrappedOpenAPINode() + case let valType as GenericOpenAPINodeType.Type: + return try valType.genericOpenAPINode(using: encoder) + default: return nil } @@ -97,6 +102,13 @@ extension Sampleable { return zip(child.label, newNode) { ($0, $1) } } + // if there are no properties, let's see if we are dealing + // with a primitive. + if properties.count == 0, + let primitive = try primitiveGuess(using: encoder) { + return primitive + } + // 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 @@ -106,6 +118,71 @@ extension Sampleable { required: true), .init(properties: propertiesDict)) } + + private static func primitiveGuess(using encoder: JSONEncoder) throws -> JSONNode? { + let data = try encoder.encode(PrimitiveWrapper(primitive: Self.sample)) + let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) + + guard let wrapperDict = wrappedValue as? [String: Any], + wrapperDict.contains(where: { $0.key == "primitive" }) else { + throw OpenAPICodableError.primitiveGuessFailed + } + + let value = (wrappedValue as! [String: Any])["primitive"]! + + return try { + switch type(of: value) { + case let valType as OpenAPINodeType.Type: + return try valType.openAPINode() + + case let valType as RawOpenAPINodeType.Type: + return try valType.rawOpenAPINode() + + case let valType as WrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() + + case let valType as DoubleWrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() + + case let valType as GenericOpenAPINodeType.Type: + return try valType.genericOpenAPINode(using: encoder) + + default: + return nil + } + }() ?? { + switch value { + case is String: + return .string(.init(format: .generic, + required: true), + .init()) + + case is Int: + return .integer(.init(format: .generic, + required: true), + .init()) + + case is Double: + return .number(.init(format: .double, + required: true), + .init()) + + case is Bool: + return .boolean(.init(format: .generic, + required: true)) + + default: + return nil + } + }() + } +} + +// The following wrapper is only needed because JSONEncoder cannot yet encode +// JSON fragments. It is a very unfortunate limitation that requires silly +// workarounds in edge cases like this. +private struct PrimitiveWrapper: Encodable { + let primitive: Wrapped } extension Sampleable { @@ -199,6 +276,12 @@ extension UnknownJSONAPIError: Sampleable { } } +extension Attribute: Sampleable where RawValue: Sampleable { + public static var sample: Attribute { + return .init(value: RawValue.sample) + } +} + extension SingleResourceBody: Sampleable where Entity: Sampleable { public static var sample: SingleResourceBody { return .init(entity: Entity.sample) diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift index 642b95e..ebb1085 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift @@ -8,6 +8,7 @@ import XCTest import JSONAPI import JSONAPIOpenAPI +import SwiftCheck import AnyCodable class JSONAPIAttributeOpenAPITests: XCTestCase { @@ -504,6 +505,124 @@ extension JSONAPIAttributeOpenAPITests { } } +// MARK: - Date +extension JSONAPIAttributeOpenAPITests { + func test_DateStringAttribute() { + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! Attribute.genericOpenAPINode(using: encoder) + + 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_DateNumberAttribute() { + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .secondsSince1970 + + let node = try! Attribute.genericOpenAPINode(using: encoder) + + 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_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 { @@ -511,3 +630,9 @@ extension JSONAPIAttributeOpenAPITests { case two } } + +extension Date: Sampleable { + public static var sample: Date { + return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate + } +} From 58a7c82436323dcf3e7654930af59273da92f21e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 24 Jan 2019 17:25:34 -0800 Subject: [PATCH 07/10] Restructure files a bit. Make Date handling relatively robust compared to my first pass at it. Make the failure to construct a generic open API node type throw an error rather than silently omit the node. --- .../JSONAPI/JSONAPITypes+OpenAPI.swift | 14 +- .../JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift | 40 +++ .../JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift | 9 +- .../Sampleable/Sampleable+JSONAPI.swift | 62 +++++ .../Sampleable/Sampleable+OpenAPI.swift | 161 ++++++++++++ .../Sampleable/Sampleable.swift | 193 -------------- .../JSONAPIAttributeOpenAPITests.swift | 247 +++++++++++++++++- 7 files changed, 526 insertions(+), 200 deletions(-) create mode 100644 Sources/JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift create mode 100644 Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift create mode 100644 Sources/JSONAPIOpenAPI/Sampleable/Sampleable+OpenAPI.swift diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index 6c07525..e15f3a2 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -65,6 +65,18 @@ extension Attribute: AnyWrappedJSONCaseIterable where RawValue: AnyJSONCaseItera } } +extension Attribute: GenericOpenAPINodeType where RawValue: GenericOpenAPINodeType { + public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { + return try RawValue.genericOpenAPINode(using: encoder) + } +} + +extension Attribute: DateOpenAPINodeType where RawValue: DateOpenAPINodeType { + public static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? { + return RawValue.dateOpenAPINodeGuess(using: encoder) + } +} + extension TransformedAttribute: OpenAPINodeType where RawValue: OpenAPINodeType { static public func openAPINode() throws -> JSONNode { // If the RawValue is not required, we actually consider it @@ -199,7 +211,7 @@ extension Document: OpenAPIEncodedNodeType, OpenAPINodeType where PrimaryResourc do { includeNode = try Includes.openAPINode() } catch let err as OpenAPITypeError { - guard err == .invalidNode else { + guard case .invalidNode = err else { throw err } includeNode = nil diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift b/Sources/JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift new file mode 100644 index 0000000..c153fe4 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/OpenAPI/Date+OpenAPI.swift @@ -0,0 +1,40 @@ +// +// Date+OpenAPI.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/24/19. +// + +import Foundation + +extension Date: DateOpenAPINodeType { + public static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? { + + switch encoder.dateEncodingStrategy { + case .deferredToDate, .custom: + // I don't know if we can say anything about this case without + // encoding the Date and looking at it, which is what `primitiveGuess()` + // does. + return nil + + case .secondsSince1970, + .millisecondsSince1970: + return .number(.init(format: .double, + required: true), + .init()) + + case .iso8601: + return .string(.init(format: .dateTime, + required: true), + .init()) + + case .formatted(let formatter): + let hasTime = formatter.timeStyle != .none + let format: JSONTypeFormat.StringFormat = hasTime ? .dateTime : .date + + return .string(.init(format: format, + required: true), + .init()) + } + } +} diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index 14a3a24..9f283f6 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -74,6 +74,12 @@ public protocol GenericOpenAPINodeType { static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode } +/// Anything conforming to `DateOpenAPINodeType` is +/// able to attempt to represent itself as a date OpenAPINode +public protocol DateOpenAPINodeType { + static func dateOpenAPINodeGuess(using encoder: JSONEncoder) -> JSONNode? +} + /// Anything conforming to `AnyJSONCaseIterable` can provide a /// list of its possible values. public protocol AnyJSONCaseIterable { @@ -603,6 +609,7 @@ public enum OpenAPICodableError: Swift.Error, Equatable { case primitiveGuessFailed } -public enum OpenAPITypeError: Swift.Error, Equatable { +public enum OpenAPITypeError: Swift.Error { case invalidNode + case unknownNodeType(Any.Type) } diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift new file mode 100644 index 0000000..6dd53f2 --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift @@ -0,0 +1,62 @@ +// +// Sampleable+JSONAPI.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/24/19. +// + +import JSONAPI + +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 + } +} + +extension NoAPIDescription: Sampleable { + public static var sample: NoAPIDescription { + return .none + } +} + +extension UnknownJSONAPIError: Sampleable { + public static var sample: UnknownJSONAPIError { + return .unknownError + } +} + +extension Attribute: Sampleable where RawValue: Sampleable { + public static var sample: Attribute { + return .init(value: RawValue.sample) + } +} + +extension SingleResourceBody: Sampleable where Entity: Sampleable { + public static var sample: SingleResourceBody { + return .init(entity: Entity.sample) + } +} + +extension ManyResourceBody: Sampleable where Entity: Sampleable { + public static var sample: ManyResourceBody { + return .init(entities: Entity.samples) + } +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+OpenAPI.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+OpenAPI.swift new file mode 100644 index 0000000..070a6db --- /dev/null +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+OpenAPI.swift @@ -0,0 +1,161 @@ +// +// Sampleable+OpenAPI.swift +// JSONAPIOpenAPI +// +// Created by Mathew Polzin on 1/24/19. +// + +import Foundation +import AnyCodable + +public typealias SampleableOpenAPIType = Sampleable & GenericOpenAPINodeType + +extension Sampleable where Self: Encodable { + public static func genericOpenAPINode(using encoder: JSONEncoder) throws -> JSONNode { + + // short circuit for dates + if let dateType = self as? Date.Type, + let node = try dateType.dateOpenAPINodeGuess(using: encoder) ?? primitiveGuess(using: encoder) { + return node + } + + 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: [AnyCodable]? = { + switch type(of: child.value) { + case let valType as AnyJSONCaseIterable.Type: + return valType.allCases(using: encoder) + case let valType as AnyWrappedJSONCaseIterable.Type: + return valType.allCases(using: encoder) + 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.rawOpenAPINode() + + case let valType as WrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() + + case let valType as DoubleWrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() + + case let valType as GenericOpenAPINodeType.Type: + return try valType.genericOpenAPINode(using: encoder) + + case let valType as DateOpenAPINodeType.Type: + return valType.dateOpenAPINodeGuess(using: encoder) + + default: + throw OpenAPITypeError.unknownNodeType(self) +// return nil + } + }() + + // put it all together + let newNode: JSONNode? + if let allCases = maybeAllCases, + let openAPINode = maybeOpenAPINode { + newNode = try openAPINode.with(allowedValues: allCases) + } else { + newNode = maybeOpenAPINode + } + + return zip(child.label, newNode) { ($0, $1) } + } + + // if there are no properties, let's see if we are dealing + // with a primitive. + if properties.count == 0, + let primitive = try primitiveGuess(using: encoder) { + return primitive + } + + // 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)) + } + + private static func primitiveGuess(using encoder: JSONEncoder) throws -> JSONNode? { + + let data = try encoder.encode(PrimitiveWrapper(primitive: Self.sample)) + let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) + + guard let wrapperDict = wrappedValue as? [String: Any], + wrapperDict.contains(where: { $0.key == "primitive" }) else { + throw OpenAPICodableError.primitiveGuessFailed + } + + let value = (wrappedValue as! [String: Any])["primitive"]! + + return try { + switch type(of: value) { + case let valType as OpenAPINodeType.Type: + return try valType.openAPINode() + + case let valType as RawOpenAPINodeType.Type: + return try valType.rawOpenAPINode() + + case let valType as WrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() + + case let valType as DoubleWrappedRawOpenAPIType.Type: + return try valType.wrappedOpenAPINode() + + case let valType as GenericOpenAPINodeType.Type: + return try valType.genericOpenAPINode(using: encoder) + + case let valType as DateOpenAPINodeType.Type: + return valType.dateOpenAPINodeGuess(using: encoder) + + default: + return nil + } + }() ?? { + switch value { + case is String: + return .string(.init(format: .generic, + required: true), + .init()) + + case is Int: + return .integer(.init(format: .generic, + required: true), + .init()) + + case is Double: + return .number(.init(format: .double, + required: true), + .init()) + + case is Bool: + return .boolean(.init(format: .generic, + required: true)) + + default: + return nil + } + }() + } +} + +// The following wrapper is only needed because JSONEncoder cannot yet encode +// JSON fragments. It is a very unfortunate limitation that requires silly +// workarounds in edge cases like this. +private struct PrimitiveWrapper: Encodable { + let primitive: Wrapped +} diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift index ae7062b..0d3cfc7 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable/Sampleable.swift @@ -5,9 +5,7 @@ // Created by Mathew Polzin on 1/15/19. // -import JSONAPI import Foundation -import AnyCodable /// A Sampleable type can provide a sample value. /// This is useful for reflection. @@ -37,8 +35,6 @@ public protocol Sampleable { static var samples: [Self] { get } } -public typealias SampleableOpenAPIType = Sampleable & GenericOpenAPINodeType - public extension Sampleable { // default implementation: public static var successSample: Self? { return nil } @@ -50,141 +46,6 @@ public extension Sampleable { public static var samples: [Self] { return [Self.sample] } } -extension Sampleable where Self: Encodable { - public static func genericOpenAPINode(using encoder: JSONEncoder) 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: [AnyCodable]? = { - switch type(of: child.value) { - case let valType as AnyJSONCaseIterable.Type: - return valType.allCases(using: encoder) - case let valType as AnyWrappedJSONCaseIterable.Type: - return valType.allCases(using: encoder) - 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.rawOpenAPINode() - - case let valType as WrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode() - - case let valType as DoubleWrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode() - - case let valType as GenericOpenAPINodeType.Type: - return try valType.genericOpenAPINode(using: encoder) - - default: - return nil - } - }() - - // put it all together - let newNode: JSONNode? - if let allCases = maybeAllCases, - let openAPINode = maybeOpenAPINode { - newNode = try openAPINode.with(allowedValues: allCases) - } else { - newNode = maybeOpenAPINode - } - - return zip(child.label, newNode) { ($0, $1) } - } - - // if there are no properties, let's see if we are dealing - // with a primitive. - if properties.count == 0, - let primitive = try primitiveGuess(using: encoder) { - return primitive - } - - // 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)) - } - - private static func primitiveGuess(using encoder: JSONEncoder) throws -> JSONNode? { - let data = try encoder.encode(PrimitiveWrapper(primitive: Self.sample)) - let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) - - guard let wrapperDict = wrappedValue as? [String: Any], - wrapperDict.contains(where: { $0.key == "primitive" }) else { - throw OpenAPICodableError.primitiveGuessFailed - } - - let value = (wrappedValue as! [String: Any])["primitive"]! - - return try { - switch type(of: value) { - case let valType as OpenAPINodeType.Type: - return try valType.openAPINode() - - case let valType as RawOpenAPINodeType.Type: - return try valType.rawOpenAPINode() - - case let valType as WrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode() - - case let valType as DoubleWrappedRawOpenAPIType.Type: - return try valType.wrappedOpenAPINode() - - case let valType as GenericOpenAPINodeType.Type: - return try valType.genericOpenAPINode(using: encoder) - - default: - return nil - } - }() ?? { - switch value { - case is String: - return .string(.init(format: .generic, - required: true), - .init()) - - case is Int: - return .integer(.init(format: .generic, - required: true), - .init()) - - case is Double: - return .number(.init(format: .double, - required: true), - .init()) - - case is Bool: - return .boolean(.init(format: .generic, - required: true)) - - default: - return nil - } - }() - } -} - -// The following wrapper is only needed because JSONEncoder cannot yet encode -// JSON fragments. It is a very unfortunate limitation that requires silly -// workarounds in edge cases like this. -private struct PrimitiveWrapper: Encodable { - let primitive: Wrapped -} - extension Sampleable { public static func samples(using s1: S1.Type, with constructor: (S1) -> Self) -> [Self] { return S1.samples.map(constructor) @@ -239,57 +100,3 @@ extension Sampleable { return zip(a, zip(b, zip(c, zip(d, e)))).map { arg in (arg.0, arg.1.0, arg.1.1.0, arg.1.1.1.0, arg.1.1.1.1) } } } - -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 - } -} - -extension NoAPIDescription: Sampleable { - public static var sample: NoAPIDescription { - return .none - } -} - -extension UnknownJSONAPIError: Sampleable { - public static var sample: UnknownJSONAPIError { - return .unknownError - } -} - -extension Attribute: Sampleable where RawValue: Sampleable { - public static var sample: Attribute { - return .init(value: RawValue.sample) - } -} - -extension SingleResourceBody: Sampleable where Entity: Sampleable { - public static var sample: SingleResourceBody { - return .init(entity: Entity.sample) - } -} - -extension ManyResourceBody: Sampleable where Entity: Sampleable { - public static var sample: ManyResourceBody { - return .init(entities: Entity.samples) - } -} diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift index ebb1085..0acab15 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIAttributeOpenAPITests.swift @@ -508,6 +508,45 @@ extension JSONAPIAttributeOpenAPITests { // MARK: - Date extension JSONAPIAttributeOpenAPITests { func test_DateStringAttribute() { + // TEST: + // Encoder is set to use + // formatter with date + // with no time. + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + XCTAssertEqual(node?.jsonTypeFormat, .string(.date)) + + guard case .string(let contextA, let stringContext)? = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .date, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_DateStringAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // formatter with date + // with no time. let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium @@ -521,14 +560,14 @@ extension JSONAPIAttributeOpenAPITests { let node = try! Attribute.genericOpenAPINode(using: encoder) XCTAssertTrue(node.required) - XCTAssertEqual(node.jsonTypeFormat, .string(.generic)) + XCTAssertEqual(node.jsonTypeFormat, .string(.date)) guard case .string(let contextA, let stringContext) = node else { XCTFail("Expected string Node") return } - XCTAssertEqual(contextA, .init(format: .generic, + XCTAssertEqual(contextA, .init(format: .date, required: true, nullable: false, allowedValues: nil)) @@ -536,17 +575,215 @@ extension JSONAPIAttributeOpenAPITests { XCTAssertEqual(stringContext, .init()) } - func test_DateNumberAttribute() { + func test_DateTimeStringAttribute() { + // TEST: + // Encoder is set to use + // formatter with date + // with time. let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .none + dateFormatter.timeStyle = .short dateFormatter.locale = Locale(identifier: "en_US") + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + XCTAssertEqual(node?.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext)? = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_DateTimeStringAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // formatter with date + // with time. + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! Attribute.genericOpenAPINode(using: encoder) + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + + func test_8601DateStringAttribute() { + if #available(OSX 10.12, *) { + // TEST: + // Encoder is set to use + // iso8601 date format + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .iso8601 + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + XCTAssertEqual(node?.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext)? = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + } + + func test_8601DateStringAttribute_Sampleable() { + if #available(OSX 10.12, *) { + // TEST: + // Encoder is set to use + // iso8601 date format + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .iso8601 + + let node = try! Attribute.genericOpenAPINode(using: encoder) + + XCTAssertTrue(node.required) + XCTAssertEqual(node.jsonTypeFormat, .string(.dateTime)) + + guard case .string(let contextA, let stringContext) = node else { + XCTFail("Expected string Node") + return + } + + XCTAssertEqual(contextA, .init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil)) + + XCTAssertEqual(stringContext, .init()) + } + } + + func test_DateNumberAttribute() { + // TEST: + // Encoder is set to use + // seconds since 1970 as + // date format + let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted encoder.dateEncodingStrategy = .secondsSince1970 + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNotNil(node) + + XCTAssertTrue(node?.required ?? false) + 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_DateNumberAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // seconds since 1970 as + // date format + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .secondsSince1970 + + let node = try! Attribute.genericOpenAPINode(using: encoder) + + 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_DateDeferredAttribute() { + // TEST: + // Encoder is set to use + // Date default encoding + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .deferredToDate + + let node = Attribute.dateOpenAPINodeGuess(using: encoder) + + XCTAssertNil(node) + } + + func test_DateDeferredAttribute_Sampleable() { + // TEST: + // Encoder is set to use + // Date default encoding + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + encoder.dateEncodingStrategy = .deferredToDate + let node = try! Attribute.genericOpenAPINode(using: encoder) XCTAssertTrue(node.required) @@ -631,7 +868,7 @@ extension JSONAPIAttributeOpenAPITests { } } -extension Date: Sampleable { +extension Date: SampleableOpenAPIType { public static var sample: Date { return TimeInterval.arbitrary.map { Date(timeIntervalSince1970: $0) }.generate } From 2b59f540676d575d63b82c832b44f0ec32a88a71 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 24 Jan 2019 17:47:44 -0800 Subject: [PATCH 08/10] Fix bug causing a supplied encoder to be used for generating an example but not for helping determine the correct Date formatting. --- .../JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift | 12 ++++----- .../JSONAPIEntityOpenAPITests.swift | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index 9f283f6..abf2e6e 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -16,12 +16,6 @@ public protocol OpenAPINodeType { static func openAPINode() throws -> JSONNode } -extension OpenAPINodeType where Self: Sampleable, Self: Encodable { - public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode { - return try openAPINode().with(example: Self.successSample ?? Self.sample, using: encoder) - } -} - /// Anything conforming to `OpenAPIEncodedNodeType` can provide an /// OpenAPI schema representing itself but it may need an Encoder /// to do its job. @@ -29,6 +23,12 @@ public protocol OpenAPIEncodedNodeType: OpenAPINodeType { static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode } +extension OpenAPIEncodedNodeType where Self: Sampleable, Self: Encodable { + public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONNode { + return try openAPINode(using: encoder).with(example: Self.successSample ?? Self.sample, using: encoder) + } +} + extension OpenAPIEncodedNodeType { public static func openAPINode() throws -> JSONNode { return try openAPINode(using: JSONEncoder()) diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift index e2289ba..a9e6b76 100644 --- a/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift +++ b/Tests/JSONAPIOpenAPITests/JSONAPIEntityOpenAPITests.swift @@ -40,7 +40,16 @@ class JSONAPIEntityOpenAPITests: XCTestCase { } func test_AttributesEntity() { - let node = try! TestType2.openAPINode(using: JSONEncoder()) + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.locale = Locale(identifier: "en_US") + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatted(dateFormatter) + + let node = try! TestType2.openAPINode(using: encoder) XCTAssertTrue(node.required) XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) @@ -83,9 +92,9 @@ class JSONAPIEntityOpenAPITests: XCTestCase { nullable: false, allowedValues: nil)) - XCTAssertEqual(attributesContext.minProperties, 3) - XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "nullableProperty"])) - XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"])) + XCTAssertEqual(attributesContext.minProperties, 4) + XCTAssertEqual(Set(attributesContext.requiredProperties), Set(["stringProperty", "enumProperty", "dateProperty", "nullableProperty"])) + XCTAssertEqual(Set(attributesContext.properties.keys), Set(["stringProperty", "enumProperty", "dateProperty", "optionalProperty", "nullableProperty", "nullableOptionalProperty"])) XCTAssertEqual(attributesContext.properties["stringProperty"], .string(.init(format: .generic, @@ -99,6 +108,13 @@ class JSONAPIEntityOpenAPITests: XCTestCase { allowedValues: ["one", "two"].map(AnyCodable.init)), .init())) + XCTAssertEqual(attributesContext.properties["dateProperty"], + .string(.init(format: .dateTime, + required: true, + nullable: false, + allowedValues: nil), + .init())) + XCTAssertEqual(attributesContext.properties["optionalProperty"], .string(.init(format: .generic, required: false, @@ -268,6 +284,7 @@ extension JSONAPIEntityOpenAPITests { public struct Attributes: JSONAPI.Attributes, Sampleable { let stringProperty: Attribute let enumProperty: Attribute + let dateProperty: Attribute let optionalProperty: Attribute? let nullableProperty: Attribute let nullableOptionalProperty: Attribute? @@ -278,6 +295,7 @@ extension JSONAPIEntityOpenAPITests { public static var sample: Attributes { return Attributes(stringProperty: .init(value: "hello"), enumProperty: .init(value: .one), + dateProperty: .init(value: Date()), optionalProperty: nil, nullableProperty: .init(value: nil), nullableOptionalProperty: nil) From 5ea83b07c13d109e8cbbaa25dce047a0dc69f145 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 24 Jan 2019 19:09:46 -0800 Subject: [PATCH 09/10] Hopefully remove some ambiguity. --- Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift | 8 ++++---- Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift | 8 +------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift index e15f3a2..4ac1da3 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPI/JSONAPITypes+OpenAPI.swift @@ -141,7 +141,7 @@ extension ToManyRelationship: OpenAPINodeType { } } -extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { +extension Entity: OpenAPIEncodedNodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { // NOTE: const for json `type` not supported by OpenAPI 3.0 // Will use "enum" with one possible value for now. @@ -184,13 +184,13 @@ extension Entity: OpenAPIEncodedNodeType, OpenAPINodeType where Description.Attr } } -extension SingleResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType { +extension SingleResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedNodeType { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return try Entity.openAPINode(using: encoder) } } -extension ManyResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity: OpenAPIEncodedNodeType { +extension ManyResourceBody: OpenAPIEncodedNodeType where Entity: OpenAPIEncodedNodeType { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { return .array(.init(format: .generic, required: true), @@ -198,7 +198,7 @@ extension ManyResourceBody: OpenAPIEncodedNodeType, OpenAPINodeType where Entity } } -extension Document: OpenAPIEncodedNodeType, OpenAPINodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType { +extension Document: OpenAPIEncodedNodeType where PrimaryResourceBody: OpenAPIEncodedNodeType, IncludeType: OpenAPINodeType { public static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode { // TODO: metadata, links, api description, errors // TODO: represent data and errors as the two distinct possible outcomes diff --git a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift index abf2e6e..580fdb3 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPI/OpenAPITypes.swift @@ -19,7 +19,7 @@ public protocol OpenAPINodeType { /// Anything conforming to `OpenAPIEncodedNodeType` can provide an /// OpenAPI schema representing itself but it may need an Encoder /// to do its job. -public protocol OpenAPIEncodedNodeType: OpenAPINodeType { +public protocol OpenAPIEncodedNodeType { static func openAPINode(using encoder: JSONEncoder) throws -> JSONNode } @@ -29,12 +29,6 @@ extension OpenAPIEncodedNodeType where Self: Sampleable, Self: Encodable { } } -extension OpenAPIEncodedNodeType { - public static func openAPINode() throws -> JSONNode { - return try openAPINode(using: JSONEncoder()) - } -} - /// Anything conforming to `RawOpenAPINodeType` can provide an /// OpenAPI schema representing itself. This second protocol is /// necessary so that one type can conditionally provide a From 2988503d7d58f9aba373339918c602980f6eb6e0 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 25 Jan 2019 11:59:05 -0800 Subject: [PATCH 10/10] Add Sampleable conformance to Unidentified. Rename file slightly. --- ...{Sampleable+JSONAPI.swift => JSONAPI+Sampleable.swift} | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) rename Sources/JSONAPIOpenAPI/Sampleable/{Sampleable+JSONAPI.swift => JSONAPI+Sampleable.swift} (89%) diff --git a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift b/Sources/JSONAPIOpenAPI/Sampleable/JSONAPI+Sampleable.swift similarity index 89% rename from Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift rename to Sources/JSONAPIOpenAPI/Sampleable/JSONAPI+Sampleable.swift index 6dd53f2..bcb832b 100644 --- a/Sources/JSONAPIOpenAPI/Sampleable/Sampleable+JSONAPI.swift +++ b/Sources/JSONAPIOpenAPI/Sampleable/JSONAPI+Sampleable.swift @@ -1,5 +1,5 @@ // -// Sampleable+JSONAPI.swift +// JSONAPI+Sampleable.swift // JSONAPIOpenAPI // // Created by Mathew Polzin on 1/24/19. @@ -43,6 +43,12 @@ extension UnknownJSONAPIError: Sampleable { } } +extension Unidentified: Sampleable { + public static var sample: Unidentified { + return Unidentified() + } +} + extension Attribute: Sampleable where RawValue: Sampleable { public static var sample: Attribute { return .init(value: RawValue.sample)