go from 3 specializations of all document related compare functions down to 2.

This commit is contained in:
Mathew Polzin
2019-11-06 23:05:21 -08:00
parent f37f44cfda
commit 832161628b
4 changed files with 143 additions and 104 deletions
+18 -14
View File
@@ -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 +<R: ResourceBodyAppendable>(_ 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<Entity: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
public let value: Entity
public struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrimaryResource>: 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<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [Entity]
public struct ManyResourceBody<PrimaryResource: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [PrimaryResource]
public init(resourceObjects: [Entity]) {
public init(resourceObjects: [PrimaryResource]) {
values = resourceObjects
}
@@ -73,6 +75,8 @@ public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: 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
}
@@ -79,28 +79,8 @@ public enum BodyComparison: Equatable, CustomStringConvertible {
public var rawValue: String { description }
}
extension EncodableJSONAPIDocument where Body: Equatable {
public func compare<T>(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody<T>, T: ResourceObjectType {
return DocumentComparison(
apiDescription: Comparison(
String(describing: apiDescription),
String(describing: other.apiDescription)
),
body: body.compare(to: other.body)
)
}
public func compare<T>(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody<T?>, T: ResourceObjectType {
return DocumentComparison(
apiDescription: Comparison(
String(describing: apiDescription),
String(describing: other.apiDescription)
),
body: body.compare(to: other.body)
)
}
public func compare<T>(to other: Self) -> DocumentComparison where PrimaryResourceBody == ManyResourceBody<T>, 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<T>(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody<T> {
// 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<T>(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody<T?> {
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<T>(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody<T> {
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 {
@@ -33,8 +33,8 @@ public struct DocumentDataComparison: Equatable, PropertyComparable {
}
}
extension DocumentBodyData {
public func compare<T>(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody<T> {
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<T>(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody<T?> {
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<T>(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody<T> {
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<T>(_ 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 }
}
@@ -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<TestDescription, NoMetadata, NoLinks, String>)"##
])
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<TestDescription2, NoMetadata, N
fileprivate typealias SingleDocument = JSONAPI.Document<SingleResourceBody<TestType>, NoMetadata, NoLinks, Include2<TestType, TestType2>, NoAPIDescription, BasicJSONAPIError<String>>
fileprivate typealias OptionalSingleDocument = JSONAPI.Document<SingleResourceBody<TestType?>, NoMetadata, NoLinks, Include2<TestType, TestType2>, NoAPIDescription, BasicJSONAPIError<String>>
fileprivate typealias ManyDocument = JSONAPI.Document<ManyResourceBody<TestType>, NoMetadata, NoLinks, Include2<TestType, TestType2>, NoAPIDescription, BasicJSONAPIError<String>>
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
)