From 33a5ff41a029333dd5a9af8fb1a2d0bf61fea4f9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:50:52 -0800 Subject: [PATCH 1/2] beginning to do some breaking change cleanup --- Sources/JSONAPI/Document/Document.swift | 259 +++++++++++------- Sources/JSONAPI/Document/ResourceBody.swift | 6 +- .../JSONAPITests/Document/DocumentTests.swift | 2 +- 3 files changed, 165 insertions(+), 102 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 5c8160b..cc907eb 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -1,5 +1,5 @@ // -// JSONAPIDocument.swift +// Document.swift // JSONAPI // // Created by Mathew Polzin on 11/5/18. @@ -7,25 +7,96 @@ import Poly -/// An `EncodableJSONAPIDocument` supports encoding but not decoding. -/// It is actually more restrictive than `JSONAPIDocument` which supports both -/// encoding and decoding. -public protocol EncodableJSONAPIDocument: Equatable, Encodable { +public protocol DocumentBodyDataContext { associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody associatedtype MetaType: JSONAPI.Meta associatedtype LinksType: JSONAPI.Links associatedtype IncludeType: JSONAPI.Include - associatedtype APIDescription: APIDescriptionType - associatedtype Error: JSONAPIError +} - typealias Body = Document.Body +public protocol DocumentBodyContext: DocumentBodyDataContext { + associatedtype Error: JSONAPIError + associatedtype BodyData: DocumentBodyData + where + BodyData.PrimaryResourceBody == PrimaryResourceBody, + BodyData.MetaType == MetaType, + BodyData.LinksType == LinksType, + BodyData.IncludeType == IncludeType +} + +public protocol DocumentBodyData: DocumentBodyDataContext { + /// The document's primary resource body + /// (contains one or many resource objects) + var primary: PrimaryResourceBody { get } + + /// The document's included objects + var includes: Includes { get } + var meta: MetaType { get } + var links: LinksType { get } +} + +public protocol DocumentBody: DocumentBodyContext { + /// `true` if the document represents one or more errors. `false` if the + /// document represents JSON:API data and/or metadata. + var isError: Bool { get } + + /// Get all errors in the document, if any. + /// + /// `nil` if the Document is _not_ an error response. Otherwise, + /// an array containing all errors. + var errors: [Error]? { get } + + /// Get the document data + /// + /// `nil` if the Document is an error response. Otherwise, + /// a structure containing the primary resource, any included + /// resources, metadata, and links. + var data: BodyData? { get } + + /// Quick access to the `data`'s primary resource. + /// + /// `nil` if the Document is an error document. Otherwise, + /// the primary resource body, which will contain zero/one, one/many + /// resources dependening on the `PrimaryResourceBody` type. + /// + /// See `SingleResourceBody` and `ManyResourceBody`. + var primaryResource: PrimaryResourceBody? { get } + + /// Quick access to the `data`'s includes. + /// + /// `nil` if the Document is an error document. Otherwise, + /// zero or more includes. + var includes: Includes? { get } + + /// The metadata for the error or data document or `nil` if + /// no metadata is found. + var meta: MetaType? { get } + + /// The links for the error or data document or `nil` if + /// no links are found. + var links: LinksType? { get } +} + +/// An `EncodableJSONAPIDocument` supports encoding but not decoding. +/// It is actually more restrictive than `JSONAPIDocument` which supports both +/// encoding and decoding. +public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyContext { + associatedtype APIDescription: APIDescriptionType + associatedtype Body: DocumentBody + where + Body.PrimaryResourceBody == PrimaryResourceBody, + Body.MetaType == MetaType, + Body.LinksType == LinksType, + Body.IncludeType == IncludeType, + Body.Error == Error, + Body.BodyData == BodyData var body: Body { get } } -/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API +/// A `CodableJSONAPIDocument` supports encoding and decoding of a JSON:API /// compliant Document. -public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} +public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of @@ -36,6 +107,7 @@ public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where Prima /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` public struct Document: EncodableJSONAPIDocument { public typealias Include = IncludeType + public typealias BodyData = Body.Data /// The JSON API Spec calls this the JSON:API Object. It contains version /// and metadata information about the API itself. @@ -47,12 +119,14 @@ public struct Document, links: LinksType) { - self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links) - } -} - -extension Document where LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes, meta: MetaType) { - self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none) - } -} - -extension Document where APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, includes: Includes, meta: MetaType, links: LinksType) { - self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links) - } -} - -extension Document where IncludeType == NoIncludes, LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) { - self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none) - } -} - -extension Document where IncludeType == NoIncludes, MetaType == NoMetadata { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) { - self.init(apiDescription: apiDescription, body: body, meta: .none, links: links) - } -} - -extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) { - self.init(apiDescription: .none, body: body, meta: meta, links: links) - } -} - -extension Document where MetaType == NoMetadata, LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes) { - self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none) - } -} - -extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, includes: Includes, links: LinksType) { - self.init(apiDescription: .none, body: body, includes: includes, links: links) - } -} - -extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody) { - self.init(apiDescription: apiDescription, body: body, includes: .none) - } -} - -extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, includes: Includes) { - self.init(apiDescription: .none, body: body, includes: includes) - } -} - -extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody) { - self.init(apiDescription: .none, body: body) - } -} -*/ - -extension Document.Body.Data where PrimaryResourceBody: Appendable { +extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable { public func merging(_ other: Document.Body.Data, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { @@ -218,7 +218,7 @@ extension Document.Body.Data where PrimaryResourceBody: Appendable { } } -extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks { +extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable, MetaType == NoMetadata, LinksType == NoLinks { public func merging(_ other: Document.Body.Data) -> Document.Body.Data { return merging(other, combiningMetaWith: { _, _ in .none }, @@ -328,7 +328,7 @@ extension Document { } } -extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { +extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootCodingKeys.self) @@ -420,8 +420,9 @@ extension Document.Body.Data: CustomStringConvertible { extension Document { /// A Document that only supports error bodies. This is useful if you wish to pass around a /// Document type but you wish to constrain it to error values. - @dynamicMemberLookup public struct ErrorDocument: EncodableJSONAPIDocument { + public typealias BodyData = Document.BodyData + public var body: Document.Body { return document.body } private let document: Document @@ -436,8 +437,27 @@ extension Document { try container.encode(document) } - public subscript(dynamicMember path: KeyPath) -> T { - return document[keyPath: path] + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + public var apiDescription: APIDescription { + return document.apiDescription + } + + /// Get all errors in the document, if any. + public var errors: [Error] { + return document.body.errors ?? [] + } + + /// The metadata for the error or data document or `nil` if + /// no metadata is found. + public var meta: MetaType? { + return document.body.meta + } + + /// The links for the error or data document or `nil` if + /// no links are found. + public var links: LinksType? { + return document.body.links } public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool { @@ -447,8 +467,9 @@ extension Document { /// A Document that only supports success bodies. This is useful if you wish to pass around a /// Document type but you wish to constrain it to success values. - @dynamicMemberLookup public struct SuccessDocument: EncodableJSONAPIDocument { + public typealias BodyData = Document.BodyData + public var body: Document.Body { return document.body } private let document: Document @@ -471,8 +492,50 @@ extension Document { try container.encode(document) } - public subscript(dynamicMember path: KeyPath) -> T { - return document[keyPath: path] + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + public var apiDescription: APIDescription { + return document.apiDescription + } + + /// Get the document data + /// + /// `nil` if the Document is an error response. Otherwise, + /// a structure containing the primary resource, any included + /// resources, metadata, and links. + var data: BodyData? { + return document.body.data + } + + /// Quick access to the `data`'s primary resource. + /// + /// `nil` if the Document is an error document. Otherwise, + /// the primary resource body, which will contain zero/one, one/many + /// resources dependening on the `PrimaryResourceBody` type. + /// + /// See `SingleResourceBody` and `ManyResourceBody`. + var primaryResource: PrimaryResourceBody? { + return document.body.primaryResource + } + + /// Quick access to the `data`'s includes. + /// + /// `nil` if the Document is an error document. Otherwise, + /// zero or more includes. + var includes: Includes? { + return document.body.includes + } + + /// The metadata for the error or data document or `nil` if + /// no metadata is found. + var meta: MetaType? { + return document.body.meta + } + + /// The links for the error or data document or `nil` if + /// no links are found. + var links: LinksType? { + return document.body.links } public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool { @@ -481,7 +544,7 @@ extension Document { } } -extension Document.ErrorDocument: Decodable, JSONAPIDocument +extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -494,7 +557,7 @@ extension Document.ErrorDocument: Decodable, JSONAPIDocument } } -extension Document.SuccessDocument: Decodable, JSONAPIDocument +extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 3fdef10..7d8b9e2 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -40,11 +40,11 @@ public protocol ResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. -public protocol Appendable { +public protocol ResourceBodyAppendable { func appending(_ other: Self) -> Self } -public func +(_ left: R, right: R) -> R { +public func +(_ left: R, right: R) -> R { return left.appending(right) } @@ -60,7 +60,7 @@ public struct SingleResourceBody: EncodableResourceBody, Appendable { +public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { public let values: [Entity] public init(resourceObjects: [Entity]) { diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index bbf36e9..d6f6351 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -12,7 +12,7 @@ import Poly class DocumentTests: XCTestCase { func test_genericDocFunc() { - func test(_ doc: Doc) { + func test(_ doc: Doc) { let _ = encoded(value: doc) XCTAssert(Doc.PrimaryResourceBody.self == NoResourceBody.self) From 706346e3a60fb8dd34e52ab9bcc485a61b8d749f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 20:04:47 -0800 Subject: [PATCH 2/2] just reformatting and rearranging --- Sources/JSONAPI/Document/Document.swift | 160 +++++++++++++----------- 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index cc907eb..98b4445 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -120,79 +120,10 @@ public struct Document - public let meta: MetaType - public let links: LinksType - - public init(primary: PrimaryResourceBody, includes: Includes, meta: MetaType, links: LinksType) { - self.primary = primary - self.includes = includes - self.meta = meta - self.links = links - } - } - - /// `true` if the document represents one or more errors. `false` if the - /// document represents JSON:API data and/or metadata. - public var isError: Bool { - guard case .errors = self else { return false } - return true - } - - public var errors: [Error]? { - guard case let .errors(errors, meta: _, links: _) = self else { return nil } - return errors - } - - public var data: Data? { - guard case let .data(data) = self else { return nil } - return data - } - - public var primaryResource: PrimaryResourceBody? { - guard case let .data(data) = self else { return nil } - return data.primary - } - - public var includes: Includes? { - guard case let .data(data) = self else { return nil } - return data.includes - } - - public var meta: MetaType? { - switch self { - case .data(let data): - return data.meta - case .errors(_, meta: let metadata?, links: _): - return metadata - default: - return nil - } - } - - public var links: LinksType? { - switch self { - case .data(let data): - return data.links - case .errors(_, meta: _, links: let links?): - return links - default: - return nil - } - } - } - - public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) { + public init(apiDescription: APIDescription, + errors: [Error], + meta: MetaType? = nil, + links: LinksType? = nil) { body = .errors(errors, meta: meta, links: links) self.apiDescription = apiDescription } @@ -202,11 +133,92 @@ public struct Document, meta: MetaType, links: LinksType) { - self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links)) + self.body = .data( + .init( + primary: body, + includes: includes, + meta: meta, + links: links + ) + ) self.apiDescription = apiDescription } } +extension Document { + public enum Body: DocumentBody, Equatable { + case errors([Error], meta: MetaType?, links: LinksType?) + case data(Data) + + public typealias BodyData = Data + + public struct Data: DocumentBodyData, Equatable { + /// The document's Primary Resource object(s) + public let primary: PrimaryResourceBody + /// The document's included objects + public let includes: Includes + public let meta: MetaType + public let links: LinksType + + public init(primary: PrimaryResourceBody, includes: Includes, meta: MetaType, links: LinksType) { + self.primary = primary + self.includes = includes + self.meta = meta + self.links = links + } + } + + /// `true` if the document represents one or more errors. `false` if the + /// document represents JSON:API data and/or metadata. + public var isError: Bool { + guard case .errors = self else { return false } + return true + } + + public var errors: [Error]? { + guard case let .errors(errors, meta: _, links: _) = self else { return nil } + return errors + } + + public var data: Data? { + guard case let .data(data) = self else { return nil } + return data + } + + public var primaryResource: PrimaryResourceBody? { + guard case let .data(data) = self else { return nil } + return data.primary + } + + public var includes: Includes? { + guard case let .data(data) = self else { return nil } + return data.includes + } + + public var meta: MetaType? { + switch self { + case .data(let data): + return data.meta + case .errors(_, meta: let metadata?, links: _): + return metadata + default: + return nil + } + } + + public var links: LinksType? { + switch self { + case .data(let data): + return data.links + case .errors(_, meta: _, links: let links?): + return links + default: + return nil + } + } + } +} + extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable { public func merging(_ other: Document.Body.Data, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,