mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c3a82ec23 | |||
| e9a3b35dc7 | |||
| 2eecf95995 | |||
| 4dc30ddc1c | |||
| 455ff64326 | |||
| 0b4baf35d5 | |||
| 11ef050d58 | |||
| 86344ef93f | |||
| 7fabe2574e | |||
| 19636a47f0 | |||
| 0538de48cb | |||
| 024fe2d452 | |||
| 832161628b | |||
| f37f44cfda | |||
| ae7e0f528a |
+2
-2
@@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf",
|
||||
"version": "2.3.0"
|
||||
"revision": "0c9c08204142babc480938d704a23513d11420e5",
|
||||
"version": "2.3.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ let package = Package(
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -349,8 +349,8 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody:
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
if let noAPIDescription = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noAPIDescription
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
@@ -389,18 +389,32 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody:
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
do {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
} catch let error as ResourceObjectDecodingError {
|
||||
throw DocumentDecodingError(error)
|
||||
} catch let error as ManyResourceBodyDecodingError {
|
||||
throw DocumentDecodingError(error)
|
||||
} catch let error as DecodingError {
|
||||
throw DocumentDecodingError(error)
|
||||
?? error
|
||||
}
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
let maybeIncludes: Includes<Include>?
|
||||
do {
|
||||
maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
} catch let error as IncludesDecodingError {
|
||||
throw DocumentDecodingError(error)
|
||||
}
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath)
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
throw JSONAPICodingError.missingOrMalformedLinks(path: decoder.codingPath)
|
||||
}
|
||||
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
@@ -569,7 +583,7 @@ extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument
|
||||
document = try container.decode(Document.self)
|
||||
|
||||
guard document.body.isError else {
|
||||
throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError
|
||||
throw DocumentDecodingError.foundSuccessDocumentWhenExpectingError
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,7 +596,7 @@ extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument
|
||||
document = try container.decode(Document.self)
|
||||
|
||||
guard !document.body.isError else {
|
||||
throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess
|
||||
throw DocumentDecodingError.foundErrorDocumentWhenExpectingSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,75 @@
|
||||
//
|
||||
// DocumentDecodingErro.swift
|
||||
// DocumentDecodingError.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 10/20/19.
|
||||
//
|
||||
|
||||
public enum JSONAPIDocumentDecodingError: Swift.Error {
|
||||
public enum DocumentDecodingError: Swift.Error, Equatable {
|
||||
case primaryResource(error: ResourceObjectDecodingError, idx: Int?)
|
||||
case primaryResourceMissing
|
||||
case primaryResourcesMissing
|
||||
|
||||
case includes(error: IncludesDecodingError)
|
||||
|
||||
case foundErrorDocumentWhenExpectingSuccess
|
||||
case foundSuccessDocumentWhenExpectingError
|
||||
|
||||
init(_ decodingError: ResourceObjectDecodingError) {
|
||||
self = .primaryResource(error: decodingError, idx: nil)
|
||||
}
|
||||
|
||||
init(_ decodingError: ManyResourceBodyDecodingError) {
|
||||
self = .primaryResource(error: decodingError.error, idx: decodingError.idx)
|
||||
}
|
||||
|
||||
init(_ decodingError: IncludesDecodingError) {
|
||||
self = .includes(error: decodingError)
|
||||
}
|
||||
|
||||
init?(_ decodingError: DecodingError) {
|
||||
switch decodingError {
|
||||
case .valueNotFound(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type:
|
||||
self = .primaryResourceMissing
|
||||
case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self:
|
||||
self = .primaryResourcesMissing
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private enum Location: Equatable {
|
||||
case data
|
||||
case other
|
||||
|
||||
init(_ context: DecodingError.Context) {
|
||||
if context.codingPath.contains(where: { $0.stringValue == "data" }) {
|
||||
self = .data
|
||||
} else {
|
||||
self = .other
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DocumentDecodingError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .primaryResource(error: let error, idx: let idx):
|
||||
let idxString = idx.map { " \($0 + 1)" } ?? ""
|
||||
return "Primary Resource\(idxString) failed to parse because \(error)"
|
||||
case .primaryResourceMissing:
|
||||
return "Primary Resource missing."
|
||||
case .primaryResourcesMissing:
|
||||
return "Primary Resources array missing."
|
||||
|
||||
case .includes(error: let error):
|
||||
return "\(error)"
|
||||
|
||||
case .foundErrorDocumentWhenExpectingSuccess:
|
||||
return "Expected a success document with a 'data' property but found an error document."
|
||||
case .foundSuccessDocumentWhenExpectingError:
|
||||
return "Expected an error document but found a success document with a 'data' property."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public struct Includes<I: Include>: Encodable, Equatable {
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
guard I.self != NoIncludes.self else {
|
||||
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
|
||||
throw JSONAPICodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath)
|
||||
}
|
||||
|
||||
for value in values {
|
||||
@@ -56,8 +56,35 @@ extension Includes: Decodable where I: Decodable {
|
||||
}
|
||||
|
||||
var valueAggregator = [I]()
|
||||
var idx = 0
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
do {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
idx = idx + 1
|
||||
} catch let error as PolyDecodeNoTypesMatchedError {
|
||||
let errors: [ResourceObjectDecodingError] = error
|
||||
.individualTypeFailures
|
||||
.compactMap { decodingError in
|
||||
switch decodingError.error {
|
||||
case .typeMismatch(_, let context),
|
||||
.valueNotFound(_, let context),
|
||||
.keyNotFound(_, let context),
|
||||
.dataCorrupted(let context):
|
||||
return context.underlyingError as? ResourceObjectDecodingError
|
||||
@unknown default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
guard errors.count == error.individualTypeFailures.count else {
|
||||
throw IncludesDecodingError(error: error, idx: idx)
|
||||
}
|
||||
throw IncludesDecodingError(
|
||||
error: IncludeDecodingError(failures: errors),
|
||||
idx: idx
|
||||
)
|
||||
} catch let error {
|
||||
throw IncludesDecodingError(error: error, idx: idx)
|
||||
}
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
@@ -177,3 +204,32 @@ extension Includes where I: _Poly11 {
|
||||
return values.compactMap { $0.k }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DecodingError
|
||||
public struct IncludesDecodingError: Swift.Error, Equatable {
|
||||
public let error: Swift.Error
|
||||
public let idx: Int
|
||||
|
||||
public static func ==(lhs: Self, rhs: Self) -> Bool {
|
||||
return lhs.idx == rhs.idx
|
||||
&& String(describing: lhs) == String(describing: rhs)
|
||||
}
|
||||
}
|
||||
|
||||
extension IncludesDecodingError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Include \(idx + 1) failed to parse: \(error)"
|
||||
}
|
||||
}
|
||||
|
||||
public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConvertible {
|
||||
public let failures: [ResourceObjectDecodingError]
|
||||
|
||||
public var description: String {
|
||||
return failures
|
||||
.enumerated()
|
||||
.map {
|
||||
"\nCould not have been Include Type \($0.offset + 1) because:\n\($0.element)"
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 @@ 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]()
|
||||
var idx = 0
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
do {
|
||||
valueAggregator.append(try container.decode(PrimaryResource.self))
|
||||
} catch let error as ResourceObjectDecodingError {
|
||||
throw ManyResourceBodyDecodingError(
|
||||
error: error,
|
||||
idx: idx
|
||||
)
|
||||
}
|
||||
idx = idx + 1
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
@@ -141,3 +154,9 @@ extension ManyResourceBody: CustomStringConvertible {
|
||||
return "PrimaryResourceBody(\(String(describing: values)))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DecodingError
|
||||
public struct ManyResourceBodyDecodingError: Swift.Error, Equatable {
|
||||
public let error: ResourceObjectDecodingError
|
||||
public let idx: Int
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// EncodingError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/7/18.
|
||||
//
|
||||
|
||||
public enum JSONAPIEncodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// JSONAPICodingError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/7/18.
|
||||
//
|
||||
|
||||
public enum JSONAPICodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String, path: [CodingKey])
|
||||
case quantityMismatch(expected: Quantity, path: [CodingKey])
|
||||
case illegalEncoding(String, path: [CodingKey])
|
||||
case illegalDecoding(String, path: [CodingKey])
|
||||
case missingOrMalformedMetadata(path: [CodingKey])
|
||||
case missingOrMalformedLinks(path: [CodingKey])
|
||||
|
||||
public enum Quantity: String, Equatable {
|
||||
case one
|
||||
case many
|
||||
|
||||
public var other: Quantity {
|
||||
switch self {
|
||||
case .one: return .many
|
||||
case .many: return .one
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,21 @@
|
||||
// Created by Mathew Polzin on 11/13/18.
|
||||
//
|
||||
|
||||
public protocol AttributeType: Codable {
|
||||
public protocol AbstractAttributeType {
|
||||
var rawValueType: Any.Type { get }
|
||||
}
|
||||
|
||||
public protocol AttributeType: Codable, AbstractAttributeType {
|
||||
associatedtype RawValue: Codable
|
||||
associatedtype ValueType
|
||||
|
||||
var value: ValueType { get }
|
||||
}
|
||||
|
||||
extension AttributeType {
|
||||
public var rawValueType: Any.Type { return RawValue.self }
|
||||
}
|
||||
|
||||
// MARK: TransformedAttribute
|
||||
|
||||
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
|
||||
|
||||
@@ -45,7 +45,10 @@ public protocol OptionalId: Codable {
|
||||
init(rawValue: RawType)
|
||||
}
|
||||
|
||||
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
|
||||
/// marker protocol
|
||||
public protocol AbstractId {}
|
||||
|
||||
public protocol IdType: AbstractId, OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
|
||||
|
||||
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
|
||||
extension Optional: OptionalId where Wrapped: IdType {
|
||||
@@ -94,7 +97,7 @@ public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equa
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
|
||||
extension Id: Hashable, CustomStringConvertible, AbstractId, IdType where RawType: RawIdType {
|
||||
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
|
||||
return Id(rawValue: rawValue)
|
||||
}
|
||||
|
||||
@@ -18,15 +18,15 @@ import Poly
|
||||
public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource
|
||||
|
||||
public typealias EncodablePolyWrapped = Encodable & Equatable
|
||||
public typealias PolyWrapped = EncodablePolyWrapped & Decodable
|
||||
public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable
|
||||
|
||||
extension Poly0: CodablePrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.")
|
||||
throw JSONAPICodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.")
|
||||
throw JSONAPICodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,42 +35,42 @@ extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
where A: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly1: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped {}
|
||||
where A: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 2 types
|
||||
extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
where A: EncodablePolyWrapped, B: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly2: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 3 types
|
||||
extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly3: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 4 types
|
||||
extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly4: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 5 types
|
||||
extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly5: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 6 types
|
||||
extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly6: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 7 types
|
||||
extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
@@ -84,7 +84,7 @@ extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
G: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly7: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 8 types
|
||||
extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
@@ -99,7 +99,7 @@ extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
H: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly8: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 9 types
|
||||
extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
@@ -115,7 +115,7 @@ extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
I: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly9: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 10 types
|
||||
extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
@@ -132,7 +132,7 @@ extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
J: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly10: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {}
|
||||
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped, J: CodablePolyWrapped {}
|
||||
|
||||
// MARK: - 11 types
|
||||
extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
@@ -151,14 +151,14 @@ extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource
|
||||
|
||||
extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource
|
||||
where
|
||||
A: PolyWrapped,
|
||||
B: PolyWrapped,
|
||||
C: PolyWrapped,
|
||||
D: PolyWrapped,
|
||||
E: PolyWrapped,
|
||||
F: PolyWrapped,
|
||||
G: PolyWrapped,
|
||||
H: PolyWrapped,
|
||||
I: PolyWrapped,
|
||||
J: PolyWrapped,
|
||||
K: PolyWrapped {}
|
||||
A: CodablePolyWrapped,
|
||||
B: CodablePolyWrapped,
|
||||
C: CodablePolyWrapped,
|
||||
D: CodablePolyWrapped,
|
||||
E: CodablePolyWrapped,
|
||||
F: CodablePolyWrapped,
|
||||
G: CodablePolyWrapped,
|
||||
H: CodablePolyWrapped,
|
||||
I: CodablePolyWrapped,
|
||||
J: CodablePolyWrapped,
|
||||
K: CodablePolyWrapped {}
|
||||
|
||||
@@ -170,18 +170,36 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
|
||||
// succeeds and then attempt to coerce nil to a Identifier
|
||||
// type at which point we can store nil in `id`.
|
||||
let anyNil: Any? = nil
|
||||
if try container.decodeNil(forKey: .data),
|
||||
let val = anyNil as? Identifiable.Identifier {
|
||||
if try container.decodeNil(forKey: .data) {
|
||||
guard let val = anyNil as? Identifiable.Identifier else {
|
||||
throw DecodingError.valueNotFound(
|
||||
Self.self,
|
||||
DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription: "Expected non-null relationship data."
|
||||
)
|
||||
)
|
||||
}
|
||||
id = val
|
||||
return
|
||||
}
|
||||
|
||||
let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
|
||||
let identifier: KeyedDecodingContainer<ResourceIdentifierCodingKeys>
|
||||
do {
|
||||
identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
|
||||
} catch let error as DecodingError {
|
||||
guard case let .typeMismatch(type, context) = error,
|
||||
type is _DictionaryType.Type else {
|
||||
throw error
|
||||
}
|
||||
throw JSONAPICodingError.quantityMismatch(expected: .one,
|
||||
path: context.codingPath)
|
||||
}
|
||||
|
||||
let type = try identifier.decode(String.self, forKey: .entityType)
|
||||
|
||||
guard type == Identifiable.jsonType else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type)
|
||||
throw JSONAPICodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath)
|
||||
}
|
||||
|
||||
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
|
||||
@@ -230,7 +248,17 @@ extension ToManyRelationship: Codable {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
|
||||
var identifiers = try container.nestedUnkeyedContainer(forKey: .data)
|
||||
var identifiers: UnkeyedDecodingContainer
|
||||
do {
|
||||
identifiers = try container.nestedUnkeyedContainer(forKey: .data)
|
||||
} catch let error as DecodingError {
|
||||
guard case let .typeMismatch(type, context) = error,
|
||||
type is _ArrayType.Type else {
|
||||
throw error
|
||||
}
|
||||
throw JSONAPICodingError.quantityMismatch(expected: .many,
|
||||
path: context.codingPath)
|
||||
}
|
||||
|
||||
var newIds = [Relatable.Identifier]()
|
||||
while !identifiers.isAtEnd {
|
||||
@@ -239,7 +267,7 @@ extension ToManyRelationship: Codable {
|
||||
let type = try identifier.decode(String.self, forKey: .entityType)
|
||||
|
||||
guard type == Relatable.jsonType else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type)
|
||||
throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath)
|
||||
}
|
||||
|
||||
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
|
||||
@@ -277,3 +305,9 @@ extension ToOneRelationship: CustomStringConvertible {
|
||||
extension ToManyRelationship: CustomStringConvertible {
|
||||
public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" }
|
||||
}
|
||||
|
||||
private protocol _DictionaryType {}
|
||||
extension Dictionary: _DictionaryType {}
|
||||
|
||||
private protocol _ArrayType {}
|
||||
extension Array: _ArrayType {}
|
||||
|
||||
@@ -96,10 +96,13 @@ extension ResourceObjectProxy {
|
||||
public static var jsonType: String { return Description.jsonType }
|
||||
}
|
||||
|
||||
/// A marker protocol.
|
||||
public protocol AbstractResourceObject {}
|
||||
|
||||
/// ResourceObjectType is the protocol that ResourceObject conforms to. This
|
||||
/// protocol lets other types accept any ResourceObject as a generic
|
||||
/// specialization.
|
||||
public protocol ResourceObjectType: ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription {
|
||||
public protocol ResourceObjectType: AbstractResourceObject, ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription {
|
||||
associatedtype Meta: JSONAPI.Meta
|
||||
associatedtype Links: JSONAPI.Links
|
||||
|
||||
@@ -414,18 +417,47 @@ public extension ResourceObject {
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
guard ResourceObject.jsonType == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
|
||||
throw ResourceObjectDecodingError(
|
||||
expectedJSONAPIType: ResourceObject.jsonType,
|
||||
found: type
|
||||
)
|
||||
}
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
do {
|
||||
attributes = try (NoAttributes() as? Description.Attributes)
|
||||
?? container.decodeIfPresent(Description.Attributes.self, forKey: .attributes)
|
||||
?? Description.Attributes(from: EmptyObjectDecoder())
|
||||
} catch let decodingError as DecodingError {
|
||||
throw ResourceObjectDecodingError(decodingError)
|
||||
?? decodingError
|
||||
} catch _ as EmptyObjectDecodingError {
|
||||
throw ResourceObjectDecodingError(
|
||||
subjectName: ResourceObjectDecodingError.entireObject,
|
||||
cause: .keyNotFound,
|
||||
location: .attributes
|
||||
)
|
||||
}
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships)
|
||||
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
|
||||
?? Description.Relationships(from: EmptyObjectDecoder())
|
||||
do {
|
||||
relationships = try (NoRelationships() as? Description.Relationships)
|
||||
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
|
||||
?? Description.Relationships(from: EmptyObjectDecoder())
|
||||
} catch let decodingError as DecodingError {
|
||||
throw ResourceObjectDecodingError(decodingError)
|
||||
?? decodingError
|
||||
} catch let decodingError as JSONAPICodingError {
|
||||
throw ResourceObjectDecodingError(decodingError)
|
||||
?? decodingError
|
||||
} catch _ as EmptyObjectDecodingError {
|
||||
throw ResourceObjectDecodingError(
|
||||
subjectName: ResourceObjectDecodingError.entireObject,
|
||||
cause: .keyNotFound,
|
||||
location: .relationships
|
||||
)
|
||||
}
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// ResourceObjectDecodingError.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/19.
|
||||
//
|
||||
|
||||
public struct ResourceObjectDecodingError: Swift.Error, Equatable {
|
||||
public let subjectName: String
|
||||
public let cause: Cause
|
||||
public let location: Location
|
||||
|
||||
static let entireObject = "entire object"
|
||||
|
||||
public enum Cause: Equatable {
|
||||
case keyNotFound
|
||||
case valueNotFound
|
||||
case typeMismatch(expectedTypeName: String)
|
||||
case jsonTypeMismatch(expectedType: String, foundType: String)
|
||||
case quantityMismatch(expected: JSONAPICodingError.Quantity)
|
||||
}
|
||||
|
||||
public enum Location: String, Equatable {
|
||||
case attributes
|
||||
case relationships
|
||||
case type
|
||||
|
||||
var singular: String {
|
||||
switch self {
|
||||
case .attributes: return "attribute"
|
||||
case .relationships: return "relationship"
|
||||
case .type: return "type"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init?(_ decodingError: DecodingError) {
|
||||
switch decodingError {
|
||||
case .typeMismatch(let expectedType, let ctx):
|
||||
(location, subjectName) = Self.context(ctx)
|
||||
let typeString = String(describing: expectedType)
|
||||
cause = .typeMismatch(expectedTypeName: typeString)
|
||||
case .valueNotFound(_, let ctx):
|
||||
(location, subjectName) = Self.context(ctx)
|
||||
cause = .valueNotFound
|
||||
case .keyNotFound(let missingKey, let ctx):
|
||||
(location, _) = Self.context(ctx)
|
||||
subjectName = missingKey.stringValue
|
||||
cause = .keyNotFound
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init?(_ jsonAPIError: JSONAPICodingError) {
|
||||
switch jsonAPIError {
|
||||
case .typeMismatch(expected: let expected, found: let found, path: let path):
|
||||
(location, subjectName) = Self.context(path: path)
|
||||
cause = .jsonTypeMismatch(expectedType: expected, foundType: found)
|
||||
case .quantityMismatch(expected: let expected, path: let path):
|
||||
(location, subjectName) = Self.context(path: path)
|
||||
cause = .quantityMismatch(expected: expected)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init(expectedJSONAPIType: String, found: String) {
|
||||
location = .type
|
||||
subjectName = "self"
|
||||
cause = .jsonTypeMismatch(expectedType: expectedJSONAPIType, foundType: found)
|
||||
}
|
||||
|
||||
init(subjectName: String, cause: Cause, location: Location) {
|
||||
self.subjectName = subjectName
|
||||
self.cause = cause
|
||||
self.location = location
|
||||
}
|
||||
|
||||
static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) {
|
||||
|
||||
return context(path: decodingContext.codingPath)
|
||||
}
|
||||
|
||||
static func context(path: [CodingKey]) -> (Location, name: String) {
|
||||
let location: Location
|
||||
if path.contains(where: { $0.stringValue == "attributes" }) {
|
||||
location = .attributes
|
||||
} else if path.contains(where: { $0.stringValue == "relationships" }) {
|
||||
location = .relationships
|
||||
} else {
|
||||
location = .type
|
||||
}
|
||||
|
||||
return (
|
||||
location,
|
||||
name: path.last?.stringValue ?? "unnamed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ResourceObjectDecodingError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch cause {
|
||||
case .keyNotFound:
|
||||
if subjectName == ResourceObjectDecodingError.entireObject {
|
||||
return "\(location) object is required and missing."
|
||||
}
|
||||
return "'\(subjectName)' \(location.singular) is required and missing."
|
||||
case .valueNotFound:
|
||||
return "'\(subjectName)' \(location.singular) is not nullable but null."
|
||||
case .typeMismatch(expectedTypeName: let expected):
|
||||
return "'\(subjectName)' \(location.singular) is not a \(expected) as expected."
|
||||
case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type:
|
||||
return "found JSON:API type \"\(found)\" but expected \"\(expected)\""
|
||||
case .jsonTypeMismatch(expectedType: let expected, foundType: let found):
|
||||
return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\""
|
||||
case .quantityMismatch(expected: let expected):
|
||||
let expecation: String = {
|
||||
switch expected {
|
||||
case .many:
|
||||
return "\(expected) values"
|
||||
case .one:
|
||||
return "\(expected) value"
|
||||
}
|
||||
}()
|
||||
return "'\(subjectName)' \(location.singular) should contain \(expecation) but found \(expected.other)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,15 +36,12 @@ public struct SparseFieldset<
|
||||
|
||||
public extension ResourceObject where Description.Attributes: SparsableAttributes {
|
||||
|
||||
/// The `SparseFieldset` type for this `ResourceObject`
|
||||
typealias SparseType = SparseFieldset<Description, MetaType, LinksType, EntityRawIdType>
|
||||
|
||||
/// Get a Sparse Fieldset of this `ResourceObject` that can be encoded
|
||||
/// as a `SparsePrimaryResource`.
|
||||
func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseFieldset<Description, MetaType, LinksType, EntityRawIdType> {
|
||||
func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseType {
|
||||
return SparseFieldset(self, fields: fields)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ResourceObject where Description.Attributes: SparsableAttributes {
|
||||
|
||||
/// The `SparseFieldset` type for this `ResourceObject`
|
||||
typealias SparseType = SparseFieldset<Description, MetaType, LinksType, EntityRawIdType>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public enum ArrayElementComparison: Equatable, CustomStringConvertible {
|
||||
case differentValues(String, String)
|
||||
case prebuilt(String)
|
||||
|
||||
public init(sameTypeComparison: Comparison) {
|
||||
public init(sameTypeComparison: BasicComparison) {
|
||||
switch sameTypeComparison {
|
||||
case .same:
|
||||
self = .same
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
import JSONAPI
|
||||
|
||||
extension Attributes {
|
||||
public func compare(to other: Self) -> [String: Comparison] {
|
||||
public func compare(to other: Self) -> [String: BasicComparison] {
|
||||
let mirror1 = Mirror(reflecting: self)
|
||||
let mirror2 = Mirror(reflecting: other)
|
||||
|
||||
var comparisons = [String: Comparison]()
|
||||
var comparisons = [String: BasicComparison]()
|
||||
|
||||
for child in mirror1.children {
|
||||
guard let childLabel = child.label else { continue }
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
// Created by Mathew Polzin on 11/3/19.
|
||||
//
|
||||
|
||||
public enum Comparison: Equatable, CustomStringConvertible {
|
||||
public protocol Comparison: CustomStringConvertible {
|
||||
var rawValue: String { get }
|
||||
|
||||
var isSame: Bool { get }
|
||||
}
|
||||
|
||||
public enum BasicComparison: Comparison, Equatable {
|
||||
case same
|
||||
case different(String, String)
|
||||
case prebuilt(String)
|
||||
@@ -50,11 +56,11 @@ public enum Comparison: Equatable, CustomStringConvertible {
|
||||
|
||||
public typealias NamedDifferences = [String: String]
|
||||
|
||||
public protocol PropertyComparable: CustomStringConvertible {
|
||||
public protocol PropertyComparison: Comparison {
|
||||
var differences: NamedDifferences { get }
|
||||
}
|
||||
|
||||
extension PropertyComparable {
|
||||
extension PropertyComparison {
|
||||
public var description: String {
|
||||
return differences
|
||||
.map { "(\($0): \($1))" }
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
import JSONAPI
|
||||
|
||||
public struct DocumentComparison: Equatable, PropertyComparable {
|
||||
public let apiDescription: Comparison
|
||||
public struct DocumentComparison: Equatable, PropertyComparison {
|
||||
public let apiDescription: BasicComparison
|
||||
public let body: BodyComparison
|
||||
|
||||
init(apiDescription: Comparison, body: BodyComparison) {
|
||||
init(apiDescription: BasicComparison, body: BodyComparison) {
|
||||
self.apiDescription = apiDescription
|
||||
self.body = body
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public enum BodyComparison: Equatable, CustomStringConvertible {
|
||||
case differentErrors(ErrorComparison)
|
||||
case differentData(DocumentDataComparison)
|
||||
|
||||
public typealias ErrorComparison = [Comparison]
|
||||
public typealias ErrorComparison = [BasicComparison]
|
||||
|
||||
static func compare<E: JSONAPIError, M: JSONAPI.Meta, L: JSONAPI.Links>(errors errors1: [E], _ meta1: M?, _ links1: L?, with errors2: [E], _ meta2: M?, _ links2: L?) -> ErrorComparison {
|
||||
return errors1.compare(
|
||||
@@ -48,9 +48,9 @@ public enum BodyComparison: Equatable, CustomStringConvertible {
|
||||
String(describing: error2)
|
||||
)
|
||||
}
|
||||
).map(Comparison.init) + [
|
||||
Comparison(meta1, meta2),
|
||||
Comparison(links1, links2)
|
||||
).map(BasicComparison.init) + [
|
||||
BasicComparison(meta1, meta2),
|
||||
BasicComparison(links1, links2)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -79,30 +79,10 @@ public enum BodyComparison: Equatable, CustomStringConvertible {
|
||||
public var rawValue: String { description }
|
||||
}
|
||||
|
||||
extension Document {
|
||||
public func compare<T>(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody<T>, T: ResourceObjectType {
|
||||
extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: TestableResourceBody {
|
||||
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) -> 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 {
|
||||
return DocumentComparison(
|
||||
apiDescription: Comparison(
|
||||
apiDescription: BasicComparison(
|
||||
String(describing: apiDescription),
|
||||
String(describing: other.apiDescription)
|
||||
),
|
||||
@@ -111,70 +91,32 @@ extension Document {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body {
|
||||
public func compare<T>(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody<T> {
|
||||
extension DocumentBody where Self: Equatable, PrimaryResourceBody: TestableResourceBody {
|
||||
public func compare(to other: Self) -> BodyComparison {
|
||||
|
||||
// rule out case where they are the same
|
||||
guard self != other else {
|
||||
return .same
|
||||
}
|
||||
|
||||
switch (self, other) {
|
||||
case (.errors(let errors1), .errors(let errors2)):
|
||||
return .differentErrors(BodyComparison.compare(errors: errors1.0,
|
||||
errors1.meta,
|
||||
errors1.links,
|
||||
with: errors2.0,
|
||||
errors2.meta,
|
||||
errors2.links))
|
||||
case (.errors, .data):
|
||||
return .dataErrorMismatch(errorOnLeft: true)
|
||||
case (.data, .errors):
|
||||
return .dataErrorMismatch(errorOnLeft: false)
|
||||
case (.data(let data1), .data(let data2)):
|
||||
// rule out case where they are both error bodies
|
||||
if let errors1 = errors, let errors2 = other.errors {
|
||||
return .differentErrors(
|
||||
BodyComparison.compare(
|
||||
errors: errors1, meta, links,
|
||||
with: errors2, meta, links
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// rule out the case where they are both data
|
||||
if let data1 = data, let data2 = other.data {
|
||||
return .differentData(data1.compare(to: data2))
|
||||
}
|
||||
}
|
||||
|
||||
public func compare<T>(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody<T?> {
|
||||
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<T>(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody<T> {
|
||||
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))
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
|
||||
import JSONAPI
|
||||
|
||||
public struct DocumentDataComparison: Equatable, PropertyComparable {
|
||||
public struct DocumentDataComparison: Equatable, PropertyComparison {
|
||||
public let primary: PrimaryResourceBodyComparison
|
||||
public let includes: IncludesComparison
|
||||
public let meta: Comparison
|
||||
public let links: Comparison
|
||||
public let meta: BasicComparison
|
||||
public let links: BasicComparison
|
||||
|
||||
init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: Comparison, links: Comparison) {
|
||||
init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: BasicComparison, links: BasicComparison) {
|
||||
self.primary = primary
|
||||
self.includes = includes
|
||||
self.meta = meta
|
||||
@@ -33,58 +33,35 @@ public struct DocumentDataComparison: Equatable, PropertyComparable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data {
|
||||
public func compare<T>(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody<T> {
|
||||
extension DocumentBodyData where PrimaryResourceBody: TestableResourceBody {
|
||||
public func compare(to other: Self) -> DocumentDataComparison {
|
||||
return .init(
|
||||
primary: primary.compare(to: other.primary),
|
||||
includes: includes.compare(to: other.includes),
|
||||
meta: Comparison(meta, other.meta),
|
||||
links: Comparison(links, other.links)
|
||||
)
|
||||
}
|
||||
|
||||
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> {
|
||||
return .init(
|
||||
primary: primary.compare(to: other.primary),
|
||||
includes: includes.compare(to: other.includes),
|
||||
meta: Comparison(meta, other.meta),
|
||||
links: Comparison(links, other.links)
|
||||
meta: BasicComparison(meta, other.meta),
|
||||
links: BasicComparison(links, other.links)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible {
|
||||
case single(ResourceObjectComparison)
|
||||
case many(ManyResourceObjectComparison)
|
||||
case other(Comparison)
|
||||
case oneOrMore(ManyResourceObjectComparison)
|
||||
case optionalSingle(BasicComparison)
|
||||
|
||||
public var isSame: Bool {
|
||||
switch self {
|
||||
case .other(let comparison):
|
||||
case .optionalSingle(let comparison):
|
||||
return comparison == .same
|
||||
case .single(let comparison):
|
||||
return comparison.isSame
|
||||
case .many(let comparison):
|
||||
case .oneOrMore(let comparison):
|
||||
return comparison.isSame
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .other(let comparison):
|
||||
case .optionalSingle(let comparison):
|
||||
return comparison.rawValue
|
||||
case .single(let comparison):
|
||||
return comparison.rawValue
|
||||
case .many(let comparison):
|
||||
case .oneOrMore(let comparison):
|
||||
return comparison.rawValue
|
||||
}
|
||||
}
|
||||
@@ -92,7 +69,7 @@ public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible {
|
||||
public var rawValue: String { return description }
|
||||
}
|
||||
|
||||
public struct ManyResourceObjectComparison: Equatable, PropertyComparable {
|
||||
public struct ManyResourceObjectComparison: Equatable, PropertyComparison {
|
||||
public let comparisons: [ArrayElementComparison]
|
||||
|
||||
public init(_ comparisons: [ArrayElementComparison]) {
|
||||
@@ -109,42 +86,19 @@ public struct ManyResourceObjectComparison: Equatable, PropertyComparable {
|
||||
}
|
||||
}
|
||||
|
||||
extension SingleResourceBody where Entity: ResourceObjectType {
|
||||
extension TestableResourceBody where TestablePrimaryResourceType: ResourceObjectType {
|
||||
public func compare(to other: Self) -> PrimaryResourceBodyComparison {
|
||||
return .single(.init(value, other.value))
|
||||
}
|
||||
}
|
||||
guard let one = testableResourceObject,
|
||||
let two = other.testableResourceObject else {
|
||||
|
||||
public protocol _OptionalResourceObjectType {
|
||||
associatedtype Wrapped: ResourceObjectType
|
||||
func nilOrName<T>(_ resObj: [T]?) -> String {
|
||||
resObj.map { _ in String(describing: T.self) } ?? "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
|
||||
return .optionalSingle(BasicComparison(nilOrName(testableResourceObject), nilOrName(other.testableResourceObject)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return .oneOrMore(.init(one.compare(to: two, using: { r1, r2 in
|
||||
let r1AsResource = r1 as? AbstractResourceObjectType
|
||||
|
||||
let maybeComparison = r1AsResource
|
||||
@@ -165,3 +119,39 @@ extension ManyResourceBody where Entity: ResourceObjectType {
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
public protocol TestableResourceBody {
|
||||
associatedtype TestablePrimaryResourceType: ResourceObjectType
|
||||
var testableResourceObject: [TestablePrimaryResourceType]? { 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 ResourceObject: OptionalResourceObjectType {
|
||||
public var maybeValue: Self? { self }
|
||||
}
|
||||
|
||||
extension ManyResourceBody: TestableResourceBody where PrimaryResource: ResourceObjectType {
|
||||
public var testableResourceObject: [PrimaryResource]? { values }
|
||||
}
|
||||
|
||||
extension SingleResourceBody: TestableResourceBody where PrimaryResource: OptionalResourceObjectType {
|
||||
public typealias TestablePrimaryResourceType = PrimaryResource.Wrapped
|
||||
|
||||
public var testableResourceObject: [TestablePrimaryResourceType]? { value.maybeValue.map { [$0] } }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user