most common relationship errors tested.

This commit is contained in:
Mathew Polzin
2019-11-08 18:47:28 -08:00
parent 86344ef93f
commit 11ef050d58
10 changed files with 300 additions and 22 deletions
+2 -2
View File
@@ -397,10 +397,10 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody:
// TODO come back to this and make robust
guard let metaVal = meta else {
throw JSONAPIEncodingError.missingOrMalformedMetadata
throw JSONAPIEncodingError.missingOrMalformedMetadata(path: decoder.codingPath)
}
guard let linksVal = links else {
throw JSONAPIEncodingError.missingOrMalformedLinks
throw JSONAPIEncodingError.missingOrMalformedLinks(path: decoder.codingPath)
}
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
+1 -1
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 JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath)
}
for value in values {
+5 -5
View File
@@ -6,9 +6,9 @@
//
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
case typeMismatch(expected: String, found: String, path: [CodingKey])
case illegalEncoding(String, path: [CodingKey])
case illegalDecoding(String, path: [CodingKey])
case missingOrMalformedMetadata(path: [CodingKey])
case missingOrMalformedLinks(path: [CodingKey])
}
+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.
@@ -22,11 +22,11 @@ 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 JSONAPIEncodingError.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 JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath)
}
}
+12 -4
View File
@@ -170,8 +170,16 @@ 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
}
@@ -181,7 +189,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
let type = try identifier.decode(String.self, forKey: .entityType)
guard type == Identifiable.jsonType else {
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type)
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath)
}
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
@@ -239,7 +247,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 JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath)
}
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
@@ -414,21 +414,114 @@ 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 JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath)
}
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.decode(Description.Attributes.self, forKey: .attributes)
} catch let decodingError as DecodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
} catch let decodingError as JSONAPIEncodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
}
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 JSONAPIEncodingError {
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)
links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links)
}
}
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)
}
public enum Location: Equatable {
case attributes
case relationships
}
init?(_ decodingError: DecodingError) {
switch decodingError {
case .typeMismatch(let expectedType, let ctx):
(location, subjectName) = Self.context(ctx)
let typeString: String
if let attrType = expectedType as? AbstractAttributeType {
typeString = String(describing: attrType.rawValueType)
} else {
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: JSONAPIEncodingError) {
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)
default:
return nil
}
}
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) {
return (
path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships,
name: path.last?.stringValue ?? "unnamed"
)
}
}
@@ -0,0 +1,114 @@
//
// ResourceObjectDecodingErrorTests.swift
//
//
// Created by Mathew Polzin on 11/8/19.
//
import XCTest
@testable import JSONAPI
// MARK: - Relationships
final class ResourceObjectDecodingErrorTests: XCTestCase {
func test_missingRelationshipsObject() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity.self,
from: entity_relationships_entirely_missing
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: ResourceObjectDecodingError.entireObject,
cause: .keyNotFound,
location: .relationships
)
)
}
}
func test_required_relationship() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity.self,
from: entity_required_relationship_is_omitted
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "required",
cause: .keyNotFound,
location: .relationships
)
)
}
}
func test_NonNullable_relationship() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity.self,
from: entity_nonNullable_relationship_is_null
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "required",
cause: .valueNotFound,
location: .relationships
)
)
}
}
func test_NonNullable_relationship2() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity.self,
from: entity_nonNullable_relationship_is_null2
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "required",
cause: .valueNotFound,
location: .relationships
)
)
}
}
func test_oneTypeVsAnother_relationship() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity.self,
from: entity_relationship_is_wrong_type
)) { error in
print(error)
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "required",
cause: .jsonTypeMismatch(expectedType: "thirteenth_test_entities", foundType: "not_the_same"),
location: .relationships
)
)
}
}
}
// MARK: - Attributes
extension ResourceObjectDecodingErrorTests {
// TODO: write tests
}
// MARK: - Test Types
extension ResourceObjectDecodingErrorTests {
enum TestEntityType: ResourceObjectDescription {
public static var jsonType: String { return "thirteenth_test_entities" }
typealias Attributes = NoAttributes
public struct Relationships: JSONAPI.Relationships {
let required: ToOneRelationship<TestEntity, NoMetadata, NoLinks>
}
}
typealias TestEntity = BasicEntity<TestEntityType>
}
@@ -393,6 +393,59 @@ let entity_all_relationships_optional_and_omitted = """
}
""".data(using: .utf8)!
let entity_nonNullable_relationship_is_null = """
{
"id": "1",
"type": "thirteenth_test_entities",
"relationships": {
"required": null
}
}
""".data(using: .utf8)!
let entity_nonNullable_relationship_is_null2 = """
{
"id": "1",
"type": "thirteenth_test_entities",
"relationships": {
"required": {
"data": null
}
}
}
""".data(using: .utf8)!
let entity_required_relationship_is_omitted = """
{
"id": "1",
"type": "thirteenth_test_entities",
"relationships": {
}
}
""".data(using: .utf8)!
let entity_relationship_is_wrong_type = """
{
"id": "1",
"type": "thirteenth_test_entities",
"relationships": {
"required": {
"data": {
"id": "123",
"type": "not_the_same"
}
}
}
}
""".data(using: .utf8)!
let entity_relationships_entirely_missing = """
{
"id": "1",
"type": "thirteenth_test_entities",
}
""".data(using: .utf8)!
let entity_unidentified = """
{
"type": "unidentified_test_entities",
@@ -8,8 +8,10 @@
import Foundation
import XCTest
let testDecoder = JSONDecoder()
func decoded<T: Decodable>(type: T.Type, data: Data) -> T {
return try! JSONDecoder().decode(T.self, from: data)
return try! testDecoder.decode(T.self, from: data)
}
func encoded<T: Encodable>(value: T) -> Data {