Compare commits

...

16 Commits

Author SHA1 Message Date
Mathew Polzin 440b649577 Merge pull request #49 from mattpolzin/alpha/3x
Alpha/3x
2019-11-12 18:38:31 -08:00
Mathew Polzin 54551617b4 Add more errors for the resource object type property 2019-11-12 18:34:34 -08:00
Mathew Polzin 8c3a82ec23 Merge pull request #53 from mattpolzin/feature/better-error-messages
Feature/better error messages
2019-11-10 23:47:05 -08:00
Mathew Polzin e9a3b35dc7 improve error messages for poly include types 2019-11-10 23:43:35 -08:00
Mathew Polzin 2eecf95995 Adding Document Decoding Errors for some common problems 2019-11-10 23:02:26 -08:00
Mathew Polzin 4dc30ddc1c Rounding out the Resource Object errors 2019-11-10 20:46:35 -08:00
Mathew Polzin 455ff64326 update linuxmain 2019-11-09 00:34:39 -08:00
Mathew Polzin 0b4baf35d5 got some attribute cases added and tested. added some descriptions (custom string convertible) 2019-11-09 00:33:42 -08:00
Mathew Polzin 11ef050d58 most common relationship errors tested. 2019-11-08 18:47:28 -08:00
Mathew Polzin 86344ef93f trivial refactor in sparse fieldset file 2019-11-07 08:08:45 -08:00
Mathew Polzin 7fabe2574e rename some of the new public protocols in JSONAPITesting 2019-11-07 07:56:58 -08:00
Mathew Polzin 19636a47f0 Merge pull request #51 from mattpolzin/feature/primary-resource-body-improvements
Feature/primary resource body improvements
2019-11-06 23:46:13 -08:00
Mathew Polzin 0538de48cb rename PolyWrapped to CodablePolyWrapped (missed when I did other similar renaming 2019-11-06 23:41:02 -08:00
Mathew Polzin 024fe2d452 Down to one sequence of compare functions for all documents 2019-11-06 23:21:22 -08:00
Mathew Polzin 832161628b go from 3 specializations of all document related compare functions down to 2. 2019-11-06 23:05:21 -08:00
Mathew Polzin f37f44cfda add comparable protocol 2019-11-06 21:42:13 -08:00
32 changed files with 1654 additions and 268 deletions
+2 -2
View File
@@ -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
View File
@@ -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(
+22 -8
View File
@@ -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,73 @@
//
// 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
init?(_ context: DecodingError.Context) {
guard context.codingPath.contains(where: { $0.stringValue == "data" }) else {
return nil
}
self = .data
}
}
}
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."
}
}
}
+58 -2
View File
@@ -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")
}
}
+33 -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,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
}
-14
View File
@@ -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
}
+27
View File
@@ -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
}
}
}
}
+9 -1
View File
@@ -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.
+5 -2
View File
@@ -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 {}
+40 -6
View File
@@ -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
@@ -411,21 +414,56 @@ public extension ResourceObject {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
let type: String
do {
type = try container.decode(String.self, forKey: .type)
} catch let error as DecodingError {
throw ResourceObjectDecodingError(error)
?? error
}
guard ResourceObject.jsonType == type else {
throw 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,135 @@
//
// 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 where subjectName == ResourceObjectDecodingError.entireObject:
return "\(location) object is required and missing."
case .keyNotFound where location == .type:
return "'type' (a.k.a. JSON:API type name) is required and missing."
case .keyNotFound:
return "'\(subjectName)' \(location.singular) is required and missing."
case .valueNotFound where location == .type:
return "'type' (a.k.a. JSON:API type name) is not nullable but null was found."
case .valueNotFound:
return "'\(subjectName)' \(location.singular) is not nullable but null was found."
case .typeMismatch(expectedTypeName: let expected) where location == .type:
return "'type' (a.k.a. the JSON:API type name) is not a \(expected) as expected."
case .typeMismatch(expectedTypeName: let expected):
return "'\(subjectName)' \(location.singular) is not a \(expected) as expected."
case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type:
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 EncodableJSONAPIDocument where Body: Equatable {
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,64 +91,8 @@ 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)
}
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)
}
public func compare<T>(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody<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 {
@@ -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 DocumentBodyData {
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