From f7bfa91ccca76231fb097c87cc63ba489c8de8ac Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:03:51 -0800 Subject: [PATCH 1/5] 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 2/5] 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 87271b93f9273e1bb801ea364e1d7b21dde68c7b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 20:48:04 -0800 Subject: [PATCH 3/5] 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 4/5] 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 adcc6bfb108d661f6af658c25eb36a4f1bf33266 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 22:43:40 -0800 Subject: [PATCH 5/5] 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