From a596ecaecc5be01fd16186b50232f36749e536b8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 09:23:44 -0700 Subject: [PATCH] A stab at separating out decoding enough to make it possible to use encode-only sparse fieldsets with JSONDocument --- Sources/JSONAPI/Document/Document.swift | 144 +++++++++--------- Sources/JSONAPI/Document/Includes.swift | 40 ++--- Sources/JSONAPI/Document/ResourceBody.swift | 72 +++++---- .../Resource/Poly+PrimaryResource.swift | 23 ++- 4 files changed, 159 insertions(+), 120 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 1a0fd5f..d0a34d9 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -7,19 +7,21 @@ import Poly -public protocol JSONAPIDocument: Codable, Equatable { - associatedtype PrimaryResourceBody: JSONAPI.ResourceBody - associatedtype MetaType: JSONAPI.Meta - associatedtype LinksType: JSONAPI.Links - associatedtype IncludeType: JSONAPI.Include - associatedtype APIDescription: APIDescriptionType - associatedtype Error: JSONAPIError +public protocol EncodableJSONAPIDocument: Equatable, Encodable { + 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 + typealias Body = Document.Body - var body: Body { get } + var body: Body { get } } +public protocol JSONAPIDocument: 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 /// a JSON API response. @@ -27,7 +29,7 @@ public protocol JSONAPIDocument: Codable, Equatable { /// API uses snake case, you will want to use /// a conversion such as the one offerred by the /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` -public struct Document: JSONAPIDocument { +public struct Document: EncodableJSONAPIDocument { public typealias Include = IncludeType /// The JSON API Spec calls this the JSON:API Object. It contains version @@ -273,66 +275,6 @@ extension Document { case links case jsonapi } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: RootCodingKeys.self) - - if let noData = NoAPIDescription() as? APIDescription { - apiDescription = noData - } else { - apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) - } - - let errors = try container.decodeIfPresent([Error].self, forKey: .errors) - - let meta: MetaType? - if let noMeta = NoMetadata() as? MetaType { - meta = noMeta - } else { - do { - meta = try container.decode(MetaType.self, forKey: .meta) - } catch { - meta = nil - } - } - - let links: LinksType? - if let noLinks = NoLinks() as? LinksType { - links = noLinks - } else { - do { - links = try container.decode(LinksType.self, forKey: .links) - } catch { - links = nil - } - } - - // If there are errors, there cannot be a body. Return errors and any metadata found. - if let errors = errors { - body = .errors(errors, meta: meta, links: links) - return - } - - let data: PrimaryResourceBody - if let noData = NoResourceBody() as? PrimaryResourceBody { - data = noData - } else { - data = try container.decode(PrimaryResourceBody.self, forKey: .data) - } - - let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) - - // TODO come back to this and make robust - - guard let metaVal = meta else { - throw JSONAPIEncodingError.missingOrMalformedMetadata - } - guard let linksVal = links else { - throw JSONAPIEncodingError.missingOrMalformedLinks - } - - body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) - } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RootCodingKeys.self) @@ -377,6 +319,68 @@ extension Document { } } +extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: RootCodingKeys.self) + + if let noData = NoAPIDescription() as? APIDescription { + apiDescription = noData + } else { + apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) + } + + let errors = try container.decodeIfPresent([Error].self, forKey: .errors) + + let meta: MetaType? + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + do { + meta = try container.decode(MetaType.self, forKey: .meta) + } catch { + meta = nil + } + } + + let links: LinksType? + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + do { + links = try container.decode(LinksType.self, forKey: .links) + } catch { + links = nil + } + } + + // If there are errors, there cannot be a body. Return errors and any metadata found. + if let errors = errors { + body = .errors(errors, meta: meta, links: links) + return + } + + let data: PrimaryResourceBody + if let noData = NoResourceBody() as? PrimaryResourceBody { + data = noData + } else { + data = try container.decode(PrimaryResourceBody.self, forKey: .data) + } + + let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) + + // TODO come back to this and make robust + + guard let metaVal = meta else { + throw JSONAPIEncodingError.missingOrMalformedMetadata + } + guard let linksVal = links else { + throw JSONAPIEncodingError.missingOrMalformedLinks + } + + body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) + } +} + // MARK: - CustomStringConvertible extension Document: CustomStringConvertible { diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 8682ee9..1608b2d 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -7,9 +7,9 @@ import Poly -public typealias Include = JSONPoly +public typealias Include = EncodableJSONPoly -public struct Includes: Codable, Equatable { +public struct Includes: Encodable, Equatable { public static var none: Includes { return .init(values: []) } let values: [I] @@ -17,23 +17,6 @@ public struct Includes: Codable, Equatable { public init(values: [I]) { self.values = values } - - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - - // If not parsing includes, no need to loop over them. - guard I.self != NoIncludes.self else { - values = [] - return - } - - var valueAggregator = [I]() - while !container.isAtEnd { - valueAggregator.append(try container.decode(I.self)) - } - - values = valueAggregator - } public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() @@ -52,6 +35,25 @@ public struct Includes: Codable, Equatable { } } +extension Includes: Decodable where I: Decodable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + // If not parsing includes, no need to loop over them. + guard I.self != NoIncludes.self else { + values = [] + return + } + + var valueAggregator = [I]() + while !container.isAtEnd { + valueAggregator.append(try container.decode(I.self)) + } + + values = valueAggregator + } +} + extension Includes { public func appending(_ other: Includes) -> Includes { return Includes(values: values + other.values) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 0752331..3e9141d 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,26 +5,36 @@ // Created by Mathew Polzin on 11/10/18. // +public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} + +public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} + /// This protocol allows for `SingleResourceBody` to contain a `null` /// data object where `ManyResourceBody` cannot (because an empty /// array should be used for no results). -public protocol OptionalPrimaryResource: Equatable, Codable {} +public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {} /// A PrimaryResource is a type that can be used in the body of a JSON API /// document as the primary resource. -public protocol PrimaryResource: OptionalPrimaryResource {} +public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} + +extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {} extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} +/// An `EncodableResourceBody` is a `ResourceBody` that only supports being +/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding +/// and decoding. +public protocol EncodableResourceBody: Equatable, Encodable {} + /// A ResourceBody is a representation of the body of the JSON API Document. /// It can either be one resource (which can be specified as optional or not) /// or it can contain many resources (and array with zero or more entries). -public protocol ResourceBody: Codable, Equatable { -} +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 AppendableResourceBody: ResourceBody { +public protocol AppendableResourceBody { func appending(_ other: Self) -> Self } @@ -32,7 +42,7 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } -public struct SingleResourceBody: ResourceBody { +public struct SingleResourceBody: EncodableResourceBody { public let value: Entity public init(resourceObject: Entity) { @@ -40,7 +50,7 @@ public struct SingleResourceBody: Resou } } -public struct ManyResourceBody: AppendableResourceBody { +public struct ManyResourceBody: EncodableResourceBody, AppendableResourceBody { public let values: [Entity] public init(resourceObjects: [Entity]) { @@ -60,19 +70,6 @@ public struct NoResourceBody: ResourceBody { // MARK: Codable extension SingleResourceBody { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let anyNil: Any? = nil - if container.decodeNil(), - let val = anyNil as? Entity { - value = val - return - } - - value = try container.decode(Entity.self) - } - public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() @@ -87,16 +84,22 @@ extension SingleResourceBody { } } -extension ManyResourceBody { - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - var valueAggregator = [Entity]() - while !container.isAtEnd { - valueAggregator.append(try container.decode(Entity.self)) - } - values = valueAggregator - } +extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let anyNil: Any? = nil + if container.decodeNil(), + let val = anyNil as? Entity { + value = val + return + } + + value = try container.decode(Entity.self) + } +} + +extension ManyResourceBody { public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() @@ -106,6 +109,17 @@ extension ManyResourceBody { } } +extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var valueAggregator = [Entity]() + while !container.isAtEnd { + valueAggregator.append(try container.decode(Entity.self)) + } + values = valueAggregator + } +} + // MARK: CustomStringConvertible extension SingleResourceBody: CustomStringConvertible { diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index e2ed829..f1cad3a 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -15,9 +15,10 @@ import Poly /// disparate types under one roof for /// the purposes of JSON API compliant /// encoding or decoding. -public typealias JSONPoly = Poly & PrimaryResource +public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource -public typealias PolyWrapped = Codable & Equatable +public typealias EncodablePolyWrapped = Encodable & Equatable +public typealias PolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: PrimaryResource { public init(from decoder: Decoder) throws { @@ -30,28 +31,46 @@ extension Poly0: PrimaryResource { } // MARK: - 1 type +extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} + extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} // MARK: - 2 types +extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} + extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} // MARK: - 3 types +extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} + extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} // MARK: - 4 types +extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} + extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} // MARK: - 5 types +extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} + extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} // MARK: - 6 types +extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} + extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} // MARK: - 7 types +extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {} + extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} // MARK: - 8 types +extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {} + extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} // MARK: - 9 types +extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {} + extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}