From f7bfa91ccca76231fb097c87cc63ba489c8de8ac Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:03:51 -0800 Subject: [PATCH 01/23] lots of comparison code, a few small breaking changes --- Package.resolved | 4 +- Package.swift | 2 +- Sources/JSONAPI/Document/Document.swift | 21 +- Sources/JSONAPI/Document/Includes.swift | 6 +- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 6 +- .../JSONAPI/Error/GenericJSONAPIError.swift | 11 +- .../Resource Object/ResourceObject.swift | 6 + .../Comparisons/ArrayCompare.swift | 78 ++++++ .../Comparisons/AttributesCompare.swift | 78 ++++++ .../Comparisons/Comparison.swift | 68 +++++ .../Comparisons/DocumentCompare.swift | 180 +++++++++++++ .../Comparisons/DocumentDataCompare.swift | 167 ++++++++++++ .../Comparisons/IncludesCompare.swift | 66 +++++ .../Comparisons/RelationshipsCompare.swift | 105 ++++++++ .../Comparisons/ResourceObjectCompare.swift | 71 ++++++ Sources/JSONAPITesting/Optional+ZipWith.swift | 12 + .../Comparisons/AttributesCompareTests.swift | 74 ++++++ .../Comparisons/DocumentCompareTests.swift | 170 +++++++++++++ .../Comparisons/IncludesCompareTests.swift | 239 ++++++++++++++++++ .../RelationshipsCompareTests.swift | 14 + .../ResourceObjectCompareTests.swift | 68 +++++ 21 files changed, 1431 insertions(+), 15 deletions(-) create mode 100644 Sources/JSONAPITesting/Comparisons/ArrayCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/AttributesCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/Comparison.swift create mode 100644 Sources/JSONAPITesting/Comparisons/DocumentCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/IncludesCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift create mode 100644 Sources/JSONAPITesting/Optional+ZipWith.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift diff --git a/Package.resolved b/Package.resolved index b1970b8..cc81a53 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "b24fd3b41bf3126d4c6dede3708135182172af60", - "version": "2.2.0" + "revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf", + "version": "2.3.0" } } ] diff --git a/Package.swift b/Package.swift index 2093673..5b780cb 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 5c8160b..00d1ce0 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -20,7 +20,16 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable { typealias Body = Document.Body + /// The Body of the Document. This body is either one or more errors + /// with links and metadata attempted to parse but not guaranteed or + /// it is a successful data struct containing all the primary and + /// included resources, the metadata, and the links that this + /// document type specifies. var body: Body { get } + + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + var apiDescription: APIDescription { get } } /// A `JSONAPIDocument` supports encoding and decoding of a JSON:API @@ -30,6 +39,7 @@ public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where Prima /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of /// a JSON API response. +/// /// Note that this type uses Camel case. If your /// API uses snake case, you will want to use /// a conversion such as the one offerred by the @@ -37,15 +47,10 @@ public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where Prima public struct Document: EncodableJSONAPIDocument { public typealias Include = IncludeType - /// The JSON API Spec calls this the JSON:API Object. It contains version - /// and metadata information about the API itself. + // See `EncodableJSONAPIDocument` for documentation. public let apiDescription: APIDescription - /// The Body of the Document. This body is either one or more errors - /// with links and metadata attempted to parse but not guaranteed or - /// it is a successful data struct containing all the primary and - /// included resources, the metadata, and the links that this - /// document type specifies. + // See `EncodableJSONAPIDocument` for documentation. public let body: Body public enum Body: Equatable { @@ -423,6 +428,7 @@ extension Document { @dynamicMemberLookup public struct ErrorDocument: EncodableJSONAPIDocument { public var body: Document.Body { return document.body } + public var apiDescription: APIDescription { return document.apiDescription } private let document: Document @@ -450,6 +456,7 @@ extension Document { @dynamicMemberLookup public struct SuccessDocument: EncodableJSONAPIDocument { public var body: Document.Body { return document.body } + public var apiDescription: APIDescription { return document.apiDescription } private let document: Document diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index b6d93e0..d74171f 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -14,15 +14,15 @@ public typealias Include = EncodableJSONPoly /// /// If you have /// -/// `let includes: Includes> = ...` +/// let includes: Includes> = ... /// /// then you can access all `Thing1` included resources with /// -/// `let includedThings = includes[Thing1.self]` +/// let includedThings = includes[Thing1.self] public struct Includes: Encodable, Equatable { public static var none: Includes { return .init(values: []) } - let values: [I] + public let values: [I] public init(values: [I]) { self.values = values diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index 28795b9..cdabc07 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -6,7 +6,7 @@ // /// Most of the JSON:API Spec defined Error fields. -public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType { +public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType, CustomStringConvertible { /// a unique identifier for this particular occurrence of the problem public let id: IdType? // public let links: Links? // we skip this for now to avoid adding complexity to using this basic type. @@ -61,6 +61,10 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq ].compactMap { $0 } return Dictionary(uniqueKeysWithValues: keysAndValues) } + + public var description: String { + return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ") + } } /// `BasicJSONAPIError` optionally decodes many possible fields diff --git a/Sources/JSONAPI/Error/GenericJSONAPIError.swift b/Sources/JSONAPI/Error/GenericJSONAPIError.swift index 09383e7..473caac 100644 --- a/Sources/JSONAPI/Error/GenericJSONAPIError.swift +++ b/Sources/JSONAPI/Error/GenericJSONAPIError.swift @@ -8,7 +8,7 @@ /// `GenericJSONAPIError` can be used to specify whatever error /// payload you expect to need to parse in responses and handle any /// other payload structure as `.unknownError`. -public enum GenericJSONAPIError: JSONAPIError { +public enum GenericJSONAPIError: JSONAPIError, CustomStringConvertible { case unknownError case error(ErrorPayload) @@ -35,6 +35,15 @@ public enum GenericJSONAPIError: JSONAPIError public static var unknown: Self { return .unknownError } + + public var description: String { + switch self { + case .unknownError: + return "unknown error" + case .error(let payload): + return String(describing: payload) + } + } } public extension GenericJSONAPIError { diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a62d271..4c59e88 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -102,6 +102,12 @@ extension ResourceObjectProxy { public protocol ResourceObjectType: ResourceObjectProxy, PrimaryResource where Description: ResourceObjectDescription { associatedtype Meta: JSONAPI.Meta associatedtype Links: JSONAPI.Links + + /// Any additional metadata packaged with the entity. + var meta: Meta { get } + + /// Links related to the entity. + var links: Links { get } } public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType {} diff --git a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift new file mode 100644 index 0000000..d14ced7 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift @@ -0,0 +1,78 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/5/19. +// + +import JSONAPI + +public enum ArrayElementComparison: Equatable, CustomStringConvertible { + case same + case missing + case differentTypes(String, String) + case differentValues(String, String) + case prebuilt(String) + + public init(sameTypeComparison: Comparison) { + switch sameTypeComparison { + case .same: + self = .same + case .different(let one, let two): + self = .differentValues(one, two) + case .prebuilt(let str): + self = .prebuilt(str) + } + } + + public init(resourceObjectComparison: ResourceObjectComparison) { + guard !resourceObjectComparison.isSame else { + self = .same + return + } + + self = .prebuilt( + resourceObjectComparison + .differences + .sorted { $0.key < $1.key } + .map { "\($0.key): \($0.value)" } + .joined(separator: ", ") + ) + } + + public var description: String { + switch self { + case .same: + return "same" + case .missing: + return "missing" + case .differentTypes(let one, let two), + .differentValues(let one, let two): + return "\(one) ≠ \(two)" + case .prebuilt(let description): + return description + } + } + + public var rawValue: String { description } +} + +extension Array { + func compare(to other: Self, using compare: (Element, Element) -> ArrayElementComparison) -> [ArrayElementComparison] { + let isSelfLonger = count >= other.count + + let longer = isSelfLonger ? self : other + let shorter = isSelfLonger ? other : self + + return longer.indices.map { idx in + guard shorter.indices.contains(idx) else { + return .missing + } + + let this = longer[idx] + let other = shorter[idx] + + return compare(this, other) + } + } +} diff --git a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift new file mode 100644 index 0000000..541c52e --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift @@ -0,0 +1,78 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import JSONAPI + +extension Attributes { + public func compare(to other: Self) -> [String: Comparison] { + let mirror1 = Mirror(reflecting: self) + let mirror2 = Mirror(reflecting: other) + + var comparisons = [String: Comparison]() + + for child in mirror1.children { + guard let childLabel = child.label else { continue } + + let childDescription = attributeDescription(of: child.value) + + guard let otherChild = mirror2.children.first(where: { $0.label == childLabel }) else { + comparisons[childLabel] = .different(childDescription, "missing") + continue + } + + if (attributesEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = attributeDescription(of: otherChild.value) + + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } + + return comparisons + } +} + +fileprivate func attributesEqual(_ one: Any, _ two: Any) -> Bool { + guard let attr = one as? AbstractAttribute else { + return false + } + + return attr.equals(two) +} + +fileprivate func attributeDescription(of thing: Any) -> String { + return (thing as? AbstractAttribute)?.abstractDescription ?? String(describing: thing) +} + +protocol AbstractAttribute { + var abstractDescription: String { get } + + func equals(_ other: Any) -> Bool +} + +extension Attribute: AbstractAttribute { + var abstractDescription: String { String(describing: value) } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} + +extension TransformedAttribute: AbstractAttribute { + var abstractDescription: String { String(describing: value) } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} diff --git a/Sources/JSONAPITesting/Comparisons/Comparison.swift b/Sources/JSONAPITesting/Comparisons/Comparison.swift new file mode 100644 index 0000000..a6b97ee --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/Comparison.swift @@ -0,0 +1,68 @@ +// +// Comparison.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +public enum Comparison: Equatable, CustomStringConvertible { + case same + case different(String, String) + case prebuilt(String) + + init(_ one: T, _ two: T) { + guard one == two else { + self = .different(String(describing: one), String(describing: two)) + return + } + self = .same + } + + init(reducing other: ArrayElementComparison) { + switch other { + case .same: + self = .same + case .differentTypes(let one, let two), + .differentValues(let one, let two): + self = .different(one, two) + case .missing: + self = .different("array length 1", "array length 2") + case .prebuilt(let str): + self = .prebuilt(str) + } + } + + public var description: String { + switch self { + case .same: + return "same" + case .different(let one, let two): + return "\(one) ≠ \(two)" + case .prebuilt(let str): + return str + } + } + + public var rawValue: String { description } + + public var isSame: Bool { self == .same } +} + +public typealias NamedDifferences = [String: String] + +public protocol PropertyComparable: CustomStringConvertible { + var differences: NamedDifferences { get } +} + +extension PropertyComparable { + public var description: String { + return differences + .map { "(\($0): \($1))" } + .sorted() + .joined(separator: ", ") + } + + public var rawValue: String { description } + + public var isSame: Bool { differences.isEmpty } +} diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift new file mode 100644 index 0000000..15c417f --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -0,0 +1,180 @@ +// +// DocumentCompare.swift +// JSONAPITesting +// +// Created by Mathew Polzin on 11/4/19. +// + +import JSONAPI + +public struct DocumentComparison: Equatable, PropertyComparable { + public let apiDescription: Comparison + public let body: BodyComparison + + init(apiDescription: Comparison, body: BodyComparison) { + self.apiDescription = apiDescription + self.body = body + } + + public var differences: NamedDifferences { + return Dictionary( + [ + apiDescription != .same ? ("API Description", apiDescription.rawValue) : nil, + body != .same ? ("Body", body.rawValue) : nil + ].compactMap { $0 }, + uniquingKeysWith: { $1 } + ) + } +} + +public enum BodyComparison: Equatable, CustomStringConvertible { + case same + case dataErrorMismatch(errorOnLeft: Bool) + case differentErrors(ErrorComparison) + case differentData(DocumentDataComparison) + + public typealias ErrorComparison = [Comparison] + + static func compare(errors errors1: [E], _ meta1: M?, _ links1: L?, with errors2: [E], _ meta2: M?, _ links2: L?) -> ErrorComparison { + return errors1.compare( + to: errors2, + using: { error1, error2 in + guard error1 != error2 else { + return .same + } + + return .differentValues( + String(describing: error1), + String(describing: error2) + ) + } + ).map(Comparison.init) + [ + Comparison(meta1, meta2), + Comparison(links1, links2) + ] + } + + public var description: String { + switch self { + case .same: + return "same" + case .dataErrorMismatch(errorOnLeft: let errorOnLeft): + let errorString = "error response" + let dataString = "data response" + let left = errorOnLeft ? errorString : dataString + let right = errorOnLeft ? dataString : errorString + + return "\(left) ≠ \(right)" + case .differentErrors(let comparisons): + return comparisons + .filter { !$0.isSame } + .map { $0.rawValue } + .sorted() + .joined(separator: ", ") + case .differentData(let comparison): + return comparison.rawValue + } + } + + public var rawValue: String { description } +} + +extension Document { + public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) + } + + public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) + } + + public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == ManyResourceBody, T: ResourceObjectType { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) + } +} + +extension Document.Body { + public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + guard self != other else { + return .same + } + + switch (self, other) { + case (.errors(let errors1), .errors(let errors2)): + return .differentErrors(BodyComparison.compare(errors: errors1.0, + errors1.meta, + errors1.links, + with: errors2.0, + errors2.meta, + errors2.links)) + case (.errors, .data): + return .dataErrorMismatch(errorOnLeft: true) + case (.data, .errors): + return .dataErrorMismatch(errorOnLeft: false) + case (.data(let data1), .data(let data2)): + return .differentData(data1.compare(to: data2)) + } + } + + public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + guard self != other else { + return .same + } + + switch (self, other) { + case (.errors(let errors1), .errors(let errors2)): + return .differentErrors(BodyComparison.compare(errors: errors1.0, + errors1.meta, + errors1.links, + with: errors2.0, + errors2.meta, + errors2.links)) + case (.errors, .data): + return .dataErrorMismatch(errorOnLeft: true) + case (.data, .errors): + return .dataErrorMismatch(errorOnLeft: false) + case (.data(let data1), .data(let data2)): + return .differentData(data1.compare(to: data2)) + } + } + + public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { + guard self != other else { + return .same + } + + switch (self, other) { + case (.errors(let errors1), .errors(let errors2)): + return .differentErrors(BodyComparison.compare(errors: errors1.0, + errors1.meta, + errors1.links, + with: errors2.0, + errors2.meta, + errors2.links)) + case (.errors, .data): + return .dataErrorMismatch(errorOnLeft: true) + case (.data, .errors): + return .dataErrorMismatch(errorOnLeft: false) + case (.data(let data1), .data(let data2)): + return .differentData(data1.compare(to: data2)) + } + } +} diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift new file mode 100644 index 0000000..392bab0 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -0,0 +1,167 @@ +// +// DocumentDataCompare.swift +// +// +// Created by Mathew Polzin on 11/5/19. +// + +import JSONAPI + +public struct DocumentDataComparison: Equatable, PropertyComparable { + public let primary: PrimaryResourceBodyComparison + public let includes: IncludesComparison + public let meta: Comparison + public let links: Comparison + + init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: Comparison, links: Comparison) { + self.primary = primary + self.includes = includes + self.meta = meta + self.links = links + } + + public var differences: NamedDifferences { + return Dictionary( + [ + !primary.isSame ? ("Primary Resource", primary.rawValue) : nil, + !includes.isSame ? ("Includes", includes.rawValue) : nil, + !meta.isSame ? ("Meta", meta.rawValue) : nil, + !links.isSame ? ("Links", links.rawValue) : nil + ].compactMap { $0 }, + uniquingKeysWith: { $1 } + ) + } +} + +extension Document.Body.Data { + public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + return .init( + primary: primary.compare(to: other.primary), + includes: includes.compare(to: other.includes), + meta: Comparison(meta, other.meta), + links: Comparison(links, other.links) + ) + } + + public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + return .init( + primary: primary.compare(to: other.primary), + includes: includes.compare(to: other.includes), + meta: Comparison(meta, other.meta), + links: Comparison(links, other.links) + ) + } + + public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { + return .init( + primary: primary.compare(to: other.primary), + includes: includes.compare(to: other.includes), + meta: Comparison(meta, other.meta), + links: Comparison(links, other.links) + ) + } +} + +public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { + case single(ResourceObjectComparison) + case many(ManyResourceObjectComparison) + case other(Comparison) + + public var isSame: Bool { + switch self { + case .other(let comparison): + return comparison == .same + case .single(let comparison): + return comparison.isSame + case .many(let comparison): + return comparison.isSame + } + } + + public var description: String { + switch self { + case .other(let comparison): + return comparison.rawValue + case .single(let comparison): + return comparison.rawValue + case .many(let comparison): + return comparison.rawValue + } + } + + public var rawValue: String { return description } +} + +public struct ManyResourceObjectComparison: Equatable, PropertyComparable { + public let comparisons: [ArrayElementComparison] + + public init(_ comparisons: [ArrayElementComparison]) { + self.comparisons = comparisons + } + + public var differences: NamedDifferences { + return comparisons + .enumerated() + .filter { $0.element != .same } + .reduce(into: [String: String]()) { hash, next in + hash["resource \(next.offset + 1)"] = next.element.rawValue + } + } +} + +extension SingleResourceBody where Entity: ResourceObjectType { + public func compare(to other: Self) -> PrimaryResourceBodyComparison { + return .single(.init(value, other.value)) + } +} + +public protocol _OptionalResourceObjectType { + associatedtype Wrapped: ResourceObjectType + + var maybeValue: Wrapped? { get } +} + +extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { + public var maybeValue: Wrapped? { + switch self { + case .none: + return nil + case .some(let value): + return value + } + } +} + +extension SingleResourceBody where Entity: _OptionalResourceObjectType { + public func compare(to other: Self) -> PrimaryResourceBodyComparison { + guard let one = value.maybeValue, + let two = other.value.maybeValue else { + return .other(Comparison(value, other.value)) + } + return .single(.init(one, two)) + } +} + +extension ManyResourceBody where Entity: ResourceObjectType { + public func compare(to other: Self) -> PrimaryResourceBodyComparison { + return .many(.init(values.compare(to: other.values, using: { r1, r2 in + let r1AsResource = r1 as? AbstractResourceObjectType + + let maybeComparison = r1AsResource + .flatMap { resource in + try? ArrayElementComparison( + resourceObjectComparison: resource.abstractCompare(to: r2) + ) + } + + guard let comparison = maybeComparison else { + return .differentValues( + String(describing: r1), + String(describing: r2) + ) + } + + return comparison + }))) + } +} diff --git a/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift new file mode 100644 index 0000000..c638628 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift @@ -0,0 +1,66 @@ +// +// IncludesCompare.swift +// +// +// Created by Mathew Polzin on 11/4/19. +// + +import JSONAPI +import Poly + +public struct IncludesComparison: Equatable, PropertyComparable { + public let comparisons: [ArrayElementComparison] + + public init(_ comparisons: [ArrayElementComparison]) { + self.comparisons = comparisons + } + + public var differences: NamedDifferences { + return comparisons + .enumerated() + .filter { $0.element != .same } + .reduce(into: [String: String]()) { hash, next in + hash["include \(next.offset + 1)"] = next.element.rawValue + } + } +} + +extension Includes { + public func compare(to other: Self) -> IncludesComparison { + + return IncludesComparison( + values.compare(to: other.values) { thisInclude, otherInclude in + guard thisInclude != otherInclude else { + return .same + } + + let thisWrappedValue = thisInclude.value + let otherWrappedValue = otherInclude.value + guard type(of: thisWrappedValue) == type(of: otherWrappedValue) else { + return .differentTypes( + String(describing: type(of: thisWrappedValue)), + String(describing: type(of: otherWrappedValue)) + ) + } + + let thisAsAResource = thisWrappedValue as? AbstractResourceObjectType + + let maybeComparison = thisAsAResource + .flatMap { resource in + try? ArrayElementComparison( + resourceObjectComparison: resource.abstractCompare(to: otherWrappedValue) + ) + } + + guard let comparison = maybeComparison else { + return .differentValues( + String(describing: thisWrappedValue), + String(describing: otherWrappedValue) + ) + } + + return comparison + } + ) + } +} diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift new file mode 100644 index 0000000..c217cb9 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -0,0 +1,105 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import JSONAPI + +extension Relationships { + public func compare(to other: Self) -> [String: Comparison] { + let mirror1 = Mirror(reflecting: self) + let mirror2 = Mirror(reflecting: other) + + var comparisons = [String: Comparison]() + + for child in mirror1.children { + guard let childLabel = child.label else { continue } + + let childDescription = relationshipDescription(of: child.value) + + guard let otherChild = mirror2.children.first(where: { $0.label == childLabel }) else { + comparisons[childLabel] = .different(childDescription, "missing") + continue + } + + if (relationshipsEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = relationshipDescription(of: otherChild.value) + + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } + + return comparisons + } +} + +fileprivate func relationshipsEqual(_ one: Any, _ two: Any) -> Bool { + guard let attr = one as? AbstractRelationship else { + return false + } + + return attr.equals(two) +} + +fileprivate func relationshipDescription(of thing: Any) -> String { + return (thing as? AbstractRelationship)?.abstractDescription ?? String(describing: thing) +} + +protocol AbstractRelationship { + var abstractDescription: String { get } + + func equals(_ other: Any) -> Bool +} + +extension ToOneRelationship: AbstractRelationship { + var abstractDescription: String { + if meta is NoMetadata && links is NoLinks { + return String(describing: id) + } + + return String(describing: + ( + String(describing: id), + String(describing: meta), + String(describing: links) + ) + ) + } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} + +extension ToManyRelationship: AbstractRelationship { + var abstractDescription: String { + + let idsString = ids.map { String.init(describing: $0.rawValue) }.joined(separator: ", ") + + if meta is NoMetadata && links is NoLinks { + return idsString + } + + return String(describing: + ( + idsString, + String(describing: meta), + String(describing: links) + ) + ) + } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} diff --git a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift new file mode 100644 index 0000000..2619008 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift @@ -0,0 +1,71 @@ +// +// ResourceObjectCompare.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import JSONAPI + +public struct ResourceObjectComparison: Equatable, PropertyComparable { + public typealias ComparisonHash = [String: Comparison] + + public let id: Comparison + public let attributes: ComparisonHash + public let relationships: ComparisonHash + public let meta: Comparison + public let links: Comparison + + public init(_ one: T, _ two: T) { + id = Comparison(one.id.rawValue, two.id.rawValue) + attributes = one.attributes.compare(to: two.attributes) + relationships = one.relationships.compare(to: two.relationships) + meta = Comparison(one.meta, two.meta) + links = Comparison(one.links, two.links) + } + + public var differences: NamedDifferences { + return attributes.reduce(into: ComparisonHash()) { hash, next in + hash["'\(next.key)' attribute"] = next.value + } + .merging( + relationships.reduce(into: ComparisonHash()) { hash, next in + hash["'\(next.key)' relationship"] = next.value + }, + uniquingKeysWith: { $1 } + ) + .merging( + [ + "id": id, + "meta": meta, + "links": links + ], + uniquingKeysWith: { $1 } + ) + .filter { $1 != .same } + .mapValues { $0.rawValue } + } +} + +extension ResourceObjectType { + public func compare(to other: Self) -> ResourceObjectComparison { + return ResourceObjectComparison(self, other) + } +} + +protocol AbstractResourceObjectType { + func abstractCompare(to other: Any) throws -> ResourceObjectComparison +} + +enum AbstractCompareError: Swift.Error { + case typeMismatch +} + +extension ResourceObject: AbstractResourceObjectType { + func abstractCompare(to other: Any) throws -> ResourceObjectComparison { + guard let otherResource = other as? Self else { + throw AbstractCompareError.typeMismatch + } + return self.compare(to: otherResource) + } +} diff --git a/Sources/JSONAPITesting/Optional+ZipWith.swift b/Sources/JSONAPITesting/Optional+ZipWith.swift new file mode 100644 index 0000000..aacbb7b --- /dev/null +++ b/Sources/JSONAPITesting/Optional+ZipWith.swift @@ -0,0 +1,12 @@ +// +// Optional+ZipWith.swift +// +// Created by Mathew Polzin on 1/19/19. +// + +/// Zip two optionals together with the given operation performed on +/// the unwrapped contents. If either optional is nil, the zip +/// yields nil. +func zip(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? { + return left.flatMap { lft in right.map { rght in fn(lft, rght) }} +} diff --git a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift new file mode 100644 index 0000000..e7a7dca --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift @@ -0,0 +1,74 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class AttributesCompareTests: XCTestCase { + func test_sameAttributes() { + let attr1 = TestAttributes( + string: "hello world", + int: 10, + bool: true, + double: 105.4, + struct: .init(value: .init()) + ) + let attr2 = attr1 + + XCTAssertEqual(attr1.compare(to: attr2), [ + "string": .same, + "int": .same, + "bool": .same, + "double": .same, + "struct": .same + ]) + } + + func test_differentAttributes() { + let attr1 = TestAttributes( + string: "hello world", + int: 10, + bool: true, + double: 105.4, + struct: .init(value: .init()) + ) + let attr2 = TestAttributes( + string: "hello", + int: 11, + bool: false, + double: 1.4, + struct: .init(value: .init(val: "there")) + ) + + XCTAssertEqual(attr1.compare(to: attr2), [ + "string": .different("hello world", "hello"), + "int": .different("10", "11"), + "bool": .different("true", "false"), + "double": .different("105.4", "1.4"), + "struct": .different("string: hello", "string: there") + ]) + } +} + +private struct TestAttributes: JSONAPI.Attributes { + let string: Attribute + let int: Attribute + let bool: Attribute + let double: Attribute + let `struct`: Attribute + + struct Struct: Equatable, Codable, CustomStringConvertible { + let string: String + + init(val: String = "hello") { + self.string = val + } + + var description: String { return "string: \(string)" } + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift new file mode 100644 index 0000000..b2e14af --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -0,0 +1,170 @@ +// +// DocumentCompareTests.swift +// +// +// Created by Mathew Polzin on 11/4/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class DocumentCompareTests: XCTestCase { + func test_same() { + XCTAssertTrue(d1.compare(to: d1).differences.isEmpty) + XCTAssertTrue(d2.compare(to: d2).differences.isEmpty) + XCTAssertTrue(d3.compare(to: d3).differences.isEmpty) + XCTAssertTrue(d4.compare(to: d4).differences.isEmpty) + } + + func test_errorAndData() { + XCTAssertEqual(d1.compare(to: d2).differences, [ + "Body": "data response ≠ error response" + ]) + + XCTAssertEqual(d2.compare(to: d1).differences, [ + "Body": "error response ≠ data response" + ]) + } + + func test_differentErrors() { + XCTAssertEqual(d2.compare(to: d4).differences, [ + "Body": "status: 500, title: Internal Error ≠ status: 404, title: Not Found" + ]) + } + + func test_differentData() { + XCTAssertEqual(d3.compare(to: d5).differences, [ + "Body": "(Includes: (include 2: missing)), (Primary Resource: (resource 2: missing))" + ]) + + XCTAssertEqual(d3.compare(to: d6).differences, [ + "Body": ##"(Includes: (include 2: missing)), (Primary Resource: (resource 2: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5), (resource 3: missing))"## + ]) + } +} + +fileprivate enum TestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +fileprivate typealias TestType = ResourceObject + +fileprivate enum TestDescription2: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type2" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +fileprivate typealias TestType2 = ResourceObject + +fileprivate typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> + +fileprivate typealias ManyDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> + +fileprivate let r1 = TestType( + id: "1", + attributes: .init( + name: "name", + age: 10, + favoriteColor: nil + ), + relationships: .init( + bestFriend: "2", + parents: ["3", "4"] + ), + meta: .none, + links: .none +) + +fileprivate let r2 = TestType( + id: "5", + attributes: .init( + name: "Fig", + age: 12, + favoriteColor: "blue" + ), + relationships: .init( + bestFriend: nil, + parents: ["3", "4"] + ), + meta: .none, + links: .none +) + +fileprivate let r3 = TestType2( + id: "2", + attributes: .init( + name: "Tully", + age: 100, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none +) + +fileprivate let d1 = SingleDocument( + apiDescription: .none, + body: .init(resourceObject: r1), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d2 = SingleDocument( + apiDescription: .none, + errors: [.error(.init(id: nil, status: "500", title: "Internal Error"))] +) + +fileprivate let d3 = ManyDocument( + apiDescription: .none, + body: .init(resourceObjects: [r1, r2]), + includes: .init(values: [.init(r3)]), + meta: .none, + links: .none +) + +fileprivate let d4 = SingleDocument( + apiDescription: .none, + errors: [.error(.init(id: nil, status: "404", title: "Not Found"))] +) + +fileprivate let d5 = ManyDocument( + apiDescription: .none, + body: .init(resourceObjects: [r1]), + includes: .init(values: [.init(r3), .init(r2)]), + meta: .none, + links: .none +) + +fileprivate let d6 = ManyDocument( + apiDescription: .none, + body: .init(resourceObjects: [r1, r1, r2]), + includes: .init(values: [.init(r3), .init(r2)]), + meta: .none, + links: .none +) diff --git a/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift new file mode 100644 index 0000000..7d34521 --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift @@ -0,0 +1,239 @@ +// +// IncludeCompareTests.swift +// +// +// Created by Mathew Polzin on 11/4/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting +import Poly + +final class IncludesCompareTests: XCTestCase { + func test_same() { + let includes1 = Includes(values: justTypeOnes) + let includes2 = Includes(values: justTypeOnes) + XCTAssertTrue(includes1.compare(to: includes2).differences.isEmpty) + + let includes3 = Includes(values: longerTypeOnes) + let includes4 = Includes(values: longerTypeOnes) + XCTAssertTrue(includes3.compare(to: includes4).differences.isEmpty) + + let includes5 = Includes(values: onesAndTwos) + let includes6 = Includes(values: onesAndTwos) + XCTAssertTrue(includes5.compare(to: includes6).differences.isEmpty) + } + + func test_missing() { + let includes1 = Includes(values: justTypeOnes) + let includes2 = Includes(values: longerTypeOnes) + XCTAssertEqual(includes1.compare(to: includes2).differences, ["include 3": "missing"]) + XCTAssertEqual(includes2.compare(to: includes1).differences, ["include 3": "missing"]) + } + + func test_typeMismatch() { + let includes1 = Includes(values: onesAndTwos) + let includes2 = Includes(values: justTypeOnes) + XCTAssertEqual(includes1.compare(to: includes2).differences, ["include 2": "ResourceObject ≠ ResourceObject"]) + XCTAssertEqual(includes2.compare(to: includes1).differences, ["include 2": "ResourceObject ≠ ResourceObject"]) + } + + func test_valueMismatch() { + let includes1 = Includes(values: onesAndTwos) + let includes2 = Includes(values: differentOnesAndTwos) + XCTAssertEqual(includes1.compare(to: includes2).differences, [ + "include 1": #"'favoriteColor' attribute: Optional("red") ≠ nil, 'name' attribute: Matt ≠ Todd, 'parents' relationship: 4, 5 ≠ 7, 8, id: 1 ≠ 2"# + ]) + } + + fileprivate let justTypeOnes: [Poly2] = [ + .a( + TestType1( + id: "1", + attributes: .init( + name: "Matt", + age: 23, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + ), + .a( + TestType1( + id: "3", + attributes: .init( + name: "Helen", + age: 24, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: ["2"] + ), + meta: .none, + links: .none + ) + ) + ] + + fileprivate let longerTypeOnes: [Poly2] = [ + .a( + TestType1( + id: "1", + attributes: .init( + name: "Matt", + age: 23, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + ), + .a( + TestType1( + id: "3", + attributes: .init( + name: "Helen", + age: 24, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: ["2"] + ), + meta: .none, + links: .none + ) + ), + .a( + TestType1( + id: "2", + attributes: .init( + name: "Troy", + age: 45, + favoriteColor: "blue" + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none + ) + ) + ] + + fileprivate let onesAndTwos: [Poly2] = [ + .a( + TestType1( + id: "1", + attributes: .init( + name: "Matt", + age: 23, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + ), + .b( + TestType2( + id: "1", + attributes: .init( + name: "Lucy", + age: 33, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none + ) + ) + ] + + fileprivate let differentOnesAndTwos: [Poly2] = [ + .a( + TestType1( + id: "2", + attributes: .init( + name: "Todd", + age: 23, + favoriteColor: nil + ), + relationships: .init( + bestFriend: "3", + parents: ["7", "8"] + ), + meta: .none, + links: .none + ) + ), + .b( + TestType2( + id: "1", + attributes: .init( + name: "Lucy", + age: 33, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none + ) + ) + ] +} + +private enum TestDescription1: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type1" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +private typealias TestType1 = ResourceObject + +private enum TestDescription2: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type2" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +private typealias TestType2 = ResourceObject diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift new file mode 100644 index 0000000..cdb7fda --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/5/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class RelationshipsCompareTests: XCTestCase { + // TODO: write tests +} diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift new file mode 100644 index 0000000..2c40e87 --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -0,0 +1,68 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class ResourceObjectCompareTests: XCTestCase { + func test_same() { + print(test1.compare(to: test1).differences) + XCTAssertTrue(test1.compare(to: test1).differences.isEmpty) + XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) + } + + func test_different() { + // TODO: write actual test + print(test1.compare(to: test2).differences.map { "\($0): \($1)" }.joined(separator: ", ")) + } + + fileprivate let test1 = TestType( + id: "2", + attributes: .init( + name: "James", + age: 12, + favoriteColor: "red"), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + + fileprivate let test2 = TestType( + id: "3", + attributes: .init( + name: "Fred", + age: 10, + favoriteColor: .init(value: nil)), + relationships: .init( + bestFriend: nil, + parents: ["1"] + ), + meta: .none, + links: .none + ) +} + +private enum TestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +private typealias TestType = ResourceObject From 0fe5c53ada020b2709825be93a7c2cf58cec3d2a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:04:47 -0800 Subject: [PATCH 02/23] generate linuxmain --- .../JSONAPITestingTests/XCTestManifests.swift | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Tests/JSONAPITestingTests/XCTestManifests.swift b/Tests/JSONAPITestingTests/XCTestManifests.swift index c916df3..b1b07fb 100644 --- a/Tests/JSONAPITestingTests/XCTestManifests.swift +++ b/Tests/JSONAPITestingTests/XCTestManifests.swift @@ -35,6 +35,28 @@ extension Attribute_LiteralTests { ] } +extension AttributesCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__AttributesCompareTests = [ + ("test_differentAttributes", test_differentAttributes), + ("test_sameAttributes", test_sameAttributes), + ] +} + +extension DocumentCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__DocumentCompareTests = [ + ("test_differentData", test_differentData), + ("test_differentErrors", test_differentErrors), + ("test_errorAndData", test_errorAndData), + ("test_same", test_same), + ] +} + extension EntityCheckTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -58,6 +80,18 @@ extension Id_LiteralTests { ] } +extension IncludesCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__IncludesCompareTests = [ + ("test_missing", test_missing), + ("test_same", test_same), + ("test_typeMismatch", test_typeMismatch), + ("test_valueMismatch", test_valueMismatch), + ] +} + extension Relationship_LiteralTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -69,12 +103,26 @@ extension Relationship_LiteralTests { ] } +extension ResourceObjectCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ResourceObjectCompareTests = [ + ("test_different", test_different), + ("test_same", test_same), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(Attribute_LiteralTests.__allTests__Attribute_LiteralTests), + testCase(AttributesCompareTests.__allTests__AttributesCompareTests), + testCase(DocumentCompareTests.__allTests__DocumentCompareTests), testCase(EntityCheckTests.__allTests__EntityCheckTests), testCase(Id_LiteralTests.__allTests__Id_LiteralTests), + testCase(IncludesCompareTests.__allTests__IncludesCompareTests), testCase(Relationship_LiteralTests.__allTests__Relationship_LiteralTests), + testCase(ResourceObjectCompareTests.__allTests__ResourceObjectCompareTests), ] } #endif From 33a5ff41a029333dd5a9af8fb1a2d0bf61fea4f9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:50:52 -0800 Subject: [PATCH 03/23] 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 04/23] 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, From 87271b93f9273e1bb801ea364e1d7b21dde68c7b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 20:48:04 -0800 Subject: [PATCH 05/23] deprecate subscript attribute accessor in favor of key path dynamic member lookup --- Sources/JSONAPI/Document/Document.swift | 8 +- Sources/JSONAPI/Document/ResourceBody.swift | 22 +- Sources/JSONAPI/Meta/Meta.swift | 8 +- .../Resource/Poly+PrimaryResource.swift | 24 +- .../Resource Object/ResourceObject.swift | 236 +----------------- .../Attribute/Attribute+FunctorTests.swift | 25 +- .../ComputedPropertiesTests.swift | 16 +- .../CustomAttributesTests.swift | 20 +- Tests/JSONAPITests/Poly/PolyProxyTests.swift | 22 +- .../ResourceObject/ResourceObjectTests.swift | 217 +++++++++++++--- .../SparseFields/SparseFieldsetTests.swift | 103 +++++++- 11 files changed, 377 insertions(+), 324 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 98b4445..6256265 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -96,7 +96,7 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyCont /// A `CodableJSONAPIDocument` supports encoding and decoding of a JSON:API /// compliant Document. -public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} +public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: Decodable {} /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of @@ -340,7 +340,7 @@ extension Document { } } -extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { +extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootCodingKeys.self) @@ -557,7 +557,7 @@ extension Document { } extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument - where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -570,7 +570,7 @@ extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument } extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument - where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + where PrimaryResourceBody: CodableResourceBody, 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 7d8b9e2..8f0005e 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -10,33 +10,31 @@ /// array should be used for no results). public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} -/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding. -/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and -/// decoding. +/// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding. 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: OptionalEncodablePrimaryResource, Decodable {} +public protocol OptionalCodablePrimaryResource: OptionalEncodablePrimaryResource, Decodable {} -/// A `PrimaryResource` is a type that can be used in the body of a JSON API +/// A `CodablePrimaryResource` is a type that can be used in the body of a JSON API /// document as the primary resource. -public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} +public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {} extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {} -extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} +extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimaryResource {} /// 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. +/// A `CodableResourceBody` 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: Decodable, EncodableResourceBody {} +public protocol CodableResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. @@ -74,7 +72,7 @@ public struct ManyResourceBody: Encoda /// Use NoResourceBody to indicate you expect a JSON API document to not /// contain a "data" top-level key. -public struct NoResourceBody: ResourceBody { +public struct NoResourceBody: CodableResourceBody { public static var none: NoResourceBody { return NoResourceBody() } } @@ -94,7 +92,7 @@ extension SingleResourceBody { } } -extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { +extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -119,7 +117,7 @@ extension ManyResourceBody { } } -extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { +extension ManyResourceBody: Decodable, CodableResourceBody where Entity: CodablePrimaryResource { public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() var valueAggregator = [Entity]() diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 68b2c94..b985477 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -6,17 +6,17 @@ // /// Conform a type to this protocol to indicate it can be encoded to or decoded from -/// the meta data attached to a component of a JSON API document. Different meta data +/// the meta data attached to a component of a JSON:API document. Different meta data /// can be stored all over the place: On the root document, on a resource object, on /// link objects, etc. /// -/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure +/// JSON:API Metadata is totally open ended. It can take whatever JSON-compliant structure /// the server and client agree upon. public protocol Meta: Codable, Equatable { } -// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as -// nullable. +// We make Optional a Meta if it wraps a Meta so that +// Metadata can be specified as nullable. extension Optional: Meta where Wrapped: Meta {} /// Use this type when you want to specify not to encode or decode any metadata diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 284e10a..b11634d 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -20,7 +20,7 @@ public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource public typealias EncodablePolyWrapped = Encodable & Equatable public typealias PolyWrapped = EncodablePolyWrapped & Decodable -extension Poly0: PrimaryResource { +extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") } @@ -33,54 +33,54 @@ extension Poly0: PrimaryResource { // MARK: - 1 type extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} -extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} +extension Poly1: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped {} // MARK: - 2 types extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} -extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} +extension Poly2: CodablePrimaryResource, OptionalCodablePrimaryResource 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 {} +extension Poly3: CodablePrimaryResource, OptionalCodablePrimaryResource 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 {} +extension Poly4: CodablePrimaryResource, OptionalCodablePrimaryResource 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 {} +extension Poly5: CodablePrimaryResource, OptionalCodablePrimaryResource 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 {} +extension Poly6: CodablePrimaryResource, OptionalCodablePrimaryResource 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 {} +extension Poly7: CodablePrimaryResource, OptionalCodablePrimaryResource 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 {} +extension Poly8: CodablePrimaryResource, OptionalCodablePrimaryResource 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 {} +extension Poly9: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} // MARK: - 10 types extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {} -extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} +extension Poly10: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} // MARK: - 11 types extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {} -extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} +extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a62d271..2418f3a 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -99,7 +99,7 @@ extension ResourceObjectProxy { /// ResourceObjectType is the protocol that ResourceObject conforms to. This /// protocol lets other types accept any ResourceObject as a generic /// specialization. -public protocol ResourceObjectType: ResourceObjectProxy, PrimaryResource where Description: ResourceObjectDescription { +public protocol ResourceObjectType: ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription { associatedtype Meta: JSONAPI.Meta associatedtype Links: JSONAPI.Links } @@ -173,236 +173,6 @@ extension ResourceObject where EntityRawIdType == Unidentified { } } -/* -extension ResourceObject where Description.Attributes == NoAttributes { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata { - public init(id: ResourceObject.Id, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships) { - self.init(id: id, relationships: relationships, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType == Unidentified { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, links: LinksType) { - self.init(id: id, attributes: attributes, meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType) { - self.init(id: id, attributes: attributes, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes) { - self.init(id: id, attributes: attributes, meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, meta: MetaType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init() { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} -*/ - // MARK: - Pointer for Relationships use public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { @@ -456,6 +226,7 @@ public extension ResourceObjectProxy { /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead /// of `resourceObject.attributes.propertyName.value`. + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath) -> T.ValueType { return attributes[keyPath: path].value } @@ -463,6 +234,7 @@ public extension ResourceObjectProxy { /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead /// of `resourceObject.attributes.propertyName.value`. + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath) -> T.ValueType? { return attributes[keyPath: path]?.value } @@ -470,6 +242,7 @@ public extension ResourceObjectProxy { /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead /// of `resourceObject.attributes.propertyName.value`. + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath) -> U? where T.ValueType == U? { // Implementation Note: Handles Transform that returns optional // type. @@ -517,6 +290,7 @@ public extension ResourceObjectProxy { // MARK: Keypath Subscript Lookup /// Access an attribute requiring a transformation on the RawValue _and_ /// a secondary transformation on this entity (self). + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath T>) -> T { return attributes[keyPath: path](self) } diff --git a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift index ab2d7b7..78f217f 100644 --- a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift +++ b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift @@ -15,27 +15,46 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertNotNil(entity) - XCTAssertEqual(entity?[\.computedString], "Frankie2") + XCTAssertEqual(entity?.computedString, "Frankie2") } + @available(*, deprecated, message: "remove next major version") + func test_mapGuaranteed_deprecated() { + let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) + + XCTAssertEqual(entity?[\.computedString], "Frankie2") + } + func test_mapOptionalSuccess() { let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) XCTAssertNotNil(entity) - XCTAssertEqual(entity?[\.computedNumber], 22) XCTAssertEqual(entity?.computedNumber, 22) } + @available(*, deprecated, message: "remove next major version") + func test_mapOptionalSuccess_deprecated() { + let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) + + XCTAssertEqual(entity?[\.computedNumber], 22) + } + func test_mapOptionalFailure() { let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)), relationships: .none, meta: .none, links: .none) XCTAssertNotNil(entity) - XCTAssertNil(entity?[\.computedNumber]) XCTAssertNil(entity?.computedNumber) } + + @available(*, deprecated, message: "remove next major version") + func test_mapOptionalFailure_deprecated() { + let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)), relationships: .none, meta: .none, links: .none) + + XCTAssertNil(entity?[\.computedNumber]) + } } // MARK: Test types diff --git a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift index add302d..39ea74d 100644 --- a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift +++ b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift @@ -14,12 +14,18 @@ class ComputedPropertiesTests: XCTestCase { let entity = decoded(type: TestType.self, data: computed_property_attribute) XCTAssertEqual(entity.id, "1234") - XCTAssertEqual(entity[\.name], "Sarah") XCTAssertEqual(entity.name, "Sarah") XCTAssertEqual(entity ~> \.other, "5678") XCTAssertNoThrow(try TestType.check(entity)) } + @available(*, deprecated, message: "remove next major version") + func test_DecodeIgnoresComputed_deprecated() { + let entity = decoded(type: TestType.self, data: computed_property_attribute) + + XCTAssertEqual(entity[\.name], "Sarah") + } + func test_EncodeIgnoresComputed() { test_DecodeEncodeEquality(type: TestType.self, data: computed_property_attribute) } @@ -27,11 +33,17 @@ class ComputedPropertiesTests: XCTestCase { func test_ComputedAttributeAccess() { let entity = decoded(type: TestType.self, data: computed_property_attribute) - XCTAssertEqual(entity[\.computed], "Sarah2") XCTAssertEqual(entity.computed, "Sarah2") XCTAssertEqual(entity[direct: \.directSecretsOut], "shhhh") } + @available(*, deprecated, message: "remove next major version") + func test_ComputedAttributeAccess_deprecated() { + let entity = decoded(type: TestType.self, data: computed_property_attribute) + + XCTAssertEqual(entity[\.computed], "Sarah2") + } + func test_ComputedNonAttributeAccess() { let entity = decoded(type: TestType.self, data: computed_property_attribute) diff --git a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift index 91e8d8f..ec0e7f1 100644 --- a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift +++ b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift @@ -13,13 +13,19 @@ class CustomAttributesTests: XCTestCase { func test_customDecode() { let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData) - XCTAssertEqual(entity[\.firstName], "Cool") XCTAssertEqual(entity.firstName, "Cool") - XCTAssertEqual(entity[\.name], "Cool Name") XCTAssertEqual(entity.name, "Cool Name") XCTAssertNoThrow(try CustomAttributeEntity.check(entity)) } + @available(*, deprecated, message: "remove next major version") + func test_customDecode_deprecated() { + let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData) + + XCTAssertEqual(entity[\.firstName], "Cool") + XCTAssertEqual(entity[\.name], "Cool Name") + } + func test_customEncode() { test_DecodeEncodeEquality(type: CustomAttributeEntity.self, data: customAttributeEntityData) @@ -28,13 +34,19 @@ class CustomAttributesTests: XCTestCase { func test_customKeysDecode() { let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData) - XCTAssertEqual(entity[\.firstNameSilly], "Cool") XCTAssertEqual(entity.firstNameSilly, "Cool") - XCTAssertEqual(entity[\.lastNameSilly], "Name") XCTAssertEqual(entity.lastNameSilly, "Name") XCTAssertNoThrow(try CustomKeysEntity.check(entity)) } + @available(*, deprecated, message: "remove next major version") + func test_customKeysDecode_deprecated() { + let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData) + + XCTAssertEqual(entity[\.firstNameSilly], "Cool") + XCTAssertEqual(entity[\.lastNameSilly], "Name") + } + func test_customKeysEncode() { test_DecodeEncodeEquality(type: CustomKeysEntity.self, data: customAttributeEntityData) diff --git a/Tests/JSONAPITests/Poly/PolyProxyTests.swift b/Tests/JSONAPITests/Poly/PolyProxyTests.swift index 3d12259..582a2fc 100644 --- a/Tests/JSONAPITests/Poly/PolyProxyTests.swift +++ b/Tests/JSONAPITests/Poly/PolyProxyTests.swift @@ -21,12 +21,19 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(polyUserA.userA, userA) XCTAssertNil(polyUserA.userB) - XCTAssertEqual(polyUserA[\.name], "Ken Moore") + XCTAssertEqual(polyUserA.name, "Ken Moore") XCTAssertEqual(polyUserA.id, "1") XCTAssertEqual(polyUserA.relationships, .none) XCTAssertEqual(polyUserA[direct: \.x], .init(x: "y")) } + @available(*, deprecated, message: "remove next major version") + func test_UserADecode_deprecated() { + let polyUserA = decoded(type: User.self, data: poly_user_stub_1) + + XCTAssertEqual(polyUserA[\.name], "Ken Moore") + } + func test_UserAAndBEncodeEquality() { test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_1) test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_2) @@ -56,11 +63,18 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(polyUserB.userB, userB) XCTAssertNil(polyUserB.userA) - XCTAssertEqual(polyUserB[\.name], "Ken Less") + XCTAssertEqual(polyUserB.name, "Ken Less") XCTAssertEqual(polyUserB.id, "2") XCTAssertEqual(polyUserB.relationships, .none) XCTAssertEqual(polyUserB[direct: \.x], .init(x: "y")) } + + @available(*, deprecated, message: "remove next major version") + func test_UserBDecode_deprecated() { + let polyUserB = decoded(type: User.self, data: poly_user_stub_2) + + XCTAssertEqual(polyUserB[\.name], "Ken Less") + } } // MARK: - Test types @@ -114,9 +128,9 @@ extension Poly2: ResourceObjectProxy, JSONTyped where A == PolyProxyTests.UserA, public var attributes: SharedUserDescription.Attributes { switch self { case .a(let a): - return .init(name: .init(value: "\(a[\.firstName]) \(a[\.lastName])"), x: .init(x: "y")) + return .init(name: .init(value: "\(a.firstName) \(a.lastName)"), x: .init(x: "y")) case .b(let b): - return .init(name: .init(value: b[\.name].joined(separator: " ")), x: .init(x: "y")) + return .init(name: .init(value: b.name.joined(separator: " ")), x: .init(x: "y")) } } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index 2e023a0..c061f1e 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -69,10 +69,16 @@ class ResourceObjectTests: XCTestCase { func test_unidentifiedEntityAttributeAccess() { let entity = UnidentifiedTestEntity(attributes: .init(me: "hello"), relationships: .none, meta: .none, links: .none) - XCTAssertEqual(entity[\.me], "hello") XCTAssertEqual(entity.me, "hello") } + @available(*, deprecated, message: "remove next major version") + func test_unidentifiedEntityAttributeAccess_deprecated() { + let entity = UnidentifiedTestEntity(attributes: .init(me: "hello"), relationships: .none, meta: .none, links: .none) + + XCTAssertEqual(entity[\.me], "hello") + } + func test_initialization() { let entity1 = TestEntity1(id: .init(rawValue: "wow"), attributes: .none, relationships: .none, meta: .none, links: .none) let entity2 = TestEntity2(id: .init(rawValue: "cool"), attributes: .none, relationships: .init(other: .init(resourceObject: entity1)), meta: .none, links: .none) @@ -158,13 +164,19 @@ extension ResourceObjectTests { XCTAssert(type(of: entity.relationships) == NoRelationships.self) - XCTAssertEqual(entity[\.floater], 123.321) XCTAssertEqual(entity.floater, 123.321) XCTAssertNoThrow(try TestEntity5.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntityNoRelationshipsSomeAttributes_deprecated() { + let entity = decoded(type: TestEntity5.self, + data: entity_no_relationships_some_attributes) + XCTAssertEqual(entity[\.floater], 123.321) + } + func test_EntityNoRelationshipsSomeAttributes_encode() { test_DecodeEncodeEquality(type: TestEntity5.self, data: entity_no_relationships_some_attributes) @@ -191,9 +203,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4.self, data: entity_some_relationships_some_attributes) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertNoThrow(try TestEntity4.check(entity)) @@ -201,6 +211,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributes_deprecated() { + let entity = decoded(type: TestEntity4.self, + data: entity_some_relationships_some_attributes) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributes_encode() { test_DecodeEncodeEquality(type: TestEntity4.self, data: entity_some_relationships_some_attributes) @@ -214,17 +233,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_one_omitted_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertNil(entity[\.maybeHere]) XCTAssertNil(entity.maybeHere) - XCTAssertEqual(entity[\.maybeNull], "World") XCTAssertEqual(entity.maybeNull, "World") XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityOneOmittedAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_one_omitted_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertNil(entity[\.maybeHere]) + XCTAssertEqual(entity[\.maybeNull], "World") + } + func test_entityOneOmittedAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_omitted_attribute) @@ -234,17 +260,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_one_null_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertEqual(entity[\.maybeHere], "World") XCTAssertEqual(entity.maybeHere, "World") - XCTAssertNil(entity[\.maybeNull]) XCTAssertNil(entity.maybeNull) XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityOneNullAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_one_null_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity[\.maybeHere], "World") + XCTAssertNil(entity[\.maybeNull]) + } + func test_entityOneNullAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_attribute) @@ -254,17 +287,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_all_attributes) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertEqual(entity[\.maybeHere], "World") XCTAssertEqual(entity.maybeHere, "World") - XCTAssertEqual(entity[\.maybeNull], "!") XCTAssertEqual(entity.maybeNull, "!") XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityAllAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_all_attributes) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity[\.maybeHere], "World") + XCTAssertEqual(entity[\.maybeNull], "!") + } + func test_entityAllAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_all_attributes) @@ -274,17 +314,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_one_null_and_one_missing_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertNil(entity[\.maybeHere]) XCTAssertNil(entity.maybeHere) - XCTAssertNil(entity[\.maybeNull]) XCTAssertNil(entity.maybeNull) XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityOneNullAndOneOmittedAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_one_null_and_one_missing_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertNil(entity[\.maybeHere]) + XCTAssertNil(entity[\.maybeNull]) + } + func test_entityOneNullAndOneOmittedAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_and_one_missing_attribute) @@ -299,15 +346,22 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity7.self, data: entity_null_optional_nullable_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertNil(entity[\.maybeHereMaybeNull]) XCTAssertNil(entity.maybeHereMaybeNull) XCTAssertNoThrow(try TestEntity7.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_NullOptionalNullableAttribute_deprecated() { + let entity = decoded(type: TestEntity7.self, + data: entity_null_optional_nullable_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertNil(entity[\.maybeHereMaybeNull]) + } + func test_NullOptionalNullableAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity7.self, data: entity_null_optional_nullable_attribute) @@ -317,15 +371,22 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity7.self, data: entity_non_null_optional_nullable_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertEqual(entity[\.maybeHereMaybeNull], "World") XCTAssertEqual(entity.maybeHereMaybeNull, "World") XCTAssertNoThrow(try TestEntity7.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_NonNullOptionalNullableAttribute_deprecated() { + let entity = decoded(type: TestEntity7.self, + data: entity_non_null_optional_nullable_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity[\.maybeHereMaybeNull], "World") + } + func test_NonNullOptionalNullableAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity7.self, data: entity_non_null_optional_nullable_attribute) @@ -338,23 +399,30 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity8.self, data: entity_int_to_string_attribute) - XCTAssertEqual(entity[\.string], "22") XCTAssertEqual(entity.string, "22") - XCTAssertEqual(entity[\.int], 22) XCTAssertEqual(entity.int, 22) - XCTAssertEqual(entity[\.stringFromInt], "22") XCTAssertEqual(entity.stringFromInt, "22") - XCTAssertEqual(entity[\.plus], 122) XCTAssertEqual(entity.plus, 122) - XCTAssertEqual(entity[\.doubleFromInt], 22.0) XCTAssertEqual(entity.doubleFromInt, 22.0) - XCTAssertEqual(entity[\.nullToString], "nil") XCTAssertEqual(entity.nullToString, "nil") XCTAssertNoThrow(try TestEntity8.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_IntToString_deprecated() { + let entity = decoded(type: TestEntity8.self, + data: entity_int_to_string_attribute) + + XCTAssertEqual(entity[\.string], "22") + XCTAssertEqual(entity[\.int], 22) + XCTAssertEqual(entity[\.stringFromInt], "22") + XCTAssertEqual(entity[\.plus], 122) + XCTAssertEqual(entity[\.doubleFromInt], 22.0) + XCTAssertEqual(entity[\.nullToString], "nil") + } + func test_IntToString_encode() { test_DecodeEncodeEquality(type: TestEntity8.self, data: entity_int_to_string_attribute) @@ -503,7 +571,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntity.self, data: entity_unidentified) - XCTAssertNil(entity[\.me]) XCTAssertNil(entity.me) XCTAssertEqual(entity.id, .unidentified) XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity)) @@ -511,6 +578,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntity_deprecated() { + let entity = decoded(type: UnidentifiedTestEntity.self, + data: entity_unidentified) + + XCTAssertNil(entity[\.me]) + } + func test_UnidentifiedEntity_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntity.self, data: entity_unidentified) @@ -520,7 +595,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntity.self, data: entity_unidentified_with_attributes) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity)) @@ -528,6 +602,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributes_deprecated() { + let entity = decoded(type: UnidentifiedTestEntity.self, + data: entity_unidentified_with_attributes) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributes_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntity.self, data: entity_unidentified_with_attributes) @@ -541,7 +623,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntityWithMeta.self, data: entity_unidentified_with_attributes_and_meta) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.meta.x, "world") @@ -551,6 +632,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributesAndMeta_deprecated() { + let entity = decoded(type: UnidentifiedTestEntityWithMeta.self, + data: entity_unidentified_with_attributes_and_meta) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributesAndMeta_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithMeta.self, data: entity_unidentified_with_attributes_and_meta) @@ -560,7 +649,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntityWithLinks.self, data: entity_unidentified_with_attributes_and_links) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png")) @@ -569,6 +657,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributesAndLinks_deprecated() { + let entity = decoded(type: UnidentifiedTestEntityWithLinks.self, + data: entity_unidentified_with_attributes_and_links) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributesAndLinks_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithLinks.self, data: entity_unidentified_with_attributes_and_links) @@ -578,7 +674,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntityWithMetaAndLinks.self, data: entity_unidentified_with_attributes_and_meta_and_links) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.meta.x, "world") @@ -589,6 +684,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated() { + let entity = decoded(type: UnidentifiedTestEntityWithMetaAndLinks.self, + data: entity_unidentified_with_attributes_and_meta_and_links) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithMetaAndLinks.self, data: entity_unidentified_with_attributes_and_meta_and_links) @@ -598,9 +701,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4WithMeta.self, data: entity_some_relationships_some_attributes_with_meta) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.meta.x, "world") @@ -610,6 +711,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated() { + let entity = decoded(type: TestEntity4WithMeta.self, + data: entity_some_relationships_some_attributes_with_meta) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributesWithMeta_encode() { test_DecodeEncodeEquality(type: TestEntity4WithMeta.self, data: entity_some_relationships_some_attributes_with_meta) @@ -619,9 +729,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4WithLinks.self, data: entity_some_relationships_some_attributes_with_links) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png")) @@ -630,6 +738,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated() { + let entity = decoded(type: TestEntity4WithLinks.self, + data: entity_some_relationships_some_attributes_with_links) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributesWithLinks_encode() { test_DecodeEncodeEquality(type: TestEntity4WithLinks.self, data: entity_some_relationships_some_attributes_with_links) @@ -639,9 +756,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4WithMetaAndLinks.self, data: entity_some_relationships_some_attributes_with_meta_and_links) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.meta.x, "world") @@ -652,6 +767,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated() { + let entity = decoded(type: TestEntity4WithMetaAndLinks.self, + data: entity_some_relationships_some_attributes_with_meta_and_links) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode() { test_DecodeEncodeEquality(type: TestEntity4WithMetaAndLinks.self, data: entity_some_relationships_some_attributes_with_meta_and_links) @@ -673,11 +797,26 @@ extension ResourceObjectTests { meta: .none, links: .none) - XCTAssertEqual(entity1[\.metaAttribute], true) XCTAssertEqual(entity1.metaAttribute, true) - XCTAssertEqual(entity2[\.metaAttribute], false) XCTAssertEqual(entity2.metaAttribute, false) } + + @available(*, deprecated, message: "remove next major version") + func test_MetaEntityAttributeAccessWorks_deprecated() { + let entity1 = TestEntityWithMetaAttribute(id: "even", + attributes: .init(), + relationships: .none, + meta: .none, + links: .none) + let entity2 = TestEntityWithMetaAttribute(id: "odd", + attributes: .init(), + relationships: .none, + meta: .none, + links: .none) + + XCTAssertEqual(entity1[\.metaAttribute], true) + XCTAssertEqual(entity2[\.metaAttribute], false) + } } // MARK: With a Meta Relationship diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift index 003dcc5..bffe9dd 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift @@ -31,6 +31,38 @@ class SparseFieldsetTests: XCTestCase { XCTAssertNil(relationships) XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted intentionally at initialization + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject.bool) + XCTAssertEqual(attributesDict?["int"] as? Int, + testEverythingObject.int) + XCTAssertEqual(attributesDict?["double"] as? Double, + testEverythingObject.double) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject.string) + XCTAssertEqual((attributesDict?["nestedStruct"] as? [String: String])?["hello"], + testEverythingObject.nestedStruct.hello) + XCTAssertEqual(attributesDict?["nestedEnum"] as? String, + testEverythingObject.nestedEnum.rawValue) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject.array) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNotNil(attributesDict?["nullable"] as? NSNull) + XCTAssertNotNil(attributesDict?["optionalNullable"] as? NSNull) + } + + @available(*, deprecated, message: "remove next major version") + func test_FullEncode_deprecated() { + let jsonEncoder = JSONEncoder() + let sparseWithEverything = SparseFieldset(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) + + let encoded = try! jsonEncoder.encode(sparseWithEverything) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let attributesDict = outerDict?["attributes"] as? [String: Any] + XCTAssertEqual(attributesDict?["bool"] as? Bool, testEverythingObject[\.bool]) XCTAssertEqual(attributesDict?["int"] as? Int, @@ -45,9 +77,6 @@ class SparseFieldsetTests: XCTestCase { testEverythingObject[\.nestedEnum].rawValue) XCTAssertEqual(attributesDict?["array"] as? [Bool], testEverythingObject[\.array]) - XCTAssertNil(attributesDict?["optional"]) - XCTAssertNotNil(attributesDict?["nullable"] as? NSNull) - XCTAssertNotNil(attributesDict?["optionalNullable"] as? NSNull) } func test_PartialEncode() { @@ -71,20 +100,48 @@ class SparseFieldsetTests: XCTestCase { XCTAssertEqual(attributesDict?.count, 3) XCTAssertEqual(attributesDict?["bool"] as? Bool, - testEverythingObject[\.bool]) + testEverythingObject.bool) XCTAssertNil(attributesDict?["int"]) XCTAssertNil(attributesDict?["double"]) XCTAssertEqual(attributesDict?["string"] as? String, - testEverythingObject[\.string]) + testEverythingObject.string) XCTAssertNil(attributesDict?["nestedStruct"]) XCTAssertNil(attributesDict?["nestedEnum"]) XCTAssertEqual(attributesDict?["array"] as? [Bool], - testEverythingObject[\.array]) + testEverythingObject.array) XCTAssertNil(attributesDict?["optional"]) XCTAssertNil(attributesDict?["nullable"]) XCTAssertNil(attributesDict?["optionalNullable"]) } + @available(*, deprecated, message: "remove next major version") + func test_PartialEncode_deprecated() { + let jsonEncoder = JSONEncoder() + let sparseObject = SparseFieldset(testEverythingObject, fields: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseObject) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + } + func test_sparseFieldsMethod() { let jsonEncoder = JSONEncoder() let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) @@ -106,19 +163,47 @@ class SparseFieldsetTests: XCTestCase { XCTAssertEqual(attributesDict?.count, 3) XCTAssertEqual(attributesDict?["bool"] as? Bool, - testEverythingObject[\.bool]) + testEverythingObject.bool) XCTAssertNil(attributesDict?["int"]) XCTAssertNil(attributesDict?["double"]) XCTAssertEqual(attributesDict?["string"] as? String, - testEverythingObject[\.string]) + testEverythingObject.string) XCTAssertNil(attributesDict?["nestedStruct"]) XCTAssertNil(attributesDict?["nestedEnum"]) XCTAssertEqual(attributesDict?["array"] as? [Bool], - testEverythingObject[\.array]) + testEverythingObject.array) XCTAssertNil(attributesDict?["optional"]) XCTAssertNil(attributesDict?["nullable"]) XCTAssertNil(attributesDict?["optionalNullable"]) } + + @available(*, deprecated, message: "remove next major version") + func test_sparseFieldsMethod_deprecated() { + let jsonEncoder = JSONEncoder() + let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseObject) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + } } struct EverythingTestDescription: JSONAPI.ResourceObjectDescription { From a7f6ed584530b138a72695566f147dfcd97db861 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 21:03:51 -0800 Subject: [PATCH 06/23] update README --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 688b5fa..c535288 100644 --- a/README.md +++ b/README.md @@ -332,13 +332,11 @@ As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup let favoriteColor: String = person.favoriteColor ``` -🗒 `Attributes` can also be accessed via the older `subscript` operator as follows: +:warning: `Attributes` can also be accessed via the older `subscript` operator, but this is a deprecated feature that will be removed in the next major version: ```swift let favoriteColor: String = person[\.favoriteColor] ``` -In both cases you retain type-safety. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated. - #### `Transformer` Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`. From d6f01d6c1d13315d5314307fc813b4fc4383238e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 22:27:59 -0800 Subject: [PATCH 07/23] indentation --- Sources/JSONAPI/Document/Document.swift | 260 ++++++++++++------------ 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 98b4445..60030c3 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -106,34 +106,34 @@ public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable wher /// a conversion such as the one offerred by the /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` public struct Document: EncodableJSONAPIDocument { - public typealias Include = IncludeType + 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. - public let apiDescription: APIDescription + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + public let apiDescription: APIDescription - /// The Body of the Document. This body is either one or more errors - /// with links and metadata attempted to parse but not guaranteed or - /// it is a successful data struct containing all the primary and - /// included resources, the metadata, and the links that this - /// document type specifies. - public let body: Body + /// The Body of the Document. This body is either one or more errors + /// with links and metadata attempted to parse but not guaranteed or + /// it is a successful data struct containing all the primary and + /// included resources, the metadata, and the links that this + /// document type specifies. + public let body: Body - public init(apiDescription: APIDescription, + public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) { - body = .errors(errors, meta: meta, links: links) - self.apiDescription = apiDescription - } + body = .errors(errors, meta: meta, links: links) + self.apiDescription = apiDescription + } - public init(apiDescription: APIDescription, - body: PrimaryResourceBody, - includes: Includes, - meta: MetaType, - links: LinksType) { - self.body = .data( + public init(apiDescription: APIDescription, + body: PrimaryResourceBody, + includes: Includes, + meta: MetaType, + links: LinksType) { + self.body = .data( .init( primary: body, includes: includes, @@ -141,8 +141,8 @@ public struct Document MetaType, - combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { - return Document.Body.Data(primary: primary.appending(other.primary), - includes: includes.appending(other.includes), - meta: metaMerge(meta, other.meta), - links: linksMerge(links, other.links)) - } + public func merging(_ other: Document.Body.Data, + combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, + combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { + return Document.Body.Data(primary: primary.appending(other.primary), + includes: includes.appending(other.includes), + meta: metaMerge(meta, other.meta), + links: linksMerge(links, other.links)) + } } 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 }, - combiningLinksWith: { _, _ in .none }) - } + public func merging(_ other: Document.Body.Data) -> Document.Body.Data { + return merging(other, + combiningMetaWith: { _, _ in .none }, + combiningLinksWith: { _, _ in .none }) + } } extension Document where IncludeType == NoIncludes { - /// Create a new Document with the given includes. - public func including(_ includes: Includes) -> Document { - // Note that if IncludeType is NoIncludes, then we allow anything - // to be included, but if IncludeType already specifies a type - // of thing to be expected then we lock that down. - // See: Document.including() where IncludeType: _Poly1 - switch body { - case .data(let data): - return .init(apiDescription: apiDescription, - body: data.primary, - includes: includes, - meta: data.meta, - links: data.links) - case .errors(let errors, meta: let meta, links: let links): - return .init(apiDescription: apiDescription, - errors: errors, - meta: meta, - links: links) - } - } + /// Create a new Document with the given includes. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType: _Poly1 + switch body { + case .data(let data): + return .init(apiDescription: apiDescription, + body: data.primary, + includes: includes, + meta: data.meta, + links: data.links) + case .errors(let errors, meta: let meta, links: let links): + return .init(apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links) + } + } } // extending where _Poly1 means all non-zero _Poly arities are included extension Document where IncludeType: _Poly1 { - /// Create a new Document adding the given includes. This does not - /// remove existing includes; it is additive. - public func including(_ includes: Includes) -> Document { - // Note that if IncludeType is NoIncludes, then we allow anything - // to be included, but if IncludeType already specifies a type - // of thing to be expected then we lock that down. - // See: Document.including() where IncludeType == NoIncludes - switch body { - case .data(let data): - return .init(apiDescription: apiDescription, - body: data.primary, - includes: data.includes + includes, - meta: data.meta, - links: data.links) - case .errors(let errors, meta: let meta, links: let links): - return .init(apiDescription: apiDescription, - errors: errors, - meta: meta, - links: links) - } - } + /// Create a new Document adding the given includes. This does not + /// remove existing includes; it is additive. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType == NoIncludes + switch body { + case .data(let data): + return .init(apiDescription: apiDescription, + body: data.primary, + includes: data.includes + includes, + meta: data.meta, + links: data.links) + case .errors(let errors, meta: let meta, links: let links): + return .init(apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links) + } + } } // MARK: - Codable extension Document { - private enum RootCodingKeys: String, CodingKey { - case data - case errors - case included - case meta - case links - case jsonapi - } + private enum RootCodingKeys: String, CodingKey { + case data + case errors + case included + case meta + case links + case jsonapi + } - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: RootCodingKeys.self) + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: RootCodingKeys.self) - switch body { - case .errors(let errors, meta: let meta, links: let links): - var errContainer = container.nestedUnkeyedContainer(forKey: .errors) + switch body { + case .errors(let errors, meta: let meta, links: let links): + var errContainer = container.nestedUnkeyedContainer(forKey: .errors) - for error in errors { - try errContainer.encode(error) - } + for error in errors { + try errContainer.encode(error) + } - if MetaType.self != NoMetadata.self, - let metaVal = meta { - try container.encode(metaVal, forKey: .meta) - } + if MetaType.self != NoMetadata.self, + let metaVal = meta { + try container.encode(metaVal, forKey: .meta) + } - if LinksType.self != NoLinks.self, - let linksVal = links { - try container.encode(linksVal, forKey: .links) - } + if LinksType.self != NoLinks.self, + let linksVal = links { + try container.encode(linksVal, forKey: .links) + } - case .data(let data): - try container.encode(data.primary, forKey: .data) + case .data(let data): + try container.encode(data.primary, forKey: .data) - if Include.self != NoIncludes.self { - try container.encode(data.includes, forKey: .included) - } + if Include.self != NoIncludes.self { + try container.encode(data.includes, forKey: .included) + } - if MetaType.self != NoMetadata.self { - try container.encode(data.meta, forKey: .meta) - } + if MetaType.self != NoMetadata.self { + try container.encode(data.meta, forKey: .meta) + } - if LinksType.self != NoLinks.self { - try container.encode(data.links, forKey: .links) - } - } + if LinksType.self != NoLinks.self { + try container.encode(data.links, forKey: .links) + } + } - if APIDescription.self != NoAPIDescription.self { - try container.encode(apiDescription, forKey: .jsonapi) - } - } + if APIDescription.self != NoAPIDescription.self { + try container.encode(apiDescription, forKey: .jsonapi) + } + } } extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { @@ -405,26 +405,26 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: // MARK: - CustomStringConvertible extension Document: CustomStringConvertible { - public var description: String { - return "Document(\(String(describing: body)))" - } + public var description: String { + return "Document(\(String(describing: body)))" + } } extension Document.Body: CustomStringConvertible { - public var description: String { - switch self { - case .errors(let errors, meta: let meta, links: let links): - return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))" - case .data(let data): - return String(describing: data) - } - } + public var description: String { + switch self { + case .errors(let errors, meta: let meta, links: let links): + return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))" + case .data(let data): + return String(describing: data) + } + } } extension Document.Body.Data: CustomStringConvertible { - public var description: String { - return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))" - } + public var description: String { + return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))" + } } // MARK: - Error and Success Document Types From e6f82c6052e6d156bc95ad491c8857d26facd0b3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 22:37:48 -0800 Subject: [PATCH 08/23] indentation --- Sources/JSONAPI/Document/APIDescription.swift | 64 +- Sources/JSONAPI/Document/Includes.swift | 114 +-- Sources/JSONAPI/Document/ResourceBody.swift | 60 +- Sources/JSONAPI/EncodingError.swift | 10 +- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 7 +- Sources/JSONAPI/Error/JSONAPIError.swift | 28 +- Sources/JSONAPI/Meta/Links.swift | 86 +- Sources/JSONAPI/Meta/Meta.swift | 6 +- .../JSONAPI/Resource/Attribute+Functor.swift | 48 +- Sources/JSONAPI/Resource/Attribute.swift | 167 ++-- Sources/JSONAPI/Resource/Id.swift | 84 +- .../Resource/Poly+PrimaryResource.swift | 134 +++- Sources/JSONAPI/Resource/Relationship.swift | 346 ++++----- .../Resource Object/ResourceObject.swift | 732 +++++++++--------- .../JSONAPITesting/Attribute+Literal.swift | 126 +-- Sources/JSONAPITesting/Id+Literal.swift | 40 +- Sources/JSONAPITesting/Optional+Literal.swift | 24 +- .../JSONAPITesting/Relationship+Literal.swift | 38 +- .../JSONAPITesting/ResourceObjectCheck.swift | 90 +-- 19 files changed, 1143 insertions(+), 1061 deletions(-) diff --git a/Sources/JSONAPI/Document/APIDescription.swift b/Sources/JSONAPI/Document/APIDescription.swift index a0ba2ff..cb417a6 100644 --- a/Sources/JSONAPI/Document/APIDescription.swift +++ b/Sources/JSONAPI/Document/APIDescription.swift @@ -7,58 +7,58 @@ /// This is what the JSON API Spec calls the "JSON:API Object" public protocol APIDescriptionType: Codable, Equatable { - associatedtype Meta + associatedtype Meta } /// This is what the JSON API Spec calls the "JSON:API Object" public struct APIDescription: APIDescriptionType { - public let version: String - public let meta: Meta + public let version: String + public let meta: Meta - public init(version: String, meta: Meta) { - self.version = version - self.meta = meta - } + public init(version: String, meta: Meta) { + self.version = version + self.meta = meta + } } /// Can be used as `APIDescriptionType` for Documents that do not /// have any API Description (a.k.a. "JSON:API Object"). public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible { - public typealias Meta = NoMetadata + public typealias Meta = NoMetadata - public init() {} + public init() {} - public static var none: NoAPIDescription { return .init() } + public static var none: NoAPIDescription { return .init() } - public var description: String { return "No JSON:API Object" } + public var description: String { return "No JSON:API Object" } } extension APIDescription { - private enum CodingKeys: String, CodingKey { - case version - case meta - } + private enum CodingKeys: String, CodingKey { + case version + case meta + } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) - // The spec says that if a version is not specified, it should be assumed to be at least 1.0 - version = (try? container.decode(String.self, forKey: .version)) ?? "1.0" + // The spec says that if a version is not specified, it should be assumed to be at least 1.0 + version = (try? container.decode(String.self, forKey: .version)) ?? "1.0" - if let metaVal = NoMetadata() as? Meta { - meta = metaVal - } else { - meta = try container.decode(Meta.self, forKey: .meta) - } - } + if let metaVal = NoMetadata() as? Meta { + meta = metaVal + } else { + meta = try container.decode(Meta.self, forKey: .meta) + } + } - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(version, forKey: .version) + try container.encode(version, forKey: .version) - if Meta.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } - } + if Meta.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } + } } diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index b6d93e0..087b2b5 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -20,29 +20,29 @@ public typealias Include = EncodableJSONPoly /// /// `let includedThings = includes[Thing1.self]` public struct Includes: Encodable, Equatable { - public static var none: Includes { return .init(values: []) } - - let values: [I] - - public init(values: [I]) { - self.values = values - } + public static var none: Includes { return .init(values: []) } - public func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() + let values: [I] - guard I.self != NoIncludes.self else { - throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") - } + public init(values: [I]) { + self.values = values + } - for value in values { - try container.encode(value) - } - } - - public var count: Int { - return values.count - } + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + guard I.self != NoIncludes.self else { + throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") + } + + for value in values { + try container.encode(value) + } + } + + public var count: Int { + return values.count + } } extension Includes: Decodable where I: Decodable { @@ -65,25 +65,25 @@ extension Includes: Decodable where I: Decodable { } extension Includes { - public func appending(_ other: Includes) -> Includes { - return Includes(values: values + other.values) - } + public func appending(_ other: Includes) -> Includes { + return Includes(values: values + other.values) + } } public func +(_ left: Includes, _ right: Includes) -> Includes { - return left.appending(right) + return left.appending(right) } extension Includes: CustomStringConvertible { - public var description: String { - return "Includes(\(String(describing: values))" - } + public var description: String { + return "Includes(\(String(describing: values))" + } } extension Includes where I == NoIncludes { - public init() { - values = [] - } + public init() { + values = [] + } } // MARK: - 0 includes @@ -93,73 +93,73 @@ public typealias NoIncludes = Include0 // MARK: - 1 include public typealias Include1 = Poly1 extension Includes where I: _Poly1 { - public subscript(_ lookup: I.A.Type) -> [I.A] { - return values.compactMap { $0.a } - } + public subscript(_ lookup: I.A.Type) -> [I.A] { + return values.compactMap { $0.a } + } } // MARK: - 2 includes public typealias Include2 = Poly2 extension Includes where I: _Poly2 { - public subscript(_ lookup: I.B.Type) -> [I.B] { - return values.compactMap { $0.b } - } + public subscript(_ lookup: I.B.Type) -> [I.B] { + return values.compactMap { $0.b } + } } // MARK: - 3 includes public typealias Include3 = Poly3 extension Includes where I: _Poly3 { - public subscript(_ lookup: I.C.Type) -> [I.C] { - return values.compactMap { $0.c } - } + public subscript(_ lookup: I.C.Type) -> [I.C] { + return values.compactMap { $0.c } + } } // MARK: - 4 includes public typealias Include4 = Poly4 extension Includes where I: _Poly4 { - public subscript(_ lookup: I.D.Type) -> [I.D] { - return values.compactMap { $0.d } - } + public subscript(_ lookup: I.D.Type) -> [I.D] { + return values.compactMap { $0.d } + } } // MARK: - 5 includes public typealias Include5 = Poly5 extension Includes where I: _Poly5 { - public subscript(_ lookup: I.E.Type) -> [I.E] { - return values.compactMap { $0.e } - } + public subscript(_ lookup: I.E.Type) -> [I.E] { + return values.compactMap { $0.e } + } } // MARK: - 6 includes public typealias Include6 = Poly6 extension Includes where I: _Poly6 { - public subscript(_ lookup: I.F.Type) -> [I.F] { - return values.compactMap { $0.f } - } + public subscript(_ lookup: I.F.Type) -> [I.F] { + return values.compactMap { $0.f } + } } // MARK: - 7 includes public typealias Include7 = Poly7 extension Includes where I: _Poly7 { - public subscript(_ lookup: I.G.Type) -> [I.G] { - return values.compactMap { $0.g } - } + public subscript(_ lookup: I.G.Type) -> [I.G] { + return values.compactMap { $0.g } + } } // MARK: - 8 includes public typealias Include8 = Poly8 extension Includes where I: _Poly8 { - public subscript(_ lookup: I.H.Type) -> [I.H] { - return values.compactMap { $0.h } - } + public subscript(_ lookup: I.H.Type) -> [I.H] { + return values.compactMap { $0.h } + } } // MARK: - 9 includes public typealias Include9 = Poly9 extension Includes where I: _Poly9 { - public subscript(_ lookup: I.I.Type) -> [I.I] { - return values.compactMap { $0.i } - } + public subscript(_ lookup: I.I.Type) -> [I.I] { + return values.compactMap { $0.i } + } } // MARK: - 10 includes diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 7d8b9e2..8fd20d6 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -41,47 +41,47 @@ 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 ResourceBodyAppendable { - func appending(_ other: Self) -> Self + func appending(_ other: Self) -> Self } public func +(_ left: R, right: R) -> R { - return left.appending(right) + return left.appending(right) } /// A type allowing for a document body containing 1 primary resource. /// If the `Entity` specialization is an `Optional` type, the body can contain /// 0 or 1 primary resources. public struct SingleResourceBody: EncodableResourceBody { - public let value: Entity + public let value: Entity - public init(resourceObject: Entity) { - self.value = resourceObject - } + public init(resourceObject: Entity) { + self.value = resourceObject + } } /// A type allowing for a document body containing 0 or more primary resources. public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { - public let values: [Entity] + public let values: [Entity] - public init(resourceObjects: [Entity]) { - values = resourceObjects - } + public init(resourceObjects: [Entity]) { + values = resourceObjects + } - public func appending(_ other: ManyResourceBody) -> ManyResourceBody { - return ManyResourceBody(resourceObjects: values + other.values) - } + public func appending(_ other: ManyResourceBody) -> ManyResourceBody { + return ManyResourceBody(resourceObjects: values + other.values) + } } /// Use NoResourceBody to indicate you expect a JSON API document to not /// contain a "data" top-level key. public struct NoResourceBody: ResourceBody { - public static var none: NoResourceBody { return NoResourceBody() } + public static var none: NoResourceBody { return NoResourceBody() } } // MARK: Codable extension SingleResourceBody { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() let anyNil: Any? = nil let nilValue = anyNil as? Entity @@ -90,8 +90,8 @@ extension SingleResourceBody { return } - try container.encode(value) - } + try container.encode(value) + } } extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { @@ -110,13 +110,13 @@ extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrim } extension ManyResourceBody { - public func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() - for value in values { - try container.encode(value) - } - } + for value in values { + try container.encode(value) + } + } } extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { @@ -133,13 +133,13 @@ extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResourc // MARK: CustomStringConvertible extension SingleResourceBody: CustomStringConvertible { - public var description: String { - return "PrimaryResourceBody(\(String(describing: value)))" - } + public var description: String { + return "PrimaryResourceBody(\(String(describing: value)))" + } } extension ManyResourceBody: CustomStringConvertible { - public var description: String { - return "PrimaryResourceBody(\(String(describing: values)))" - } + public var description: String { + return "PrimaryResourceBody(\(String(describing: values)))" + } } diff --git a/Sources/JSONAPI/EncodingError.swift b/Sources/JSONAPI/EncodingError.swift index 1d8145f..8b461cd 100644 --- a/Sources/JSONAPI/EncodingError.swift +++ b/Sources/JSONAPI/EncodingError.swift @@ -6,9 +6,9 @@ // public enum JSONAPIEncodingError: Swift.Error { - case typeMismatch(expected: String, found: String) - case illegalEncoding(String) - case illegalDecoding(String) - case missingOrMalformedMetadata - case missingOrMalformedLinks + case typeMismatch(expected: String, found: String) + case illegalEncoding(String) + case illegalDecoding(String) + case missingOrMalformedMetadata + case missingOrMalformedLinks } diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index 28795b9..88ed95e 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -9,7 +9,9 @@ public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType { /// a unique identifier for this particular occurrence of the problem public let id: IdType? -// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type. + + // public let links: Links? // we skip this for now to avoid adding complexity to using this basic type. + /// the HTTP status code applicable to this problem public let status: String? /// an application-specific error code @@ -20,7 +22,8 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq public let detail: String? /// an object containing references to the source of the error public let source: Source? -// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type + + // public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type public init(id: IdType? = nil, status: String? = nil, diff --git a/Sources/JSONAPI/Error/JSONAPIError.swift b/Sources/JSONAPI/Error/JSONAPIError.swift index b997a69..6f19105 100644 --- a/Sources/JSONAPI/Error/JSONAPIError.swift +++ b/Sources/JSONAPI/Error/JSONAPIError.swift @@ -6,7 +6,7 @@ // public protocol JSONAPIError: Swift.Error, Equatable, Codable { - static var unknown: Self { get } + static var unknown: Self { get } } /// `UnknownJSONAPIError` can actually be used in any sitaution @@ -16,18 +16,18 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable { /// information the server might be providing in the error payload, /// use `BasicJSONAPIError` instead. public enum UnknownJSONAPIError: JSONAPIError { - case unknownError - - public init(from decoder: Decoder) throws { - self = .unknown - } + case unknownError - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode("unknown") - } - - public static var unknown: Self { - return .unknownError - } + public init(from decoder: Decoder) throws { + self = .unknown + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("unknown") + } + + public static var unknown: Self { + return .unknownError + } } diff --git a/Sources/JSONAPI/Meta/Links.swift b/Sources/JSONAPI/Meta/Links.swift index 00658cb..876995f 100644 --- a/Sources/JSONAPI/Meta/Links.swift +++ b/Sources/JSONAPI/Meta/Links.swift @@ -10,58 +10,58 @@ public protocol Links: Codable, Equatable {} /// Use NoLinks where no links should belong to a JSON API component public struct NoLinks: Links, CustomStringConvertible { - public static var none: NoLinks { return NoLinks() } - public init() {} - - public var description: String { return "No Links" } + public static var none: NoLinks { return NoLinks() } + public init() {} + + public var description: String { return "No Links" } } public protocol JSONAPIURL: Codable, Equatable {} public struct Link: Equatable, Codable { - public let url: URL - public let meta: Meta - - public init(url: URL, meta: Meta) { - self.url = url - self.meta = meta - } + public let url: URL + public let meta: Meta + + public init(url: URL, meta: Meta) { + self.url = url + self.meta = meta + } } extension Link where Meta == NoMetadata { - public init(url: URL) { - self.init(url: url, meta: .none) - } + public init(url: URL) { + self.init(url: url, meta: .none) + } } public extension Link { - private enum CodingKeys: String, CodingKey { - case href - case meta - } - - init(from decoder: Decoder) throws { - guard Meta.self == NoMetadata.self, - let noMeta = NoMetadata() as? Meta else { - let container = try decoder.container(keyedBy: CodingKeys.self) - meta = try container.decode(Meta.self, forKey: .meta) - url = try container.decode(URL.self, forKey: .href) - return - } - let container = try decoder.singleValueContainer() - url = try container.decode(URL.self) - meta = noMeta - } - - func encode(to encoder: Encoder) throws { - guard Meta.self == NoMetadata.self else { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(url, forKey: .href) - try container.encode(meta, forKey: .meta) - return - } - var container = encoder.singleValueContainer() - - try container.encode(url) - } + private enum CodingKeys: String, CodingKey { + case href + case meta + } + + init(from decoder: Decoder) throws { + guard Meta.self == NoMetadata.self, + let noMeta = NoMetadata() as? Meta else { + let container = try decoder.container(keyedBy: CodingKeys.self) + meta = try container.decode(Meta.self, forKey: .meta) + url = try container.decode(URL.self, forKey: .href) + return + } + let container = try decoder.singleValueContainer() + url = try container.decode(URL.self) + meta = noMeta + } + + func encode(to encoder: Encoder) throws { + guard Meta.self == NoMetadata.self else { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(url, forKey: .href) + try container.encode(meta, forKey: .meta) + return + } + var container = encoder.singleValueContainer() + + try container.encode(url) + } } diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 68b2c94..b49149f 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -22,9 +22,9 @@ extension Optional: Meta where Wrapped: Meta {} /// Use this type when you want to specify not to encode or decode any metadata /// for a type. public struct NoMetadata: Meta, CustomStringConvertible { - public static var none: NoMetadata { return NoMetadata() } + public static var none: NoMetadata { return NoMetadata() } - public init() { } + public init() { } - public var description: String { return "No Metadata" } + public var description: String { return "No Metadata" } } diff --git a/Sources/JSONAPI/Resource/Attribute+Functor.swift b/Sources/JSONAPI/Resource/Attribute+Functor.swift index 1da6ff0..6f4b4dc 100644 --- a/Sources/JSONAPI/Resource/Attribute+Functor.swift +++ b/Sources/JSONAPI/Resource/Attribute+Functor.swift @@ -6,31 +6,31 @@ // public extension TransformedAttribute { - /// Map an Attribute to a new wrapped type. - /// Note that the resulting Attribute will have no transformer, even if the - /// source Attribute has a transformer. - /// You are mapping the output of the source transform into - /// the RawValue of a new transformerless Attribute. - /// - /// Generally, this is the most useful operation. The transformer gives you - /// control over the decoding of the Attribute, but once the Attribute exists, - /// mapping on it is most useful for creating computed Attribute properties. - func map(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute { - return Attribute(value: try transform(value)) - } + /// Map an Attribute to a new wrapped type. + /// Note that the resulting Attribute will have no transformer, even if the + /// source Attribute has a transformer. + /// You are mapping the output of the source transform into + /// the RawValue of a new transformerless Attribute. + /// + /// Generally, this is the most useful operation. The transformer gives you + /// control over the decoding of the Attribute, but once the Attribute exists, + /// mapping on it is most useful for creating computed Attribute properties. + func map(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute { + return Attribute(value: try transform(value)) + } } public extension Attribute { - /// Map an Attribute to a new wrapped type. - /// Note that the resulting Attribute will have no transformer, even if the - /// source Attribute has a transformer. - /// You are mapping the output of the source transform into - /// the RawValue of a new transformerless Attribute. - /// - /// Generally, this is the most useful operation. The transformer gives you - /// control over the decoding of the Attribute, but once the Attribute exists, - /// mapping on it is most useful for creating computed Attribute properties. - func map(_ transform: (ValueType) throws -> T) rethrows -> Attribute { - return Attribute(value: try transform(value)) - } + /// Map an Attribute to a new wrapped type. + /// Note that the resulting Attribute will have no transformer, even if the + /// source Attribute has a transformer. + /// You are mapping the output of the source transform into + /// the RawValue of a new transformerless Attribute. + /// + /// Generally, this is the most useful operation. The transformer gives you + /// control over the decoding of the Attribute, but once the Attribute exists, + /// mapping on it is most useful for creating computed Attribute properties. + func map(_ transform: (ValueType) throws -> T) rethrows -> Attribute { + return Attribute(value: try transform(value)) + } } diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index c05983d..f73d672 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -6,49 +6,49 @@ // public protocol AttributeType: Codable { - associatedtype RawValue: Codable - associatedtype ValueType + associatedtype RawValue: Codable + associatedtype ValueType - var value: ValueType { get } + var value: ValueType { get } } // MARK: TransformedAttribute /// A TransformedAttribute takes a Codable type and attempts to turn it into another type. public struct TransformedAttribute: AttributeType where Transformer.From == RawValue { - public let rawValue: RawValue + public let rawValue: RawValue - public let value: Transformer.To + public let value: Transformer.To - public init(rawValue: RawValue) throws { - self.rawValue = rawValue - value = try Transformer.transform(rawValue) - } + public init(rawValue: RawValue) throws { + self.rawValue = rawValue + value = try Transformer.transform(rawValue) + } } extension TransformedAttribute where Transformer == IdentityTransformer { - // If we are using the identity transform, we can skip the transform and guarantee no - // error is thrown. - public init(value: RawValue) { - rawValue = value - self.value = value - } + // If we are using the identity transform, we can skip the transform and guarantee no + // error is thrown. + public init(value: RawValue) { + rawValue = value + self.value = value + } } extension TransformedAttribute where Transformer: ReversibleTransformer { - /// Initialize a TransformedAttribute from its transformed value. The - /// RawValue, which is what gets encoded/decoded, is determined using - /// The Transformer's reverse function. - public init(transformedValue: Transformer.To) throws { - self.value = transformedValue - rawValue = try Transformer.reverse(value) - } + /// Initialize a TransformedAttribute from its transformed value. The + /// RawValue, which is what gets encoded/decoded, is determined using + /// The Transformer's reverse function. + public init(transformedValue: Transformer.To) throws { + self.value = transformedValue + rawValue = try Transformer.reverse(value) + } } extension TransformedAttribute: CustomStringConvertible { - public var description: String { - return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))" - } + public var description: String { + return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))" + } } extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {} @@ -63,85 +63,86 @@ public typealias ValidatedAttribute: AttributeType { - let attribute: TransformedAttribute> + let attribute: TransformedAttribute> - public var value: RawValue { - return attribute.value - } + public var value: RawValue { + return attribute.value + } - public init(value: RawValue) { - attribute = .init(value: value) - } + public init(value: RawValue) { + attribute = .init(value: value) + } } extension Attribute: CustomStringConvertible { - public var description: String { - return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))" - } + public var description: String { + return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))" + } } extension Attribute: Equatable where RawValue: Equatable {} // MARK: - Codable extension TransformedAttribute { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let rawVal: RawValue - - // A little trickery follows. If the value is nil, the - // container.decode(Value.self) will fail even if Value - // is Optional. However, we can check if decoding nil - // succeeds and then attempt to coerce nil to a Value - // type at which point we can store nil in `value`. - let anyNil: Any? = nil - if container.decodeNil(), - let val = anyNil as? Transformer.From { - rawVal = val - } else { - rawVal = try container.decode(Transformer.From.self) - } - - rawValue = rawVal - value = try Transformer.transform(rawVal) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() - try container.encode(rawValue) - } + let rawVal: RawValue + + // A little trickery follows. If the value is nil, the + // container.decode(Value.self) will fail even if Value + // is Optional. However, we can check if decoding nil + // succeeds and then attempt to coerce nil to a Value + // type at which point we can store nil in `value`. + let anyNil: Any? = nil + if container.decodeNil(), + let val = anyNil as? Transformer.From { + rawVal = val + } else { + rawVal = try container.decode(Transformer.From.self) + } + + rawValue = rawVal + value = try Transformer.transform(rawVal) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(rawValue) + } } extension Attribute { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() - // A little trickery follows. If the value is nil, the - // container.decode(Value.self) will fail even if Value - // is Optional. However, we can check if decoding nil - // succeeds and then attempt to coerce nil to a Value - // type at which point we can store nil in `value`. - let anyNil: Any? = nil - if container.decodeNil(), - let val = anyNil as? RawValue { - attribute = .init(value: val) - } else { - attribute = try container.decode(TransformedAttribute>.self) - } - } + // A little trickery follows. If the value is nil, the + // container.decode(Value.self) will fail even if Value + // is Optional. However, we can check if decoding nil + // succeeds and then attempt to coerce nil to a Value + // type at which point we can store nil in `value`. + let anyNil: Any? = nil + if container.decodeNil(), + let val = anyNil as? RawValue { + attribute = .init(value: val) + } else { + attribute = try container.decode(TransformedAttribute>.self) + } + } - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() - try container.encode(attribute) - } + try container.encode(attribute) + } } // MARK: Attribute decoding and encoding defaults extension AttributeType { - public static func defaultDecoding(from container: Container, forKey key: Container.Key) throws -> Self { - return try container.decode(Self.self, forKey: key) - } + public static func defaultDecoding(from container: Container, + forKey key: Container.Key) throws -> Self { + return try container.decode(Self.self, forKey: key) + } } diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index a375c08..d66e3d9 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -23,7 +23,7 @@ public protocol RawIdType: MaybeRawId, Hashable {} /// Conformances for `String` and `UUID` /// are given in the README for this library. public protocol CreatableRawIdType: RawIdType { - static func unique() -> Self + static func unique() -> Self } extension String: RawIdType {} @@ -32,80 +32,80 @@ extension String: RawIdType {} /// have an Id (most likely because it was created by a client and the server will be responsible /// for assigning it an Id). public struct Unidentified: MaybeRawId, CustomStringConvertible { - public init() {} - - public var description: String { return "Unidentified" } + public init() {} + + public var description: String { return "Unidentified" } } public protocol OptionalId: Codable { - associatedtype IdentifiableType: JSONAPI.JSONTyped - associatedtype RawType: MaybeRawId + associatedtype IdentifiableType: JSONAPI.JSONTyped + associatedtype RawType: MaybeRawId - var rawValue: RawType { get } - init(rawValue: RawType) + var rawValue: RawType { get } + init(rawValue: RawType) } public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {} extension Optional: MaybeRawId where Wrapped: Codable & Equatable {} extension Optional: OptionalId where Wrapped: IdType { - public typealias IdentifiableType = Wrapped.IdentifiableType - public typealias RawType = Wrapped.RawType? + public typealias IdentifiableType = Wrapped.IdentifiableType + public typealias RawType = Wrapped.RawType? - public var rawValue: Wrapped.RawType? { - guard case .some(let value) = self else { - return nil - } - return value.rawValue - } + public var rawValue: Wrapped.RawType? { + guard case .some(let value) = self else { + return nil + } + return value.rawValue + } - public init(rawValue: Wrapped.RawType?) { - self = rawValue.map { Wrapped(rawValue: $0) } - } + public init(rawValue: Wrapped.RawType?) { + self = rawValue.map { Wrapped(rawValue: $0) } + } } public extension IdType { - var description: String { return "Id(\(String(describing: rawValue)))" } + var description: String { return "Id(\(String(describing: rawValue)))" } } public protocol CreatableIdType: IdType { - init() + init() } /// An ResourceObject ID. These IDs can be encoded to or decoded from /// JSON API IDs. public struct Id: Equatable, OptionalId { - public let rawValue: RawType - - public init(rawValue: RawType) { - self.rawValue = rawValue - } + public let rawValue: RawType - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(RawType.self) - self.init(rawValue: rawValue) - } + public init(rawValue: RawType) { + self.rawValue = rawValue + } - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawValue) - } + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(RawType.self) + self.init(rawValue: rawValue) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } } extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType { - public static func id(from rawValue: RawType) -> Id { - return Id(rawValue: rawValue) - } + public static func id(from rawValue: RawType) -> Id { + return Id(rawValue: rawValue) + } } extension Id: CreatableIdType where RawType: CreatableRawIdType { - public init() { - rawValue = .unique() - } + public init() { + rawValue = .unique() + } } extension Id where RawType == Unidentified { - public static var unidentified: Id { return .init(rawValue: Unidentified()) } + public static var unidentified: Id { return .init(rawValue: Unidentified()) } } diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 284e10a..3ee23a1 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -21,66 +21,144 @@ public typealias EncodablePolyWrapped = Encodable & Equatable public typealias PolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: PrimaryResource { - public init(from decoder: Decoder) throws { - throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") - } + public init(from decoder: Decoder) throws { + throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") + } - public func encode(to encoder: Encoder) throws { - throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.") - } + public func encode(to encoder: Encoder) throws { + throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.") + } } // MARK: - 1 type -extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} +extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped {} -extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} +extension Poly1: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped {} // MARK: - 2 types -extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} +extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} -extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} +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: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} -extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} +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: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} -extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} +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: 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 {} +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: 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 {} +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: 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 {} +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: 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 {} +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: 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 {} +extension Poly9: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} // MARK: - 10 types -extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {} +extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where + A: EncodablePolyWrapped, + B: EncodablePolyWrapped, + C: EncodablePolyWrapped, + D: EncodablePolyWrapped, + E: EncodablePolyWrapped, + F: EncodablePolyWrapped, + G: EncodablePolyWrapped, + H: EncodablePolyWrapped, + I: EncodablePolyWrapped, + J: EncodablePolyWrapped {} -extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} +extension Poly10: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} // MARK: - 11 types -extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {} +extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where + A: EncodablePolyWrapped, + B: EncodablePolyWrapped, + C: EncodablePolyWrapped, + D: EncodablePolyWrapped, + E: EncodablePolyWrapped, + F: EncodablePolyWrapped, + G: EncodablePolyWrapped, + H: EncodablePolyWrapped, + I: EncodablePolyWrapped, + J: EncodablePolyWrapped, + K: EncodablePolyWrapped {} -extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} +extension Poly11: PrimaryResource, OptionalPrimaryResource + where + A: PolyWrapped, + B: PolyWrapped, + C: PolyWrapped, + D: PolyWrapped, + E: PolyWrapped, + F: PolyWrapped, + G: PolyWrapped, + H: PolyWrapped, + I: PolyWrapped, + J: PolyWrapped, + K: PolyWrapped {} diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 43f5457..69fabd9 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -6,11 +6,11 @@ // public protocol RelationshipType { - associatedtype LinksType - associatedtype MetaType + associatedtype LinksType + associatedtype MetaType - var links: LinksType { get } - var meta: MetaType { get } + var links: LinksType { get } + var meta: MetaType { get } } /// An ResourceObject relationship that can be encoded to or decoded from @@ -19,46 +19,46 @@ public protocol RelationshipType { /// A convenient typealias might make your code much more legible: `One` public struct ToOneRelationship: RelationshipType, Equatable { - public let id: Identifiable.Identifier + public let id: Identifiable.Identifier - public let meta: MetaType - public let links: LinksType + public let meta: MetaType + public let links: LinksType - public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) { - self.id = id - self.meta = meta - self.links = links - } + public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) { + self.id = id + self.meta = meta + self.links = links + } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(id: Identifiable.Identifier) { - self.init(id: id, meta: .none, links: .none) - } + public init(id: Identifiable.Identifier) { + self.init(id: id, meta: .none, links: .none) + } } extension ToOneRelationship { - public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.Identifier { - self.init(id: resourceObject.id, meta: meta, links: links) - } + public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.Identifier { + self.init(id: resourceObject.id, meta: meta, links: links) + } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T) where T.Id == Identifiable.Identifier { - self.init(id: resourceObject.id, meta: .none, links: .none) - } + public init(resourceObject: T) where T.Id == Identifiable.Identifier { + self.init(id: resourceObject.id, meta: .none, links: .none) + } } extension ToOneRelationship where Identifiable: OptionalRelatable { - public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.Identifier { - self.init(id: resourceObject?.id, meta: meta, links: links) - } + public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.Identifier { + self.init(id: resourceObject?.id, meta: meta, links: links) + } } extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.Identifier { - self.init(id: resourceObject?.id, meta: .none, links: .none) - } + public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.Identifier { + self.init(id: resourceObject?.id, meta: .none, links: .none) + } } /// An ResourceObject relationship that can be encoded to or decoded from @@ -67,57 +67,57 @@ extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == N /// A convenient typealias might make your code much more legible: `Many` public struct ToManyRelationship: RelationshipType, Equatable { - public let ids: [Relatable.Identifier] + public let ids: [Relatable.Identifier] - public let meta: MetaType - public let links: LinksType + public let meta: MetaType + public let links: LinksType - public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) { - self.ids = ids - self.meta = meta - self.links = links - } + public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) { + self.ids = ids + self.meta = meta + self.links = links + } - public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier { - ids = pointers.map { $0.id } - self.meta = meta - self.links = links - } + public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier { + ids = pointers.map { $0.id } + self.meta = meta + self.links = links + } - public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier { - self.init(ids: resourceObjects.map { $0.id }, meta: meta, links: links) - } + public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier { + self.init(ids: resourceObjects.map { $0.id }, meta: meta, links: links) + } - private init(meta: MetaType, links: LinksType) { - self.init(ids: [], meta: meta, links: links) - } + private init(meta: MetaType, links: LinksType) { + self.init(ids: [], meta: meta, links: links) + } - public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship { - return ToManyRelationship(meta: meta, links: links) - } + public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship { + return ToManyRelationship(meta: meta, links: links) + } } extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(ids: [Relatable.Identifier]) { - self.init(ids: ids, meta: .none, links: .none) - } + public init(ids: [Relatable.Identifier]) { + self.init(ids: ids, meta: .none, links: .none) + } - public init(pointers: [ToOneRelationship]) where T.Identifier == Relatable.Identifier { - self.init(pointers: pointers, meta: .none, links: .none) - } + public init(pointers: [ToOneRelationship]) where T.Identifier == Relatable.Identifier { + self.init(pointers: pointers, meta: .none, links: .none) + } - public static var none: ToManyRelationship { - return .none(withMeta: .none, links: .none) - } + public static var none: ToManyRelationship { + return .none(withMeta: .none, links: .none) + } - public init(resourceObjects: [T]) where T.Id == Relatable.Identifier { - self.init(resourceObjects: resourceObjects, meta: .none, links: .none) - } + public init(resourceObjects: [T]) where T.Id == Relatable.Identifier { + self.init(resourceObjects: resourceObjects, meta: .none, links: .none) + } } public protocol Identifiable: JSONTyped { - associatedtype Identifier: Equatable + associatedtype Identifier: Equatable } /// The Relatable protocol describes anything that @@ -128,152 +128,152 @@ public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType { /// OptionalRelatable just describes an Optional /// with a Reltable Wrapped type. public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? { - associatedtype Wrapped: JSONAPI.Relatable + associatedtype Wrapped: JSONAPI.Relatable } extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable { - public typealias Identifier = Wrapped.Identifier? + public typealias Identifier = Wrapped.Identifier? - public static var jsonType: String { return Wrapped.jsonType } + public static var jsonType: String { return Wrapped.jsonType } } // MARK: Codable private enum ResourceLinkageCodingKeys: String, CodingKey { - case data = "data" - case meta = "meta" - case links = "links" + case data = "data" + case meta = "meta" + case links = "links" } private enum ResourceIdentifierCodingKeys: String, CodingKey { - case id = "id" - case entityType = "type" + case id = "id" + case entityType = "type" } extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) - if let noMeta = NoMetadata() as? MetaType { - meta = noMeta - } else { - meta = try container.decode(MetaType.self, forKey: .meta) - } + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + meta = try container.decode(MetaType.self, forKey: .meta) + } - if let noLinks = NoLinks() as? LinksType { - links = noLinks - } else { - links = try container.decode(LinksType.self, forKey: .links) - } + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + links = try container.decode(LinksType.self, forKey: .links) + } - // A little trickery follows. If the id is nil, the - // container.decode(Identifier.self) will fail even if Identifier - // is Optional. However, we can check if decoding nil - // succeeds and then attempt to coerce nil to a Identifier - // type at which point we can store nil in `id`. - let anyNil: Any? = nil - if try container.decodeNil(forKey: .data), - let val = anyNil as? Identifiable.Identifier { - id = val - return - } + // A little trickery follows. If the id is nil, the + // container.decode(Identifier.self) will fail even if Identifier + // is Optional. However, we can check if decoding nil + // succeeds and then attempt to coerce nil to a Identifier + // type at which point we can store nil in `id`. + let anyNil: Any? = nil + if try container.decodeNil(forKey: .data), + let val = anyNil as? Identifiable.Identifier { + id = val + return + } - let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) - - let type = try identifier.decode(String.self, forKey: .entityType) - - guard type == Identifiable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type) - } - - id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) + let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) - if MetaType.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } + let type = try identifier.decode(String.self, forKey: .entityType) - if LinksType.self != NoLinks.self { - try container.encode(links, forKey: .links) - } + guard type == Identifiable.jsonType else { + throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type) + } - // If id is nil, instead of {id: , type: } we will just - // encode `null` - let anyNil: Any? = nil - let nilId = anyNil as? Identifiable.Identifier - guard id != nilId else { - try container.encodeNil(forKey: .data) - return - } + id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) + } - var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) - - try identifier.encode(id.rawValue, forKey: .id) - try identifier.encode(Identifiable.jsonType, forKey: .entityType) - } + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if MetaType.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } + + if LinksType.self != NoLinks.self { + try container.encode(links, forKey: .links) + } + + // If id is nil, instead of {id: , type: } we will just + // encode `null` + let anyNil: Any? = nil + let nilId = anyNil as? Identifiable.Identifier + guard id != nilId else { + try container.encodeNil(forKey: .data) + return + } + + var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) + + try identifier.encode(id.rawValue, forKey: .id) + try identifier.encode(Identifiable.jsonType, forKey: .entityType) + } } extension ToManyRelationship: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) - if let noMeta = NoMetadata() as? MetaType { - meta = noMeta - } else { - meta = try container.decode(MetaType.self, forKey: .meta) - } + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + meta = try container.decode(MetaType.self, forKey: .meta) + } - if let noLinks = NoLinks() as? LinksType { - links = noLinks - } else { - links = try container.decode(LinksType.self, forKey: .links) - } + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + links = try container.decode(LinksType.self, forKey: .links) + } - var identifiers = try container.nestedUnkeyedContainer(forKey: .data) - - var newIds = [Relatable.Identifier]() - while !identifiers.isAtEnd { - let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) - - let type = try identifier.decode(String.self, forKey: .entityType) - - guard type == Relatable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type) - } - - newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) - } - ids = newIds - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) + var identifiers = try container.nestedUnkeyedContainer(forKey: .data) - if MetaType.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } + var newIds = [Relatable.Identifier]() + while !identifiers.isAtEnd { + let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) - if LinksType.self != NoLinks.self { - try container.encode(links, forKey: .links) - } + let type = try identifier.decode(String.self, forKey: .entityType) - var identifiers = container.nestedUnkeyedContainer(forKey: .data) - - for id in ids { - var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) - - try identifier.encode(id.rawValue, forKey: .id) - try identifier.encode(Relatable.jsonType, forKey: .entityType) - } - } + guard type == Relatable.jsonType else { + throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type) + } + + newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) + } + ids = newIds + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if MetaType.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } + + if LinksType.self != NoLinks.self { + try container.encode(links, forKey: .links) + } + + var identifiers = container.nestedUnkeyedContainer(forKey: .data) + + for id in ids { + var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) + + try identifier.encode(id.rawValue, forKey: .id) + try identifier.encode(Relatable.jsonType, forKey: .entityType) + } + } } // MARK: CustomStringDescribable extension ToOneRelationship: CustomStringConvertible { - public var description: String { return "Relationship(\(String(describing: id)))" } + public var description: String { return "Relationship(\(String(describing: id)))" } } extension ToManyRelationship: CustomStringConvertible { - public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } + public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a62d271..b242778 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -28,34 +28,34 @@ public protocol SparsableAttributes: Attributes { /// Can be used as `Relationships` Type for Entities that do not /// have any Relationships. public struct NoRelationships: Relationships { - public static var none: NoRelationships { return .init() } + public static var none: NoRelationships { return .init() } } extension NoRelationships: CustomStringConvertible { - public var description: String { return "No Relationships" } + public var description: String { return "No Relationships" } } /// Can be used as `Attributes` Type for Entities that do not /// have any Attributes. public struct NoAttributes: Attributes { - public static var none: NoAttributes { return .init() } + public static var none: NoAttributes { return .init() } } extension NoAttributes: CustomStringConvertible { - public var description: String { return "No Attributes" } + public var description: String { return "No Attributes" } } /// Something that is JSONTyped provides a String representation /// of its type. public protocol JSONTyped { - static var jsonType: String { get } + static var jsonType: String { get } } /// A `ResourceObjectProxyDescription` is an `ResourceObjectDescription` /// without Codable conformance. public protocol ResourceObjectProxyDescription: JSONTyped { - associatedtype Attributes: Equatable - associatedtype Relationships: Equatable + associatedtype Attributes: Equatable + associatedtype Relationships: Equatable } /// A `ResourceObjectDescription` describes a JSON API @@ -70,38 +70,38 @@ public protocol ResourceObjectDescription: ResourceObjectProxyDescription where /// or decoded as ResourceObjects. @dynamicMemberLookup public protocol ResourceObjectProxy: Equatable, JSONTyped { - associatedtype Description: ResourceObjectProxyDescription - associatedtype EntityRawIdType: JSONAPI.MaybeRawId + associatedtype Description: ResourceObjectProxyDescription + associatedtype EntityRawIdType: JSONAPI.MaybeRawId - typealias Id = JSONAPI.Id + typealias Id = JSONAPI.Id - typealias Attributes = Description.Attributes - typealias Relationships = Description.Relationships + typealias Attributes = Description.Attributes + typealias Relationships = Description.Relationships - /// The `Entity`'s Id. This can be of type `Unidentified` if - /// the entity is being created clientside and the - /// server is being asked to create a unique Id. Otherwise, - /// this should be of a type conforming to `IdType`. - var id: Id { get } + /// The `Entity`'s Id. This can be of type `Unidentified` if + /// the entity is being created clientside and the + /// server is being asked to create a unique Id. Otherwise, + /// this should be of a type conforming to `IdType`. + var id: Id { get } - /// The JSON API compliant attributes of this `Entity`. - var attributes: Attributes { get } + /// The JSON API compliant attributes of this `Entity`. + var attributes: Attributes { get } - /// The JSON API compliant relationships of this `Entity`. - var relationships: Relationships { get } + /// The JSON API compliant relationships of this `Entity`. + var relationships: Relationships { get } } extension ResourceObjectProxy { - /// The JSON API compliant "type" of this `ResourceObject`. - public static var jsonType: String { return Description.jsonType } + /// The JSON API compliant "type" of this `ResourceObject`. + public static var jsonType: String { return Description.jsonType } } /// ResourceObjectType is the protocol that ResourceObject conforms to. This /// protocol lets other types accept any ResourceObject as a generic /// specialization. public protocol ResourceObjectType: ResourceObjectProxy, PrimaryResource where Description: ResourceObjectDescription { - associatedtype Meta: JSONAPI.Meta - associatedtype Links: JSONAPI.Links + associatedtype Meta: JSONAPI.Meta + associatedtype Links: JSONAPI.Links } public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType {} @@ -112,369 +112,369 @@ public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable wh /// See https://jsonapi.org/format/#document-resource-objects public struct ResourceObject: ResourceObjectType { - public typealias Meta = MetaType - public typealias Links = LinksType + public typealias Meta = MetaType + public typealias Links = LinksType - /// The `ResourceObject`'s Id. This can be of type `Unidentified` if - /// the entity is being created clientside and the - /// server is being asked to create a unique Id. Otherwise, - /// this should be of a type conforming to `IdType`. - public let id: ResourceObject.Id - - /// The JSON API compliant attributes of this `ResourceObject`. - public let attributes: Description.Attributes - - /// The JSON API compliant relationships of this `ResourceObject`. - public let relationships: Description.Relationships + /// The `ResourceObject`'s Id. This can be of type `Unidentified` if + /// the entity is being created clientside and the + /// server is being asked to create a unique Id. Otherwise, + /// this should be of a type conforming to `IdType`. + public let id: ResourceObject.Id - /// Any additional metadata packaged with the entity. - public let meta: MetaType + /// The JSON API compliant attributes of this `ResourceObject`. + public let attributes: Description.Attributes - /// Links related to the entity. - public let links: LinksType - - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.id = id - self.attributes = attributes - self.relationships = relationships - self.meta = meta - self.links = links - } + /// The JSON API compliant relationships of this `ResourceObject`. + public let relationships: Description.Relationships + + /// Any additional metadata packaged with the entity. + public let meta: MetaType + + /// Links related to the entity. + public let links: LinksType + + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.id = id + self.attributes = attributes + self.relationships = relationships + self.meta = meta + self.links = links + } } extension ResourceObject: Identifiable, IdentifiableResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType { - public typealias Identifier = ResourceObject.Id + public typealias Identifier = ResourceObject.Id } extension ResourceObject: CustomStringConvertible { - public var description: String { - return "ResourceObject<\(ResourceObject.jsonType)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))" - } + public var description: String { + return "ResourceObject<\(ResourceObject.jsonType)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))" + } } // MARK: - Convenience initializers extension ResourceObject where EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.id = ResourceObject.Id() - self.attributes = attributes - self.relationships = relationships - self.meta = meta - self.links = links - } + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.id = ResourceObject.Id() + self.attributes = attributes + self.relationships = relationships + self.meta = meta + self.links = links + } } extension ResourceObject where EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.id = .unidentified - self.attributes = attributes - self.relationships = relationships - self.meta = meta - self.links = links - } + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.id = .unidentified + self.attributes = attributes + self.relationships = relationships + self.meta = meta + self.links = links + } } /* -extension ResourceObject where Description.Attributes == NoAttributes { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes { + public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata { - public init(id: ResourceObject.Id, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, relationships: relationships, meta: .none, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata { + public init(id: ResourceObject.Id, relationships: Description.Relationships, links: LinksType) { + self.init(id: id, relationships: relationships, meta: .none, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, relationships: relationships, meta: meta, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks { + public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType) { + self.init(id: id, relationships: relationships, meta: meta, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships) { - self.init(id: id, relationships: relationships, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id, relationships: Description.Relationships) { + self.init(id: id, relationships: relationships, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships, meta: MetaType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType == Unidentified { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType == Unidentified { + public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) + } + } -extension ResourceObject where Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} + extension ResourceObject where Description.Relationships == NoRelationships { + public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType, links: LinksType) { + self.init(id: id, attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, links: LinksType) { - self.init(id: id, attributes: attributes, meta: .none, links: links) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata { + public init(id: ResourceObject.Id, attributes: Description.Attributes, links: LinksType) { + self.init(id: id, attributes: attributes, meta: .none, links: links) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType) { - self.init(id: id, attributes: attributes, meta: meta, links: .none) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType) { + self.init(id: id, attributes: attributes, meta: meta, links: .none) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes) { - self.init(id: id, attributes: attributes, meta: .none, links: .none) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes) { + self.init(id: id, attributes: attributes, meta: .none, links: .none) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, meta: MetaType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, meta: MetaType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) + } + } -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships { + public init(id: ResourceObject.Id, meta: MetaType, links: LinksType) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata { + public init(id: ResourceObject.Id, links: LinksType) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, meta: MetaType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks { + public init(id: ResourceObject.Id, meta: MetaType) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { + public init(meta: MetaType, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(links: LinksType) { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(meta: MetaType) { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) + } + } -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init() { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init() { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) + } + } -extension ResourceObject where MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} + extension ResourceObject where MetaType == NoMetadata { + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { + self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: links) + } + } -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} + extension ResourceObject where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) + } + } -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} + extension ResourceObject where MetaType == NoMetadata, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) + } + } -extension ResourceObject where LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} + extension ResourceObject where LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { + self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none) + } + } -extension ResourceObject where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} + extension ResourceObject where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { + self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) + } + } -extension ResourceObject where LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} + extension ResourceObject where LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { + self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) + } + } -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} + extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships) { + self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none) + } + } -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} + extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, relationships: Description.Relationships) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) + } + } -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} -*/ + extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, relationships: Description.Relationships) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) + } + } + */ // MARK: - Pointer for Relationships use public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { - /// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links. - /// This is just a convenient way to reference a `ResourceObject` so that - /// other ResourceObjects' Relationships can be built up from it. - typealias Pointer = ToOneRelationship + /// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links. + /// This is just a convenient way to reference a `ResourceObject` so that + /// other ResourceObjects' Relationships can be built up from it. + typealias Pointer = ToOneRelationship - /// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links. - /// This is just a convenient way to reference a bunch of ResourceObjects so - /// that other ResourceObjects' Relationships can be built up from them. - typealias Pointers = ToManyRelationship + /// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links. + /// This is just a convenient way to reference a bunch of ResourceObjects so + /// that other ResourceObjects' Relationships can be built up from them. + typealias Pointers = ToManyRelationship - /// Get a pointer to this resource object that can be used as a - /// relationship to another resource object. - var pointer: Pointer { - return Pointer(resourceObject: self) - } + /// Get a pointer to this resource object that can be used as a + /// relationship to another resource object. + var pointer: Pointer { + return Pointer(resourceObject: self) + } /// Get a pointer (i.e. `ToOneRelationship`) to this resource /// object with the given metadata and links attached. - func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { - return ToOneRelationship(resourceObject: self, meta: meta, links: links) - } + func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { + return ToOneRelationship(resourceObject: self, meta: meta, links: links) + } } // MARK: - Identifying Unidentified Entities public extension ResourceObject where EntityRawIdType == Unidentified { - /// Create a new `ResourceObject` from this one with a newly created - /// unique Id of the given type. - func identified(byType: RawIdType.Type) -> ResourceObject { - return .init(attributes: attributes, relationships: relationships, meta: meta, links: links) - } + /// Create a new `ResourceObject` from this one with a newly created + /// unique Id of the given type. + func identified(byType: RawIdType.Type) -> ResourceObject { + return .init(attributes: attributes, relationships: relationships, meta: meta, links: links) + } - /// Create a new `ResourceObject` from this one with the given Id. - func identified(by id: RawIdType) -> ResourceObject { - return .init(id: ResourceObject.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) - } + /// Create a new `ResourceObject` from this one with the given Id. + func identified(by id: RawIdType) -> ResourceObject { + return .init(id: ResourceObject.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) + } } public extension ResourceObject where EntityRawIdType: CreatableRawIdType { - /// Create a copy of this `ResourceObject` with a new unique Id. - func withNewIdentifier() -> ResourceObject { - return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links) - } + /// Create a copy of this `ResourceObject` with a new unique Id. + func withNewIdentifier() -> ResourceObject { + return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links) + } } // MARK: - Attribute Access public extension ResourceObjectProxy { // MARK: Keypath Subscript Lookup - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - subscript(_ path: KeyPath) -> T.ValueType { - return attributes[keyPath: path].value - } + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(_ path: KeyPath) -> T.ValueType { + return attributes[keyPath: path].value + } - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - subscript(_ path: KeyPath) -> T.ValueType? { - return attributes[keyPath: path]?.value - } + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(_ path: KeyPath) -> T.ValueType? { + return attributes[keyPath: path]?.value + } - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - subscript(_ path: KeyPath) -> U? where T.ValueType == U? { - // Implementation Note: Handles Transform that returns optional - // type. - return attributes[keyPath: path].flatMap { $0.value } - } + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(_ path: KeyPath) -> U? where T.ValueType == U? { + // Implementation Note: Handles Transform that returns optional + // type. + return attributes[keyPath: path].flatMap { $0.value } + } // MARK: Dynaminc Member Keypath Lookup /// Access the attribute at the given keypath. This just @@ -499,27 +499,27 @@ public extension ResourceObjectProxy { } // MARK: Direct Keypath Subscript Lookup - /// Access the storage of the attribute at the given keypath. This just + /// Access the storage of the attribute at the given keypath. This just /// allows you to write `resourceObject[direct: \.propertyName]` instead - /// of `resourceObject.attributes.propertyName`. + /// of `resourceObject.attributes.propertyName`. /// Most of the subscripts dig into an `AttributeType`. This subscript /// returns the `AttributeType` (or another type, if you are accessing /// an attribute that is not stored in an `AttributeType`). - subscript(direct path: KeyPath) -> T { - // Implementation Note: Handles attributes that are not - // AttributeType. These should only exist as computed properties. - return attributes[keyPath: path] - } + subscript(direct path: KeyPath) -> T { + // Implementation Note: Handles attributes that are not + // AttributeType. These should only exist as computed properties. + return attributes[keyPath: path] + } } // MARK: - Meta-Attribute Access public extension ResourceObjectProxy { // MARK: Keypath Subscript Lookup - /// Access an attribute requiring a transformation on the RawValue _and_ - /// a secondary transformation on this entity (self). - subscript(_ path: KeyPath T>) -> T { - return attributes[keyPath: path](self) - } + /// Access an attribute requiring a transformation on the RawValue _and_ + /// a secondary transformation on this entity (self). + subscript(_ path: KeyPath T>) -> T { + return attributes[keyPath: path](self) + } // MARK: Dynamic Member Keypath Lookup /// Access an attribute requiring a transformation on the RawValue _and_ @@ -531,61 +531,61 @@ public extension ResourceObjectProxy { // MARK: - Relationship Access public extension ResourceObjectProxy { - /// Access to an Id of a `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other.id`. - static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.Identifier { - return entity.relationships[keyPath: path].id - } + /// Access to an Id of a `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other.id`. + static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.Identifier { + return entity.relationships[keyPath: path].id + } - /// Access to an Id of an optional `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier { - // Implementation Note: This signature applies to `ToOneRelationship?` - // whereas the one below applies to `ToOneRelationship?` - return entity.relationships[keyPath: path]?.id - } + /// Access to an Id of an optional `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other?.id`. + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier { + // Implementation Note: This signature applies to `ToOneRelationship?` + // whereas the one below applies to `ToOneRelationship?` + return entity.relationships[keyPath: path]?.id + } - /// Access to an Id of an optional `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier? { - // Implementation Note: This signature applies to `ToOneRelationship?` - // whereas the one above applies to `ToOneRelationship?` - return entity.relationships[keyPath: path]?.id - } + /// Access to an Id of an optional `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other?.id`. + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier? { + // Implementation Note: This signature applies to `ToOneRelationship?` + // whereas the one above applies to `ToOneRelationship?` + return entity.relationships[keyPath: path]?.id + } - /// Access to all Ids of a `ToManyRelationship`. - /// This allows you to write `resourceObject ~> \.others` instead - /// of `resourceObject.relationships.others.ids`. - static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.Identifier] { - return entity.relationships[keyPath: path].ids - } + /// Access to all Ids of a `ToManyRelationship`. + /// This allows you to write `resourceObject ~> \.others` instead + /// of `resourceObject.relationships.others.ids`. + static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.Identifier] { + return entity.relationships[keyPath: path].ids + } - /// Access to all Ids of an optional `ToManyRelationship`. - /// This allows you to write `resourceObject ~> \.others` instead - /// of `resourceObject.relationships.others?.ids`. - static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.Identifier]? { - return entity.relationships[keyPath: path]?.ids - } + /// Access to all Ids of an optional `ToManyRelationship`. + /// This allows you to write `resourceObject ~> \.others` instead + /// of `resourceObject.relationships.others?.ids`. + static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.Identifier]? { + return entity.relationships[keyPath: path]?.ids + } } // MARK: - Meta-Relationship Access public extension ResourceObjectProxy { - /// Access to an Id of a `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other.id`. - static func ~>(entity: Self, path: KeyPath Identifier>) -> Identifier { - return entity.relationships[keyPath: path](entity) - } + /// Access to an Id of a `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other.id`. + static func ~>(entity: Self, path: KeyPath Identifier>) -> Identifier { + return entity.relationships[keyPath: path](entity) + } - /// Access to all Ids of a `ToManyRelationship`. - /// This allows you to write `resourceObject ~> \.others` instead - /// of `resourceObject.relationships.others.ids`. - static func ~>(entity: Self, path: KeyPath [Identifier]>) -> [Identifier] { - return entity.relationships[keyPath: path](entity) - } + /// Access to all Ids of a `ToManyRelationship`. + /// This allows you to write `resourceObject ~> \.others` instead + /// of `resourceObject.relationships.others.ids`. + static func ~>(entity: Self, path: KeyPath [Identifier]>) -> [Identifier] { + return entity.relationships[keyPath: path](entity) + } } infix operator ~> diff --git a/Sources/JSONAPITesting/Attribute+Literal.swift b/Sources/JSONAPITesting/Attribute+Literal.swift index 706b6e4..b631bab 100644 --- a/Sources/JSONAPITesting/Attribute+Literal.swift +++ b/Sources/JSONAPITesting/Attribute+Literal.swift @@ -2,81 +2,81 @@ import JSONAPI extension Attribute: ExpressibleByUnicodeScalarLiteral where RawValue: ExpressibleByUnicodeScalarLiteral { - public typealias UnicodeScalarLiteralType = RawValue.UnicodeScalarLiteralType + public typealias UnicodeScalarLiteralType = RawValue.UnicodeScalarLiteralType - public init(unicodeScalarLiteral value: RawValue.UnicodeScalarLiteralType) { - self.init(value: RawValue(unicodeScalarLiteral: value)) - } + public init(unicodeScalarLiteral value: RawValue.UnicodeScalarLiteralType) { + self.init(value: RawValue(unicodeScalarLiteral: value)) + } } extension Attribute: ExpressibleByExtendedGraphemeClusterLiteral where RawValue: ExpressibleByExtendedGraphemeClusterLiteral { - public typealias ExtendedGraphemeClusterLiteralType = RawValue.ExtendedGraphemeClusterLiteralType + public typealias ExtendedGraphemeClusterLiteralType = RawValue.ExtendedGraphemeClusterLiteralType - public init(extendedGraphemeClusterLiteral value: RawValue.ExtendedGraphemeClusterLiteralType) { - self.init(value: RawValue(extendedGraphemeClusterLiteral: value)) - } + public init(extendedGraphemeClusterLiteral value: RawValue.ExtendedGraphemeClusterLiteralType) { + self.init(value: RawValue(extendedGraphemeClusterLiteral: value)) + } } extension Attribute: ExpressibleByStringLiteral where RawValue: ExpressibleByStringLiteral { - public typealias StringLiteralType = RawValue.StringLiteralType + public typealias StringLiteralType = RawValue.StringLiteralType - public init(stringLiteral value: RawValue.StringLiteralType) { - self.init(value: RawValue(stringLiteral: value)) - } + public init(stringLiteral value: RawValue.StringLiteralType) { + self.init(value: RawValue(stringLiteral: value)) + } } extension Attribute: ExpressibleByNilLiteral where RawValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { - self.init(value: RawValue(nilLiteral: ())) - } + public init(nilLiteral: ()) { + self.init(value: RawValue(nilLiteral: ())) + } } extension Attribute: ExpressibleByFloatLiteral where RawValue: ExpressibleByFloatLiteral { - public typealias FloatLiteralType = RawValue.FloatLiteralType + public typealias FloatLiteralType = RawValue.FloatLiteralType - public init(floatLiteral value: RawValue.FloatLiteralType) { - self.init(value: RawValue(floatLiteral: value)) - } + public init(floatLiteral value: RawValue.FloatLiteralType) { + self.init(value: RawValue(floatLiteral: value)) + } } extension Optional: ExpressibleByFloatLiteral where Wrapped: ExpressibleByFloatLiteral { - public typealias FloatLiteralType = Wrapped.FloatLiteralType + public typealias FloatLiteralType = Wrapped.FloatLiteralType - public init(floatLiteral value: FloatLiteralType) { - self = .some(Wrapped(floatLiteral: value)) - } + public init(floatLiteral value: FloatLiteralType) { + self = .some(Wrapped(floatLiteral: value)) + } } extension Attribute: ExpressibleByBooleanLiteral where RawValue: ExpressibleByBooleanLiteral { - public typealias BooleanLiteralType = RawValue.BooleanLiteralType + public typealias BooleanLiteralType = RawValue.BooleanLiteralType - public init(booleanLiteral value: BooleanLiteralType) { - self.init(value: RawValue(booleanLiteral: value)) - } + public init(booleanLiteral value: BooleanLiteralType) { + self.init(value: RawValue(booleanLiteral: value)) + } } extension Optional: ExpressibleByBooleanLiteral where Wrapped: ExpressibleByBooleanLiteral { - public typealias BooleanLiteralType = Wrapped.BooleanLiteralType + public typealias BooleanLiteralType = Wrapped.BooleanLiteralType - public init(booleanLiteral value: BooleanLiteralType) { - self = .some(Wrapped(booleanLiteral: value)) - } + public init(booleanLiteral value: BooleanLiteralType) { + self = .some(Wrapped(booleanLiteral: value)) + } } extension Attribute: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIntegerLiteral { - public typealias IntegerLiteralType = RawValue.IntegerLiteralType + public typealias IntegerLiteralType = RawValue.IntegerLiteralType - public init(integerLiteral value: IntegerLiteralType) { - self.init(value: RawValue(integerLiteral: value)) - } + public init(integerLiteral value: IntegerLiteralType) { + self.init(value: RawValue(integerLiteral: value)) + } } extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral { - public typealias IntegerLiteralType = Wrapped.IntegerLiteralType + public typealias IntegerLiteralType = Wrapped.IntegerLiteralType - public init(integerLiteral value: IntegerLiteralType) { - self = .some(Wrapped(integerLiteral: value)) - } + public init(integerLiteral value: IntegerLiteralType) { + self = .some(Wrapped(integerLiteral: value)) + } } // regretably, array and dictionary literals are not so easy because Dictionaries and Arrays @@ -84,55 +84,55 @@ extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByInte // we can still provide a case for the Array and Dictionary types, though. public protocol DictionaryType { - associatedtype Key: Hashable - associatedtype Value + associatedtype Key: Hashable + associatedtype Value - init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) + init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) } extension Dictionary: DictionaryType {} extension Attribute: ExpressibleByDictionaryLiteral where RawValue: DictionaryType { - public typealias Key = RawValue.Key + public typealias Key = RawValue.Key - public typealias Value = RawValue.Value + public typealias Value = RawValue.Value - public init(dictionaryLiteral elements: (RawValue.Key, RawValue.Value)...) { + public init(dictionaryLiteral elements: (RawValue.Key, RawValue.Value)...) { - // we arbitrarily keep the first value if two values are assigned to the same key - self.init(value: RawValue(elements, uniquingKeysWith: { val, _ in val })) - } + // we arbitrarily keep the first value if two values are assigned to the same key + self.init(value: RawValue(elements, uniquingKeysWith: { val, _ in val })) + } } extension Optional: DictionaryType where Wrapped: DictionaryType { - public typealias Key = Wrapped.Key + public typealias Key = Wrapped.Key - public typealias Value = Wrapped.Value + public typealias Value = Wrapped.Value - public init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) { - self = try .some(Wrapped(keysAndValues, uniquingKeysWith: combine)) - } + public init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) { + self = try .some(Wrapped(keysAndValues, uniquingKeysWith: combine)) + } } public protocol ArrayType { - associatedtype Element + associatedtype Element - init(_ s: S) where Element == S.Element, S : Sequence + init(_ s: S) where Element == S.Element, S : Sequence } extension Array: ArrayType {} extension ArraySlice: ArrayType {} extension Attribute: ExpressibleByArrayLiteral where RawValue: ArrayType { - public typealias ArrayLiteralElement = RawValue.Element + public typealias ArrayLiteralElement = RawValue.Element - public init(arrayLiteral elements: ArrayLiteralElement...) { - self.init(value: RawValue(elements)) - } + public init(arrayLiteral elements: ArrayLiteralElement...) { + self.init(value: RawValue(elements)) + } } extension Optional: ArrayType where Wrapped: ArrayType { - public typealias Element = Wrapped.Element + public typealias Element = Wrapped.Element - public init(_ s: S) where Element == S.Element, S : Sequence { - self = .some(Wrapped(s)) - } + public init(_ s: S) where Element == S.Element, S : Sequence { + self = .some(Wrapped(s)) + } } diff --git a/Sources/JSONAPITesting/Id+Literal.swift b/Sources/JSONAPITesting/Id+Literal.swift index b05b8b1..92121f9 100644 --- a/Sources/JSONAPITesting/Id+Literal.swift +++ b/Sources/JSONAPITesting/Id+Literal.swift @@ -8,33 +8,33 @@ import JSONAPI extension Id: ExpressibleByUnicodeScalarLiteral where RawType: ExpressibleByUnicodeScalarLiteral { - public typealias UnicodeScalarLiteralType = RawType.UnicodeScalarLiteralType - - public init(unicodeScalarLiteral value: RawType.UnicodeScalarLiteralType) { - self.init(rawValue: RawType(unicodeScalarLiteral: value)) - } + public typealias UnicodeScalarLiteralType = RawType.UnicodeScalarLiteralType + + public init(unicodeScalarLiteral value: RawType.UnicodeScalarLiteralType) { + self.init(rawValue: RawType(unicodeScalarLiteral: value)) + } } extension Id: ExpressibleByExtendedGraphemeClusterLiteral where RawType: ExpressibleByExtendedGraphemeClusterLiteral { - public typealias ExtendedGraphemeClusterLiteralType = RawType.ExtendedGraphemeClusterLiteralType - - public init(extendedGraphemeClusterLiteral value: RawType.ExtendedGraphemeClusterLiteralType) { - self.init(rawValue: RawType(extendedGraphemeClusterLiteral: value)) - } + public typealias ExtendedGraphemeClusterLiteralType = RawType.ExtendedGraphemeClusterLiteralType + + public init(extendedGraphemeClusterLiteral value: RawType.ExtendedGraphemeClusterLiteralType) { + self.init(rawValue: RawType(extendedGraphemeClusterLiteral: value)) + } } extension Id: ExpressibleByStringLiteral where RawType: ExpressibleByStringLiteral { - public typealias StringLiteralType = RawType.StringLiteralType - - public init(stringLiteral value: RawType.StringLiteralType) { - self.init(rawValue: RawType(stringLiteral: value)) - } + public typealias StringLiteralType = RawType.StringLiteralType + + public init(stringLiteral value: RawType.StringLiteralType) { + self.init(rawValue: RawType(stringLiteral: value)) + } } extension Id: ExpressibleByIntegerLiteral where RawType: ExpressibleByIntegerLiteral { - public typealias IntegerLiteralType = RawType.IntegerLiteralType - - public init(integerLiteral value: IntegerLiteralType) { - self.init(rawValue: RawType(integerLiteral: value)) - } + public typealias IntegerLiteralType = RawType.IntegerLiteralType + + public init(integerLiteral value: IntegerLiteralType) { + self.init(rawValue: RawType(integerLiteral: value)) + } } diff --git a/Sources/JSONAPITesting/Optional+Literal.swift b/Sources/JSONAPITesting/Optional+Literal.swift index e0d81b5..8c87fab 100644 --- a/Sources/JSONAPITesting/Optional+Literal.swift +++ b/Sources/JSONAPITesting/Optional+Literal.swift @@ -6,25 +6,25 @@ // extension Optional: ExpressibleByUnicodeScalarLiteral where Wrapped: ExpressibleByUnicodeScalarLiteral { - public typealias UnicodeScalarLiteralType = Wrapped.UnicodeScalarLiteralType + public typealias UnicodeScalarLiteralType = Wrapped.UnicodeScalarLiteralType - public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - self = .some(Wrapped(unicodeScalarLiteral: value)) - } + public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { + self = .some(Wrapped(unicodeScalarLiteral: value)) + } } extension Optional: ExpressibleByExtendedGraphemeClusterLiteral where Wrapped: ExpressibleByExtendedGraphemeClusterLiteral { - public typealias ExtendedGraphemeClusterLiteralType = Wrapped.ExtendedGraphemeClusterLiteralType + public typealias ExtendedGraphemeClusterLiteralType = Wrapped.ExtendedGraphemeClusterLiteralType - public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - self = .some(Wrapped(extendedGraphemeClusterLiteral: value)) - } + public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { + self = .some(Wrapped(extendedGraphemeClusterLiteral: value)) + } } extension Optional: ExpressibleByStringLiteral where Wrapped: ExpressibleByStringLiteral { - public typealias StringLiteralType = Wrapped.StringLiteralType + public typealias StringLiteralType = Wrapped.StringLiteralType - public init(stringLiteral value: StringLiteralType) { - self = .some(Wrapped(stringLiteral: value)) - } + public init(stringLiteral value: StringLiteralType) { + self = .some(Wrapped(stringLiteral: value)) + } } diff --git a/Sources/JSONAPITesting/Relationship+Literal.swift b/Sources/JSONAPITesting/Relationship+Literal.swift index 9af692c..551fd88 100644 --- a/Sources/JSONAPITesting/Relationship+Literal.swift +++ b/Sources/JSONAPITesting/Relationship+Literal.swift @@ -8,40 +8,40 @@ import JSONAPI extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.Identifier: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public init(nilLiteral: ()) { + public init(nilLiteral: ()) { - self.init(id: Identifiable.Identifier(nilLiteral: ())) - } + self.init(id: Identifiable.Identifier(nilLiteral: ())) + } } extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.Identifier: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias UnicodeScalarLiteralType = Identifiable.Identifier.UnicodeScalarLiteralType + public typealias UnicodeScalarLiteralType = Identifiable.Identifier.UnicodeScalarLiteralType - public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - self.init(id: Identifiable.Identifier(unicodeScalarLiteral: value)) - } + public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { + self.init(id: Identifiable.Identifier(unicodeScalarLiteral: value)) + } } extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.Identifier: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias ExtendedGraphemeClusterLiteralType = Identifiable.Identifier.ExtendedGraphemeClusterLiteralType + public typealias ExtendedGraphemeClusterLiteralType = Identifiable.Identifier.ExtendedGraphemeClusterLiteralType - public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - self.init(id: Identifiable.Identifier(extendedGraphemeClusterLiteral: value)) - } + public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { + self.init(id: Identifiable.Identifier(extendedGraphemeClusterLiteral: value)) + } } extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.Identifier: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias StringLiteralType = Identifiable.Identifier.StringLiteralType + public typealias StringLiteralType = Identifiable.Identifier.StringLiteralType - public init(stringLiteral value: StringLiteralType) { - self.init(id: Identifiable.Identifier(stringLiteral: value)) - } + public init(stringLiteral value: StringLiteralType) { + self.init(id: Identifiable.Identifier(stringLiteral: value)) + } } extension ToManyRelationship: ExpressibleByArrayLiteral where MetaType == NoMetadata, LinksType == NoLinks { - public typealias ArrayLiteralElement = Relatable.Identifier + public typealias ArrayLiteralElement = Relatable.Identifier - public init(arrayLiteral elements: ArrayLiteralElement...) { - self.init(ids: elements) - } + public init(arrayLiteral elements: ArrayLiteralElement...) { + self.init(ids: elements) + } } diff --git a/Sources/JSONAPITesting/ResourceObjectCheck.swift b/Sources/JSONAPITesting/ResourceObjectCheck.swift index 0749ec8..13c1724 100644 --- a/Sources/JSONAPITesting/ResourceObjectCheck.swift +++ b/Sources/JSONAPITesting/ResourceObjectCheck.swift @@ -8,29 +8,29 @@ import JSONAPI public enum ResourceObjectCheckError: Swift.Error { - /// The attributes should live in a struct, not - /// another type class. - case attributesNotStruct + /// The attributes should live in a struct, not + /// another type class. + case attributesNotStruct - /// The relationships should live in a struct, not - /// another type class. - case relationshipsNotStruct + /// The relationships should live in a struct, not + /// another type class. + case relationshipsNotStruct - /// All stored properties on an Attributes struct should - /// be one of the supplied Attribute types. - case nonAttribute(named: String) + /// All stored properties on an Attributes struct should + /// be one of the supplied Attribute types. + case nonAttribute(named: String) - /// All stored properties on a Relationships struct should - /// be one of the supplied Relationship types. - case nonRelationship(named: String) + /// All stored properties on a Relationships struct should + /// be one of the supplied Relationship types. + case nonRelationship(named: String) - /// It is explicitly stated by the JSON spec - /// a "none" value for arrays is an empty array, not `nil`. - case nullArray(named: String) + /// It is explicitly stated by the JSON spec + /// a "none" value for arrays is an empty array, not `nil`. + case nullArray(named: String) } public struct ResourceObjectCheckErrors: Swift.Error { - let problems: [ResourceObjectCheckError] + let problems: [ResourceObjectCheckError] } private protocol OptionalAttributeType {} @@ -55,40 +55,40 @@ extension TransformedAttribute: _AttributeType {} extension Attribute: _AttributeType {} public extension ResourceObject { - static func check(_ entity: ResourceObject) throws { - var problems = [ResourceObjectCheckError]() + static func check(_ entity: ResourceObject) throws { + var problems = [ResourceObjectCheckError]() - let attributesMirror = Mirror(reflecting: entity.attributes) + let attributesMirror = Mirror(reflecting: entity.attributes) - if attributesMirror.displayStyle != .`struct` { - problems.append(.attributesNotStruct) - } + if attributesMirror.displayStyle != .`struct` { + problems.append(.attributesNotStruct) + } - for attribute in attributesMirror.children { - if attribute.value as? _AttributeType == nil, - attribute.value as? OptionalAttributeType == nil { - problems.append(.nonAttribute(named: attribute.label ?? "unnamed")) - } - if attribute.value as? AttributeTypeWithOptionalArray != nil { - problems.append(.nullArray(named: attribute.label ?? "unnamed")) - } - } + for attribute in attributesMirror.children { + if attribute.value as? _AttributeType == nil, + attribute.value as? OptionalAttributeType == nil { + problems.append(.nonAttribute(named: attribute.label ?? "unnamed")) + } + if attribute.value as? AttributeTypeWithOptionalArray != nil { + problems.append(.nullArray(named: attribute.label ?? "unnamed")) + } + } - let relationshipsMirror = Mirror(reflecting: entity.relationships) + let relationshipsMirror = Mirror(reflecting: entity.relationships) - if relationshipsMirror.displayStyle != .`struct` { - problems.append(.relationshipsNotStruct) - } + if relationshipsMirror.displayStyle != .`struct` { + problems.append(.relationshipsNotStruct) + } - for relationship in relationshipsMirror.children { - if relationship.value as? _RelationshipType == nil, - relationship.value as? OptionalRelationshipType == nil { - problems.append(.nonRelationship(named: relationship.label ?? "unnamed")) - } - } + for relationship in relationshipsMirror.children { + if relationship.value as? _RelationshipType == nil, + relationship.value as? OptionalRelationshipType == nil { + problems.append(.nonRelationship(named: relationship.label ?? "unnamed")) + } + } - guard problems.count == 0 else { - throw ResourceObjectCheckErrors(problems: problems) - } - } + guard problems.count == 0 else { + throw ResourceObjectCheckErrors(problems: problems) + } + } } From adcc6bfb108d661f6af658c25eb36a4f1bf33266 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 22:43:40 -0800 Subject: [PATCH 09/23] fix error after merge --- Sources/JSONAPI/Document/Document.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 08c5d9b..bfe3268 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -441,7 +441,6 @@ extension Document { public typealias BodyData = Document.BodyData public var body: Document.Body { return document.body } - public var apiDescription: APIDescription { return document.apiDescription } private let document: Document @@ -489,7 +488,6 @@ extension Document { public typealias BodyData = Document.BodyData public var body: Document.Body { return document.body } - public var apiDescription: APIDescription { return document.apiDescription } private let document: Document From ae7e0f528a9080f85abb47ceb95fb519276d7da5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 00:19:27 -0800 Subject: [PATCH 10/23] abstract away document comparison --- .../Comparisons/DocumentCompare.swift | 100 +++++++++++------- .../Comparisons/DocumentDataCompare.swift | 2 +- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 15c417f..587d6a0 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -79,7 +79,7 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension Document { +extension EncodableJSONAPIDocument where Body: Equatable { public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { return DocumentComparison( apiDescription: Comparison( @@ -111,70 +111,88 @@ extension Document { } } -extension Document.Body { +extension DocumentBody where Self: Equatable { public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + + // rule out case where they are the same guard self != other else { return .same } - switch (self, other) { - case (.errors(let errors1), .errors(let errors2)): - return .differentErrors(BodyComparison.compare(errors: errors1.0, - errors1.meta, - errors1.links, - with: errors2.0, - errors2.meta, - errors2.links)) - case (.errors, .data): - return .dataErrorMismatch(errorOnLeft: true) - case (.data, .errors): - return .dataErrorMismatch(errorOnLeft: false) - case (.data(let data1), .data(let data2)): + // rule out case where they are both error bodies + if let errors1 = errors, let errors2 = other.errors { + return .differentErrors( + BodyComparison.compare( + errors: errors1, meta, links, + with: errors2, meta, links + ) + ) + } + + // rule out the case where they are both data + if let data1 = data, let data2 = other.data { return .differentData(data1.compare(to: data2)) } + + // we are left with the case where one is data and the + // other is an error if self.isError, then "the error + // is on the left" + return .dataErrorMismatch(errorOnLeft: isError) } public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + + // rule out case where they are the same guard self != other else { return .same } - switch (self, other) { - case (.errors(let errors1), .errors(let errors2)): - return .differentErrors(BodyComparison.compare(errors: errors1.0, - errors1.meta, - errors1.links, - with: errors2.0, - errors2.meta, - errors2.links)) - case (.errors, .data): - return .dataErrorMismatch(errorOnLeft: true) - case (.data, .errors): - return .dataErrorMismatch(errorOnLeft: false) - case (.data(let data1), .data(let data2)): + // rule out case where they are both error bodies + if let errors1 = errors, let errors2 = other.errors { + return .differentErrors( + BodyComparison.compare( + errors: errors1, meta, links, + with: errors2, meta, links + ) + ) + } + + // rule out the case where they are both data + if let data1 = data, let data2 = other.data { return .differentData(data1.compare(to: data2)) } + + // we are left with the case where one is data and the + // other is an error if self.isError, then "the error + // is on the left" + return .dataErrorMismatch(errorOnLeft: isError) } public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { + + // rule out case where they are the same guard self != other else { return .same } - switch (self, other) { - case (.errors(let errors1), .errors(let errors2)): - return .differentErrors(BodyComparison.compare(errors: errors1.0, - errors1.meta, - errors1.links, - with: errors2.0, - errors2.meta, - errors2.links)) - case (.errors, .data): - return .dataErrorMismatch(errorOnLeft: true) - case (.data, .errors): - return .dataErrorMismatch(errorOnLeft: false) - case (.data(let data1), .data(let data2)): + // rule out case where they are both error bodies + if let errors1 = errors, let errors2 = other.errors { + return .differentErrors( + BodyComparison.compare( + errors: errors1, meta, links, + with: errors2, meta, links + ) + ) + } + + // rule out the case where they are both data + if let data1 = data, let data2 = other.data { return .differentData(data1.compare(to: data2)) } + + // we are left with the case where one is data and the + // other is an error if self.isError, then "the error + // is on the left" + return .dataErrorMismatch(errorOnLeft: isError) } } diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index 392bab0..d268572 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -33,7 +33,7 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension Document.Body.Data { +extension DocumentBodyData { public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { return .init( primary: primary.compare(to: other.primary), From f37f44cfda87e23a2607feeb2c6b5af5001ed2c8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 21:42:13 -0800 Subject: [PATCH 11/23] add comparable protocol --- Sources/JSONAPITesting/Comparisons/Comparison.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/Comparison.swift b/Sources/JSONAPITesting/Comparisons/Comparison.swift index a6b97ee..e55af29 100644 --- a/Sources/JSONAPITesting/Comparisons/Comparison.swift +++ b/Sources/JSONAPITesting/Comparisons/Comparison.swift @@ -5,7 +5,13 @@ // Created by Mathew Polzin on 11/3/19. // -public enum Comparison: Equatable, CustomStringConvertible { +public protocol Comparable: CustomStringConvertible { + var rawValue: String { get } + + var isSame: Bool { get } +} + +public enum Comparison: Comparable, Equatable { case same case different(String, String) case prebuilt(String) @@ -50,7 +56,7 @@ public enum Comparison: Equatable, CustomStringConvertible { public typealias NamedDifferences = [String: String] -public protocol PropertyComparable: CustomStringConvertible { +public protocol PropertyComparable: Comparable { var differences: NamedDifferences { get } } From 832161628b64bb68e7fc842c20371da656185a7f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 23:05:21 -0800 Subject: [PATCH 12/23] go from 3 specializations of all document related compare functions down to 2. --- Sources/JSONAPI/Document/ResourceBody.swift | 32 ++++--- .../Comparisons/DocumentCompare.swift | 68 ++++--------- .../Comparisons/DocumentDataCompare.swift | 95 +++++++++++-------- .../Comparisons/DocumentCompareTests.swift | 52 ++++++++++ 4 files changed, 143 insertions(+), 104 deletions(-) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index c8408de..25090c8 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -29,7 +29,9 @@ extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimary /// 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 {} +public protocol EncodableResourceBody: Equatable, Encodable { + associatedtype PrimaryResource +} /// A `CodableResourceBody` 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) @@ -49,19 +51,19 @@ public func +(_ left: R, right: R) -> R { /// A type allowing for a document body containing 1 primary resource. /// If the `Entity` specialization is an `Optional` type, the body can contain /// 0 or 1 primary resources. -public struct SingleResourceBody: EncodableResourceBody { - public let value: Entity +public struct SingleResourceBody: EncodableResourceBody { + public let value: PrimaryResource - public init(resourceObject: Entity) { + public init(resourceObject: PrimaryResource) { self.value = resourceObject } } /// A type allowing for a document body containing 0 or more primary resources. -public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { - public let values: [Entity] +public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { + public let values: [PrimaryResource] - public init(resourceObjects: [Entity]) { + public init(resourceObjects: [PrimaryResource]) { values = resourceObjects } @@ -73,6 +75,8 @@ public struct ManyResourceBody: Encoda /// Use NoResourceBody to indicate you expect a JSON API document to not /// contain a "data" top-level key. public struct NoResourceBody: CodableResourceBody { + public typealias PrimaryResource = Void + public static var none: NoResourceBody { return NoResourceBody() } } @@ -82,7 +86,7 @@ extension SingleResourceBody { var container = encoder.singleValueContainer() let anyNil: Any? = nil - let nilValue = anyNil as? Entity + let nilValue = anyNil as? PrimaryResource guard value != nilValue else { try container.encodeNil() return @@ -92,18 +96,18 @@ extension SingleResourceBody { } } -extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource { +extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let anyNil: Any? = nil if container.decodeNil(), - let val = anyNil as? Entity { + let val = anyNil as? PrimaryResource { value = val return } - value = try container.decode(Entity.self) + value = try container.decode(PrimaryResource.self) } } @@ -117,12 +121,12 @@ extension ManyResourceBody { } } -extension ManyResourceBody: Decodable, CodableResourceBody where Entity: CodablePrimaryResource { +extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource { public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() - var valueAggregator = [Entity]() + var valueAggregator = [PrimaryResource]() while !container.isAtEnd { - valueAggregator.append(try container.decode(Entity.self)) + valueAggregator.append(try container.decode(PrimaryResource.self)) } values = valueAggregator } diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 587d6a0..3f3ee23 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -79,28 +79,8 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension EncodableJSONAPIDocument where Body: Equatable { - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { - return DocumentComparison( - apiDescription: Comparison( - String(describing: apiDescription), - String(describing: other.apiDescription) - ), - body: body.compare(to: other.body) - ) - } - - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { - return DocumentComparison( - apiDescription: Comparison( - String(describing: apiDescription), - String(describing: other.apiDescription) - ), - body: body.compare(to: other.body) - ) - } - - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == ManyResourceBody, T: ResourceObjectType { +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( apiDescription: Comparison( String(describing: apiDescription), @@ -111,36 +91,20 @@ extension EncodableJSONAPIDocument where Body: Equatable { } } -extension DocumentBody where Self: Equatable { - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { - - // rule out case where they are the same - guard self != other else { - return .same - } - - // rule out case where they are both error bodies - if let errors1 = errors, let errors2 = other.errors { - return .differentErrors( - BodyComparison.compare( - errors: errors1, meta, links, - with: errors2, meta, links - ) - ) - } - - // rule out the case where they are both data - if let data1 = data, let data2 = other.data { - return .differentData(data1.compare(to: data2)) - } - - // we are left with the case where one is data and the - // other is an error if self.isError, then "the error - // is on the left" - return .dataErrorMismatch(errorOnLeft: isError) +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> DocumentComparison { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) } +} - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same guard self != other else { @@ -167,8 +131,10 @@ extension DocumentBody where Self: Equatable { // is on the left" return .dataErrorMismatch(errorOnLeft: isError) } +} - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same guard self != other else { diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index d268572..87b6819 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -33,8 +33,8 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension DocumentBodyData { - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { +extension DocumentBodyData where PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), @@ -42,17 +42,10 @@ extension DocumentBodyData { links: Comparison(links, other.links) ) } +} - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { - return .init( - primary: primary.compare(to: other.primary), - includes: includes.compare(to: other.includes), - meta: Comparison(meta, other.meta), - links: Comparison(links, other.links) - ) - } - - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { +extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), @@ -109,42 +102,24 @@ public struct ManyResourceObjectComparison: Equatable, PropertyComparable { } } -extension SingleResourceBody where Entity: ResourceObjectType { +extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .single(.init(value, other.value)) - } -} + guard let one = optionalResourceObject, + let two = other.optionalResourceObject else { -public protocol _OptionalResourceObjectType { - associatedtype Wrapped: ResourceObjectType + func nilOrName(_ resObj: T?) -> String { + resObj.map { String(describing: type(of: $0)) } ?? "nil" + } - var maybeValue: Wrapped? { get } -} - -extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { - public var maybeValue: Wrapped? { - switch self { - case .none: - return nil - case .some(let value): - return value - } - } -} - -extension SingleResourceBody where Entity: _OptionalResourceObjectType { - public func compare(to other: Self) -> PrimaryResourceBodyComparison { - guard let one = value.maybeValue, - let two = other.value.maybeValue else { - return .other(Comparison(value, other.value)) + return .other(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) } return .single(.init(one, two)) } } -extension ManyResourceBody where Entity: ResourceObjectType { +extension _ResourceBody where PrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .many(.init(values.compare(to: other.values, using: { r1, r2 in + return .many(.init(resourceObjects.compare(to: other.resourceObjects, using: { r1, r2 in let r1AsResource = r1 as? AbstractResourceObjectType let maybeComparison = r1AsResource @@ -165,3 +140,45 @@ extension ManyResourceBody where Entity: ResourceObjectType { }))) } } + +public protocol _ResourceBody { + associatedtype PrimaryResourceType: ResourceObjectType + var resourceObjects: [PrimaryResourceType] { get } +} + +public protocol _OptionalResourceBody { + associatedtype WrappedPrimaryResourceType: ResourceObjectType + var optionalResourceObject: WrappedPrimaryResourceType? { get } +} + +public protocol _OptionalResourceObjectType { + associatedtype Wrapped: ResourceObjectType + + var maybeValue: Wrapped? { get } +} + +extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { + public var maybeValue: Wrapped? { + switch self { + case .none: + return nil + case .some(let value): + return value + } + } +} + +extension ManyResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { + public var resourceObjects: [PrimaryResource] { values } +} + +extension SingleResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { + public typealias PrimaryResourceType = PrimaryResource + public var resourceObjects: [PrimaryResource] { [value] } +} + +extension SingleResourceBody: _OptionalResourceBody where PrimaryResource: _OptionalResourceObjectType { + public typealias WrappedPrimaryResourceType = PrimaryResource.Wrapped + + public var optionalResourceObject: WrappedPrimaryResourceType? { value.maybeValue } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index b2e14af..7c00e38 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -15,6 +15,12 @@ final class DocumentCompareTests: XCTestCase { XCTAssertTrue(d2.compare(to: d2).differences.isEmpty) XCTAssertTrue(d3.compare(to: d3).differences.isEmpty) XCTAssertTrue(d4.compare(to: d4).differences.isEmpty) + XCTAssertTrue(d5.compare(to: d5).differences.isEmpty) + XCTAssertTrue(d6.compare(to: d6).differences.isEmpty) + XCTAssertTrue(d7.compare(to: d7).differences.isEmpty) + XCTAssertTrue(d8.compare(to: d8).differences.isEmpty) + XCTAssertTrue(d9.compare(to: d9).differences.isEmpty) + XCTAssertTrue(d10.compare(to: d10).differences.isEmpty) } func test_errorAndData() { @@ -41,6 +47,18 @@ final class DocumentCompareTests: XCTestCase { XCTAssertEqual(d3.compare(to: d6).differences, [ "Body": ##"(Includes: (include 2: missing)), (Primary Resource: (resource 2: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5), (resource 3: missing))"## ]) + + XCTAssertEqual(d7.compare(to: d8).differences, [ + "Body": ##"(Primary Resource: nil ≠ ResourceObject)"## + ]) + + XCTAssertEqual(d8.compare(to: d9).differences, [ + "Body": ##"(Primary Resource: ('age' attribute: 10 ≠ 12), ('bestFriend' relationship: Optional(Id(2)) ≠ nil), ('favoriteColor' attribute: nil ≠ Optional("blue")), ('name' attribute: name ≠ Fig), (id: 1 ≠ 5))"## + ]) + + XCTAssertEqual(d1.compare(to: d10).differences, [ + "Body": ##"(Primary Resource: (resource 1: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5))"## + ]) } } @@ -80,6 +98,8 @@ fileprivate typealias TestType2 = ResourceObject, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> +fileprivate typealias OptionalSingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> + fileprivate typealias ManyDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> fileprivate let r1 = TestType( @@ -168,3 +188,35 @@ fileprivate let d6 = ManyDocument( meta: .none, links: .none ) + +fileprivate let d7 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: nil), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d8 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: r1), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d9 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: r2), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d10 = SingleDocument( + apiDescription: .none, + body: .init(resourceObject: r2), + includes: .none, + meta: .none, + links: .none +) From 024fe2d4528e93b091e5a3900a2ca258bf40b911 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 23:21:22 -0800 Subject: [PATCH 13/23] Down to one sequence of compare functions for all documents --- .../Comparisons/DocumentCompare.swift | 42 ------------- .../Comparisons/DocumentDataCompare.swift | 59 +++++-------------- .../Comparisons/DocumentCompareTests.swift | 2 +- 3 files changed, 17 insertions(+), 86 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 3f3ee23..2ffc37e 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -79,18 +79,6 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ResourceBody { - public func compare(to other: Self) -> DocumentComparison { - return DocumentComparison( - apiDescription: Comparison( - String(describing: apiDescription), - String(describing: other.apiDescription) - ), - body: body.compare(to: other.body) - ) - } -} - extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _OptionalResourceBody { public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( @@ -103,36 +91,6 @@ extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ } } -extension DocumentBody where Self: Equatable, PrimaryResourceBody: _ResourceBody { - public func compare(to other: Self) -> BodyComparison { - - // rule out case where they are the same - guard self != other else { - return .same - } - - // rule out case where they are both error bodies - if let errors1 = errors, let errors2 = other.errors { - return .differentErrors( - BodyComparison.compare( - errors: errors1, meta, links, - with: errors2, meta, links - ) - ) - } - - // rule out the case where they are both data - if let data1 = data, let data2 = other.data { - return .differentData(data1.compare(to: data2)) - } - - // we are left with the case where one is data and the - // other is an error if self.isError, then "the error - // is on the left" - return .dataErrorMismatch(errorOnLeft: isError) - } -} - extension DocumentBody where Self: Equatable, PrimaryResourceBody: _OptionalResourceBody { public func compare(to other: Self) -> BodyComparison { diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index 87b6819..607bfa7 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -33,17 +33,6 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension DocumentBodyData where PrimaryResourceBody: _ResourceBody { - public func compare(to other: Self) -> DocumentDataComparison { - return .init( - primary: primary.compare(to: other.primary), - includes: includes.compare(to: other.includes), - meta: Comparison(meta, other.meta), - links: Comparison(links, other.links) - ) - } -} - extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { public func compare(to other: Self) -> DocumentDataComparison { return .init( @@ -56,28 +45,23 @@ extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { } public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { - case single(ResourceObjectComparison) - case many(ManyResourceObjectComparison) - case other(Comparison) + case oneOrMore(ManyResourceObjectComparison) + case optionalSingle(Comparison) public var isSame: Bool { switch self { - case .other(let comparison): + case .optionalSingle(let comparison): return comparison == .same - case .single(let comparison): - return comparison.isSame - case .many(let comparison): + case .oneOrMore(let comparison): return comparison.isSame } } public var description: String { switch self { - case .other(let comparison): + case .optionalSingle(let comparison): return comparison.rawValue - case .single(let comparison): - return comparison.rawValue - case .many(let comparison): + case .oneOrMore(let comparison): return comparison.rawValue } } @@ -107,19 +91,14 @@ extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObject guard let one = optionalResourceObject, let two = other.optionalResourceObject else { - func nilOrName(_ resObj: T?) -> String { - resObj.map { String(describing: type(of: $0)) } ?? "nil" + func nilOrName(_ resObj: [T]?) -> String { + resObj.map { _ in String(describing: T.self) } ?? "nil" } - return .other(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) + return .optionalSingle(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) } - return .single(.init(one, two)) - } -} -extension _ResourceBody where PrimaryResourceType: ResourceObjectType { - public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .many(.init(resourceObjects.compare(to: other.resourceObjects, using: { r1, r2 in + return .oneOrMore(.init(one.compare(to: two, using: { r1, r2 in let r1AsResource = r1 as? AbstractResourceObjectType let maybeComparison = r1AsResource @@ -141,14 +120,9 @@ extension _ResourceBody where PrimaryResourceType: ResourceObjectType { } } -public protocol _ResourceBody { - associatedtype PrimaryResourceType: ResourceObjectType - var resourceObjects: [PrimaryResourceType] { get } -} - public protocol _OptionalResourceBody { associatedtype WrappedPrimaryResourceType: ResourceObjectType - var optionalResourceObject: WrappedPrimaryResourceType? { get } + var optionalResourceObject: [WrappedPrimaryResourceType]? { get } } public protocol _OptionalResourceObjectType { @@ -168,17 +142,16 @@ extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectTyp } } -extension ManyResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { - public var resourceObjects: [PrimaryResource] { values } +extension ResourceObject: _OptionalResourceObjectType { + public var maybeValue: Self? { self } } -extension SingleResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { - public typealias PrimaryResourceType = PrimaryResource - public var resourceObjects: [PrimaryResource] { [value] } +extension ManyResourceBody: _OptionalResourceBody where PrimaryResource: ResourceObjectType { + public var optionalResourceObject: [PrimaryResource]? { values } } extension SingleResourceBody: _OptionalResourceBody where PrimaryResource: _OptionalResourceObjectType { public typealias WrappedPrimaryResourceType = PrimaryResource.Wrapped - public var optionalResourceObject: WrappedPrimaryResourceType? { value.maybeValue } + public var optionalResourceObject: [WrappedPrimaryResourceType]? { value.maybeValue.map { [$0] } } } diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index 7c00e38..0d19a5c 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -53,7 +53,7 @@ final class DocumentCompareTests: XCTestCase { ]) XCTAssertEqual(d8.compare(to: d9).differences, [ - "Body": ##"(Primary Resource: ('age' attribute: 10 ≠ 12), ('bestFriend' relationship: Optional(Id(2)) ≠ nil), ('favoriteColor' attribute: nil ≠ Optional("blue")), ('name' attribute: name ≠ Fig), (id: 1 ≠ 5))"## + "Body": ##"(Primary Resource: (resource 1: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5))"## ]) XCTAssertEqual(d1.compare(to: d10).differences, [ From 0538de48cbbcdec391f02c21d8f78f166856d953 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 23:41:02 -0800 Subject: [PATCH 14/23] rename PolyWrapped to CodablePolyWrapped (missed when I did other similar renaming --- .../Resource/Poly+PrimaryResource.swift | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 0ca0256..7f1fa1c 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -18,7 +18,7 @@ import Poly public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource public typealias EncodablePolyWrapped = Encodable & Equatable -public typealias PolyWrapped = EncodablePolyWrapped & Decodable +public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { @@ -35,42 +35,42 @@ extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} extension Poly1: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped {} + where A: CodablePolyWrapped {} // MARK: - 2 types extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} extension Poly2: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped {} // MARK: - 3 types extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} extension Poly3: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped {} // MARK: - 4 types extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} extension Poly4: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped {} // MARK: - 5 types extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} extension Poly5: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped {} // MARK: - 6 types extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} extension Poly6: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped {} // MARK: - 7 types extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -84,7 +84,7 @@ extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource G: EncodablePolyWrapped {} extension Poly7: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped {} // MARK: - 8 types extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -99,7 +99,7 @@ extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource H: EncodablePolyWrapped {} extension Poly8: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped {} // MARK: - 9 types extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -115,7 +115,7 @@ extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource I: EncodablePolyWrapped {} extension Poly9: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped {} // MARK: - 10 types extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -132,7 +132,7 @@ extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource J: EncodablePolyWrapped {} extension Poly10: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped, J: CodablePolyWrapped {} // MARK: - 11 types extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -151,14 +151,14 @@ extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource where - A: PolyWrapped, - B: PolyWrapped, - C: PolyWrapped, - D: PolyWrapped, - E: PolyWrapped, - F: PolyWrapped, - G: PolyWrapped, - H: PolyWrapped, - I: PolyWrapped, - J: PolyWrapped, - K: PolyWrapped {} + A: CodablePolyWrapped, + B: CodablePolyWrapped, + C: CodablePolyWrapped, + D: CodablePolyWrapped, + E: CodablePolyWrapped, + F: CodablePolyWrapped, + G: CodablePolyWrapped, + H: CodablePolyWrapped, + I: CodablePolyWrapped, + J: CodablePolyWrapped, + K: CodablePolyWrapped {} From 7fabe2574e044aad0781f48d6dc4e5867f9b8382 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 Nov 2019 07:56:58 -0800 Subject: [PATCH 15/23] rename some of the new public protocols in JSONAPITesting --- .../Comparisons/ArrayCompare.swift | 2 +- .../Comparisons/AttributesCompare.swift | 4 +- .../Comparisons/Comparison.swift | 8 ++-- .../Comparisons/DocumentCompare.swift | 20 ++++---- .../Comparisons/DocumentDataCompare.swift | 48 +++++++++---------- .../Comparisons/IncludesCompare.swift | 2 +- .../Comparisons/RelationshipsCompare.swift | 4 +- .../Comparisons/ResourceObjectCompare.swift | 16 +++---- 8 files changed, 52 insertions(+), 52 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift index d14ced7..a1f7e5c 100644 --- a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift @@ -14,7 +14,7 @@ public enum ArrayElementComparison: Equatable, CustomStringConvertible { case differentValues(String, String) case prebuilt(String) - public init(sameTypeComparison: Comparison) { + public init(sameTypeComparison: BasicComparison) { switch sameTypeComparison { case .same: self = .same diff --git a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift index 541c52e..147432c 100644 --- a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift @@ -8,11 +8,11 @@ import JSONAPI extension Attributes { - public func compare(to other: Self) -> [String: Comparison] { + public func compare(to other: Self) -> [String: BasicComparison] { let mirror1 = Mirror(reflecting: self) let mirror2 = Mirror(reflecting: other) - var comparisons = [String: Comparison]() + var comparisons = [String: BasicComparison]() for child in mirror1.children { guard let childLabel = child.label else { continue } diff --git a/Sources/JSONAPITesting/Comparisons/Comparison.swift b/Sources/JSONAPITesting/Comparisons/Comparison.swift index e55af29..c0f3240 100644 --- a/Sources/JSONAPITesting/Comparisons/Comparison.swift +++ b/Sources/JSONAPITesting/Comparisons/Comparison.swift @@ -5,13 +5,13 @@ // Created by Mathew Polzin on 11/3/19. // -public protocol Comparable: CustomStringConvertible { +public protocol Comparison: CustomStringConvertible { var rawValue: String { get } var isSame: Bool { get } } -public enum Comparison: Comparable, Equatable { +public enum BasicComparison: Comparison, Equatable { case same case different(String, String) case prebuilt(String) @@ -56,11 +56,11 @@ public enum Comparison: Comparable, Equatable { public typealias NamedDifferences = [String: String] -public protocol PropertyComparable: Comparable { +public protocol PropertyComparison: Comparison { var differences: NamedDifferences { get } } -extension PropertyComparable { +extension PropertyComparison { public var description: String { return differences .map { "(\($0): \($1))" } diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 2ffc37e..d56c84d 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -7,11 +7,11 @@ import JSONAPI -public struct DocumentComparison: Equatable, PropertyComparable { - public let apiDescription: Comparison +public struct DocumentComparison: Equatable, PropertyComparison { + public let apiDescription: BasicComparison public let body: BodyComparison - init(apiDescription: Comparison, body: BodyComparison) { + init(apiDescription: BasicComparison, body: BodyComparison) { self.apiDescription = apiDescription self.body = body } @@ -33,7 +33,7 @@ public enum BodyComparison: Equatable, CustomStringConvertible { case differentErrors(ErrorComparison) case differentData(DocumentDataComparison) - public typealias ErrorComparison = [Comparison] + public typealias ErrorComparison = [BasicComparison] static func compare(errors errors1: [E], _ meta1: M?, _ links1: L?, with errors2: [E], _ meta2: M?, _ links2: L?) -> ErrorComparison { return errors1.compare( @@ -48,9 +48,9 @@ public enum BodyComparison: Equatable, CustomStringConvertible { String(describing: error2) ) } - ).map(Comparison.init) + [ - Comparison(meta1, meta2), - Comparison(links1, links2) + ).map(BasicComparison.init) + [ + BasicComparison(meta1, meta2), + BasicComparison(links1, links2) ] } @@ -79,10 +79,10 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _OptionalResourceBody { +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: TestableResourceBody { public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( - apiDescription: Comparison( + apiDescription: BasicComparison( String(describing: apiDescription), String(describing: other.apiDescription) ), @@ -91,7 +91,7 @@ extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ } } -extension DocumentBody where Self: Equatable, PrimaryResourceBody: _OptionalResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: TestableResourceBody { public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index 607bfa7..da7f27e 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -7,13 +7,13 @@ import JSONAPI -public struct DocumentDataComparison: Equatable, PropertyComparable { +public struct DocumentDataComparison: Equatable, PropertyComparison { public let primary: PrimaryResourceBodyComparison public let includes: IncludesComparison - public let meta: Comparison - public let links: Comparison + public let meta: BasicComparison + public let links: BasicComparison - init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: Comparison, links: Comparison) { + init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: BasicComparison, links: BasicComparison) { self.primary = primary self.includes = includes self.meta = meta @@ -33,20 +33,20 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { +extension DocumentBodyData where PrimaryResourceBody: TestableResourceBody { public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), - meta: Comparison(meta, other.meta), - links: Comparison(links, other.links) + meta: BasicComparison(meta, other.meta), + links: BasicComparison(links, other.links) ) } } public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { case oneOrMore(ManyResourceObjectComparison) - case optionalSingle(Comparison) + case optionalSingle(BasicComparison) public var isSame: Bool { switch self { @@ -69,7 +69,7 @@ public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { return description } } -public struct ManyResourceObjectComparison: Equatable, PropertyComparable { +public struct ManyResourceObjectComparison: Equatable, PropertyComparison { public let comparisons: [ArrayElementComparison] public init(_ comparisons: [ArrayElementComparison]) { @@ -86,16 +86,16 @@ public struct ManyResourceObjectComparison: Equatable, PropertyComparable { } } -extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObjectType { +extension TestableResourceBody where TestablePrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - guard let one = optionalResourceObject, - let two = other.optionalResourceObject else { + guard let one = testableResourceObject, + let two = other.testableResourceObject else { func nilOrName(_ resObj: [T]?) -> String { resObj.map { _ in String(describing: T.self) } ?? "nil" } - return .optionalSingle(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) + return .optionalSingle(BasicComparison(nilOrName(testableResourceObject), nilOrName(other.testableResourceObject))) } return .oneOrMore(.init(one.compare(to: two, using: { r1, r2 in @@ -120,18 +120,18 @@ extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObject } } -public protocol _OptionalResourceBody { - associatedtype WrappedPrimaryResourceType: ResourceObjectType - var optionalResourceObject: [WrappedPrimaryResourceType]? { get } +public protocol TestableResourceBody { + associatedtype TestablePrimaryResourceType: ResourceObjectType + var testableResourceObject: [TestablePrimaryResourceType]? { get } } -public protocol _OptionalResourceObjectType { +public protocol OptionalResourceObjectType { associatedtype Wrapped: ResourceObjectType var maybeValue: Wrapped? { get } } -extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { +extension Optional: OptionalResourceObjectType where Wrapped: ResourceObjectType { public var maybeValue: Wrapped? { switch self { case .none: @@ -142,16 +142,16 @@ extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectTyp } } -extension ResourceObject: _OptionalResourceObjectType { +extension ResourceObject: OptionalResourceObjectType { public var maybeValue: Self? { self } } -extension ManyResourceBody: _OptionalResourceBody where PrimaryResource: ResourceObjectType { - public var optionalResourceObject: [PrimaryResource]? { values } +extension ManyResourceBody: TestableResourceBody where PrimaryResource: ResourceObjectType { + public var testableResourceObject: [PrimaryResource]? { values } } -extension SingleResourceBody: _OptionalResourceBody where PrimaryResource: _OptionalResourceObjectType { - public typealias WrappedPrimaryResourceType = PrimaryResource.Wrapped +extension SingleResourceBody: TestableResourceBody where PrimaryResource: OptionalResourceObjectType { + public typealias TestablePrimaryResourceType = PrimaryResource.Wrapped - public var optionalResourceObject: [WrappedPrimaryResourceType]? { value.maybeValue.map { [$0] } } + public var testableResourceObject: [TestablePrimaryResourceType]? { value.maybeValue.map { [$0] } } } diff --git a/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift index c638628..13b293d 100644 --- a/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift @@ -8,7 +8,7 @@ import JSONAPI import Poly -public struct IncludesComparison: Equatable, PropertyComparable { +public struct IncludesComparison: Equatable, PropertyComparison { public let comparisons: [ArrayElementComparison] public init(_ comparisons: [ArrayElementComparison]) { diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift index c217cb9..3bfa7d6 100644 --- a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -8,11 +8,11 @@ import JSONAPI extension Relationships { - public func compare(to other: Self) -> [String: Comparison] { + public func compare(to other: Self) -> [String: BasicComparison] { let mirror1 = Mirror(reflecting: self) let mirror2 = Mirror(reflecting: other) - var comparisons = [String: Comparison]() + var comparisons = [String: BasicComparison]() for child in mirror1.children { guard let childLabel = child.label else { continue } diff --git a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift index 2619008..ddfeb49 100644 --- a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift @@ -7,21 +7,21 @@ import JSONAPI -public struct ResourceObjectComparison: Equatable, PropertyComparable { - public typealias ComparisonHash = [String: Comparison] +public struct ResourceObjectComparison: Equatable, PropertyComparison { + public typealias ComparisonHash = [String: BasicComparison] - public let id: Comparison + public let id: BasicComparison public let attributes: ComparisonHash public let relationships: ComparisonHash - public let meta: Comparison - public let links: Comparison + public let meta: BasicComparison + public let links: BasicComparison public init(_ one: T, _ two: T) { - id = Comparison(one.id.rawValue, two.id.rawValue) + id = BasicComparison(one.id.rawValue, two.id.rawValue) attributes = one.attributes.compare(to: two.attributes) relationships = one.relationships.compare(to: two.relationships) - meta = Comparison(one.meta, two.meta) - links = Comparison(one.links, two.links) + meta = BasicComparison(one.meta, two.meta) + links = BasicComparison(one.links, two.links) } public var differences: NamedDifferences { From 86344ef93f16bc21725de8b301991994dabc9869 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 Nov 2019 08:08:45 -0800 Subject: [PATCH 16/23] trivial refactor in sparse fieldset file --- Sources/JSONAPI/SparseFields/SparseFieldset.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index b326f13..f7fca89 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -36,15 +36,12 @@ public struct SparseFieldset< public extension ResourceObject where Description.Attributes: SparsableAttributes { + /// The `SparseFieldset` type for this `ResourceObject` + typealias SparseType = SparseFieldset + /// Get a Sparse Fieldset of this `ResourceObject` that can be encoded /// as a `SparsePrimaryResource`. - func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseFieldset { + func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseType { return SparseFieldset(self, fields: fields) } } - -public extension ResourceObject where Description.Attributes: SparsableAttributes { - - /// The `SparseFieldset` type for this `ResourceObject` - typealias SparseType = SparseFieldset -} From 11ef050d58afb91098c1fb486af28f744963564d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 8 Nov 2019 18:47:28 -0800 Subject: [PATCH 17/23] most common relationship errors tested. --- Sources/JSONAPI/Document/Document.swift | 4 +- Sources/JSONAPI/Document/Includes.swift | 2 +- Sources/JSONAPI/EncodingError.swift | 10 +- Sources/JSONAPI/Resource/Attribute.swift | 10 +- .../Resource/Poly+PrimaryResource.swift | 4 +- Sources/JSONAPI/Resource/Relationship.swift | 16 ++- .../Resource Object/ResourceObject.swift | 105 +++++++++++++++- .../ResourceObjectDecodingErrorTests.swift | 114 ++++++++++++++++++ .../stubs/ResourceObjectStubs.swift | 53 ++++++++ .../Test Helpers/EncodeDecode.swift | 4 +- 10 files changed, 300 insertions(+), 22 deletions(-) create mode 100644 Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index e1d1e69..ff28502 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -397,10 +397,10 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: // TODO come back to this and make robust guard let metaVal = meta else { - throw JSONAPIEncodingError.missingOrMalformedMetadata + throw JSONAPIEncodingError.missingOrMalformedMetadata(path: decoder.codingPath) } guard let linksVal = links else { - throw JSONAPIEncodingError.missingOrMalformedLinks + throw JSONAPIEncodingError.missingOrMalformedLinks(path: decoder.codingPath) } body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 5c0ab3f..b8ff4b2 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -32,7 +32,7 @@ public struct Includes: Encodable, Equatable { var container = encoder.unkeyedContainer() guard I.self != NoIncludes.self else { - throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") + throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath) } for value in values { diff --git a/Sources/JSONAPI/EncodingError.swift b/Sources/JSONAPI/EncodingError.swift index 8b461cd..d2423be 100644 --- a/Sources/JSONAPI/EncodingError.swift +++ b/Sources/JSONAPI/EncodingError.swift @@ -6,9 +6,9 @@ // public enum JSONAPIEncodingError: Swift.Error { - case typeMismatch(expected: String, found: String) - case illegalEncoding(String) - case illegalDecoding(String) - case missingOrMalformedMetadata - case missingOrMalformedLinks + case typeMismatch(expected: String, found: String, path: [CodingKey]) + case illegalEncoding(String, path: [CodingKey]) + case illegalDecoding(String, path: [CodingKey]) + case missingOrMalformedMetadata(path: [CodingKey]) + case missingOrMalformedLinks(path: [CodingKey]) } diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index f73d672..5b0ed41 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -5,13 +5,21 @@ // Created by Mathew Polzin on 11/13/18. // -public protocol AttributeType: Codable { +public protocol AbstractAttributeType { + var rawValueType: Any.Type { get } +} + +public protocol AttributeType: Codable, AbstractAttributeType { associatedtype RawValue: Codable associatedtype ValueType var value: ValueType { get } } +extension AttributeType { + public var rawValueType: Any.Type { return RawValue.self } +} + // MARK: TransformedAttribute /// A TransformedAttribute takes a Codable type and attempts to turn it into another type. diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 7f1fa1c..92719b5 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -22,11 +22,11 @@ public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { - throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") + throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath) } public func encode(to encoder: Encoder) throws { - throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.") + throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath) } } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 69fabd9..d7202bc 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -170,8 +170,16 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { // succeeds and then attempt to coerce nil to a Identifier // type at which point we can store nil in `id`. let anyNil: Any? = nil - if try container.decodeNil(forKey: .data), - let val = anyNil as? Identifiable.Identifier { + if try container.decodeNil(forKey: .data) { + guard let val = anyNil as? Identifiable.Identifier else { + throw DecodingError.valueNotFound( + Self.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected non-null relationship data." + ) + ) + } id = val return } @@ -181,7 +189,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Identifiable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type) + throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath) } id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) @@ -239,7 +247,7 @@ extension ToManyRelationship: Codable { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Relatable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type) + throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) } newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a746e6e..b336655 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -414,21 +414,114 @@ public extension ResourceObject { let type = try container.decode(String.self, forKey: .type) guard ResourceObject.jsonType == type else { - throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type) + throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) } let maybeUnidentified = Unidentified() as? EntityRawIdType id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) - attributes = try (NoAttributes() as? Description.Attributes) ?? - container.decode(Description.Attributes.self, forKey: .attributes) + do { + attributes = try (NoAttributes() as? Description.Attributes) ?? + container.decode(Description.Attributes.self, forKey: .attributes) + } catch let decodingError as DecodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } catch let decodingError as JSONAPIEncodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } - relationships = try (NoRelationships() as? Description.Relationships) - ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) - ?? Description.Relationships(from: EmptyObjectDecoder()) + do { + relationships = try (NoRelationships() as? Description.Relationships) + ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) + ?? Description.Relationships(from: EmptyObjectDecoder()) + } catch let decodingError as DecodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } catch let decodingError as JSONAPIEncodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } catch _ as EmptyObjectDecodingError { + throw ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .relationships + ) + } meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) } } + +public struct ResourceObjectDecodingError: Swift.Error, Equatable { + public let subjectName: String + public let cause: Cause + public let location: Location + + static let entireObject = "entire object" + + public enum Cause: Equatable { + case keyNotFound + case valueNotFound + case typeMismatch(expectedTypeName: String) + case jsonTypeMismatch(expectedType: String, foundType: String) + } + + public enum Location: Equatable { + case attributes + case relationships + } + + init?(_ decodingError: DecodingError) { + switch decodingError { + case .typeMismatch(let expectedType, let ctx): + (location, subjectName) = Self.context(ctx) + let typeString: String + if let attrType = expectedType as? AbstractAttributeType { + typeString = String(describing: attrType.rawValueType) + } else { + typeString = String(describing: expectedType) + } + cause = .typeMismatch(expectedTypeName: typeString) + case .valueNotFound(_, let ctx): + (location, subjectName) = Self.context(ctx) + cause = .valueNotFound + case .keyNotFound(let missingKey, let ctx): + (location, _) = Self.context(ctx) + subjectName = missingKey.stringValue + cause = .keyNotFound + default: + return nil + } + } + + init?(_ jsonAPIError: JSONAPIEncodingError) { + switch jsonAPIError { + case .typeMismatch(expected: let expected, found: let found, path: let path): + (location, subjectName) = Self.context(path: path) + cause = .jsonTypeMismatch(expectedType: expected, foundType: found) + default: + return nil + } + } + + init(subjectName: String, cause: Cause, location: Location) { + self.subjectName = subjectName + self.cause = cause + self.location = location + } + + static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) { + + return context(path: decodingContext.codingPath) + } + + static func context(path: [CodingKey]) -> (Location, name: String) { + return ( + path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, + name: path.last?.stringValue ?? "unnamed" + ) + } +} diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift new file mode 100644 index 0000000..a0d28b0 --- /dev/null +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -0,0 +1,114 @@ +// +// ResourceObjectDecodingErrorTests.swift +// +// +// Created by Mathew Polzin on 11/8/19. +// + +import XCTest +@testable import JSONAPI + +// MARK: - Relationships +final class ResourceObjectDecodingErrorTests: XCTestCase { + func test_missingRelationshipsObject() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_relationships_entirely_missing + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .relationships + ) + ) + } + } + + func test_required_relationship() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_required_relationship_is_omitted + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .keyNotFound, + location: .relationships + ) + ) + } + } + + func test_NonNullable_relationship() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_nonNullable_relationship_is_null + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .relationships + ) + ) + } + } + + func test_NonNullable_relationship2() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_nonNullable_relationship_is_null2 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .relationships + ) + ) + } + } + + func test_oneTypeVsAnother_relationship() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_relationship_is_wrong_type + )) { error in + print(error) + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .jsonTypeMismatch(expectedType: "thirteenth_test_entities", foundType: "not_the_same"), + location: .relationships + ) + ) + } + } +} + +// MARK: - Attributes +extension ResourceObjectDecodingErrorTests { + // TODO: write tests +} + +// MARK: - Test Types +extension ResourceObjectDecodingErrorTests { + enum TestEntityType: ResourceObjectDescription { + public static var jsonType: String { return "thirteenth_test_entities" } + + typealias Attributes = NoAttributes + + public struct Relationships: JSONAPI.Relationships { + + let required: ToOneRelationship + } + } + + typealias TestEntity = BasicEntity +} diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 58343ce..06cf6cd 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -393,6 +393,59 @@ let entity_all_relationships_optional_and_omitted = """ } """.data(using: .utf8)! +let entity_nonNullable_relationship_is_null = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": null + } +} +""".data(using: .utf8)! + +let entity_nonNullable_relationship_is_null2 = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": null + } + } +} +""".data(using: .utf8)! + +let entity_required_relationship_is_omitted = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + } +} +""".data(using: .utf8)! + +let entity_relationship_is_wrong_type = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": { + "id": "123", + "type": "not_the_same" + } + } + } +} +""".data(using: .utf8)! + +let entity_relationships_entirely_missing = """ +{ + "id": "1", + "type": "thirteenth_test_entities", +} +""".data(using: .utf8)! + let entity_unidentified = """ { "type": "unidentified_test_entities", diff --git a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift index a2eeb56..d0b1c56 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift @@ -8,8 +8,10 @@ import Foundation import XCTest +let testDecoder = JSONDecoder() + func decoded(type: T.Type, data: Data) -> T { - return try! JSONDecoder().decode(T.self, from: data) + return try! testDecoder.decode(T.self, from: data) } func encoded(value: T) -> Data { From 0b4baf35d52df475d0c854f02988edc45ec5ab56 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 9 Nov 2019 00:33:42 -0800 Subject: [PATCH 18/23] got some attribute cases added and tested. added some descriptions (custom string convertible) --- Sources/JSONAPI/Document/Document.swift | 4 +- Sources/JSONAPI/Document/Includes.swift | 2 +- ...ngError.swift => JSONAPICodingError.swift} | 4 +- .../Resource/Poly+PrimaryResource.swift | 4 +- Sources/JSONAPI/Resource/Relationship.swift | 4 +- .../Resource Object/ResourceObject.swift | 54 ++++-- .../ResourceObjectDecodingErrorTests.swift | 170 +++++++++++++++++- .../stubs/ResourceObjectStubs.swift | 58 ++++++ 8 files changed, 275 insertions(+), 25 deletions(-) rename Sources/JSONAPI/{EncodingError.swift => JSONAPICodingError.swift} (82%) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index ff28502..7c387be 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -397,10 +397,10 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: // TODO come back to this and make robust guard let metaVal = meta else { - throw JSONAPIEncodingError.missingOrMalformedMetadata(path: decoder.codingPath) + throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath) } guard let linksVal = links else { - throw JSONAPIEncodingError.missingOrMalformedLinks(path: decoder.codingPath) + throw JSONAPICodingError.missingOrMalformedLinks(path: decoder.codingPath) } body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index b8ff4b2..e7a701b 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -32,7 +32,7 @@ public struct Includes: Encodable, Equatable { var container = encoder.unkeyedContainer() guard I.self != NoIncludes.self else { - throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath) + throw JSONAPICodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath) } for value in values { diff --git a/Sources/JSONAPI/EncodingError.swift b/Sources/JSONAPI/JSONAPICodingError.swift similarity index 82% rename from Sources/JSONAPI/EncodingError.swift rename to Sources/JSONAPI/JSONAPICodingError.swift index d2423be..281f1de 100644 --- a/Sources/JSONAPI/EncodingError.swift +++ b/Sources/JSONAPI/JSONAPICodingError.swift @@ -1,11 +1,11 @@ // -// EncodingError.swift +// JSONAPICodingError.swift // JSONAPI // // Created by Mathew Polzin on 12/7/18. // -public enum JSONAPIEncodingError: Swift.Error { +public enum JSONAPICodingError: Swift.Error { case typeMismatch(expected: String, found: String, path: [CodingKey]) case illegalEncoding(String, path: [CodingKey]) case illegalDecoding(String, path: [CodingKey]) diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 92719b5..73c1246 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -22,11 +22,11 @@ public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { - throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath) + throw JSONAPICodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath) } public func encode(to encoder: Encoder) throws { - throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath) + throw JSONAPICodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath) } } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index d7202bc..92b43f2 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -189,7 +189,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Identifiable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath) + throw JSONAPICodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath) } id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) @@ -247,7 +247,7 @@ extension ToManyRelationship: Codable { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Relatable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) + throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) } newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index b336655..6305ead 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -414,21 +414,25 @@ public extension ResourceObject { let type = try container.decode(String.self, forKey: .type) guard ResourceObject.jsonType == type else { - throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) + throw JSONAPICodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) } let maybeUnidentified = Unidentified() as? EntityRawIdType id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) do { - attributes = try (NoAttributes() as? Description.Attributes) ?? - container.decode(Description.Attributes.self, forKey: .attributes) + attributes = try (NoAttributes() as? Description.Attributes) + ?? container.decodeIfPresent(Description.Attributes.self, forKey: .attributes) + ?? Description.Attributes(from: EmptyObjectDecoder()) } catch let decodingError as DecodingError { throw ResourceObjectDecodingError(decodingError) ?? decodingError - } catch let decodingError as JSONAPIEncodingError { - throw ResourceObjectDecodingError(decodingError) - ?? decodingError + } catch _ as EmptyObjectDecodingError { + throw ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .attributes + ) } do { @@ -438,7 +442,7 @@ public extension ResourceObject { } catch let decodingError as DecodingError { throw ResourceObjectDecodingError(decodingError) ?? decodingError - } catch let decodingError as JSONAPIEncodingError { + } catch let decodingError as JSONAPICodingError { throw ResourceObjectDecodingError(decodingError) ?? decodingError } catch _ as EmptyObjectDecodingError { @@ -469,21 +473,23 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { case jsonTypeMismatch(expectedType: String, foundType: String) } - public enum Location: Equatable { + public enum Location: String, Equatable { case attributes case relationships + + var singular: String { + switch self { + case .attributes: return "attribute" + case .relationships: return "relationship" + } + } } init?(_ decodingError: DecodingError) { switch decodingError { case .typeMismatch(let expectedType, let ctx): (location, subjectName) = Self.context(ctx) - let typeString: String - if let attrType = expectedType as? AbstractAttributeType { - typeString = String(describing: attrType.rawValueType) - } else { - typeString = String(describing: expectedType) - } + let typeString = String(describing: expectedType) cause = .typeMismatch(expectedTypeName: typeString) case .valueNotFound(_, let ctx): (location, subjectName) = Self.context(ctx) @@ -497,7 +503,7 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } } - init?(_ jsonAPIError: JSONAPIEncodingError) { + init?(_ jsonAPIError: JSONAPICodingError) { switch jsonAPIError { case .typeMismatch(expected: let expected, found: let found, path: let path): (location, subjectName) = Self.context(path: path) @@ -525,3 +531,21 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { ) } } + +extension ResourceObjectDecodingError: CustomStringConvertible { + public var description: String { + switch cause { + case .keyNotFound: + if subjectName == ResourceObjectDecodingError.entireObject { + return "\(location) object is required and missing." + } + return "'\(subjectName)' \(location.singular) is required and missing." + case .valueNotFound: + return "'\(subjectName)' \(location.singular) is not nullable but null." + case .typeMismatch(expectedTypeName: let expected): + return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." + case .jsonTypeMismatch(expectedType: let expected, foundType: let found): + return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" + } + } +} diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index a0d28b0..3dbb971 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -23,6 +23,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "relationships object is required and missing." + ) } } @@ -39,6 +44,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship is required and missing." + ) } } @@ -55,6 +65,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship is not nullable but null." + ) } } @@ -71,6 +86,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship is not nullable but null." + ) } } @@ -88,13 +108,146 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'required' relationship is of JSON:API type "not_the_same" but it was expected to be "thirteenth_test_entities""# + ) } } + + func test_twoOneVsToMany_relationship() { + // TODO: write test + } } // MARK: - Attributes extension ResourceObjectDecodingErrorTests { - // TODO: write tests + func test_missingAttributesObject() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attributes_entirely_missing + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "attributes object is required and missing." + ) + } + } + + func test_required_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_required_attribute_is_omitted + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .keyNotFound, + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' attribute is required and missing." + ) + } + } + + func test_NonNullable_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_nonNullable_attribute_is_null + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' attribute is not nullable but null." + ) + } + } + + func test_oneTypeVsAnother_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .typeMismatch(expectedTypeName: String(describing: String.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' attribute is not a String as expected." + ) + } + } + + func test_oneTypeVsAnother_attribute2() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type2 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "other", + cause: .typeMismatch(expectedTypeName: String(describing: Int.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'other' attribute is not a Int as expected." + ) + } + } + + func test_oneTypeVsAnother_attribute3() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type3 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "yetAnother", + cause: .typeMismatch(expectedTypeName: String(describing: Bool.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'yetAnother' attribute is not a Bool as expected." + ) + } + } } // MARK: - Test Types @@ -111,4 +264,19 @@ extension ResourceObjectDecodingErrorTests { } typealias TestEntity = BasicEntity + + enum TestEntityType2: ResourceObjectDescription { + public static var jsonType: String { return "thirteenth_test_entities" } + + public struct Attributes: JSONAPI.Attributes { + + let required: Attribute + let other: Attribute? + let yetAnother: Attribute? + } + + typealias Relationships = NoRelationships + } + + typealias TestEntity2 = BasicEntity } diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 06cf6cd..6817bff 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -446,6 +446,64 @@ let entity_relationships_entirely_missing = """ } """.data(using: .utf8)! +let entity_required_attribute_is_omitted = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + } +} +""".data(using: .utf8)! + +let entity_nonNullable_attribute_is_null = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": null + } +} +""".data(using: .utf8)! + +let entity_attribute_is_wrong_type = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": 10 + } +} +""".data(using: .utf8)! + +let entity_attribute_is_wrong_type2 = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": "hello", + "other": "world" + } +} +""".data(using: .utf8)! + +let entity_attribute_is_wrong_type3 = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": "hello", + "yetAnother": 101 + } +} +""".data(using: .utf8)! + +let entity_attributes_entirely_missing = """ +{ + "id": "1", + "type": "thirteenth_test_entities" +} +""".data(using: .utf8)! + let entity_unidentified = """ { "type": "unidentified_test_entities", From 455ff64326c4d8be79e5b8ac5a7318c0450cee16 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 9 Nov 2019 00:34:39 -0800 Subject: [PATCH 19/23] update linuxmain --- Tests/JSONAPITests/XCTestManifests.swift | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index cd6323d..0b2c004 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -37,8 +37,11 @@ extension Attribute_FunctorTests { // to regenerate. static let __allTests__Attribute_FunctorTests = [ ("test_mapGuaranteed", test_mapGuaranteed), + ("test_mapGuaranteed_deprecated", test_mapGuaranteed_deprecated), ("test_mapOptionalFailure", test_mapOptionalFailure), + ("test_mapOptionalFailure_deprecated", test_mapOptionalFailure_deprecated), ("test_mapOptionalSuccess", test_mapOptionalSuccess), + ("test_mapOptionalSuccess_deprecated", test_mapOptionalSuccess_deprecated), ] } @@ -59,9 +62,11 @@ extension ComputedPropertiesTests { // to regenerate. static let __allTests__ComputedPropertiesTests = [ ("test_ComputedAttributeAccess", test_ComputedAttributeAccess), + ("test_ComputedAttributeAccess_deprecated", test_ComputedAttributeAccess_deprecated), ("test_ComputedNonAttributeAccess", test_ComputedNonAttributeAccess), ("test_ComputedRelationshipAccess", test_ComputedRelationshipAccess), ("test_DecodeIgnoresComputed", test_DecodeIgnoresComputed), + ("test_DecodeIgnoresComputed_deprecated", test_DecodeIgnoresComputed_deprecated), ("test_EncodeIgnoresComputed", test_EncodeIgnoresComputed), ] } @@ -72,8 +77,10 @@ extension CustomAttributesTests { // to regenerate. static let __allTests__CustomAttributesTests = [ ("test_customDecode", test_customDecode), + ("test_customDecode_deprecated", test_customDecode_deprecated), ("test_customEncode", test_customEncode), ("test_customKeysDecode", test_customKeysDecode), + ("test_customKeysDecode_deprecated", test_customKeysDecode_deprecated), ("test_customKeysEncode", test_customKeysEncode), ] } @@ -301,7 +308,9 @@ extension PolyProxyTests { ("test_generalReasonableness", test_generalReasonableness), ("test_UserAAndBEncodeEquality", test_UserAAndBEncodeEquality), ("test_UserADecode", test_UserADecode), + ("test_UserADecode_deprecated", test_UserADecode_deprecated), ("test_UserBDecode", test_UserBDecode), + ("test_UserBDecode_deprecated", test_UserBDecode_deprecated), ] } @@ -353,6 +362,26 @@ extension ResourceBodyTests { ] } +extension ResourceObjectDecodingErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ResourceObjectDecodingErrorTests = [ + ("test_missingAttributesObject", test_missingAttributesObject), + ("test_missingRelationshipsObject", test_missingRelationshipsObject), + ("test_NonNullable_attribute", test_NonNullable_attribute), + ("test_NonNullable_relationship", test_NonNullable_relationship), + ("test_NonNullable_relationship2", test_NonNullable_relationship2), + ("test_oneTypeVsAnother_attribute", test_oneTypeVsAnother_attribute), + ("test_oneTypeVsAnother_attribute2", test_oneTypeVsAnother_attribute2), + ("test_oneTypeVsAnother_attribute3", test_oneTypeVsAnother_attribute3), + ("test_oneTypeVsAnother_relationship", test_oneTypeVsAnother_relationship), + ("test_required_attribute", test_required_attribute), + ("test_required_relationship", test_required_relationship), + ("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship), + ] +} + extension ResourceObjectReplacingTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -378,37 +407,49 @@ extension ResourceObjectTests { ("test_copyIdentifiedByValue", test_copyIdentifiedByValue), ("test_copyWithNewId", test_copyWithNewId), ("test_entityAllAttribute", test_entityAllAttribute), + ("test_entityAllAttribute_deprecated", test_entityAllAttribute_deprecated), ("test_entityAllAttribute_encode", test_entityAllAttribute_encode), ("test_entityBrokenNullableOmittedAttribute", test_entityBrokenNullableOmittedAttribute), ("test_EntityNoRelationshipsNoAttributes", test_EntityNoRelationshipsNoAttributes), ("test_EntityNoRelationshipsNoAttributes_encode", test_EntityNoRelationshipsNoAttributes_encode), ("test_EntityNoRelationshipsSomeAttributes", test_EntityNoRelationshipsSomeAttributes), + ("test_EntityNoRelationshipsSomeAttributes_deprecated", test_EntityNoRelationshipsSomeAttributes_deprecated), ("test_EntityNoRelationshipsSomeAttributes_encode", test_EntityNoRelationshipsSomeAttributes_encode), ("test_entityOneNullAndOneOmittedAttribute", test_entityOneNullAndOneOmittedAttribute), + ("test_entityOneNullAndOneOmittedAttribute_deprecated", test_entityOneNullAndOneOmittedAttribute_deprecated), ("test_entityOneNullAndOneOmittedAttribute_encode", test_entityOneNullAndOneOmittedAttribute_encode), ("test_entityOneNullAttribute", test_entityOneNullAttribute), + ("test_entityOneNullAttribute_deprecated", test_entityOneNullAttribute_deprecated), ("test_entityOneNullAttribute_encode", test_entityOneNullAttribute_encode), ("test_entityOneOmittedAttribute", test_entityOneOmittedAttribute), + ("test_entityOneOmittedAttribute_deprecated", test_entityOneOmittedAttribute_deprecated), ("test_entityOneOmittedAttribute_encode", test_entityOneOmittedAttribute_encode), ("test_EntitySomeRelationshipsNoAttributes", test_EntitySomeRelationshipsNoAttributes), ("test_EntitySomeRelationshipsNoAttributes_encode", test_EntitySomeRelationshipsNoAttributes_encode), ("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes), + ("test_EntitySomeRelationshipsSomeAttributes_deprecated", test_EntitySomeRelationshipsSomeAttributes_deprecated), ("test_EntitySomeRelationshipsSomeAttributes_encode", test_EntitySomeRelationshipsSomeAttributes_encode), ("test_EntitySomeRelationshipsSomeAttributesWithLinks", test_EntitySomeRelationshipsSomeAttributesWithLinks), + ("test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated", test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated), ("test_EntitySomeRelationshipsSomeAttributesWithLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithLinks_encode), ("test_EntitySomeRelationshipsSomeAttributesWithMeta", test_EntitySomeRelationshipsSomeAttributesWithMeta), + ("test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated", test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated), ("test_EntitySomeRelationshipsSomeAttributesWithMeta_encode", test_EntitySomeRelationshipsSomeAttributesWithMeta_encode), ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks), + ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated), ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode), ("test_initialization", test_initialization), ("test_IntOver10_encode", test_IntOver10_encode), ("test_IntOver10_failure", test_IntOver10_failure), ("test_IntOver10_success", test_IntOver10_success), ("test_IntToString", test_IntToString), + ("test_IntToString_deprecated", test_IntToString_deprecated), ("test_IntToString_encode", test_IntToString_encode), ("test_MetaEntityAttributeAccessWorks", test_MetaEntityAttributeAccessWorks), + ("test_MetaEntityAttributeAccessWorks_deprecated", test_MetaEntityAttributeAccessWorks_deprecated), ("test_MetaEntityRelationshipAccessWorks", test_MetaEntityRelationshipAccessWorks), ("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute), + ("test_NonNullOptionalNullableAttribute_deprecated", test_NonNullOptionalNullableAttribute_deprecated), ("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode), ("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull), ("test_nullableRelationshipIsNull_encode", test_nullableRelationshipIsNull_encode), @@ -417,6 +458,7 @@ extension ResourceObjectTests { ("test_nullableRelationshipNotNullOrOmitted", test_nullableRelationshipNotNullOrOmitted), ("test_nullableRelationshipNotNullOrOmitted_encode", test_nullableRelationshipNotNullOrOmitted_encode), ("test_NullOptionalNullableAttribute", test_NullOptionalNullableAttribute), + ("test_NullOptionalNullableAttribute_deprecated", test_NullOptionalNullableAttribute_deprecated), ("test_NullOptionalNullableAttribute_encode", test_NullOptionalNullableAttribute_encode), ("test_optional_relationship_operator_access", test_optional_relationship_operator_access), ("test_optionalNullableRelationshipNulled", test_optionalNullableRelationshipNulled), @@ -434,15 +476,21 @@ extension ResourceObjectTests { ("test_toMany_relationship_operator_access", test_toMany_relationship_operator_access), ("test_toManyMetaRelationshipAccessWorks", test_toManyMetaRelationshipAccessWorks), ("test_UnidentifiedEntity", test_UnidentifiedEntity), + ("test_UnidentifiedEntity_deprecated", test_UnidentifiedEntity_deprecated), ("test_UnidentifiedEntity_encode", test_UnidentifiedEntity_encode), ("test_unidentifiedEntityAttributeAccess", test_unidentifiedEntityAttributeAccess), + ("test_unidentifiedEntityAttributeAccess_deprecated", test_unidentifiedEntityAttributeAccess_deprecated), ("test_UnidentifiedEntityWithAttributes", test_UnidentifiedEntityWithAttributes), + ("test_UnidentifiedEntityWithAttributes_deprecated", test_UnidentifiedEntityWithAttributes_deprecated), ("test_UnidentifiedEntityWithAttributes_encode", test_UnidentifiedEntityWithAttributes_encode), ("test_UnidentifiedEntityWithAttributesAndLinks", test_UnidentifiedEntityWithAttributesAndLinks), + ("test_UnidentifiedEntityWithAttributesAndLinks_deprecated", test_UnidentifiedEntityWithAttributesAndLinks_deprecated), ("test_UnidentifiedEntityWithAttributesAndLinks_encode", test_UnidentifiedEntityWithAttributesAndLinks_encode), ("test_UnidentifiedEntityWithAttributesAndMeta", test_UnidentifiedEntityWithAttributesAndMeta), + ("test_UnidentifiedEntityWithAttributesAndMeta_deprecated", test_UnidentifiedEntityWithAttributesAndMeta_deprecated), ("test_UnidentifiedEntityWithAttributesAndMeta_encode", test_UnidentifiedEntityWithAttributesAndMeta_encode), ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks", test_UnidentifiedEntityWithAttributesAndMetaAndLinks), + ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated), ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode), ] } @@ -468,8 +516,11 @@ extension SparseFieldsetTests { // to regenerate. static let __allTests__SparseFieldsetTests = [ ("test_FullEncode", test_FullEncode), + ("test_FullEncode_deprecated", test_FullEncode_deprecated), ("test_PartialEncode", test_PartialEncode), + ("test_PartialEncode_deprecated", test_PartialEncode_deprecated), ("test_sparseFieldsMethod", test_sparseFieldsMethod), + ("test_sparseFieldsMethod_deprecated", test_sparseFieldsMethod_deprecated), ] } @@ -500,6 +551,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(PolyProxyTests.__allTests__PolyProxyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), + testCase(ResourceObjectDecodingErrorTests.__allTests__ResourceObjectDecodingErrorTests), testCase(ResourceObjectReplacingTests.__allTests__ResourceObjectReplacingTests), testCase(ResourceObjectTests.__allTests__ResourceObjectTests), testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), From 4dc30ddc1c579075bd359b841d5f50223d19789e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 10 Nov 2019 20:46:35 -0800 Subject: [PATCH 20/23] Rounding out the Resource Object errors --- Sources/JSONAPI/JSONAPICodingError.swift | 13 ++ Sources/JSONAPI/Resource/Relationship.swift | 30 ++++- .../Resource Object/ResourceObject.swift | 91 -------------- .../ResourceObjectDecodingError.swift | 111 ++++++++++++++++++ .../ResourceObjectDecodingErrorTests.swift | 42 ++++++- .../stubs/ResourceObjectStubs.swift | 48 +++++++- 6 files changed, 234 insertions(+), 101 deletions(-) create mode 100644 Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift diff --git a/Sources/JSONAPI/JSONAPICodingError.swift b/Sources/JSONAPI/JSONAPICodingError.swift index 281f1de..da0705d 100644 --- a/Sources/JSONAPI/JSONAPICodingError.swift +++ b/Sources/JSONAPI/JSONAPICodingError.swift @@ -7,8 +7,21 @@ public enum JSONAPICodingError: Swift.Error { case typeMismatch(expected: String, found: String, path: [CodingKey]) + case quantityMismatch(expected: Quantity, path: [CodingKey]) case illegalEncoding(String, path: [CodingKey]) case illegalDecoding(String, path: [CodingKey]) case missingOrMalformedMetadata(path: [CodingKey]) case missingOrMalformedLinks(path: [CodingKey]) + + public enum Quantity: String, Equatable { + case one + case many + + public var other: Quantity { + switch self { + case .one: return .many + case .many: return .one + } + } + } } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 92b43f2..a11031e 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -184,7 +184,17 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { return } - let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) + let identifier: KeyedDecodingContainer + do { + identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) + } catch let error as DecodingError { + guard case let .typeMismatch(type, context) = error, + type is _DictionaryType.Type else { + throw error + } + throw JSONAPICodingError.quantityMismatch(expected: .one, + path: context.codingPath) + } let type = try identifier.decode(String.self, forKey: .entityType) @@ -238,7 +248,17 @@ extension ToManyRelationship: Codable { links = try container.decode(LinksType.self, forKey: .links) } - var identifiers = try container.nestedUnkeyedContainer(forKey: .data) + var identifiers: UnkeyedDecodingContainer + do { + identifiers = try container.nestedUnkeyedContainer(forKey: .data) + } catch let error as DecodingError { + guard case let .typeMismatch(type, context) = error, + type is _ArrayType.Type else { + throw error + } + throw JSONAPICodingError.quantityMismatch(expected: .many, + path: context.codingPath) + } var newIds = [Relatable.Identifier]() while !identifiers.isAtEnd { @@ -285,3 +305,9 @@ extension ToOneRelationship: CustomStringConvertible { extension ToManyRelationship: CustomStringConvertible { public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } } + +private protocol _DictionaryType {} +extension Dictionary: _DictionaryType {} + +private protocol _ArrayType {} +extension Array: _ArrayType {} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 6305ead..e85d8f0 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -458,94 +458,3 @@ public extension ResourceObject { links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) } } - -public struct ResourceObjectDecodingError: Swift.Error, Equatable { - public let subjectName: String - public let cause: Cause - public let location: Location - - static let entireObject = "entire object" - - public enum Cause: Equatable { - case keyNotFound - case valueNotFound - case typeMismatch(expectedTypeName: String) - case jsonTypeMismatch(expectedType: String, foundType: String) - } - - public enum Location: String, Equatable { - case attributes - case relationships - - var singular: String { - switch self { - case .attributes: return "attribute" - case .relationships: return "relationship" - } - } - } - - init?(_ decodingError: DecodingError) { - switch decodingError { - case .typeMismatch(let expectedType, let ctx): - (location, subjectName) = Self.context(ctx) - let typeString = String(describing: expectedType) - cause = .typeMismatch(expectedTypeName: typeString) - case .valueNotFound(_, let ctx): - (location, subjectName) = Self.context(ctx) - cause = .valueNotFound - case .keyNotFound(let missingKey, let ctx): - (location, _) = Self.context(ctx) - subjectName = missingKey.stringValue - cause = .keyNotFound - default: - return nil - } - } - - init?(_ jsonAPIError: JSONAPICodingError) { - switch jsonAPIError { - case .typeMismatch(expected: let expected, found: let found, path: let path): - (location, subjectName) = Self.context(path: path) - cause = .jsonTypeMismatch(expectedType: expected, foundType: found) - default: - return nil - } - } - - init(subjectName: String, cause: Cause, location: Location) { - self.subjectName = subjectName - self.cause = cause - self.location = location - } - - static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) { - - return context(path: decodingContext.codingPath) - } - - static func context(path: [CodingKey]) -> (Location, name: String) { - return ( - path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, - name: path.last?.stringValue ?? "unnamed" - ) - } -} - -extension ResourceObjectDecodingError: CustomStringConvertible { - public var description: String { - switch cause { - case .keyNotFound: - if subjectName == ResourceObjectDecodingError.entireObject { - return "\(location) object is required and missing." - } - return "'\(subjectName)' \(location.singular) is required and missing." - case .valueNotFound: - return "'\(subjectName)' \(location.singular) is not nullable but null." - case .typeMismatch(expectedTypeName: let expected): - return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." - case .jsonTypeMismatch(expectedType: let expected, foundType: let found): - return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" - } - } -} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift new file mode 100644 index 0000000..d1ac49c --- /dev/null +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -0,0 +1,111 @@ +// +// ResourceObjectDecodingError.swift +// +// +// Created by Mathew Polzin on 11/10/19. +// + +public struct ResourceObjectDecodingError: Swift.Error, Equatable { + public let subjectName: String + public let cause: Cause + public let location: Location + + static let entireObject = "entire object" + + public enum Cause: Equatable { + case keyNotFound + case valueNotFound + case typeMismatch(expectedTypeName: String) + case jsonTypeMismatch(expectedType: String, foundType: String) + case quantityMismatch(expected: JSONAPICodingError.Quantity) + } + + public enum Location: String, Equatable { + case attributes + case relationships + + var singular: String { + switch self { + case .attributes: return "attribute" + case .relationships: return "relationship" + } + } + } + + init?(_ decodingError: DecodingError) { + switch decodingError { + case .typeMismatch(let expectedType, let ctx): + (location, subjectName) = Self.context(ctx) + let typeString = String(describing: expectedType) + cause = .typeMismatch(expectedTypeName: typeString) + case .valueNotFound(_, let ctx): + (location, subjectName) = Self.context(ctx) + cause = .valueNotFound + case .keyNotFound(let missingKey, let ctx): + (location, _) = Self.context(ctx) + subjectName = missingKey.stringValue + cause = .keyNotFound + default: + return nil + } + } + + init?(_ jsonAPIError: JSONAPICodingError) { + switch jsonAPIError { + case .typeMismatch(expected: let expected, found: let found, path: let path): + (location, subjectName) = Self.context(path: path) + cause = .jsonTypeMismatch(expectedType: expected, foundType: found) + case .quantityMismatch(expected: let expected, path: let path): + (location, subjectName) = Self.context(path: path) + cause = .quantityMismatch(expected: expected) + default: + return nil + } + } + + init(subjectName: String, cause: Cause, location: Location) { + self.subjectName = subjectName + self.cause = cause + self.location = location + } + + static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) { + + return context(path: decodingContext.codingPath) + } + + static func context(path: [CodingKey]) -> (Location, name: String) { + return ( + path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, + name: path.last?.stringValue ?? "unnamed" + ) + } +} + +extension ResourceObjectDecodingError: CustomStringConvertible { + public var description: String { + switch cause { + case .keyNotFound: + if subjectName == ResourceObjectDecodingError.entireObject { + return "\(location) object is required and missing." + } + return "'\(subjectName)' \(location.singular) is required and missing." + case .valueNotFound: + return "'\(subjectName)' \(location.singular) is not nullable but null." + case .typeMismatch(expectedTypeName: let expected): + return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." + case .jsonTypeMismatch(expectedType: let expected, foundType: let found): + return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" + case .quantityMismatch(expected: let expected): + let expecation: String = { + switch expected { + case .many: + return "\(expected) values" + case .one: + return "\(expected) value" + } + }() + return "'\(subjectName)' \(location.singular) should contain \(expecation) but found \(expected.other)" + } + } +} diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index 3dbb971..0891d23 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -117,7 +117,44 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { } func test_twoOneVsToMany_relationship() { - // TODO: write test + + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_single_relationship_is_many + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .quantityMismatch(expected: .one), + location: .relationships + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship should contain one value but found many" + ) + } + + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_many_relationship_is_single + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "omittable", + cause: .quantityMismatch(expected: .many), + location: .relationships + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'omittable' relationship should contain many values but found one" + ) + } } } @@ -260,13 +297,14 @@ extension ResourceObjectDecodingErrorTests { public struct Relationships: JSONAPI.Relationships { let required: ToOneRelationship + let omittable: ToManyRelationship? } } typealias TestEntity = BasicEntity enum TestEntityType2: ResourceObjectDescription { - public static var jsonType: String { return "thirteenth_test_entities" } + public static var jsonType: String { return "fourteenth_test_entities" } public struct Attributes: JSONAPI.Attributes { diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 6817bff..9fe7355 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -439,6 +439,42 @@ let entity_relationship_is_wrong_type = """ } """.data(using: .utf8)! +let entity_single_relationship_is_many = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": [{ + "id": "123", + "type": "thirteenth_test_entities" + }] + } + } +} +""".data(using: .utf8)! + +let entity_many_relationship_is_single = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": { + "id": "123", + "type": "thirteenth_test_entities" + } + }, + "omittable": { + "data": { + "id": "456", + "type": "thirteenth_test_entities" + } + } + } +} +""".data(using: .utf8)! + let entity_relationships_entirely_missing = """ { "id": "1", @@ -449,7 +485,7 @@ let entity_relationships_entirely_missing = """ let entity_required_attribute_is_omitted = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { } } @@ -458,7 +494,7 @@ let entity_required_attribute_is_omitted = """ let entity_nonNullable_attribute_is_null = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": null } @@ -468,7 +504,7 @@ let entity_nonNullable_attribute_is_null = """ let entity_attribute_is_wrong_type = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": 10 } @@ -478,7 +514,7 @@ let entity_attribute_is_wrong_type = """ let entity_attribute_is_wrong_type2 = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": "hello", "other": "world" @@ -489,7 +525,7 @@ let entity_attribute_is_wrong_type2 = """ let entity_attribute_is_wrong_type3 = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": "hello", "yetAnother": 101 @@ -500,7 +536,7 @@ let entity_attribute_is_wrong_type3 = """ let entity_attributes_entirely_missing = """ { "id": "1", - "type": "thirteenth_test_entities" + "type": "fourteenth_test_entities" } """.data(using: .utf8)! From 2eecf959953f18d8cd1dffb068a2dc60dbcbed30 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 10 Nov 2019 23:02:26 -0800 Subject: [PATCH 21/23] Adding Document Decoding Errors for some common problems --- Sources/JSONAPI/Document/Document.swift | 26 ++- .../Document/DocumentDecodingError.swift | 68 ++++++- Sources/JSONAPI/Document/Includes.swift | 25 ++- Sources/JSONAPI/Document/ResourceBody.swift | 17 +- Sources/JSONAPI/Resource/Id.swift | 7 +- .../Resource Object/ResourceObject.swift | 10 +- .../ResourceObjectDecodingError.swift | 21 +- .../Document/DocumentDecodingErrorTests.swift | 189 ++++++++++++++++++ .../Document/stubs/DocumentStubs.swift | 85 ++++++++ .../ResourceObjectDecodingErrorTests.swift | 24 +++ .../stubs/ResourceObjectStubs.swift | 11 + Tests/JSONAPITests/XCTestManifests.swift | 15 ++ 12 files changed, 483 insertions(+), 15 deletions(-) create mode 100644 Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 7c387be..34c32a4 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -349,8 +349,8 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootCodingKeys.self) - if let noData = NoAPIDescription() as? APIDescription { - apiDescription = noData + if let noAPIDescription = NoAPIDescription() as? APIDescription { + apiDescription = noAPIDescription } else { apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) } @@ -389,10 +389,24 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: if let noData = NoResourceBody() as? PrimaryResourceBody { data = noData } else { - data = try container.decode(PrimaryResourceBody.self, forKey: .data) + do { + data = try container.decode(PrimaryResourceBody.self, forKey: .data) + } catch let error as ResourceObjectDecodingError { + throw DocumentDecodingError(error) + } catch let error as ManyResourceBodyDecodingError { + throw DocumentDecodingError(error) + } catch let error as DecodingError { + throw DocumentDecodingError(error) + ?? error + } } - let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) + let maybeIncludes: Includes? + do { + maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) + } catch let error as IncludesDecodingError { + throw DocumentDecodingError(error) + } // TODO come back to this and make robust @@ -569,7 +583,7 @@ extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument document = try container.decode(Document.self) guard document.body.isError else { - throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError + throw DocumentDecodingError.foundSuccessDocumentWhenExpectingError } } } @@ -582,7 +596,7 @@ extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument document = try container.decode(Document.self) guard !document.body.isError else { - throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess + throw DocumentDecodingError.foundErrorDocumentWhenExpectingSuccess } } } diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 912a8f8..0e58fe3 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -1,11 +1,75 @@ // -// DocumentDecodingErro.swift +// DocumentDecodingError.swift // // // Created by Mathew Polzin on 10/20/19. // -public enum JSONAPIDocumentDecodingError: Swift.Error { +public enum DocumentDecodingError: Swift.Error, Equatable { + case primaryResource(error: ResourceObjectDecodingError, idx: Int?) + case primaryResourceMissing + case primaryResourcesMissing + + case includes(error: IncludesDecodingError) + case foundErrorDocumentWhenExpectingSuccess case foundSuccessDocumentWhenExpectingError + + init(_ decodingError: ResourceObjectDecodingError) { + self = .primaryResource(error: decodingError, idx: nil) + } + + init(_ decodingError: ManyResourceBodyDecodingError) { + self = .primaryResource(error: decodingError.error, idx: decodingError.idx) + } + + init(_ decodingError: IncludesDecodingError) { + self = .includes(error: decodingError) + } + + init?(_ decodingError: DecodingError) { + switch decodingError { + case .valueNotFound(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type: + self = .primaryResourceMissing + case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self: + self = .primaryResourcesMissing + default: + return nil + } + } + + private enum Location: Equatable { + case data + case other + + init(_ context: DecodingError.Context) { + if context.codingPath.contains(where: { $0.stringValue == "data" }) { + self = .data + } else { + self = .other + } + } + } +} + +extension DocumentDecodingError: CustomStringConvertible { + public var description: String { + switch self { + case .primaryResource(error: let error, idx: let idx): + let idxString = idx.map { " \($0 + 1)" } ?? "" + return "Primary Resource\(idxString) failed to parse because \(error)" + case .primaryResourceMissing: + return "Primary Resource missing." + case .primaryResourcesMissing: + return "Primary Resources array missing." + + case .includes(error: let error): + return "\(error)" + + case .foundErrorDocumentWhenExpectingSuccess: + return "Expected a success document with a 'data' property but found an error document." + case .foundSuccessDocumentWhenExpectingError: + return "Expected an error document but found a success document with a 'data' property." + } + } } diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index e7a701b..2174c67 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -56,8 +56,14 @@ extension Includes: Decodable where I: Decodable { } var valueAggregator = [I]() + var idx = 0 while !container.isAtEnd { - valueAggregator.append(try container.decode(I.self)) + do { + valueAggregator.append(try container.decode(I.self)) + idx = idx + 1 + } catch let error { + throw IncludesDecodingError(error: error, idx: idx) + } } values = valueAggregator @@ -177,3 +183,20 @@ extension Includes where I: _Poly11 { return values.compactMap { $0.k } } } + +// MARK: - DecodingError +public struct IncludesDecodingError: Swift.Error, Equatable { + public let error: Swift.Error + public let idx: Int + + public static func ==(lhs: Self, rhs: Self) -> Bool { + return lhs.idx == rhs.idx + && String(describing: lhs) == String(describing: rhs) + } +} + +extension IncludesDecodingError: CustomStringConvertible { + public var description: String { + return "Include \(idx + 1) failed to parse: \(error)" + } +} diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 25090c8..5a7c96f 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -125,8 +125,17 @@ extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() var valueAggregator = [PrimaryResource]() + var idx = 0 while !container.isAtEnd { - valueAggregator.append(try container.decode(PrimaryResource.self)) + do { + valueAggregator.append(try container.decode(PrimaryResource.self)) + } catch let error as ResourceObjectDecodingError { + throw ManyResourceBodyDecodingError( + error: error, + idx: idx + ) + } + idx = idx + 1 } values = valueAggregator } @@ -145,3 +154,9 @@ extension ManyResourceBody: CustomStringConvertible { return "PrimaryResourceBody(\(String(describing: values)))" } } + +// MARK: - DecodingError +public struct ManyResourceBodyDecodingError: Swift.Error, Equatable { + public let error: ResourceObjectDecodingError + public let idx: Int +} diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index d66e3d9..1957b87 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -45,7 +45,10 @@ public protocol OptionalId: Codable { init(rawValue: RawType) } -public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {} +/// marker protocol +public protocol AbstractId {} + +public protocol IdType: AbstractId, OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {} extension Optional: MaybeRawId where Wrapped: Codable & Equatable {} extension Optional: OptionalId where Wrapped: IdType { @@ -94,7 +97,7 @@ public struct Id: Equa } } -extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType { +extension Id: Hashable, CustomStringConvertible, AbstractId, IdType where RawType: RawIdType { public static func id(from rawValue: RawType) -> Id { return Id(rawValue: rawValue) } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index e85d8f0..0604b00 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -96,10 +96,13 @@ extension ResourceObjectProxy { public static var jsonType: String { return Description.jsonType } } +/// A marker protocol. +public protocol AbstractResourceObject {} + /// ResourceObjectType is the protocol that ResourceObject conforms to. This /// protocol lets other types accept any ResourceObject as a generic /// specialization. -public protocol ResourceObjectType: ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription { +public protocol ResourceObjectType: AbstractResourceObject, ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription { associatedtype Meta: JSONAPI.Meta associatedtype Links: JSONAPI.Links @@ -414,7 +417,10 @@ public extension ResourceObject { let type = try container.decode(String.self, forKey: .type) guard ResourceObject.jsonType == type else { - throw JSONAPICodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) + throw ResourceObjectDecodingError( + expectedJSONAPIType: ResourceObject.jsonType, + found: type + ) } let maybeUnidentified = Unidentified() as? EntityRawIdType diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index d1ac49c..b644ce6 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -23,11 +23,13 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { public enum Location: String, Equatable { case attributes case relationships + case type var singular: String { switch self { case .attributes: return "attribute" case .relationships: return "relationship" + case .type: return "type" } } } @@ -63,6 +65,12 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } } + init(expectedJSONAPIType: String, found: String) { + location = .type + subjectName = "self" + cause = .jsonTypeMismatch(expectedType: expectedJSONAPIType, foundType: found) + } + init(subjectName: String, cause: Cause, location: Location) { self.subjectName = subjectName self.cause = cause @@ -75,8 +83,17 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } static func context(path: [CodingKey]) -> (Location, name: String) { + let location: Location + if path.contains(where: { $0.stringValue == "attributes" }) { + location = .attributes + } else if path.contains(where: { $0.stringValue == "relationships" }) { + location = .relationships + } else { + location = .type + } + return ( - path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, + location, name: path.last?.stringValue ?? "unnamed" ) } @@ -94,6 +111,8 @@ extension ResourceObjectDecodingError: CustomStringConvertible { return "'\(subjectName)' \(location.singular) is not nullable but null." case .typeMismatch(expectedTypeName: let expected): return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." + case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type: + return "found JSON:API type \"\(found)\" but expected \"\(expected)\"" case .jsonTypeMismatch(expectedType: let expected, foundType: let found): return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" case .quantityMismatch(expected: let expected): diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift new file mode 100644 index 0000000..055e007 --- /dev/null +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -0,0 +1,189 @@ +// +// DocumentDecodingErrorTests.swift +// +// +// Created by Mathew Polzin on 11/10/19. +// + +import XCTest +import JSONAPI +import Poly + +final class DocumentDecodingErrorTests: XCTestCase { + func test_singlePrimaryResource_missing() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_null + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResourceMissing = docError else { + XCTFail("Expected primary resource missing error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resource missing.") + } + } + + func test_singlePrimaryResource_failure() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_no_includes_missing_relationship + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResource = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resource failed to parse because 'author' relationship is required and missing.") + } + } + + func test_manyPrimaryResource_missing() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: many_document_no_includes_data_is_null + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResourcesMissing = docError else { + XCTFail("Expected primary resource missing error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resources array missing.") + } + } + + func test_manyPrimaryResource_failure() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: many_document_no_includes_missing_relationship + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResource = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resource 2 failed to parse because 'author' relationship is required and missing.") + } + } + + func test_include_failure() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_some_includes_wrong_type + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .includes = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), #"Include 3 failed to parse: found JSON:API type "not_an_author" but expected "authors""#) + } + } +} + +// MARK: - Test Types +extension DocumentDecodingErrorTests { + enum AuthorType: ResourceObjectDescription { + static var jsonType: String { return "authors" } + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + typealias Author = BasicEntity + + enum ArticleType: ResourceObjectDescription { + static var jsonType: String { return "articles" } + + typealias Attributes = NoAttributes + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + } + } + + typealias Article = BasicEntity + + enum BookType: ResourceObjectDescription { + static var jsonType: String { return "books" } + + struct Attributes: JSONAPI.SparsableAttributes { + let pageCount: Attribute + + enum CodingKeys: String, SparsableCodingKey { + case pageCount + } + } + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + let series: ToManyRelationship + } + } + + typealias Book = BasicEntity + + struct TestPageMetadata: JSONAPI.Meta { + let total: Int + let limit: Int + let offset: Int + } + + struct TestLinks: JSONAPI.Links { + let link: Link + let link2: Link + + struct TestMetadata: JSONAPI.Meta { + let hello: String + } + } + + typealias TestAPIDescription = APIDescription + + enum TestError: JSONAPIError { + case unknownError + case basic(BasicError) + + struct BasicError: Codable, Equatable { + let code: Int + let description: String + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + self = (try? .basic(container.decode(BasicError.self))) ?? .unknown + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case .unknownError: + try container.encode("unknown") + case .basic(let error): + try container.encode(error) + } + } + + public static var unknown: Self { + return .unknownError + } + } +} + diff --git a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift index 7455a0c..ed83ebf 100644 --- a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift +++ b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift @@ -37,6 +37,17 @@ let single_document_no_includes = """ } """.data(using: .utf8)! +let single_document_no_includes_missing_relationship = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + } + } +} +""".data(using: .utf8)! + let single_document_no_includes_with_api_description = """ { "data": { @@ -247,6 +258,37 @@ let single_document_some_includes = """ } """.data(using: .utf8)! +let single_document_some_includes_wrong_type = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "included": [ + { + "id": "30", + "type": "authors" + }, + { + "id": "31", + "type": "authors" + }, + { + "id": "33", + "type": "not_an_author" + } + ] +} +""".data(using: .utf8)! + let single_document_some_includes_with_api_description = """ { "data": { @@ -452,6 +494,49 @@ let many_document_no_includes = """ } """.data(using: .utf8)! +let many_document_no_includes_data_is_null = """ +{ + "data": null +} +""".data(using: .utf8)! + +let many_document_no_includes_missing_relationship = """ +{ + "data": [ + { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + { + "id": "2", + "type": "articles", + "relationships": { + } + }, + { + "id": "3", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "11" + } + } + } + } + ] +} +""".data(using: .utf8)! + let many_document_no_includes_with_api_description = """ { "data": [ diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index 0891d23..d0abb6e 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -287,6 +287,30 @@ extension ResourceObjectDecodingErrorTests { } } +// MARK: - JSON:API Type +extension ResourceObjectDecodingErrorTests { + func test_wrongType() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_is_wrong_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "self", + cause: .jsonTypeMismatch(expectedType: "fourteenth_test_entities", foundType: "not_correct_type"), + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"found JSON:API type "not_correct_type" but expected "fourteenth_test_entities""# + ) + } + } +} + // MARK: - Test Types extension ResourceObjectDecodingErrorTests { enum TestEntityType: ResourceObjectDescription { diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 9fe7355..861ea1c 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -533,6 +533,17 @@ let entity_attribute_is_wrong_type3 = """ } """.data(using: .utf8)! +let entity_is_wrong_type = """ +{ + "id": "1", + "type": "not_correct_type", + "attributes": { + "required": "hello", + "yetAnother": 101 + } +} +""".data(using: .utf8)! + let entity_attributes_entirely_missing = """ { "id": "1", diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 0b2c004..0768089 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -85,6 +85,19 @@ extension CustomAttributesTests { ] } +extension DocumentDecodingErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__DocumentDecodingErrorTests = [ + ("test_include_failure", test_include_failure), + ("test_manyPrimaryResource_failure", test_manyPrimaryResource_failure), + ("test_manyPrimaryResource_missing", test_manyPrimaryResource_missing), + ("test_singlePrimaryResource_failure", test_singlePrimaryResource_failure), + ("test_singlePrimaryResource_missing", test_singlePrimaryResource_missing), + ] +} + extension DocumentTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -379,6 +392,7 @@ extension ResourceObjectDecodingErrorTests { ("test_required_attribute", test_required_attribute), ("test_required_relationship", test_required_relationship), ("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship), + ("test_wrongType", test_wrongType), ] } @@ -542,6 +556,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(BasicJSONAPIErrorTests.__allTests__BasicJSONAPIErrorTests), testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), testCase(CustomAttributesTests.__allTests__CustomAttributesTests), + testCase(DocumentDecodingErrorTests.__allTests__DocumentDecodingErrorTests), testCase(DocumentTests.__allTests__DocumentTests), testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests), From e9a3b35dc7ae27e1a6390056a276d71064e8b0e6 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 10 Nov 2019 23:43:35 -0800 Subject: [PATCH 22/23] improve error messages for poly include types --- Package.resolved | 4 +-- Package.swift | 2 +- Sources/JSONAPI/Document/Includes.swift | 33 +++++++++++++++++++ .../Document/DocumentDecodingErrorTests.swift | 26 +++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index cc81a53..add4b9a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf", - "version": "2.3.0" + "revision": "0c9c08204142babc480938d704a23513d11420e5", + "version": "2.3.1" } } ] diff --git a/Package.swift b/Package.swift index 5b780cb..9b3e038 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 2174c67..32f90ea 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -61,6 +61,27 @@ extension Includes: Decodable where I: Decodable { do { valueAggregator.append(try container.decode(I.self)) idx = idx + 1 + } catch let error as PolyDecodeNoTypesMatchedError { + let errors: [ResourceObjectDecodingError] = error + .individualTypeFailures + .compactMap { decodingError in + switch decodingError.error { + case .typeMismatch(_, let context), + .valueNotFound(_, let context), + .keyNotFound(_, let context), + .dataCorrupted(let context): + return context.underlyingError as? ResourceObjectDecodingError + @unknown default: + return nil + } + } + guard errors.count == error.individualTypeFailures.count else { + throw IncludesDecodingError(error: error, idx: idx) + } + throw IncludesDecodingError( + error: IncludeDecodingError(failures: errors), + idx: idx + ) } catch let error { throw IncludesDecodingError(error: error, idx: idx) } @@ -200,3 +221,15 @@ extension IncludesDecodingError: CustomStringConvertible { return "Include \(idx + 1) failed to parse: \(error)" } } + +public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConvertible { + public let failures: [ResourceObjectDecodingError] + + public var description: String { + return failures + .enumerated() + .map { + "\nCould not have been Include Type \($0.offset + 1) because:\n\($0.element)" + }.joined(separator: "\n") + } +} diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 055e007..30e2740 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -94,6 +94,32 @@ final class DocumentDecodingErrorTests: XCTestCase { XCTAssertEqual(String(describing: error), #"Include 3 failed to parse: found JSON:API type "not_an_author" but expected "authors""#) } } + + func test_include_failure2() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_some_includes_wrong_type + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .includes = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), +#""" +Include 3 failed to parse: +Could not have been Include Type 1 because: +found JSON:API type "not_an_author" but expected "articles" + +Could not have been Include Type 2 because: +found JSON:API type "not_an_author" but expected "authors" +"""# + ) + } + } } // MARK: - Test Types From 54551617b4d56e81456feeb5b17e387df9766fbe Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 12 Nov 2019 18:34:34 -0800 Subject: [PATCH 23/23] Add more errors for the resource object type property --- .../Document/DocumentDecodingError.swift | 10 +- .../Resource Object/ResourceObject.swift | 8 +- .../ResourceObjectDecodingError.swift | 13 +- .../ResourceObjectCompareTests.swift | 1 - .../ResourceObjectDecodingErrorTests.swift | 125 +++++++++++++++++- .../stubs/ResourceObjectStubs.swift | 55 +++++++- .../SparseFieldEncoderTests.swift | 1 - 7 files changed, 193 insertions(+), 20 deletions(-) diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 0e58fe3..2bcfa60 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -40,14 +40,12 @@ public enum DocumentDecodingError: Swift.Error, Equatable { private enum Location: Equatable { case data - case other - init(_ context: DecodingError.Context) { - if context.codingPath.contains(where: { $0.stringValue == "data" }) { - self = .data - } else { - self = .other + init?(_ context: DecodingError.Context) { + guard context.codingPath.contains(where: { $0.stringValue == "data" }) else { + return nil } + self = .data } } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 0604b00..83f0d1c 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -414,7 +414,13 @@ public extension ResourceObject { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self) - let type = try container.decode(String.self, forKey: .type) + let type: String + do { + type = try container.decode(String.self, forKey: .type) + } catch let error as DecodingError { + throw ResourceObjectDecodingError(error) + ?? error + } guard ResourceObject.jsonType == type else { throw ResourceObjectDecodingError( diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index b644ce6..c61e9be 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -102,13 +102,18 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { extension ResourceObjectDecodingError: CustomStringConvertible { public var description: String { switch cause { + case .keyNotFound where subjectName == ResourceObjectDecodingError.entireObject: + return "\(location) object is required and missing." + case .keyNotFound where location == .type: + return "'type' (a.k.a. JSON:API type name) is required and missing." case .keyNotFound: - if subjectName == ResourceObjectDecodingError.entireObject { - return "\(location) object is required and missing." - } return "'\(subjectName)' \(location.singular) is required and missing." + case .valueNotFound where location == .type: + return "'type' (a.k.a. JSON:API type name) is not nullable but null was found." case .valueNotFound: - return "'\(subjectName)' \(location.singular) is not nullable but null." + return "'\(subjectName)' \(location.singular) is not nullable but null was found." + case .typeMismatch(expectedTypeName: let expected) where location == .type: + return "'type' (a.k.a. the JSON:API type name) is not a \(expected) as expected." case .typeMismatch(expectedTypeName: let expected): return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type: diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index 2c40e87..fd43103 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -11,7 +11,6 @@ import JSONAPITesting final class ResourceObjectCompareTests: XCTestCase { func test_same() { - print(test1.compare(to: test1).differences) XCTAssertTrue(test1.compare(to: test1).differences.isEmpty) XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index d0abb6e..ce8e334 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -68,7 +68,7 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' relationship is not nullable but null." + "'required' relationship is not nullable but null was found." ) } } @@ -89,7 +89,7 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' relationship is not nullable but null." + "'required' relationship is not nullable but null was found." ) } } @@ -99,7 +99,6 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { TestEntity.self, from: entity_relationship_is_wrong_type )) { error in - print(error) XCTAssertEqual( error as? ResourceObjectDecodingError, ResourceObjectDecodingError( @@ -218,7 +217,7 @@ extension ResourceObjectDecodingErrorTests { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' attribute is not nullable but null." + "'required' attribute is not nullable but null was found." ) } } @@ -285,11 +284,44 @@ extension ResourceObjectDecodingErrorTests { ) } } + + func test_transformed_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type4 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "transformed", + cause: .typeMismatch(expectedTypeName: String(describing: Int.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'transformed' attribute is not a Int as expected." + ) + } + } + + func test_transformed_attribute2() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_always_fails + )) { error in + XCTAssertEqual( + String(describing: error), + "Error: Always Fails" + ) + } + } } // MARK: - JSON:API Type extension ResourceObjectDecodingErrorTests { - func test_wrongType() { + func test_wrongJSONAPIType() { XCTAssertThrowsError(try testDecoder.decode( TestEntity2.self, from: entity_is_wrong_type @@ -309,6 +341,69 @@ extension ResourceObjectDecodingErrorTests { ) } } + + func test_wrongDecodedType() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_wrong_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .typeMismatch(expectedTypeName: String(describing: String.self)), + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. the JSON:API type name) is not a String as expected."# + ) + } + } + + func test_type_missing() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_missing + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .keyNotFound, + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. JSON:API type name) is required and missing."# + ) + } + } + + func test_type_null() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_null + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .valueNotFound, + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. JSON:API type name) is not nullable but null was found."# + ) + } + } } // MARK: - Test Types @@ -335,10 +430,30 @@ extension ResourceObjectDecodingErrorTests { let required: Attribute let other: Attribute? let yetAnother: Attribute? + let transformed: TransformedAttribute? + let transformed2: TransformedAttribute? } typealias Relationships = NoRelationships } typealias TestEntity2 = BasicEntity + + enum IntToString: Transformer { + static func transform(_ value: Int) throws -> String { + return "\(value)" + } + typealias From = Int + typealias To = String + } + + enum AlwaysFails: Transformer { + static func transform(_ value: String) throws -> String { + throw Error() + } + + struct Error: Swift.Error, CustomStringConvertible { + let description: String = "Error: Always Fails" + } + } } diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 861ea1c..181826f 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -533,6 +533,35 @@ let entity_attribute_is_wrong_type3 = """ } """.data(using: .utf8)! +let entity_attribute_is_wrong_type4 = """ +{ + "id": "1", + "type": "fourteenth_test_entities", + "attributes": { + "required": "hello", + "transformed": "world" + } +} +""".data(using: .utf8)! + +let entity_attribute_always_fails = """ +{ + "id": "1", + "type": "fourteenth_test_entities", + "attributes": { + "required": "hello", + "transformed2": "world" + } +} +""".data(using: .utf8)! + +let entity_attributes_entirely_missing = """ +{ + "id": "1", + "type": "fourteenth_test_entities" +} +""".data(using: .utf8)! + let entity_is_wrong_type = """ { "id": "1", @@ -544,10 +573,32 @@ let entity_is_wrong_type = """ } """.data(using: .utf8)! -let entity_attributes_entirely_missing = """ +let entity_type_is_wrong_type = """ { "id": "1", - "type": "fourteenth_test_entities" + "type": 10, + "attributes": { + "required": "hello" + } +} +""".data(using: .utf8)! + +let entity_type_is_missing = """ +{ + "id": "1", + "attributes": { + "required": "hello" + } +} +""".data(using: .utf8)! + +let entity_type_is_null = """ +{ + "id": "1", + "type": null, + "attributes": { + "required": "hello" + } } """.data(using: .utf8)! diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index c6192f8..2aa9fb2 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -17,7 +17,6 @@ class SparseFieldEncoderTests: XCTestCase { do { let _ = try encoder.encode(Wrapper()) } catch let err as Wrapper.OuterFail.FailError { - print(err.path) XCTAssertEqual(err.path.first as? Wrapper.OuterFail.CodingKeys, Wrapper.OuterFail.CodingKeys.inner) } catch { XCTFail("received unexpected error during test")