From 54551617b4d56e81456feeb5b17e387df9766fbe Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 12 Nov 2019 18:34:34 -0800 Subject: [PATCH] Add more errors for the resource object type property --- .../Document/DocumentDecodingError.swift | 10 +- .../Resource Object/ResourceObject.swift | 8 +- .../ResourceObjectDecodingError.swift | 13 +- .../ResourceObjectCompareTests.swift | 1 - .../ResourceObjectDecodingErrorTests.swift | 125 +++++++++++++++++- .../stubs/ResourceObjectStubs.swift | 55 +++++++- .../SparseFieldEncoderTests.swift | 1 - 7 files changed, 193 insertions(+), 20 deletions(-) diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 0e58fe3..2bcfa60 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -40,14 +40,12 @@ public enum DocumentDecodingError: Swift.Error, Equatable { 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 + init?(_ context: DecodingError.Context) { + guard context.codingPath.contains(where: { $0.stringValue == "data" }) else { + return nil } + self = .data } } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 0604b00..83f0d1c 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -414,7 +414,13 @@ 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 ResourceObjectDecodingError( diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index b644ce6..c61e9be 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -102,13 +102,18 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { 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: - if subjectName == ResourceObjectDecodingError.entireObject { - return "\(location) object is required and missing." - } 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." + 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: diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index 2c40e87..fd43103 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -11,7 +11,6 @@ import JSONAPITesting final class ResourceObjectCompareTests: XCTestCase { func test_same() { - print(test1.compare(to: test1).differences) XCTAssertTrue(test1.compare(to: test1).differences.isEmpty) XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index d0abb6e..ce8e334 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -68,7 +68,7 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' relationship is not nullable but null." + "'required' relationship is not nullable but null was found." ) } } @@ -89,7 +89,7 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' relationship is not nullable but null." + "'required' relationship is not nullable but null was found." ) } } @@ -99,7 +99,6 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { TestEntity.self, from: entity_relationship_is_wrong_type )) { error in - print(error) XCTAssertEqual( error as? ResourceObjectDecodingError, ResourceObjectDecodingError( @@ -218,7 +217,7 @@ extension ResourceObjectDecodingErrorTests { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' attribute is not nullable but null." + "'required' attribute is not nullable but null was found." ) } } @@ -285,11 +284,44 @@ extension ResourceObjectDecodingErrorTests { ) } } + + func test_transformed_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type4 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "transformed", + cause: .typeMismatch(expectedTypeName: String(describing: Int.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'transformed' attribute is not a Int as expected." + ) + } + } + + func test_transformed_attribute2() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_always_fails + )) { error in + XCTAssertEqual( + String(describing: error), + "Error: Always Fails" + ) + } + } } // MARK: - JSON:API Type extension ResourceObjectDecodingErrorTests { - func test_wrongType() { + func test_wrongJSONAPIType() { XCTAssertThrowsError(try testDecoder.decode( TestEntity2.self, from: entity_is_wrong_type @@ -309,6 +341,69 @@ extension ResourceObjectDecodingErrorTests { ) } } + + func test_wrongDecodedType() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_wrong_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .typeMismatch(expectedTypeName: String(describing: String.self)), + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. the JSON:API type name) is not a String as expected."# + ) + } + } + + func test_type_missing() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_missing + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .keyNotFound, + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. JSON:API type name) is required and missing."# + ) + } + } + + func test_type_null() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_null + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .valueNotFound, + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. JSON:API type name) is not nullable but null was found."# + ) + } + } } // MARK: - Test Types @@ -335,10 +430,30 @@ extension ResourceObjectDecodingErrorTests { let required: Attribute let other: Attribute? let yetAnother: Attribute? + let transformed: TransformedAttribute? + let transformed2: TransformedAttribute? } typealias Relationships = NoRelationships } typealias TestEntity2 = BasicEntity + + enum IntToString: Transformer { + static func transform(_ value: Int) throws -> String { + return "\(value)" + } + typealias From = Int + typealias To = String + } + + enum AlwaysFails: Transformer { + static func transform(_ value: String) throws -> String { + throw Error() + } + + struct Error: Swift.Error, CustomStringConvertible { + let description: String = "Error: Always Fails" + } + } } diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 861ea1c..181826f 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -533,6 +533,35 @@ let entity_attribute_is_wrong_type3 = """ } """.data(using: .utf8)! +let entity_attribute_is_wrong_type4 = """ +{ + "id": "1", + "type": "fourteenth_test_entities", + "attributes": { + "required": "hello", + "transformed": "world" + } +} +""".data(using: .utf8)! + +let entity_attribute_always_fails = """ +{ + "id": "1", + "type": "fourteenth_test_entities", + "attributes": { + "required": "hello", + "transformed2": "world" + } +} +""".data(using: .utf8)! + +let entity_attributes_entirely_missing = """ +{ + "id": "1", + "type": "fourteenth_test_entities" +} +""".data(using: .utf8)! + let entity_is_wrong_type = """ { "id": "1", @@ -544,10 +573,32 @@ let entity_is_wrong_type = """ } """.data(using: .utf8)! -let entity_attributes_entirely_missing = """ +let entity_type_is_wrong_type = """ { "id": "1", - "type": "fourteenth_test_entities" + "type": 10, + "attributes": { + "required": "hello" + } +} +""".data(using: .utf8)! + +let entity_type_is_missing = """ +{ + "id": "1", + "attributes": { + "required": "hello" + } +} +""".data(using: .utf8)! + +let entity_type_is_null = """ +{ + "id": "1", + "type": null, + "attributes": { + "required": "hello" + } } """.data(using: .utf8)! diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index c6192f8..2aa9fb2 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -17,7 +17,6 @@ class SparseFieldEncoderTests: XCTestCase { do { let _ = try encoder.encode(Wrapper()) } catch let err as Wrapper.OuterFail.FailError { - print(err.path) XCTAssertEqual(err.path.first as? Wrapper.OuterFail.CodingKeys, Wrapper.OuterFail.CodingKeys.inner) } catch { XCTFail("received unexpected error during test")