Adding Document Decoding Errors for some common problems

This commit is contained in:
Mathew Polzin
2019-11-10 23:02:26 -08:00
parent 4dc30ddc1c
commit 2eecf95995
12 changed files with 483 additions and 15 deletions
+20 -6
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,10 +389,24 @@ 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
@@ -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."
}
}
}
+24 -1
View File
@@ -56,8 +56,14 @@ 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 {
throw IncludesDecodingError(error: error, idx: idx)
}
}
values = valueAggregator
@@ -177,3 +183,20 @@ 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)"
}
}
+16 -1
View File
@@ -125,8 +125,17 @@ extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var valueAggregator = [PrimaryResource]()
var idx = 0
while !container.isAtEnd {
valueAggregator.append(try container.decode(PrimaryResource.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
}
@@ -145,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
}
+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)
}
@@ -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,7 +417,10 @@ public extension ResourceObject {
let type = try container.decode(String.self, forKey: .type)
guard ResourceObject.jsonType == type else {
throw JSONAPICodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath)
throw ResourceObjectDecodingError(
expectedJSONAPIType: ResourceObject.jsonType,
found: type
)
}
let maybeUnidentified = Unidentified() as? EntityRawIdType
@@ -23,11 +23,13 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable {
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"
}
}
}
@@ -63,6 +65,12 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable {
}
}
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
@@ -75,8 +83,17 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable {
}
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 (
path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships,
location,
name: path.last?.stringValue ?? "unnamed"
)
}
@@ -94,6 +111,8 @@ extension ResourceObjectDecodingError: CustomStringConvertible {
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):
@@ -0,0 +1,189 @@
//
// DocumentDecodingErrorTests.swift
//
//
// Created by Mathew Polzin on 11/10/19.
//
import XCTest
import JSONAPI
import Poly
final class DocumentDecodingErrorTests: XCTestCase {
func test_singlePrimaryResource_missing() {
XCTAssertThrowsError(
try testDecoder.decode(
Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
from: single_document_null
)
) { error in
guard let docError = error as? DocumentDecodingError,
case .primaryResourceMissing = docError else {
XCTFail("Expected primary resource missing error. Got \(error)")
return
}
XCTAssertEqual(String(describing: error), "Primary Resource missing.")
}
}
func test_singlePrimaryResource_failure() {
XCTAssertThrowsError(
try testDecoder.decode(
Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
from: single_document_no_includes_missing_relationship
)
) { error in
guard let docError = error as? DocumentDecodingError,
case .primaryResource = docError else {
XCTFail("Expected primary resource document error. Got \(error)")
return
}
XCTAssertEqual(String(describing: error), "Primary Resource failed to parse because 'author' relationship is required and missing.")
}
}
func test_manyPrimaryResource_missing() {
XCTAssertThrowsError(
try testDecoder.decode(
Document<ManyResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
from: many_document_no_includes_data_is_null
)
) { error in
guard let docError = error as? DocumentDecodingError,
case .primaryResourcesMissing = docError else {
XCTFail("Expected primary resource missing error. Got \(error)")
return
}
XCTAssertEqual(String(describing: error), "Primary Resources array missing.")
}
}
func test_manyPrimaryResource_failure() {
XCTAssertThrowsError(
try testDecoder.decode(
Document<ManyResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
from: many_document_no_includes_missing_relationship
)
) { error in
guard let docError = error as? DocumentDecodingError,
case .primaryResource = docError else {
XCTFail("Expected primary resource document error. Got \(error)")
return
}
XCTAssertEqual(String(describing: error), "Primary Resource 2 failed to parse because 'author' relationship is required and missing.")
}
}
func test_include_failure() {
XCTAssertThrowsError(
try testDecoder.decode(
Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.self,
from: single_document_some_includes_wrong_type
)
) { error in
guard let docError = error as? DocumentDecodingError,
case .includes = docError else {
XCTFail("Expected primary resource document error. Got \(error)")
return
}
XCTAssertEqual(String(describing: error), #"Include 3 failed to parse: found JSON:API type "not_an_author" but expected "authors""#)
}
}
}
// MARK: - Test Types
extension DocumentDecodingErrorTests {
enum AuthorType: ResourceObjectDescription {
static var jsonType: String { return "authors" }
typealias Attributes = NoAttributes
typealias Relationships = NoRelationships
}
typealias Author = BasicEntity<AuthorType>
enum ArticleType: ResourceObjectDescription {
static var jsonType: String { return "articles" }
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
let author: ToOneRelationship<Author, NoMetadata, NoLinks>
}
}
typealias Article = BasicEntity<ArticleType>
enum BookType: ResourceObjectDescription {
static var jsonType: String { return "books" }
struct Attributes: JSONAPI.SparsableAttributes {
let pageCount: Attribute<Int>
enum CodingKeys: String, SparsableCodingKey {
case pageCount
}
}
struct Relationships: JSONAPI.Relationships {
let author: ToOneRelationship<Author, NoMetadata, NoLinks>
let series: ToManyRelationship<Book, NoMetadata, NoLinks>
}
}
typealias Book = BasicEntity<BookType>
struct TestPageMetadata: JSONAPI.Meta {
let total: Int
let limit: Int
let offset: Int
}
struct TestLinks: JSONAPI.Links {
let link: Link<String, NoMetadata>
let link2: Link<String,TestMetadata>
struct TestMetadata: JSONAPI.Meta {
let hello: String
}
}
typealias TestAPIDescription = APIDescription<NoMetadata>
enum TestError: JSONAPIError {
case unknownError
case basic(BasicError)
struct BasicError: Codable, Equatable {
let code: Int
let description: String
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = (try? .basic(container.decode(BasicError.self))) ?? .unknown
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .unknownError:
try container.encode("unknown")
case .basic(let error):
try container.encode(error)
}
}
public static var unknown: Self {
return .unknownError
}
}
}
@@ -37,6 +37,17 @@ let single_document_no_includes = """
}
""".data(using: .utf8)!
let single_document_no_includes_missing_relationship = """
{
"data": {
"id": "1",
"type": "articles",
"relationships": {
}
}
}
""".data(using: .utf8)!
let single_document_no_includes_with_api_description = """
{
"data": {
@@ -247,6 +258,37 @@ let single_document_some_includes = """
}
""".data(using: .utf8)!
let single_document_some_includes_wrong_type = """
{
"data": {
"id": "1",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "33"
}
}
}
},
"included": [
{
"id": "30",
"type": "authors"
},
{
"id": "31",
"type": "authors"
},
{
"id": "33",
"type": "not_an_author"
}
]
}
""".data(using: .utf8)!
let single_document_some_includes_with_api_description = """
{
"data": {
@@ -452,6 +494,49 @@ let many_document_no_includes = """
}
""".data(using: .utf8)!
let many_document_no_includes_data_is_null = """
{
"data": null
}
""".data(using: .utf8)!
let many_document_no_includes_missing_relationship = """
{
"data": [
{
"id": "1",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "33"
}
}
}
},
{
"id": "2",
"type": "articles",
"relationships": {
}
},
{
"id": "3",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "11"
}
}
}
}
]
}
""".data(using: .utf8)!
let many_document_no_includes_with_api_description = """
{
"data": [
@@ -287,6 +287,30 @@ extension ResourceObjectDecodingErrorTests {
}
}
// MARK: - JSON:API Type
extension ResourceObjectDecodingErrorTests {
func test_wrongType() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity2.self,
from: entity_is_wrong_type
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "self",
cause: .jsonTypeMismatch(expectedType: "fourteenth_test_entities", foundType: "not_correct_type"),
location: .type
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
#"found JSON:API type "not_correct_type" but expected "fourteenth_test_entities""#
)
}
}
}
// MARK: - Test Types
extension ResourceObjectDecodingErrorTests {
enum TestEntityType: ResourceObjectDescription {
@@ -533,6 +533,17 @@ let entity_attribute_is_wrong_type3 = """
}
""".data(using: .utf8)!
let entity_is_wrong_type = """
{
"id": "1",
"type": "not_correct_type",
"attributes": {
"required": "hello",
"yetAnother": 101
}
}
""".data(using: .utf8)!
let entity_attributes_entirely_missing = """
{
"id": "1",
+15
View File
@@ -85,6 +85,19 @@ extension CustomAttributesTests {
]
}
extension DocumentDecodingErrorTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__DocumentDecodingErrorTests = [
("test_include_failure", test_include_failure),
("test_manyPrimaryResource_failure", test_manyPrimaryResource_failure),
("test_manyPrimaryResource_missing", test_manyPrimaryResource_missing),
("test_singlePrimaryResource_failure", test_singlePrimaryResource_failure),
("test_singlePrimaryResource_missing", test_singlePrimaryResource_missing),
]
}
extension DocumentTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
@@ -379,6 +392,7 @@ extension ResourceObjectDecodingErrorTests {
("test_required_attribute", test_required_attribute),
("test_required_relationship", test_required_relationship),
("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship),
("test_wrongType", test_wrongType),
]
}
@@ -542,6 +556,7 @@ public func __allTests() -> [XCTestCaseEntry] {
testCase(BasicJSONAPIErrorTests.__allTests__BasicJSONAPIErrorTests),
testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests),
testCase(CustomAttributesTests.__allTests__CustomAttributesTests),
testCase(DocumentDecodingErrorTests.__allTests__DocumentDecodingErrorTests),
testCase(DocumentTests.__allTests__DocumentTests),
testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests),
testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests),