From 832161628b64bb68e7fc842c20371da656185a7f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 23:05:21 -0800 Subject: [PATCH] go from 3 specializations of all document related compare functions down to 2. --- Sources/JSONAPI/Document/ResourceBody.swift | 32 ++++--- .../Comparisons/DocumentCompare.swift | 68 ++++--------- .../Comparisons/DocumentDataCompare.swift | 95 +++++++++++-------- .../Comparisons/DocumentCompareTests.swift | 52 ++++++++++ 4 files changed, 143 insertions(+), 104 deletions(-) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index c8408de..25090c8 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -29,7 +29,9 @@ extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimary /// An `EncodableResourceBody` is a `ResourceBody` that only supports being /// encoded. It is actually weaker than `ResourceBody`, which supports both encoding /// and decoding. -public protocol EncodableResourceBody: Equatable, Encodable {} +public protocol EncodableResourceBody: Equatable, Encodable { + associatedtype PrimaryResource +} /// A `CodableResourceBody` is a representation of the body of the JSON:API Document. /// It can either be one resource (which can be specified as optional or not) @@ -49,19 +51,19 @@ public func +(_ left: R, right: R) -> R { /// A type allowing for a document body containing 1 primary resource. /// If the `Entity` specialization is an `Optional` type, the body can contain /// 0 or 1 primary resources. -public struct SingleResourceBody: EncodableResourceBody { - public let value: Entity +public struct SingleResourceBody: EncodableResourceBody { + public let value: PrimaryResource - public init(resourceObject: Entity) { + public init(resourceObject: PrimaryResource) { self.value = resourceObject } } /// A type allowing for a document body containing 0 or more primary resources. -public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { - public let values: [Entity] +public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { + public let values: [PrimaryResource] - public init(resourceObjects: [Entity]) { + public init(resourceObjects: [PrimaryResource]) { values = resourceObjects } @@ -73,6 +75,8 @@ public struct ManyResourceBody: Encoda /// Use NoResourceBody to indicate you expect a JSON API document to not /// contain a "data" top-level key. public struct NoResourceBody: CodableResourceBody { + public typealias PrimaryResource = Void + public static var none: NoResourceBody { return NoResourceBody() } } @@ -82,7 +86,7 @@ extension SingleResourceBody { var container = encoder.singleValueContainer() let anyNil: Any? = nil - let nilValue = anyNil as? Entity + let nilValue = anyNil as? PrimaryResource guard value != nilValue else { try container.encodeNil() return @@ -92,18 +96,18 @@ extension SingleResourceBody { } } -extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource { +extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let anyNil: Any? = nil if container.decodeNil(), - let val = anyNil as? Entity { + let val = anyNil as? PrimaryResource { value = val return } - value = try container.decode(Entity.self) + value = try container.decode(PrimaryResource.self) } } @@ -117,12 +121,12 @@ extension ManyResourceBody { } } -extension ManyResourceBody: Decodable, CodableResourceBody where Entity: CodablePrimaryResource { +extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource { public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() - var valueAggregator = [Entity]() + var valueAggregator = [PrimaryResource]() while !container.isAtEnd { - valueAggregator.append(try container.decode(Entity.self)) + valueAggregator.append(try container.decode(PrimaryResource.self)) } values = valueAggregator } diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 587d6a0..3f3ee23 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -79,28 +79,8 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension EncodableJSONAPIDocument where Body: Equatable { - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { - return DocumentComparison( - apiDescription: Comparison( - String(describing: apiDescription), - String(describing: other.apiDescription) - ), - body: body.compare(to: other.body) - ) - } - - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { - return DocumentComparison( - apiDescription: Comparison( - String(describing: apiDescription), - String(describing: other.apiDescription) - ), - body: body.compare(to: other.body) - ) - } - - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == ManyResourceBody, T: ResourceObjectType { +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( apiDescription: Comparison( String(describing: apiDescription), @@ -111,36 +91,20 @@ extension EncodableJSONAPIDocument where Body: Equatable { } } -extension DocumentBody where Self: Equatable { - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { - - // rule out case where they are the same - guard self != other else { - return .same - } - - // rule out case where they are both error bodies - if let errors1 = errors, let errors2 = other.errors { - return .differentErrors( - BodyComparison.compare( - errors: errors1, meta, links, - with: errors2, meta, links - ) - ) - } - - // rule out the case where they are both data - if let data1 = data, let data2 = other.data { - return .differentData(data1.compare(to: data2)) - } - - // we are left with the case where one is data and the - // other is an error if self.isError, then "the error - // is on the left" - return .dataErrorMismatch(errorOnLeft: isError) +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> DocumentComparison { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) } +} - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same guard self != other else { @@ -167,8 +131,10 @@ extension DocumentBody where Self: Equatable { // is on the left" return .dataErrorMismatch(errorOnLeft: isError) } +} - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same guard self != other else { diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index d268572..87b6819 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -33,8 +33,8 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension DocumentBodyData { - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { +extension DocumentBodyData where PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), @@ -42,17 +42,10 @@ extension DocumentBodyData { links: Comparison(links, other.links) ) } +} - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { - return .init( - primary: primary.compare(to: other.primary), - includes: includes.compare(to: other.includes), - meta: Comparison(meta, other.meta), - links: Comparison(links, other.links) - ) - } - - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { +extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), @@ -109,42 +102,24 @@ public struct ManyResourceObjectComparison: Equatable, PropertyComparable { } } -extension SingleResourceBody where Entity: ResourceObjectType { +extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .single(.init(value, other.value)) - } -} + guard let one = optionalResourceObject, + let two = other.optionalResourceObject else { -public protocol _OptionalResourceObjectType { - associatedtype Wrapped: ResourceObjectType + func nilOrName(_ resObj: T?) -> String { + resObj.map { String(describing: type(of: $0)) } ?? "nil" + } - var maybeValue: Wrapped? { get } -} - -extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { - public var maybeValue: Wrapped? { - switch self { - case .none: - return nil - case .some(let value): - return value - } - } -} - -extension SingleResourceBody where Entity: _OptionalResourceObjectType { - public func compare(to other: Self) -> PrimaryResourceBodyComparison { - guard let one = value.maybeValue, - let two = other.value.maybeValue else { - return .other(Comparison(value, other.value)) + return .other(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) } return .single(.init(one, two)) } } -extension ManyResourceBody where Entity: ResourceObjectType { +extension _ResourceBody where PrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .many(.init(values.compare(to: other.values, using: { r1, r2 in + return .many(.init(resourceObjects.compare(to: other.resourceObjects, using: { r1, r2 in let r1AsResource = r1 as? AbstractResourceObjectType let maybeComparison = r1AsResource @@ -165,3 +140,45 @@ extension ManyResourceBody where Entity: ResourceObjectType { }))) } } + +public protocol _ResourceBody { + associatedtype PrimaryResourceType: ResourceObjectType + var resourceObjects: [PrimaryResourceType] { get } +} + +public protocol _OptionalResourceBody { + associatedtype WrappedPrimaryResourceType: ResourceObjectType + var optionalResourceObject: WrappedPrimaryResourceType? { get } +} + +public protocol _OptionalResourceObjectType { + associatedtype Wrapped: ResourceObjectType + + var maybeValue: Wrapped? { get } +} + +extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { + public var maybeValue: Wrapped? { + switch self { + case .none: + return nil + case .some(let value): + return value + } + } +} + +extension ManyResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { + public var resourceObjects: [PrimaryResource] { values } +} + +extension SingleResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { + public typealias PrimaryResourceType = PrimaryResource + public var resourceObjects: [PrimaryResource] { [value] } +} + +extension SingleResourceBody: _OptionalResourceBody where PrimaryResource: _OptionalResourceObjectType { + public typealias WrappedPrimaryResourceType = PrimaryResource.Wrapped + + public var optionalResourceObject: WrappedPrimaryResourceType? { value.maybeValue } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index b2e14af..7c00e38 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -15,6 +15,12 @@ final class DocumentCompareTests: XCTestCase { XCTAssertTrue(d2.compare(to: d2).differences.isEmpty) XCTAssertTrue(d3.compare(to: d3).differences.isEmpty) XCTAssertTrue(d4.compare(to: d4).differences.isEmpty) + XCTAssertTrue(d5.compare(to: d5).differences.isEmpty) + XCTAssertTrue(d6.compare(to: d6).differences.isEmpty) + XCTAssertTrue(d7.compare(to: d7).differences.isEmpty) + XCTAssertTrue(d8.compare(to: d8).differences.isEmpty) + XCTAssertTrue(d9.compare(to: d9).differences.isEmpty) + XCTAssertTrue(d10.compare(to: d10).differences.isEmpty) } func test_errorAndData() { @@ -41,6 +47,18 @@ final class DocumentCompareTests: XCTestCase { XCTAssertEqual(d3.compare(to: d6).differences, [ "Body": ##"(Includes: (include 2: missing)), (Primary Resource: (resource 2: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5), (resource 3: missing))"## ]) + + XCTAssertEqual(d7.compare(to: d8).differences, [ + "Body": ##"(Primary Resource: nil ≠ ResourceObject)"## + ]) + + XCTAssertEqual(d8.compare(to: d9).differences, [ + "Body": ##"(Primary Resource: ('age' attribute: 10 ≠ 12), ('bestFriend' relationship: Optional(Id(2)) ≠ nil), ('favoriteColor' attribute: nil ≠ Optional("blue")), ('name' attribute: name ≠ Fig), (id: 1 ≠ 5))"## + ]) + + XCTAssertEqual(d1.compare(to: d10).differences, [ + "Body": ##"(Primary Resource: (resource 1: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5))"## + ]) } } @@ -80,6 +98,8 @@ fileprivate typealias TestType2 = ResourceObject, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> +fileprivate typealias OptionalSingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> + fileprivate typealias ManyDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> fileprivate let r1 = TestType( @@ -168,3 +188,35 @@ fileprivate let d6 = ManyDocument( meta: .none, links: .none ) + +fileprivate let d7 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: nil), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d8 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: r1), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d9 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: r2), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d10 = SingleDocument( + apiDescription: .none, + body: .init(resourceObject: r2), + includes: .none, + meta: .none, + links: .none +)