From f7bfa91ccca76231fb097c87cc63ba489c8de8ac Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:03:51 -0800 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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") From ae855c85eef0f62894f655ddb34e415636dab46a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 08:30:17 -0800 Subject: [PATCH 24/30] going through and fleshing out tests. minor adjustments and bug fixes. --- Sources/JSONAPI/Document/Document.swift | 18 ++- Sources/JSONAPI/Meta/Links.swift | 2 +- .../ResourceObjectDecodingError.swift | 4 +- .../Comparisons/ArrayCompare.swift | 11 -- .../Comparisons/DocumentCompare.swift | 26 +++- .../Comparisons/RelationshipsCompare.swift | 2 +- .../Comparisons/ArrayCompareTests.swift | 86 +++++++++++ .../Comparisons/AttributesCompareTests.swift | 26 +++- .../Comparisons/DocumentCompareTests.swift | 95 +++++++++++- .../RelationshipsCompareTests.swift | 20 ++- .../ResourceObjectCompareTests.swift | 24 ++- .../Test Helpers/EntityTestTypes.swift | 2 + .../Attribute/AttributeTests.swift | 8 + .../Document/DocumentDecodingErrorTests.swift | 22 +++ .../JSONAPITests/Document/DocumentTests.swift | 2 - .../SuccessAndErrorDocumentTests.swift | 139 ++++++++++++++++++ .../Error/GenericJSONAPIErrorTests.swift | 1 + .../Includes/IncludesDecodingErrorTests.swift | 109 ++++++++++++++ .../Includes/stubs/IncludeStubs.swift | 8 +- Tests/JSONAPITests/Poly/PolyProxyTests.swift | 6 + .../Test Helpers/EncodeDecode.swift | 3 +- .../Test Helpers/EncodedAttributeTest.swift | 14 +- .../Test Helpers/EntityTestTypes.swift | 2 + 23 files changed, 573 insertions(+), 57 deletions(-) create mode 100644 Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift create mode 100644 Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift create mode 100644 Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 34c32a4..1343559 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -494,6 +494,10 @@ extension Document { public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool { return lhs == rhs.document } + + public static func ==(lhs: ErrorDocument, rhs: Document) -> Bool { + return lhs.document == rhs + } } /// A Document that only supports success bodies. This is useful if you wish to pass around a @@ -534,7 +538,7 @@ extension Document { /// `nil` if the Document is an error response. Otherwise, /// a structure containing the primary resource, any included /// resources, metadata, and links. - var data: BodyData? { + public var data: BodyData? { return document.body.data } @@ -545,7 +549,7 @@ extension Document { /// resources dependening on the `PrimaryResourceBody` type. /// /// See `SingleResourceBody` and `ManyResourceBody`. - var primaryResource: PrimaryResourceBody? { + public var primaryResource: PrimaryResourceBody? { return document.body.primaryResource } @@ -553,25 +557,29 @@ extension Document { /// /// `nil` if the Document is an error document. Otherwise, /// zero or more includes. - var includes: Includes? { + public 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? { + public 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? { + public var links: LinksType? { return document.body.links } public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool { return lhs == rhs.document } + + public static func ==(lhs: SuccessDocument, rhs: Document) -> Bool { + return lhs.document == rhs + } } } diff --git a/Sources/JSONAPI/Meta/Links.swift b/Sources/JSONAPI/Meta/Links.swift index 876995f..ab2b7e1 100644 --- a/Sources/JSONAPI/Meta/Links.swift +++ b/Sources/JSONAPI/Meta/Links.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 11/24/18. // -/// A Links structure should contain nothing but JSONAPI.Link properties. +/// A Links structure should contain nothing but `JSONAPI.Link` properties. public protocol Links: Codable, Equatable {} /// Use NoLinks where no links should belong to a JSON API component diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index c61e9be..a79f5a1 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -109,11 +109,11 @@ extension ResourceObjectDecodingError: CustomStringConvertible { case .keyNotFound: 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." + return "'\(location.singular)' (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 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." + return "'\(location.singular)' (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/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift index a1f7e5c..f93ab62 100644 --- a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift @@ -14,17 +14,6 @@ public enum ArrayElementComparison: Equatable, CustomStringConvertible { case differentValues(String, String) case prebuilt(String) - public init(sameTypeComparison: BasicComparison) { - 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 diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index d56c84d..4cee720 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -33,10 +33,10 @@ public enum BodyComparison: Equatable, CustomStringConvertible { case differentErrors(ErrorComparison) case differentData(DocumentDataComparison) - public typealias ErrorComparison = [BasicComparison] + public typealias ErrorComparison = [String: BasicComparison] static func compare(errors errors1: [E], _ meta1: M?, _ links1: L?, with errors2: [E], _ meta2: M?, _ links2: L?) -> ErrorComparison { - return errors1.compare( + let errorComparisons = errors1.compare( to: errors2, using: { error1, error2 in guard error1 != error2 else { @@ -48,9 +48,19 @@ public enum BodyComparison: Equatable, CustomStringConvertible { String(describing: error2) ) } - ).map(BasicComparison.init) + [ - BasicComparison(meta1, meta2), - BasicComparison(links1, links2) + ).map(BasicComparison.init) + .filter { !$0.isSame } + .map { $0.rawValue } + .joined(separator: ", ") + + let errorComparisonString = errorComparisons.isEmpty + ? nil + : errorComparisons + + return [ + "Errors": errorComparisonString.map { BasicComparison.prebuilt("(\($0))") } ?? .same, + "Metadata": BasicComparison(meta1, meta2), + "Links": BasicComparison(links1, links2) ] } @@ -67,8 +77,8 @@ public enum BodyComparison: Equatable, CustomStringConvertible { return "\(left) ≠ \(right)" case .differentErrors(let comparisons): return comparisons - .filter { !$0.isSame } - .map { $0.rawValue } + .filter { !$0.value.isSame } + .map { "\($0.key): \($0.value.rawValue)" } .sorted() .joined(separator: ", ") case .differentData(let comparison): @@ -104,7 +114,7 @@ extension DocumentBody where Self: Equatable, PrimaryResourceBody: TestableResou return .differentErrors( BodyComparison.compare( errors: errors1, meta, links, - with: errors2, meta, links + with: errors2, other.meta, other.links ) ) } diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift index 3bfa7d6..0c24386 100644 --- a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -1,5 +1,5 @@ // -// File.swift +// RelationshipsCompare.swift // // // Created by Mathew Polzin on 11/3/19. diff --git a/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift new file mode 100644 index 0000000..fccf84c --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift @@ -0,0 +1,86 @@ +// +// ArrayCompareTests.swift +// JSONAPITestingTests +// +// Created by Mathew Polzin on 11/14/19. +// + +import XCTest +@testable import JSONAPITesting + +final class ArrayCompareTests: XCTestCase { + func test_same() { + let arr1 = ["a", "b", "c"] + let arr2 = ["a", "b", "c"] + + let comparison = arr1.compare(to: arr2) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison, + [.same, .same, .same] + ) + + XCTAssertEqual(comparison.map { $0.description }, ["same", "same", "same"]) + + XCTAssertEqual(comparison.map(BasicComparison.init(reducing:)), [.same, .same, .same]) + + XCTAssertEqual(comparison.map(BasicComparison.init(reducing:)).map { $0.description }, ["same", "same", "same"]) + } + + func test_differentLengths() { + let arr1 = ["a", "b", "c"] + let arr2 = ["a", "b"] + + let comparison1 = arr1.compare(to: arr2) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison1, + [.same, .same, .missing] + ) + + XCTAssertEqual(comparison1.map { $0.description }, ["same", "same", "missing"]) + + XCTAssertEqual(comparison1.map(BasicComparison.init(reducing:)), [.same, .same, .different("array length 1", "array length 2")]) + + let comparison2 = arr2.compare(to: arr1) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison2, + [.same, .same, .missing] + ) + + XCTAssertEqual(comparison2.map { $0.description }, ["same", "same", "missing"]) + + XCTAssertEqual(comparison2.map(BasicComparison.init(reducing:)), [.same, .same, .different("array length 1", "array length 2")]) + } + + func test_differentValues() { + let arr1 = ["c", "b", "a"] + let arr2 = ["a", "b", "c"] + + let comparison = arr1.compare(to: arr2) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison, + [.differentValues("c", "a"), .same, .differentValues("a", "c")] + ) + + XCTAssertEqual(comparison.map { $0.description }, ["c ≠ a", "same", "a ≠ c"]) + } + + func test_reducePrebuilt() { + let prebuilt = ArrayElementComparison.prebuilt("hello world") + + XCTAssertEqual(BasicComparison(reducing: prebuilt), .prebuilt("hello world")) + + XCTAssertEqual(BasicComparison(reducing: prebuilt).description, "hello world") + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift index e7a7dca..e54031c 100644 --- a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift @@ -10,13 +10,14 @@ import JSONAPI import JSONAPITesting final class AttributesCompareTests: XCTestCase { - func test_sameAttributes() { + func test_sameAttributes() throws { let attr1 = TestAttributes( string: "hello world", int: 10, bool: true, double: 105.4, - struct: .init(value: .init()) + struct: .init(value: .init()), + transformed: try .init(rawValue: 10) ) let attr2 = attr1 @@ -25,24 +26,27 @@ final class AttributesCompareTests: XCTestCase { "int": .same, "bool": .same, "double": .same, - "struct": .same + "struct": .same, + "transformed": .same ]) } - func test_differentAttributes() { + func test_differentAttributes() throws { let attr1 = TestAttributes( string: "hello world", int: 10, bool: true, double: 105.4, - struct: .init(value: .init()) + struct: .init(value: .init()), + transformed: try .init(rawValue: 10) ) let attr2 = TestAttributes( string: "hello", int: 11, bool: false, double: 1.4, - struct: .init(value: .init(val: "there")) + struct: .init(value: .init(val: "there")), + transformed: try .init(rawValue: 11) ) XCTAssertEqual(attr1.compare(to: attr2), [ @@ -50,7 +54,8 @@ final class AttributesCompareTests: XCTestCase { "int": .different("10", "11"), "bool": .different("true", "false"), "double": .different("105.4", "1.4"), - "struct": .different("string: hello", "string: there") + "struct": .different("string: hello", "string: there"), + "transformed": .different("10", "11") ]) } } @@ -61,6 +66,7 @@ private struct TestAttributes: JSONAPI.Attributes { let bool: Attribute let double: Attribute let `struct`: Attribute + let transformed: TransformedAttribute struct Struct: Equatable, Codable, CustomStringConvertible { let string: String @@ -72,3 +78,9 @@ private struct TestAttributes: JSONAPI.Attributes { var description: String { return "string: \(string)" } } } + +private enum TestTransformer: Transformer { + static func transform(_ value: Int) throws -> String { + return "\(value)" + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index 0d19a5c..9cbf93f 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -34,8 +34,33 @@ final class DocumentCompareTests: XCTestCase { } func test_differentErrors() { - XCTAssertEqual(d2.compare(to: d4).differences, [ - "Body": "status: 500, title: Internal Error ≠ status: 404, title: Not Found" + let comparison = d2.compare(to: d4) + XCTAssertEqual(comparison.differences, [ + "Body": "Errors: (status: 500, title: Internal Error ≠ status: 404, title: Not Found)" + ]) + + XCTAssertEqual(String(describing: comparison), "(Body: Errors: (status: 500, title: Internal Error ≠ status: 404, title: Not Found))") + } + + func test_sameErrorsDifferentMetadata() { + let errors = [ + BasicJSONAPIError.error(.init(id: nil, status: "500", title: "Internal Error")) + ] + let doc1 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "1", meta: .none), + errors: errors, + meta: nil, + links: nil + ) + let doc2 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "1", meta: .none), + errors: errors, + meta: .init(total: 11), + links: nil + ) + + XCTAssertEqual(doc1.compare(to: doc2).differences, [ + "Body": "Metadata: nil ≠ Optional(total: 11)" ]) } @@ -60,6 +85,24 @@ final class DocumentCompareTests: XCTestCase { "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))"## ]) } + + func test_differentMetadata() { + XCTAssertEqual(d11.compare(to: d12).differences, [ + "Body": "(Meta: total: 10 ≠ total: 10000)" + ]) + } + + func test_differentLinks() { + XCTAssertEqual(d11.compare(to: d13).differences, [ + "Body": ##"(Links: TestLinks(link: JSONAPI.Link(url: "http://google.com", meta: No Metadata)) ≠ TestLinks(link: JSONAPI.Link(url: "http://yahoo.com", meta: No Metadata)))"## + ]) + } + + func test_differentAPIDescription() { + XCTAssertEqual(d11.compare(to: d14).differences, [ + "API Description": ##"APIDescription(version: "10", meta: No Metadata) ≠ APIDescription(version: "1", meta: No Metadata)"## + ]) + } } fileprivate enum TestDescription: JSONAPI.ResourceObjectDescription { @@ -98,6 +141,22 @@ fileprivate typealias TestType2 = ResourceObject, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> +fileprivate struct TestMetadata: JSONAPI.Meta, CustomStringConvertible { + let total: Int + + var description: String { + "total: \(total)" + } +} + +fileprivate struct TestLinks: JSONAPI.Links { + let link: Link +} + +typealias TestAPIDescription = APIDescription + +fileprivate typealias SingleDocumentWithMetaAndLinks = JSONAPI.Document, TestMetadata, TestLinks, Include2, TestAPIDescription, BasicJSONAPIError> + fileprivate typealias OptionalSingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> fileprivate typealias ManyDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> @@ -220,3 +279,35 @@ fileprivate let d10 = SingleDocument( meta: .none, links: .none ) + +fileprivate let d11 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "10", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10), + links: TestLinks(link: .init(url: "http://google.com")) +) + +fileprivate let d12 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "10", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10000), + links: TestLinks(link: .init(url: "http://google.com")) +) + +fileprivate let d13 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "10", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10), + links: TestLinks(link: .init(url: "http://yahoo.com")) +) + +fileprivate let d14 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "1", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10), + links: TestLinks(link: .init(url: "http://google.com")) +) diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift index cdb7fda..b2dc83e 100644 --- a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -10,5 +10,23 @@ import JSONAPI import JSONAPITesting final class RelationshipsCompareTests: XCTestCase { - // TODO: write tests + func test_same() { + // TODO: write test + } + + func test_differentIds() { + // TODO: write test + } + + func test_differentTypes() { + // TODO: write test + } + + func test_differentMetadata() { + // TODO: write test + } + + func test_differentLinks() { + // TODO: write tests + } } diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index fd43103..d62f9e2 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// ResourceObjectCompareTests.swift // // // Created by Mathew Polzin on 11/3/19. @@ -15,11 +15,31 @@ final class ResourceObjectCompareTests: XCTestCase { XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) } - func test_different() { + func test_differentAttributes() { // TODO: write actual test print(test1.compare(to: test2).differences.map { "\($0): \($1)" }.joined(separator: ", ")) } + func test_differentRelationships() { + // TODO: write test + } + + func test_differentTypes() { + // TODO: write test + } + + func test_differentIds() { + // TODO: write test + } + + func test_differentMetadata() { + // TODO: write test + } + + func test_differentLinks() { + // TODO: write test + } + fileprivate let test1 = TestType( id: "2", attributes: .init( diff --git a/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift b/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift index af99a71..1729dbc 100644 --- a/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift +++ b/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift @@ -12,3 +12,5 @@ public typealias Entity = Entity public typealias NewEntity = JSONAPI.ResourceObject + +extension String: JSONAPI.JSONAPIURL {} diff --git a/Tests/JSONAPITests/Attribute/AttributeTests.swift b/Tests/JSONAPITests/Attribute/AttributeTests.swift index f68831c..6ffdd22 100644 --- a/Tests/JSONAPITests/Attribute/AttributeTests.swift +++ b/Tests/JSONAPITests/Attribute/AttributeTests.swift @@ -14,6 +14,10 @@ class AttributeTests: XCTestCase { XCTAssertEqual(Attribute(value: "hello").value, "hello") } + func test_AttributeRawType() { + XCTAssert(Attribute(value: "hello").rawValueType == String.self) + } + func test_TransformedAttributeNoThrow() { XCTAssertNoThrow(try TransformedAttribute(rawValue: "10")) } @@ -26,6 +30,10 @@ class AttributeTests: XCTestCase { XCTAssertNoThrow(try TransformedAttribute(transformedValue: 10)) } + func test_TransformedAttributeRawType() throws { + try XCTAssert(TransformedAttribute(rawValue: "10").rawValueType == String.self) + } + func test_EncodedPrimitives() { testEncodedPrimitive(attribute: Attribute(value: 10)) testEncodedPrimitive(attribute: Attribute(value: false)) diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 30e2740..6fa2fb4 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -120,6 +120,28 @@ found JSON:API type "not_an_author" but expected "authors" ) } } + + func test_wantSuccess_foundError() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self, + from: error_document_no_metadata + ) + ) { error in + XCTAssertEqual(String(describing: error), #"Expected a success document with a 'data' property but found an error document."#) + } + } + + func test_wantError_foundSuccess() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self, + from: single_document_some_includes_with_metadata_with_api_description + ) + ) { error in + XCTAssertEqual(String(describing: error), #"Expected an error document but found a success document with a 'data' property."#) + } + } } // MARK: - Test Types diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index d6f6351..5911a0d 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -1554,5 +1554,3 @@ extension DocumentTests { } } } - -extension String: JSONAPI.JSONAPIURL {} diff --git a/Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift b/Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift new file mode 100644 index 0000000..9fa1144 --- /dev/null +++ b/Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift @@ -0,0 +1,139 @@ +// +// SuccessAndErrorDocumentTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/19. +// + +import XCTest +import JSONAPI + +final class SuccessAndErrorDocumentTests: XCTestCase { + func test_errorAccessors() { + let apiDescription = TestErrorDocument.APIDescription( + version: "1.0", + meta: .none + ) + let errors = [ + BasicJSONAPIError.error(.init(status: "500")) + ] + let meta = TestMeta(hello: "world") + let links = TestLinks(testLink: .init(url: "http://google.com")) + let errorDoc = TestErrorDocument( + apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links + ) + + guard case let .errors(testErrors, meta: testMeta, links: testLinks) = errorDoc.body else { + XCTFail("Expected an error body") + return + } + + XCTAssertEqual(testErrors, errors) + XCTAssertEqual(testMeta, meta) + XCTAssertEqual(testLinks, links) + + XCTAssertEqual(errorDoc.apiDescription, apiDescription) + XCTAssertEqual(errorDoc.errors, errors) + XCTAssertEqual(errorDoc.meta, meta) + XCTAssertEqual(errorDoc.links, links) + + let equivalentDocument = TestDocument( + apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links + ) + + XCTAssert(equivalentDocument == errorDoc) + XCTAssert(errorDoc == equivalentDocument) + } + + func test_successAccessors() { + let apiDescription = TestErrorDocument.APIDescription( + version: "1.0", + meta: .none + ) + let primaryResource = TestType( + id: "123", + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + let resourceBody = SingleResourceBody(resourceObject: primaryResource) + let includedResource = TestType( + id: "456", + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + let includes = Includes(values: [.init(includedResource)]) + let meta = TestMeta(hello: "world") + let links = TestLinks(testLink: .init(url: "http://google.com")) + let successDoc = TestSuccessDocument( + apiDescription: apiDescription, + body: resourceBody, + includes: includes, + meta: meta, + links: links + ) + + guard case let .data(data) = successDoc.body else { + XCTFail("Expected an data body") + return + } + + XCTAssertEqual(data.primary, resourceBody) + XCTAssertEqual(data.includes, includes) + XCTAssertEqual(data.meta, meta) + XCTAssertEqual(data.links, links) + + XCTAssertEqual(successDoc.data, data) + XCTAssertEqual(successDoc.apiDescription, apiDescription) + XCTAssertEqual(successDoc.primaryResource, resourceBody) + XCTAssertEqual(successDoc.includes, includes) + XCTAssertEqual(successDoc.meta, meta) + XCTAssertEqual(successDoc.links, links) + + let equivalentDocument = TestDocument( + apiDescription: apiDescription, + body: resourceBody, + includes: includes, + meta: meta, + links: links + ) + + XCTAssert(equivalentDocument == successDoc) + XCTAssert(successDoc == equivalentDocument) + } +} + +// MARK: - Test Type +extension SuccessAndErrorDocumentTests { + enum TestTypeDescription: ResourceObjectDescription { + static let jsonType: String = "tests" + + typealias Attributes = NoAttributes + + typealias Relationships = NoRelationships + } + + struct TestMeta: JSONAPI.Meta { + let hello: String + } + + struct TestLinks: JSONAPI.Links { + let testLink: Link + } + + typealias TestType = ResourceObject + + typealias TestDocument = Document, TestMeta, TestLinks, Include1, APIDescription, BasicJSONAPIError> + + typealias TestSuccessDocument = TestDocument.SuccessDocument + typealias TestErrorDocument = TestDocument.ErrorDocument +} diff --git a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift index 21172ed..a3c3552 100644 --- a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift +++ b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift @@ -64,6 +64,7 @@ final class GenericJSONAPIErrorTests: XCTestCase { let error = decoded(type: TestGenericJSONAPIError.self, data: data) XCTAssertEqual(error, .unknown) + XCTAssertEqual(String(describing: error), "unknown error") } func test_encode() { diff --git a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift new file mode 100644 index 0000000..2db4f75 --- /dev/null +++ b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift @@ -0,0 +1,109 @@ +// +// IncludesDecodingErrorTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/14/19. +// + +import XCTest +import JSONAPI + +final class IncludesDecodingErrorTests: XCTestCase { + func test_unexpectedIncludeType() { + var error1: Error! + XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: three_different_type_includes)) { (error: Error) -> Void in + XCTAssertEqual( + (error as? IncludesDecodingError)?.idx, + 2 + ) + + XCTAssertEqual( + (error as? IncludesDecodingError).map(String.init(describing:)), +""" +Include 3 failed to parse: \nCould not have been Include Type 1 because: +found JSON:API type "test_entity4" but expected "test_entity1" + +Could not have been Include Type 2 because: +found JSON:API type "test_entity4" but expected "test_entity2" +""" + ) + + error1 = error + } + + // now test that we get the same error from a different test stub + XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: four_different_type_includes)) { (error2: Error) -> Void in + XCTAssertEqual( + error1 as? IncludesDecodingError, + error2 as? IncludesDecodingError + ) + } + } +} + +// MARK: - Test Types +extension IncludesDecodingErrorTests { + enum TestEntityType: ResourceObjectDescription { + + typealias Relationships = NoRelationships + + public static var jsonType: String { return "test_entity1" } + + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } + } + } + + typealias TestEntity = BasicEntity + + enum TestEntityType2: ResourceObjectDescription { + + public static var jsonType: String { return "test_entity2" } + + public struct Relationships: JSONAPI.Relationships { + let entity1: ToOneRelationship + } + + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } + } + } + + typealias TestEntity2 = BasicEntity + + enum TestEntityType4: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + typealias Relationships = NoRelationships + + public static var jsonType: String { return "test_entity4" } + } + + typealias TestEntity4 = BasicEntity + + enum TestEntityType6: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity6" } + + struct Relationships: JSONAPI.Relationships { + let entity4: ToOneRelationship + } + } + + typealias TestEntity6 = BasicEntity +} diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 5e5f593..e53b144 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -129,6 +129,10 @@ let four_different_type_includes = """ } } }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, { "type": "test_entity6", "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", @@ -140,10 +144,6 @@ let four_different_type_includes = """ } } } - }, - { - "type": "test_entity4", - "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" } ] """.data(using: .utf8)! diff --git a/Tests/JSONAPITests/Poly/PolyProxyTests.swift b/Tests/JSONAPITests/Poly/PolyProxyTests.swift index 582a2fc..efae216 100644 --- a/Tests/JSONAPITests/Poly/PolyProxyTests.swift +++ b/Tests/JSONAPITests/Poly/PolyProxyTests.swift @@ -15,6 +15,12 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(User.jsonType, "users") } + func test_CannotEncodeOrDecodePoly0() { + XCTAssertThrowsError(try testDecoder.decode(Poly0.self, from: poly_user_stub_1)) + + XCTAssertThrowsError(try testEncoder.encode(Poly0())) + } + func test_UserADecode() { let polyUserA = decoded(type: User.self, data: poly_user_stub_1) let userA = decoded(type: UserA.self, data: poly_user_stub_1) diff --git a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift index d0b1c56..a588b71 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift @@ -9,13 +9,14 @@ import Foundation import XCTest let testDecoder = JSONDecoder() +let testEncoder = JSONEncoder() func decoded(type: T.Type, data: Data) -> T { return try! testDecoder.decode(T.self, from: data) } func encoded(value: T) -> Data { - return try! JSONEncoder().encode(value) + return try! testEncoder.encode(value) } /// A helper function that tests that decode() == decode().encode().decode(). diff --git a/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift b/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift index 56070a0..2a340d1 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift @@ -33,12 +33,9 @@ func testEncodedPrimitive(at let wrapperObject = try! JSONSerialization.jsonObject(with: encodedAttributeData, options: []) as! [String: Any] let jsonObject = wrapperObject["x"] - guard let jsonAttribute = jsonObject as? Transform.From else { - XCTFail("Attribute did not encode to the correct type") - return - } + XCTAssert(jsonObject is Transform.From) - XCTAssertEqual(attribute.rawValue, jsonAttribute) + XCTAssertEqual(attribute.rawValue, jsonObject as? Transform.From) } /// This function attempts to just cast to the type, so it only works @@ -48,10 +45,7 @@ func testEncodedPrimitive(attribute: Attribute = Entity public typealias NewEntity = JSONAPI.ResourceObject + +extension String: JSONAPI.JSONAPIURL {} From 1010489a02425b4e59f0fe5f564987a97f9d6d80 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 16:59:00 -0800 Subject: [PATCH 25/30] compare(to:) bug fixes and test additions --- .../Comparisons/AttributesCompare.swift | 54 +++++- .../Comparisons/RelationshipsCompare.swift | 52 +++++- .../Comparisons/AttributesCompareTests.swift | 67 +++++-- .../Comparisons/DocumentCompareTests.swift | 2 + .../Optional+AbstractWrapper.swift | 16 ++ .../RelationshipsCompareTests.swift | 169 +++++++++++++++++- 6 files changed, 324 insertions(+), 36 deletions(-) create mode 100644 Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift diff --git a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift index 147432c..83aeea2 100644 --- a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift @@ -1,5 +1,5 @@ // -// File.swift +// AttributesCompare.swift // // // Created by Mathew Polzin on 11/3/19. @@ -24,12 +24,16 @@ extension Attributes { continue } - if (attributesEqual(child.value, otherChild.value)) { - comparisons[childLabel] = .same - } else { - let otherChildDescription = attributeDescription(of: otherChild.value) + do { + if (try attributesEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = attributeDescription(of: otherChild.value) - comparisons[childLabel] = .different(childDescription, otherChildDescription) + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } catch let error { + comparisons[childLabel] = .prebuilt(String(describing: error)) } } @@ -37,9 +41,20 @@ extension Attributes { } } -fileprivate func attributesEqual(_ one: Any, _ two: Any) -> Bool { +enum AttributeCompareError: Swift.Error, CustomStringConvertible { + case nonAttributeTypeProperty(String) + + var description: String { + switch self { + case .nonAttributeTypeProperty(let type): + return "comparison on non-JSON:API Attribute type (\(type)) not supported." + } + } +} + +fileprivate func attributesEqual(_ one: Any, _ two: Any) throws -> Bool { guard let attr = one as? AbstractAttribute else { - return false + throw AttributeCompareError.nonAttributeTypeProperty(String(describing: type(of: one))) } return attr.equals(two) @@ -55,6 +70,29 @@ protocol AbstractAttribute { func equals(_ other: Any) -> Bool } +extension Optional: AbstractAttribute where Wrapped: AbstractAttribute { + var abstractDescription: String { + switch self { + case .none: + return "nil" + case .some(let rel): + return rel.abstractDescription + } + } + + func equals(_ other: Any) -> Bool { + switch self { + case .none: + return (other as? _AbstractWrapper).map { $0.abstractSelf == nil } ?? false + case .some(let rel): + guard case let .some(otherVal) = (other as? _AbstractWrapper)?.abstractSelf else { + return rel.equals(other) + } + return rel.equals(otherVal) + } + } +} + extension Attribute: AbstractAttribute { var abstractDescription: String { String(describing: value) } diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift index 0c24386..9a8c010 100644 --- a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -24,12 +24,16 @@ extension Relationships { continue } - if (relationshipsEqual(child.value, otherChild.value)) { - comparisons[childLabel] = .same - } else { - let otherChildDescription = relationshipDescription(of: otherChild.value) + do { + if (try relationshipsEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = relationshipDescription(of: otherChild.value) - comparisons[childLabel] = .different(childDescription, otherChildDescription) + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } catch let error { + comparisons[childLabel] = .prebuilt(String(describing: error)) } } @@ -37,9 +41,20 @@ extension Relationships { } } -fileprivate func relationshipsEqual(_ one: Any, _ two: Any) -> Bool { +enum RelationshipCompareError: Swift.Error, CustomStringConvertible { + case nonRelationshipTypeProperty(String) + + var description: String { + switch self { + case .nonRelationshipTypeProperty(let type): + return "comparison on non-JSON:API Relationship type (\(type)) not supported." + } + } +} + +fileprivate func relationshipsEqual(_ one: Any, _ two: Any) throws -> Bool { guard let attr = one as? AbstractRelationship else { - return false + throw RelationshipCompareError.nonRelationshipTypeProperty(String(describing: type(of: one))) } return attr.equals(two) @@ -55,6 +70,29 @@ protocol AbstractRelationship { func equals(_ other: Any) -> Bool } +extension Optional: AbstractRelationship where Wrapped: AbstractRelationship { + var abstractDescription: String { + switch self { + case .none: + return "nil" + case .some(let rel): + return rel.abstractDescription + } + } + + func equals(_ other: Any) -> Bool { + switch self { + case .none: + return (other as? _AbstractWrapper).map { $0.abstractSelf == nil } ?? false + case .some(let rel): + guard case let .some(otherVal) = (other as? _AbstractWrapper)?.abstractSelf else { + return rel.equals(other) + } + return rel.equals(otherVal) + } + } +} + extension ToOneRelationship: AbstractRelationship { var abstractDescription: String { if meta is NoMetadata && links is NoLinks { diff --git a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift index e54031c..6141be6 100644 --- a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// AttributesCompareTests.swift // // // Created by Mathew Polzin on 11/3/19. @@ -17,7 +17,9 @@ final class AttributesCompareTests: XCTestCase { bool: true, double: 105.4, struct: .init(value: .init()), - transformed: try .init(rawValue: 10) + transformed: try .init(rawValue: 10), + optional: .init(value: 20), + optionalTransformed: try .init(rawValue: 10) ) let attr2 = attr1 @@ -27,7 +29,9 @@ final class AttributesCompareTests: XCTestCase { "bool": .same, "double": .same, "struct": .same, - "transformed": .same + "transformed": .same, + "optional": .same, + "optionalTransformed": .same ]) } @@ -38,7 +42,9 @@ final class AttributesCompareTests: XCTestCase { bool: true, double: 105.4, struct: .init(value: .init()), - transformed: try .init(rawValue: 10) + transformed: try .init(rawValue: 10), + optional: nil, + optionalTransformed: nil ) let attr2 = TestAttributes( string: "hello", @@ -46,7 +52,9 @@ final class AttributesCompareTests: XCTestCase { bool: false, double: 1.4, struct: .init(value: .init(val: "there")), - transformed: try .init(rawValue: 11) + transformed: try .init(rawValue: 11), + optional: .init(value: 20.5), + optionalTransformed: try .init(rawValue: 10) ) XCTAssertEqual(attr1.compare(to: attr2), [ @@ -55,7 +63,29 @@ final class AttributesCompareTests: XCTestCase { "bool": .different("true", "false"), "double": .different("105.4", "1.4"), "struct": .different("string: hello", "string: there"), - "transformed": .different("10", "11") + "transformed": .different("10", "11"), + "optional": .different("nil", "20.5"), + "optionalTransformed": .different("nil", "10") + ]) + } + + func test_nonAttributeTypes() { + let attr1 = NonAttributeTest( + string: "hello", + int: 10, + double: 11.2, + bool: true, + struct: .init(), + optional: nil + ) + + XCTAssertEqual(attr1.compare(to: attr1), [ + "string": .prebuilt("comparison on non-JSON:API Attribute type (String) not supported."), + "int": .prebuilt("comparison on non-JSON:API Attribute type (Int) not supported."), + "double": .prebuilt("comparison on non-JSON:API Attribute type (Double) not supported."), + "bool": .prebuilt("comparison on non-JSON:API Attribute type (Bool) not supported."), + "struct": .prebuilt("comparison on non-JSON:API Attribute type (Struct) not supported."), + "optional": .prebuilt("comparison on non-JSON:API Attribute type (Optional) not supported.") ]) } } @@ -67,16 +97,18 @@ private struct TestAttributes: JSONAPI.Attributes { let double: Attribute let `struct`: Attribute let transformed: TransformedAttribute + let optional: Attribute? + let optionalTransformed: TransformedAttribute? +} - struct Struct: Equatable, Codable, CustomStringConvertible { - let string: String +private struct Struct: Equatable, Codable, CustomStringConvertible { + let string: String - init(val: String = "hello") { - self.string = val - } - - var description: String { return "string: \(string)" } + init(val: String = "hello") { + self.string = val } + + var description: String { return "string: \(string)" } } private enum TestTransformer: Transformer { @@ -84,3 +116,12 @@ private enum TestTransformer: Transformer { return "\(value)" } } + +private struct NonAttributeTest: JSONAPI.Attributes { + let string: String + let int: Int + let double: Double + let bool: Bool + let `struct`: Struct + let optional: Int? +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index 9cbf93f..8b464e6 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -21,6 +21,8 @@ final class DocumentCompareTests: XCTestCase { XCTAssertTrue(d8.compare(to: d8).differences.isEmpty) XCTAssertTrue(d9.compare(to: d9).differences.isEmpty) XCTAssertTrue(d10.compare(to: d10).differences.isEmpty) + + XCTAssertEqual(String(describing: d1.compare(to: d1).body), "same") } func test_errorAndData() { diff --git a/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift b/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift new file mode 100644 index 0000000..2a068d0 --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift @@ -0,0 +1,16 @@ +// +// Optional+AbstractWrapper.swift +// JSONAPITesting +// +// Created by Mathew Polzin on 11/15/19. +// + +protocol _AbstractWrapper { + var abstractSelf: Any? { get } +} + +extension Optional: _AbstractWrapper { + var abstractSelf: Any? { + return self + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift index b2dc83e..44b720a 100644 --- a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// RelationshipCompareTests.swift // // // Created by Mathew Polzin on 11/5/19. @@ -11,22 +11,175 @@ import JSONAPITesting final class RelationshipsCompareTests: XCTestCase { func test_same() { - // TODO: write test + let r1 = TestRelationships( + a: t1, + b: t2, + c: t3, + d: t4 + ) + let r2 = r1 + + XCTAssertTrue(r1.compare(to: r2).allSatisfy { $0.value == .same }) + + let r3 = TestRelationships( + a: t1_differentId, + b: t2_differentLinks, + c: t3_differentId, + d: t4_differentLinks + ) + let r4 = r3 + + XCTAssertTrue(r3.compare(to: r4).allSatisfy { $0.value == .same }) + + let r5 = TestRelationships( + a: nil, + b: nil, + c: nil, + d: nil + ) + let r6 = r5 + + XCTAssertTrue(r5.compare(to: r6).allSatisfy { $0.value == .same }) } func test_differentIds() { - // TODO: write test - } + let r1 = TestRelationships( + a: t1, + b: nil, + c: t3, + d: nil + ) - func test_differentTypes() { - // TODO: write test + let r2 = TestRelationships( + a: t1_differentId, + b: nil, + c: t3_differentId, + d: nil + ) + + XCTAssertEqual(r1.compare(to: r2), [ + "a": .different("Id(123)", "Id(999)"), + "b": .same, + "c": .different("123, 456", "999, 1010"), + "d": .same + ]) } func test_differentMetadata() { - // TODO: write test + let r1 = TestRelationships( + a: nil, + b: t2, + c: nil, + d: t4 + ) + + let r2 = TestRelationships( + a: nil, + b: t2_differentMeta, + c: nil, + d: t4_differentMeta + ) + + XCTAssertEqual(r1.compare(to: r2), [ + "a": .same, + "b": .different(#"("Id(456)", "hello: world", "link: http://google.com")"#, #"("Id(456)", "hello: there", "link: http://google.com")"#), + "c": .same, + "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: there", "link: http://google.com")"#) + ]) } func test_differentLinks() { - // TODO: write tests + let r1 = TestRelationships( + a: nil, + b: t2, + c: nil, + d: t4 + ) + + let r2 = TestRelationships( + a: nil, + b: t2_differentLinks, + c: nil, + d: t4_differentLinks + ) + + XCTAssertEqual(r1.compare(to: r2), [ + "a": .same, + "b": .different(#"("Id(456)", "hello: world", "link: http://google.com")"#, #"("Id(456)", "hello: world", "link: http://yahoo.com")"#), + "c": .same, + "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: world", "link: http://yahoo.com")"#) + ]) + } + + func test_nonRelationshipTypes() { + let r1 = TestNonRelationships( + a: .init(attributes: .none, relationships: .none, meta: .none, links: .none), + b: false, + c: 10, + d: "1234" + ) + + XCTAssertEqual(r1.compare(to: r1), [ + "a": .prebuilt("comparison on non-JSON:API Relationship type (ResourceObject) not supported."), + "b": .prebuilt("comparison on non-JSON:API Relationship type (Bool) not supported."), + "c": .prebuilt("comparison on non-JSON:API Relationship type (Int) not supported."), + "d": .prebuilt("comparison on non-JSON:API Relationship type (Id>) not supported.") + ]) + } + + let t1 = ToOneRelationship(id: "123") + let t2 = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) + let t3 = ToManyRelationship(ids: ["123", "456"]) + let t4 = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) + + let t1_differentId = ToOneRelationship(id: "999") + let t3_differentId = ToManyRelationship(ids: ["999", "1010"]) + + let t2_differentLinks = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) + let t4_differentLinks = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) + + let t2_differentMeta = ToOneRelationship(id: "456", meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) + let t4_differentMeta = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) +} + +// MARK: - Test Types +extension RelationshipsCompareTests { + enum TestTypeDescription: ResourceObjectDescription { + static let jsonType: String = "test" + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + typealias TestType = ResourceObject + + struct TestMeta: JSONAPI.Meta, CustomDebugStringConvertible { + let hello: String + + var debugDescription: String { + "hello: \(hello)" + } + } + + struct TestLinks: JSONAPI.Links, CustomDebugStringConvertible { + let link: Link + + var debugDescription: String { + "link: \(link.url)" + } + } + + struct TestRelationships: JSONAPI.Relationships { + let a: ToOneRelationship? + let b: ToOneRelationship? + let c: ToManyRelationship? + let d: ToManyRelationship? + } + + struct TestNonRelationships: JSONAPI.Relationships { + let a: TestType + let b: Bool + let c: Int + let d: JSONAPI.Id } } From a6b7d7a94a4a4ec3c30939b13df14222790f0146 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:05:45 -0800 Subject: [PATCH 26/30] woops, abstract wrapper protocol landed in wrong module by accident --- Sources/JSONAPI/Document/Document.swift | 2 -- Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift | 4 ++-- .../Comparisons/Optional+AbstractWrapper.swift | 0 3 files changed, 2 insertions(+), 4 deletions(-) rename {Tests/JSONAPITestingTests => Sources/JSONAPITesting}/Comparisons/Optional+AbstractWrapper.swift (100%) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 1343559..a266216 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -408,8 +408,6 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: throw DocumentDecodingError(error) } - // TODO come back to this and make robust - guard let metaVal = meta else { throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath) } diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index 36a1a95..2c555bd 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -158,7 +158,7 @@ struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainer forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { guard shouldAllow(key: key) else { return KeyedEncodingContainer( - // TODO: not needed by JSONAPI library, but for completeness could + // NOTE: not needed by JSONAPI library, but for completeness could // add an EmptyObjectEncoder that can be returned here so that // at least nothing gets encoded within the nested container SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, @@ -176,7 +176,7 @@ struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainer public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { guard shouldAllow(key: key) else { - // TODO: not needed by JSONAPI library, but for completeness could + // NOTE: not needed by JSONAPI library, but for completeness could // add an EmptyObjectEncoder that can be returned here so that // at least nothing gets encoded within the nested container return wrappedContainer.nestedUnkeyedContainer(forKey: key) diff --git a/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift b/Sources/JSONAPITesting/Comparisons/Optional+AbstractWrapper.swift similarity index 100% rename from Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift rename to Sources/JSONAPITesting/Comparisons/Optional+AbstractWrapper.swift From 4a7a14b1b0ee18b9275b9a72758fb693d9200e59 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:36:42 -0800 Subject: [PATCH 27/30] Add test coverage for resource object compare(to:) --- .../ResourceObjectCompareTests.swift | 119 ++++++++++++++++-- 1 file changed, 106 insertions(+), 13 deletions(-) diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index d62f9e2..1f3a245 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -12,32 +12,71 @@ import JSONAPITesting final class ResourceObjectCompareTests: XCTestCase { func test_same() { XCTAssertTrue(test1.compare(to: test1).differences.isEmpty) - XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) + XCTAssertTrue(test1_differentId.compare(to: test1_differentId).differences.isEmpty) + XCTAssertTrue(test1_differentAttributes.compare(to: test1_differentAttributes).differences.isEmpty) } func test_differentAttributes() { - // TODO: write actual test - print(test1.compare(to: test2).differences.map { "\($0): \($1)" }.joined(separator: ", ")) + XCTAssertEqual(test1.compare(to: test1_differentAttributes).differences, [ + "'favoriteColor' attribute": #"Optional("red") ≠ nil"#, + "'name' attribute": "James ≠ Fred", + "'age' attribute": "12 ≠ 10" + ]) } func test_differentRelationships() { - // TODO: write test - } - - func test_differentTypes() { - // TODO: write test + XCTAssertEqual(test1.compare(to: test1_differentRelationships).differences, [ + "'parents' relationship": "4, 5 ≠ 3", + "'bestFriend' relationship": "Optional(Id(3)) ≠ nil" + ]) } func test_differentIds() { - // TODO: write test + XCTAssertEqual(test1.compare(to: test1_differentId).differences, [ + "id": "2 ≠ 3" + ]) } func test_differentMetadata() { - // TODO: write test + let test1 = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 10), + links: .init(link: .init(url: "http://google.com")) + ) + let test1_differentMeta = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 12), + links: .init(link: .init(url: "http://google.com")) + ) + + XCTAssertEqual(test1.compare(to: test1_differentMeta).differences, [ + "meta": "total: 10 ≠ total: 12" + ]) } func test_differentLinks() { - // TODO: write test + let test1 = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 10), + links: .init(link: .init(url: "http://google.com")) + ) + let test1_differentLinks = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 10), + links: .init(link: .init(url: "http://yahoo.com")) + ) + + XCTAssertEqual(test1.compare(to: test1_differentLinks).differences, [ + "links": "link: http://google.com ≠ link: http://yahoo.com" + ]) } fileprivate let test1 = TestType( @@ -54,15 +93,43 @@ final class ResourceObjectCompareTests: XCTestCase { links: .none ) - fileprivate let test2 = TestType( + fileprivate let test1_differentId = TestType( id: "3", + attributes: .init( + name: "James", + age: 12, + favoriteColor: "red"), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + + fileprivate let test1_differentAttributes = TestType( + id: "2", attributes: .init( name: "Fred", age: 10, favoriteColor: .init(value: nil)), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + + fileprivate let test1_differentRelationships = TestType( + id: "2", + attributes: .init( + name: "James", + age: 12, + favoriteColor: "red"), relationships: .init( bestFriend: nil, - parents: ["1"] + parents: ["3"] ), meta: .none, links: .none @@ -85,3 +152,29 @@ private enum TestDescription: JSONAPI.ResourceObjectDescription { } private typealias TestType = ResourceObject + +private struct TestMetadata: JSONAPI.Meta, CustomStringConvertible { + let total: Int + + var description: String { + "total: \(total)" + } +} + +private struct TestLinks: JSONAPI.Links, CustomStringConvertible { + let link: Link + + var description: String { + "link: \(link.url)" + } +} + +private enum TestDescription2: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type2" + + typealias Attributes = NoAttributes + + typealias Relationships = NoRelationships +} + +private typealias TestType2 = ResourceObject From 8ee04d89323a23acf80a98aebd5c2cf02a5c68d3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:38:04 -0800 Subject: [PATCH 28/30] generate linuxmain --- .../JSONAPITestingTests/XCTestManifests.swift | 38 ++++++++++++++++++- Tests/JSONAPITests/XCTestManifests.swift | 34 ++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/Tests/JSONAPITestingTests/XCTestManifests.swift b/Tests/JSONAPITestingTests/XCTestManifests.swift index b1b07fb..e77e922 100644 --- a/Tests/JSONAPITestingTests/XCTestManifests.swift +++ b/Tests/JSONAPITestingTests/XCTestManifests.swift @@ -1,6 +1,18 @@ #if !canImport(ObjectiveC) import XCTest +extension ArrayCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ArrayCompareTests = [ + ("test_differentLengths", test_differentLengths), + ("test_differentValues", test_differentValues), + ("test_reducePrebuilt", test_reducePrebuilt), + ("test_same", test_same), + ] +} + extension Attribute_LiteralTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -41,6 +53,7 @@ extension AttributesCompareTests { // to regenerate. static let __allTests__AttributesCompareTests = [ ("test_differentAttributes", test_differentAttributes), + ("test_nonAttributeTypes", test_nonAttributeTypes), ("test_sameAttributes", test_sameAttributes), ] } @@ -50,10 +63,14 @@ extension DocumentCompareTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__DocumentCompareTests = [ + ("test_differentAPIDescription", test_differentAPIDescription), ("test_differentData", test_differentData), ("test_differentErrors", test_differentErrors), + ("test_differentLinks", test_differentLinks), + ("test_differentMetadata", test_differentMetadata), ("test_errorAndData", test_errorAndData), ("test_same", test_same), + ("test_sameErrorsDifferentMetadata", test_sameErrorsDifferentMetadata), ] } @@ -103,18 +120,36 @@ extension Relationship_LiteralTests { ] } +extension RelationshipsCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__RelationshipsCompareTests = [ + ("test_differentIds", test_differentIds), + ("test_differentLinks", test_differentLinks), + ("test_differentMetadata", test_differentMetadata), + ("test_nonRelationshipTypes", test_nonRelationshipTypes), + ("test_same", test_same), + ] +} + extension ResourceObjectCompareTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` // to regenerate. static let __allTests__ResourceObjectCompareTests = [ - ("test_different", test_different), + ("test_differentAttributes", test_differentAttributes), + ("test_differentIds", test_differentIds), + ("test_differentLinks", test_differentLinks), + ("test_differentMetadata", test_differentMetadata), + ("test_differentRelationships", test_differentRelationships), ("test_same", test_same), ] } public func __allTests() -> [XCTestCaseEntry] { return [ + testCase(ArrayCompareTests.__allTests__ArrayCompareTests), testCase(Attribute_LiteralTests.__allTests__Attribute_LiteralTests), testCase(AttributesCompareTests.__allTests__AttributesCompareTests), testCase(DocumentCompareTests.__allTests__DocumentCompareTests), @@ -122,6 +157,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(Id_LiteralTests.__allTests__Id_LiteralTests), testCase(IncludesCompareTests.__allTests__IncludesCompareTests), testCase(Relationship_LiteralTests.__allTests__Relationship_LiteralTests), + testCase(RelationshipsCompareTests.__allTests__RelationshipsCompareTests), testCase(ResourceObjectCompareTests.__allTests__ResourceObjectCompareTests), ] } diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 0768089..ed1efbb 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -22,10 +22,12 @@ extension AttributeTests { // to regenerate. static let __allTests__AttributeTests = [ ("test_AttributeConstructor", test_AttributeConstructor), + ("test_AttributeRawType", test_AttributeRawType), ("test_EncodedPrimitives", test_EncodedPrimitives), ("test_NullableIsEqualToNonNullableIfNotNil", test_NullableIsEqualToNonNullableIfNotNil), ("test_NullableIsNullIfNil", test_NullableIsNullIfNil), ("test_TransformedAttributeNoThrow", test_TransformedAttributeNoThrow), + ("test_TransformedAttributeRawType", test_TransformedAttributeRawType), ("test_TransformedAttributeReversNoThrow", test_TransformedAttributeReversNoThrow), ("test_TransformedAttributeThrows", test_TransformedAttributeThrows), ] @@ -91,10 +93,13 @@ extension DocumentDecodingErrorTests { // to regenerate. static let __allTests__DocumentDecodingErrorTests = [ ("test_include_failure", test_include_failure), + ("test_include_failure2", test_include_failure2), ("test_manyPrimaryResource_failure", test_manyPrimaryResource_failure), ("test_manyPrimaryResource_missing", test_manyPrimaryResource_missing), ("test_singlePrimaryResource_failure", test_singlePrimaryResource_failure), ("test_singlePrimaryResource_missing", test_singlePrimaryResource_missing), + ("test_wantError_foundSuccess", test_wantError_foundSuccess), + ("test_wantSuccess_foundError", test_wantSuccess_foundError), ] } @@ -283,6 +288,15 @@ extension IncludedTests { ] } +extension IncludesDecodingErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__IncludesDecodingErrorTests = [ + ("test_unexpectedIncludeType", test_unexpectedIncludeType), + ] +} + extension LinksTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -318,6 +332,7 @@ extension PolyProxyTests { static let __allTests__PolyProxyTests = [ ("test_AsymmetricEncodeDecodeUserA", test_AsymmetricEncodeDecodeUserA), ("test_AsymmetricEncodeDecodeUserB", test_AsymmetricEncodeDecodeUserB), + ("test_CannotEncodeOrDecodePoly0", test_CannotEncodeOrDecodePoly0), ("test_generalReasonableness", test_generalReasonableness), ("test_UserAAndBEncodeEquality", test_UserAAndBEncodeEquality), ("test_UserADecode", test_UserADecode), @@ -391,8 +406,13 @@ extension ResourceObjectDecodingErrorTests { ("test_oneTypeVsAnother_relationship", test_oneTypeVsAnother_relationship), ("test_required_attribute", test_required_attribute), ("test_required_relationship", test_required_relationship), + ("test_transformed_attribute", test_transformed_attribute), + ("test_transformed_attribute2", test_transformed_attribute2), ("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship), - ("test_wrongType", test_wrongType), + ("test_type_missing", test_type_missing), + ("test_type_null", test_type_null), + ("test_wrongDecodedType", test_wrongDecodedType), + ("test_wrongJSONAPIType", test_wrongJSONAPIType), ] } @@ -538,6 +558,16 @@ extension SparseFieldsetTests { ] } +extension SuccessAndErrorDocumentTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__SuccessAndErrorDocumentTests = [ + ("test_errorAccessors", test_errorAccessors), + ("test_successAccessors", test_successAccessors), + ] +} + extension TransformerTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -561,6 +591,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests), testCase(IncludedTests.__allTests__IncludedTests), + testCase(IncludesDecodingErrorTests.__allTests__IncludesDecodingErrorTests), testCase(LinksTests.__allTests__LinksTests), testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests), testCase(PolyProxyTests.__allTests__PolyProxyTests), @@ -571,6 +602,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(ResourceObjectTests.__allTests__ResourceObjectTests), testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests), + testCase(SuccessAndErrorDocumentTests.__allTests__SuccessAndErrorDocumentTests), testCase(TransformerTests.__allTests__TransformerTests), ] } From c7696d83fa57b9e28ddbd218d6119d05b92da0c3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:46:53 -0800 Subject: [PATCH 29/30] update Playground pages to run --- .../Contents.swift | 2 +- .../Pages/Usage.xcplaygroundpage/Contents.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index eabd561..96a7168 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -37,7 +37,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document> +typealias Document = JSONAPI.Document> // MARK: Entity Definitions diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index e5434e5..5917bef 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -64,11 +64,11 @@ if case let .data(bodyData) = peopleResponse.body { // MARK: - Work in the abstract print("-----") -func process(document: T) { - guard case let .data(body) = document.body else { +func process(document: T) { + guard let body = document.body.data else { return } - let x: T.Body.Data = body + let x: T.BodyData = body } process(document: peopleResponse) From 96da1b4e215a5dcd5a9819814b32fbdcb1722723 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 23:15:32 -0800 Subject: [PATCH 30/30] update documentation --- .../Contents.swift | 2 +- README.md | 692 ++---------------- documentation/usage.md | 611 ++++++++++++++++ 3 files changed, 668 insertions(+), 637 deletions(-) create mode 100644 documentation/usage.md diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 96a7168..280a614 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -37,7 +37,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document> +typealias Document = JSONAPI.Document> // MARK: Entity Definitions diff --git a/README.md b/README.md index c535288..7a1c6cd 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ -:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. +:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Correct code will always compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (i.e. declaratively) like you might for unit testing. Writing a client that uses this framework to ingest and decode JSON API Compliant API responses is much less painful. ## Quick Start @@ -26,69 +26,21 @@ See the JSON API Spec here: https://jsonapi.org/format/ This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README. ## Table of Contents - -- [JSONAPI](#jsonapi) +- JSONAPI - [Primary Goals](#primary-goals) - - [Caveat](#caveat) - [Dev Environment](#dev-environment) - [Prerequisites](#prerequisites) - [Swift Package Manager](#swift-package-manager) - - [CocoaPods](#cocoapods) - [Xcode project](#xcode-project) + - [CocoaPods](#cocoapods) - [Running the Playground](#running-the-playground) - [Project Status](#project-status) - - [JSON:API](#jsonapi) - - [Document](#document) - - [Resource Object](#resource-object) - - [Relationship Object](#relationship-object) - - [Links Object](#links-object) - - [Misc](#misc) - - [Testing](#testing) - - [Resource Object Validator](#resource-object-validator) - - [Potential Improvements](#potential-improvements) - - [Usage](#usage) - - [`JSONAPI.ResourceObjectDescription`](#jsonapiresourceobjectdescription) - - [`JSONAPI.ResourceObject`](#jsonapiresourceobject) - - [`Meta`](#meta) - - [`Links`](#links) - - [`MaybeRawId`](#mayberawid) - - [`RawIdType`](#rawidtype) - - [Convenient `typealiases`](#convenient-typealiases) - - [`JSONAPI.Relationships`](#jsonapirelationships) - - [`JSONAPI.Attributes`](#jsonapiattributes) - - [`Transformer`](#transformer) - - [`Validator`](#validator) - - [Computed `Attribute`](#computed-attribute) - - [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) - - [`JSONAPI.Document`](#jsonapidocument) - - [`ResourceBody`](#resourcebody) - - [nullable `PrimaryResource`](#nullable-primaryresource) - - [`MetaType`](#metatype) - - [`LinksType`](#linkstype) - - [`IncludeType`](#includetype) - - [`APIDescriptionType`](#apidescriptiontype) - - [`Error`](#error) - - [`UnknownJSONAPIError`](#unknownjsonapierror) - - [`BasicJSONAPIError`](#basicjsonapierror) - - [`GenericJSONAPIError`](#genericjsonapierror) - - [`JSONAPI.Meta`](#jsonapimeta) - - [`JSONAPI.Links`](#jsonapilinks) - - [`JSONAPI.RawIdType`](#jsonapirawidtype) - - [Sparse Fieldsets](#sparse-fieldsets) - - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) - - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) - - [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships) - - [Tapping](#tapping) - - [Replacing](#replacing) - - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - - [Meta-Attributes](#meta-attributes) - - [Meta-Relationships](#meta-relationships) - [Example](#example) - - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) - - [Server Pseudo-example](#server-pseudo-example) - - [Client Pseudo-example](#client-pseudo-example) + - [Usage](./documentation/usage.md) - [JSONAPI+Testing](#jsonapitesting) + - [Literal Expressibility](#literal-expressibility) + - [Resource Object `check()`](#resource-object-check) + - [Comparisons](#comparisons) - [JSONAPI+Arbitrary](#jsonapiarbitrary) - [JSONAPI+OpenAPI](#jsonapiopenapi) @@ -99,6 +51,7 @@ The primary goals of this framework are: 2. Leverage `Codable` to avoid additional outside dependencies and get operability with non-JSON encoders/decoders for free. 3. Do not sacrifice type safety. 4. Be platform agnostic so that Swift code can be written once and used by both the client and the server. +5. Provide _human readable_ error output. The errors thrown when decoding an API response and the results of the `JSONAPITesting` framework's `compare(to:)` functions all have digestible human readable descriptions (just use `String(describing:)`). ### Caveat The big caveat is that, although the aim is to support the JSON API spec, this framework ends up being _naturally_ opinionated about certain things that the API Spec does not specify. These caveats are largely a side effect of attempting to write the library in a "Swifty" way. @@ -108,7 +61,7 @@ If you find something wrong with this library and it isn't already mentioned und ## Dev Environment ### Prerequisites 1. Swift 5.1+ -2. Swift Package Manager *OR* Cocoapods +2. Swift Package Manager, Xcode 11+, or Cocoapods ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. @@ -116,6 +69,12 @@ Just include the following in your package's dependencies and add `JSONAPI` to t .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.2.0")) ``` +### Xcode project +To create an Xcode project for JSONAPI, run +`swift package generate-xcodeproj` + +With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working. + ### CocoaPods To use this framework in your project via Cocoapods, add the following dependencies to your Podfile. ``` @@ -123,12 +82,6 @@ To use this framework in your project via Cocoapods, add the following dependenc pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' ``` -### Xcode project -To create an Xcode project for JSONAPI, run -`swift package generate-xcodeproj` - -With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working. - ### Running the Playground To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace. @@ -181,575 +134,6 @@ These ideas could be implemented in future versions. - [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. - [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default). -## Usage - -In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**. - -### `JSONAPI.ResourceObjectDescription` - -A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: - -```swift -enum PersonDescription: IdentifiedResourceObjectDescription { - static var jsonType: String { return "people" } - - struct Attributes: JSONAPI.Attributes { - let name: Attribute<[String]> - let favoriteColor: Attribute - } - - struct Relationships: JSONAPI.Relationships { - let friends: ToManyRelationship - } -} -``` - -The requirements of a `ResourceObjectDescription` are: -1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type". -2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes` -3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships` - -Note that an `enum` type is used here for the `ResourceObjectDescription`; it could have been a `struct`, but `ResourceObjectDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job. - -This readme doesn't go into detail on the **SPEC**, but the following *Resource Object* would be described by the above `PersonDescription`: - -```json -{ - "type": "people", - "id": "9", - "attributes": { - "name": [ - "Jane", - "Doe" - ], - "favoriteColor": "Green" - }, - "relationships": { - "friends": { - "data": [ - { - "id": "7", - "type": "people" - }, - { - "id": "8", - "type": "people" - } - ] - } - } -} -``` - -### `JSONAPI.ResourceObject` - -Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. - -The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*. - -A `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`. - -#### `Meta` - -The second generic specialization on `ResourceObject` is `Meta`. This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules. You can use `NoMetadata` if you do not need to package any metadata with the `ResourceObject`. - -#### `Links` - -The third generic specialization on `ResourceObject` is `Links`. This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document. You can use `NoLinks` if you do not need to package any links with the `ResourceObject`. - -#### `MaybeRawId` - -The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`). - -##### `RawIdType` - -The raw type of `Id` to use for the `ResourceObject`. The actual `Id` of the `ResourceObject` will not be a `RawIdType`, though. The `Id` will package a value of `RawIdType` with a specialized reference back to the `ResourceObject` type it identifies. This just looks like `Id>`. - -Having the `ResourceObject` type associated with the `Id` makes it easy to store all of your resource objects in a hash broken out by `ResourceObject` type; You can pass `Ids` around and always know where to look for the `ResourceObject` to which the `Id` refers. This encapsulation provides some type safety because the Ids of two `ResourceObjects` with the "raw ID" of `"1"` but different types will not compare as equal. - -A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject`. This is often a `String` or a `UUID`. - -#### Convenient `typealiases` - -Often you can use one `RawIdType` for many if not all of your `ResourceObjects`. That means you can save yourself some boilerplate by using `typealias`es like the following: -```swift -public typealias ResourceObject = JSONAPI.ResourceObject - -public typealias NewResourceObject = JSONAPI.ResourceObject -``` - -It can also be nice to create a `typealias` for each type of resource object you want to work with: -```swift -typealias Person = ResourceObject - -typealias NewPerson = NewResourceObject -``` - -Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. - -### `JSONAPI.Relationships` - -There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. - -In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document. - -To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: -```swift -let nullableRelative: ToOneRelationship -``` - -A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: -```swift -typealias Relationships = NoRelationships -``` - -`Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): -```swift -let friendIds: [Person.Identifier] = person ~> \.friends -``` - -### `JSONAPI.Attributes` - -The `Attributes` of a `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`. - -To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional: -```swift -let optionalAttribute: Attribute? -``` - -To describe an attribute that is expected to exist but might have a `null` value, you make the value within the `Attribute` optional: -```swift -let nullableAttribute: Attribute -``` - -A resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`: -```swift -typealias Attributes = NoAttributes -``` - -As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows: -```swift -let favoriteColor: String = person.favoriteColor -``` - -: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] -``` - -#### `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`. - -A `Transformer` just provides one static function that transforms one type to another. You might define one for an ISO 8601 compliant `Date` like this: -```swift -enum ISODateTransformer: Transformer { - public static func transform(_ value: String) throws -> Date { - // parse Date out of input and return - } -} -``` - -Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`: -```swift -let date: TransformedAttribute -``` - -Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation. - -If you make your `Transformer` a `ReversibleTransformer` then your life will be a bit easier when you construct `TransformedAttributes` because you have access to initializers for both the pre- and post-transformed value types. Continuing with the above example of a `ISODateTransformer`: -```swift -extension ISODateTransformer: ReversibleTransformer { - public static func reverse(_ value: Date) throws -> String { - // serialize Date to a String - } -} - -let exampleAttribute = try? TransformedAttribute(transformedValue: Date()) -let otherAttribute = try? TransformedAttribute(rawValue: "2018-12-01 09:06:41 +0000") -``` - -#### `Validator` - -You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid. - -#### Computed `Attribute` - -You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property. - -```swift -public var fullName: Attribute { - return name.map { $0.joined(separator: " ") } -} -``` - -If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. - -### Copying/Mutating `ResourceObjects` -`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`: -1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. -2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. - -The above can be accomplished with code like the following: - -```swift -// use case 1 -let person1 = person.withNewIdentifier() - -// use case 2 -let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self) - -let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232") -``` - -### `JSONAPI.Document` - -The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included resource objects could be decoded as follows: -```swift -let decoder = JSONDecoder() - -let responseStructure = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError>.self - -let document = try decoder.decode(responseStructure, from: data) -``` - -A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata." - -#### `ResourceBody` - -The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. - -If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead. - -##### nullable `PrimaryResource` - -If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON. - -You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` because the **SPEC** requires that an empty document in that case be represented by an empty array rather than `null`. - -#### `MetaType` - -The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry: -```json -{ - "meta": { - "total": 100, - "limit": 50, - "offset": 50 - } -} -``` - -You would then create the following `Meta` type: -```swift -struct PageMetadata: JSONAPI.Meta { - let total: Int - let limit: Int - let offset: Int -} -``` - -You can always use `NoMetadata` if this JSON API feature is not needed. - -#### `LinksType` - -The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks). - -#### `IncludeType` - -The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. - -**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_. - -To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1` instead of `NoIncludes`. - -#### `APIDescriptionType` - -The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API. - -You can specify this is not part of the document by using the `NoAPIDescription` type. - -You can describe the API by a version with no metadata by using `APIDescription`. - -You can supply any `JSONAPI.Meta` type as the metadata type of the API description. - -#### `Error` - -The final generic type of a `JSONAPIDocument` is the `Error`. - -You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`. - -##### `UnknownJSONAPIError` -The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described. - -##### `BasicJSONAPIError` -The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects. - -🗒Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type. - -The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError>`. - -The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary. - -As an example, let's say you have the following `Document` type that is destined for errors: -```swift -typealias ErrorDoc = JSONAPI.Document> -``` -And you've parsed an error response -```swift -let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData) -``` -You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body: -```swift -switch errorResponse.body { -case .data: - print("cool, data!") - -case .errors(let errors, let meta, let links): - let errorDetails = errors.compactMap { $0.payload?.detail } - - print("error details: \(errorDetails)") -} -``` - -##### `GenericJSONAPIError` -This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError` as the error type for a `Document`. - -### `JSONAPI.Meta` - -A `Meta` struct is totally open-ended. It is described by the **SPEC** as a place to put any information that does not fit into the standard JSON API Document structure anywhere else. - -You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`. - -If you need to support metadata with structure that is not pre-determined, consider an "Any Codable" type such as that found at https://github.com/Flight-School/AnyCodable. - -### `JSONAPI.Links` - -A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included. - -You can specify `NoLinks` if the part of the document being described should not contain any `Links`. - -### `JSONAPI.RawIdType` - -If you want to create new `JSONAPI.ResourceObject` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`): -```swift -extension UUID: CreatableRawIdType { - public static func unique() -> UUID { - return UUID() - } -} - -extension String: CreatableRawIdType { - public static func unique() -> String { - return UUID().uuidString - } -} -``` - -### Sparse Fieldsets -Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork. - -You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`. - -There is a sparse fieldsets example included with this repository as a Playground page. - -#### Supporting Sparse Fieldset Encoding -1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`. -2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`. -3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons. -4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding. -5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes. - -#### Sparse Fieldset `typealias` comparisons -You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): -```swift -typealias Document = JSONAPI.Document> -``` - -In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): -```swift -typealias SparseDocument = JSONAPI.Document> -``` - -### Replacing and Tapping Attributes/Relationships -When you are working with an immutable Resource Object, it can be useful to replace its attributes or relationships. As a client, you might receive a resource from the server, update something, and then send the server a PATCH request. - -`ResourceObject` is immutable, but you can create a new copy of a `ResourceObject` having updated attributes or relationships. - -#### Tapping -If your `Attributes` or `Relationships` struct is mutable (i.e. its properties are `var`s) then you may find `ResourceObject`'s `tappingAttributes()` and `tappingRelationships()` functions useful. For both, you pass a function that takes an `inout` copy of the respective object or value that you can mutate. The mutated value is then used to create a new `ResourceObject`. - -For example, to take a hypothetical `Dog` resource object and change the name attribute: -```swift -let resourceObject = Dog(...) - -let newResourceObject = resourceObject - .tappingAttributes { $0.name = .init(value: "Charlie") } -``` - -#### Replacing -If your `Attributes` or `Relationships` struct is immutable (i.e. its properties are `let`s) then you may find `ResourceObject`'s `replacingAttributes()` and `replacingRelationships()` functions useful. For both, you pass a function that takes the current attributes or relationships and you return a new value. The new value is then used to create a new `ResourceObject`. - -For example, to take a hypothetical `Dog` resource object and change the name attribute: -```swift -let resourceObject = Dog(...) - -let newResourceObject = resourceObject - .replacingAttributes { _ in - return Dog.Attributes(name: .init(value: "Charlie")) -} -``` - -### Custom Attribute or Relationship Key Mapping -There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase: -```swift -public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { - public static var jsonType: String { return "entity" } - - public struct Attributes: JSONAPI.Attributes { - public let coolProperty: Attribute - } - - public typealias Relationships = NoRelationships -} - -public enum ResourceObjectDescription2: JSONAPI.ResourceObjectDescription { - public static var jsonType: String { return "entity" } - - public struct Attributes: JSONAPI.Attributes { - public let wholeOtherThing: Attribute - - enum CodingKeys: String, CodingKey { - case wholeOtherThing = "coolProperty" - } - } - - public typealias Relationships = NoRelationships -} -``` - -### Custom Attribute Encode/Decode -You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like: -```swift -public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { - public static var jsonType: String { return "entity" } - - public struct Attributes: JSONAPI.Attributes { - public let property1: Attribute - public let property2: Attribute - public let property3: Attribute - - public let weirdThing: Attribute - - enum CodingKeys: String, CodingKey { - case property1 - case property2 - case property3 - } - } - - public typealias Relationships = NoRelationships -} - -extension ResourceObjectDescription1.Attributes { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - property1 = try .defaultDecoding(from: container, forKey: .property1) - property2 = try .defaultDecoding(from: container, forKey: .property2) - property3 = try .defaultDecoding(from: container, forKey: .property3) - - weirdThing = .init(value: "hello world") - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(property1, forKey: .property1) - try container.encode(property2, forKey: .property2) - try container.encode(property3, forKey: .property3) - } -} -``` - -### Meta-Attributes -This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your resource objects more natural to work with. - -Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of a resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially. - -```swift -enum UserDescription: ResourceObjectDescription { - public static var jsonType: String { return "users" } - - struct Attributes: JSONAPI.Attributes { - var createdAt: (User) -> Date { - return { user in - let components = user.id.rawValue.split(separator: "-") - - guard components.count == 2 else { - assertionFailure() - return Date() - } - - let timestamp = TimeInterval(components[1]) - - guard let date = timestamp.map(Date.init(timeIntervalSince1970:)) else { - assertionFailure() - return Date() - } - - return date - } - } - } - - typealias Relationships = NoRelationships -} - -typealias User = JSONAPI.ResourceObject -``` - -Given a value `user` of the above resource object type, you can access the `createdAt` attribute just like you would any other: - -```swift -let createdAt = user.createdAt -``` - -This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute. - -### Meta-Relationships -This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-relationships are just the thing to make your resource objects more natural to work with. - -Similarly to Meta-Attributes, Meta-Relationships allow you to represent non-compliant relationships as computed relationship properties. In the following example, a relationship is created from some attributes on the JSON model. - -```swift -enum UserDescription: ResourceObjectDescription { - public static var jsonType: String { return "users" } - - struct Attributes: JSONAPI.Attributes { - let friend_id: Attribute - } - - struct Relationships: JSONAPI.Relationships { - public var friend: (User) -> User.Identifier { - return { user in - return User.Identifier(rawValue: user.friend_id) - } - } - } -} - -typealias User = JSONAPI.ResourceObject -``` - -Given a value `user` of the above resource object type, you can access the `friend` relationship just like you would any other: - -```swift -let friendId = user ~> \.friend -``` - -This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Identifier}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. - ## Example The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo. @@ -785,7 +169,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document> +typealias Document = JSONAPI.Document> // MARK: Entity Definitions @@ -926,14 +310,50 @@ print(response.author) ``` # JSONAPI+Testing -The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. +The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. You can see `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. -The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. +## Literal Expressibility +Literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` are provided so that you can easily write test `ResourceObject` values into your unit tests. -You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. +For example, you could create a mock `Author` (from the above example) as follows +```swift +let author = Author(id: "1234", // You can just use a String directly as an Id + attributes: .init(name: "Janice Bluff"), // The name Attribute does not need to be initialized, you just use a String directly. + relationships: .none, + meta: .none, + links: .none) +``` + +## Resource Object `check()` +The `ResourceObject` gets a `check()` function that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. + +To catch malformed `JSONAPI.Attributes` and `JSONAPI.Relationships`, just call `check()` in your unit test functions: +```swift +func test_initAuthor() { + let author = Author(...) + Author.check(author) +} +``` + +## Comparisons +You can compare `Documents`, `ResourceObjects`, `Attributes`, etc. and get human-readable output using the `compare(to:)` methods included with `JSONAPITesting`. + +```swift +func test_articleResponse() { + let endToEndAPITestResponse: SingleArticleDocumentWithIncludes = ... + + let expectedResponse: SingleArticleDocumentWithIncludes = ... + + let comparison = endToEndAPITestResponse.compare(to: expectedResponse) + + XCTAssert(comparison.isSame, String(describing: comparison)) +} +``` # JSONAPI+Arbitrary -This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. +The `JSONAPI+Arbitrary` library provides `SwiftCheck` `Arbitrary` conformance for many of teh `JSONAPI` types. + +See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. # JSONAPI+OpenAPI The `JSONAPI+OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API. diff --git a/documentation/usage.md b/documentation/usage.md new file mode 100644 index 0000000..7f6e8ee --- /dev/null +++ b/documentation/usage.md @@ -0,0 +1,611 @@ + +## Usage + +In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**. + + + +- [`JSONAPI.ResourceObjectDescription`](#jsonapiresourceobjectdescription) +- [`JSONAPI.ResourceObject`](#jsonapiresourceobject) + - [`Meta`](#meta) + - [`Links`](#links) + - [`MaybeRawId`](#mayberawid) + - [`RawIdType`](#rawidtype) + - [Convenient `typealiases`](#convenient-typealiases) +- [`JSONAPI.Relationships`](#jsonapirelationships) +- [`JSONAPI.Attributes`](#jsonapiattributes) + - [`Transformer`](#transformer) + - [`Validator`](#validator) + - [Computed `Attribute`](#computed-attribute) +- [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) +- [`JSONAPI.Document`](#jsonapidocument) + - [`ResourceBody`](#resourcebody) + - [nullable `PrimaryResource`](#nullable-primaryresource) + - [`MetaType`](#metatype) + - [`LinksType`](#linkstype) + - [`IncludeType`](#includetype) + - [`APIDescriptionType`](#apidescriptiontype) + - [`Error`](#error) + - [`UnknownJSONAPIError`](#unknownjsonapierror) + - [`BasicJSONAPIError`](#basicjsonapierror) + - [`GenericJSONAPIError`](#genericjsonapierror) +- [`JSONAPI.Meta`](#jsonapimeta) +- [`JSONAPI.Links`](#jsonapilinks) +- [`JSONAPI.RawIdType`](#jsonapirawidtype) +- [Sparse Fieldsets](#sparse-fieldsets) + - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) + - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) +- [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships) + - [Tapping](#tapping) + - [Replacing](#replacing) +- [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) +- [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) +- [Meta-Attributes](#meta-attributes) +- [Meta-Relationships](#meta-relationships) + + + +### `JSONAPI.ResourceObjectDescription` + +A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: + +```swift +enum PersonDescription: IdentifiedResourceObjectDescription { + static var jsonType: String { return "people" } + + struct Attributes: JSONAPI.Attributes { + let name: Attribute<[String]> + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let friends: ToManyRelationship + } +} +``` + +The requirements of a `ResourceObjectDescription` are: +1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type". +2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes` +3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships` + +Note that an `enum` type is used here for the `ResourceObjectDescription`; it could have been a `struct`, but `ResourceObjectDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job. + +This readme doesn't go into detail on the **SPEC**, but the following *Resource Object* would be described by the above `PersonDescription`: + +```json +{ + "type": "people", + "id": "9", + "attributes": { + "name": [ + "Jane", + "Doe" + ], + "favoriteColor": "Green" + }, + "relationships": { + "friends": { + "data": [ + { + "id": "7", + "type": "people" + }, + { + "id": "8", + "type": "people" + } + ] + } + } +} +``` + +### `JSONAPI.ResourceObject` + +Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. + +The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*. + +A `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`. + +#### `Meta` + +The second generic specialization on `ResourceObject` is `Meta`. This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules. You can use `NoMetadata` if you do not need to package any metadata with the `ResourceObject`. + +#### `Links` + +The third generic specialization on `ResourceObject` is `Links`. This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document. You can use `NoLinks` if you do not need to package any links with the `ResourceObject`. + +#### `MaybeRawId` + +The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`). + +##### `RawIdType` + +The raw type of `Id` to use for the `ResourceObject`. The actual `Id` of the `ResourceObject` will not be a `RawIdType`, though. The `Id` will package a value of `RawIdType` with a specialized reference back to the `ResourceObject` type it identifies. This just looks like `Id>`. + +Having the `ResourceObject` type associated with the `Id` makes it easy to store all of your resource objects in a hash broken out by `ResourceObject` type; You can pass `Ids` around and always know where to look for the `ResourceObject` to which the `Id` refers. This encapsulation provides some type safety because the Ids of two `ResourceObjects` with the "raw ID" of `"1"` but different types will not compare as equal. + +A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject`. This is often a `String` or a `UUID`. + +#### Convenient `typealiases` + +Often you can use one `RawIdType` for many if not all of your `ResourceObjects`. That means you can save yourself some boilerplate by using `typealias`es like the following: +```swift +public typealias ResourceObject = JSONAPI.ResourceObject + +public typealias NewResourceObject = JSONAPI.ResourceObject +``` + +It can also be nice to create a `typealias` for each type of resource object you want to work with: +```swift +typealias Person = ResourceObject + +typealias NewPerson = NewResourceObject +``` + +Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. + +### `JSONAPI.Relationships` + +There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. + +In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document. + +To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: +```swift +let nullableRelative: ToOneRelationship +``` + +A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: +```swift +typealias Relationships = NoRelationships +``` + +`Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): +```swift +let friendIds: [Person.Identifier] = person ~> \.friends +``` + +### `JSONAPI.Attributes` + +The `Attributes` of a `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`. + +To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional: +```swift +let optionalAttribute: Attribute? +``` + +To describe an attribute that is expected to exist but might have a `null` value, you make the value within the `Attribute` optional: +```swift +let nullableAttribute: Attribute +``` + +A resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`: +```swift +typealias Attributes = NoAttributes +``` + +As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows: +```swift +let favoriteColor: String = person.favoriteColor +``` + +: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] +``` + +#### `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`. + +A `Transformer` just provides one static function that transforms one type to another. You might define one for an ISO 8601 compliant `Date` like this: +```swift +enum ISODateTransformer: Transformer { + public static func transform(_ value: String) throws -> Date { + // parse Date out of input and return + } +} +``` + +Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`: +```swift +let date: TransformedAttribute +``` + +Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation. + +If you make your `Transformer` a `ReversibleTransformer` then your life will be a bit easier when you construct `TransformedAttributes` because you have access to initializers for both the pre- and post-transformed value types. Continuing with the above example of a `ISODateTransformer`: +```swift +extension ISODateTransformer: ReversibleTransformer { + public static func reverse(_ value: Date) throws -> String { + // serialize Date to a String + } +} + +let exampleAttribute = try? TransformedAttribute(transformedValue: Date()) +let otherAttribute = try? TransformedAttribute(rawValue: "2018-12-01 09:06:41 +0000") +``` + +#### `Validator` + +You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid. + +#### Computed `Attribute` + +You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property. + +```swift +public var fullName: Attribute { + return name.map { $0.joined(separator: " ") } +} +``` + +If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. + +### Copying/Mutating `ResourceObjects` +`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`: +1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. +2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. + +The above can be accomplished with code like the following: + +```swift +// use case 1 +let person1 = person.withNewIdentifier() + +// use case 2 +let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self) + +let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232") +``` + +### `JSONAPI.Document` + +The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included resource objects could be decoded as follows: +```swift +let decoder = JSONDecoder() + +let responseStructure = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError>.self + +let document = try decoder.decode(responseStructure, from: data) +``` + +A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata." + +#### `ResourceBody` + +The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. + +If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead. + +##### nullable `PrimaryResource` + +If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON. + +You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` because the **SPEC** requires that an empty document in that case be represented by an empty array rather than `null`. + +#### `MetaType` + +The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry: +```json +{ + "meta": { + "total": 100, + "limit": 50, + "offset": 50 + } +} +``` + +You would then create the following `Meta` type: +```swift +struct PageMetadata: JSONAPI.Meta { + let total: Int + let limit: Int + let offset: Int +} +``` + +You can always use `NoMetadata` if this JSON API feature is not needed. + +#### `LinksType` + +The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks). + +#### `IncludeType` + +The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. + +**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_. + +To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1` instead of `NoIncludes`. + +#### `APIDescriptionType` + +The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API. + +You can specify this is not part of the document by using the `NoAPIDescription` type. + +You can describe the API by a version with no metadata by using `APIDescription`. + +You can supply any `JSONAPI.Meta` type as the metadata type of the API description. + +#### `Error` + +The final generic type of a `JSONAPIDocument` is the `Error`. + +You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`. + +##### `UnknownJSONAPIError` +The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described. + +##### `BasicJSONAPIError` +The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects. + +🗒Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type. + +The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError>`. + +The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary. + +As an example, let's say you have the following `Document` type that is destined for errors: +```swift +typealias ErrorDoc = JSONAPI.Document> +``` +And you've parsed an error response +```swift +let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData) +``` +You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body: +```swift +switch errorResponse.body { +case .data: + print("cool, data!") + +case .errors(let errors, let meta, let links): + let errorDetails = errors.compactMap { $0.payload?.detail } + + print("error details: \(errorDetails)") +} +``` + +##### `GenericJSONAPIError` +This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError` as the error type for a `Document`. + +### `JSONAPI.Meta` + +A `Meta` struct is totally open-ended. It is described by the **SPEC** as a place to put any information that does not fit into the standard JSON API Document structure anywhere else. + +You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`. + +If you need to support metadata with structure that is not pre-determined, consider an "Any Codable" type such as that found at https://github.com/Flight-School/AnyCodable. + +### `JSONAPI.Links` + +A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included. + +You can specify `NoLinks` if the part of the document being described should not contain any `Links`. + +### `JSONAPI.RawIdType` + +If you want to create new `JSONAPI.ResourceObject` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`): +```swift +extension UUID: CreatableRawIdType { + public static func unique() -> UUID { + return UUID() + } +} + +extension String: CreatableRawIdType { + public static func unique() -> String { + return UUID().uuidString + } +} +``` + +### Sparse Fieldsets +Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork. + +You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`. + +There is a sparse fieldsets example included with this repository as a Playground page. + +#### Supporting Sparse Fieldset Encoding +1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`. +2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`. +3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons. +4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding. +5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes. + +#### Sparse Fieldset `typealias` comparisons +You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): +```swift +typealias Document = JSONAPI.Document> +``` + +In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): +```swift +typealias SparseDocument = JSONAPI.Document> +``` + +### Replacing and Tapping Attributes/Relationships +When you are working with an immutable Resource Object, it can be useful to replace its attributes or relationships. As a client, you might receive a resource from the server, update something, and then send the server a PATCH request. + +`ResourceObject` is immutable, but you can create a new copy of a `ResourceObject` having updated attributes or relationships. + +#### Tapping +If your `Attributes` or `Relationships` struct is mutable (i.e. its properties are `var`s) then you may find `ResourceObject`'s `tappingAttributes()` and `tappingRelationships()` functions useful. For both, you pass a function that takes an `inout` copy of the respective object or value that you can mutate. The mutated value is then used to create a new `ResourceObject`. + +For example, to take a hypothetical `Dog` resource object and change the name attribute: +```swift +let resourceObject = Dog(...) + +let newResourceObject = resourceObject + .tappingAttributes { $0.name = .init(value: "Charlie") } +``` + +#### Replacing +If your `Attributes` or `Relationships` struct is immutable (i.e. its properties are `let`s) then you may find `ResourceObject`'s `replacingAttributes()` and `replacingRelationships()` functions useful. For both, you pass a function that takes the current attributes or relationships and you return a new value. The new value is then used to create a new `ResourceObject`. + +For example, to take a hypothetical `Dog` resource object and change the name attribute: +```swift +let resourceObject = Dog(...) + +let newResourceObject = resourceObject + .replacingAttributes { _ in + return Dog.Attributes(name: .init(value: "Charlie")) +} +``` + +### Custom Attribute or Relationship Key Mapping +There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase: +```swift +public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { + public static var jsonType: String { return "entity" } + + public struct Attributes: JSONAPI.Attributes { + public let coolProperty: Attribute + } + + public typealias Relationships = NoRelationships +} + +public enum ResourceObjectDescription2: JSONAPI.ResourceObjectDescription { + public static var jsonType: String { return "entity" } + + public struct Attributes: JSONAPI.Attributes { + public let wholeOtherThing: Attribute + + enum CodingKeys: String, CodingKey { + case wholeOtherThing = "coolProperty" + } + } + + public typealias Relationships = NoRelationships +} +``` + +### Custom Attribute Encode/Decode +You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like: +```swift +public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { + public static var jsonType: String { return "entity" } + + public struct Attributes: JSONAPI.Attributes { + public let property1: Attribute + public let property2: Attribute + public let property3: Attribute + + public let weirdThing: Attribute + + enum CodingKeys: String, CodingKey { + case property1 + case property2 + case property3 + } + } + + public typealias Relationships = NoRelationships +} + +extension ResourceObjectDescription1.Attributes { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + property1 = try .defaultDecoding(from: container, forKey: .property1) + property2 = try .defaultDecoding(from: container, forKey: .property2) + property3 = try .defaultDecoding(from: container, forKey: .property3) + + weirdThing = .init(value: "hello world") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(property1, forKey: .property1) + try container.encode(property2, forKey: .property2) + try container.encode(property3, forKey: .property3) + } +} +``` + +### Meta-Attributes +This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your resource objects more natural to work with. + +Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of a resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially. + +```swift +enum UserDescription: ResourceObjectDescription { + public static var jsonType: String { return "users" } + + struct Attributes: JSONAPI.Attributes { + var createdAt: (User) -> Date { + return { user in + let components = user.id.rawValue.split(separator: "-") + + guard components.count == 2 else { + assertionFailure() + return Date() + } + + let timestamp = TimeInterval(components[1]) + + guard let date = timestamp.map(Date.init(timeIntervalSince1970:)) else { + assertionFailure() + return Date() + } + + return date + } + } + } + + typealias Relationships = NoRelationships +} + +typealias User = JSONAPI.ResourceObject +``` + +Given a value `user` of the above resource object type, you can access the `createdAt` attribute just like you would any other: + +```swift +let createdAt = user.createdAt +``` + +This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute. + +### Meta-Relationships +This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-relationships are just the thing to make your resource objects more natural to work with. + +Similarly to Meta-Attributes, Meta-Relationships allow you to represent non-compliant relationships as computed relationship properties. In the following example, a relationship is created from some attributes on the JSON model. + +```swift +enum UserDescription: ResourceObjectDescription { + public static var jsonType: String { return "users" } + + struct Attributes: JSONAPI.Attributes { + let friend_id: Attribute + } + + struct Relationships: JSONAPI.Relationships { + public var friend: (User) -> User.Identifier { + return { user in + return User.Identifier(rawValue: user.friend_id) + } + } + } +} + +typealias User = JSONAPI.ResourceObject +``` + +Given a value `user` of the above resource object type, you can access the `friend` relationship just like you would any other: + +```swift +let friendId = user ~> \.friend +``` + +This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Identifier}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship.