mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
most common relationship errors tested.
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user