got some attribute cases added and tested. added some descriptions (custom string convertible)

This commit is contained in:
Mathew Polzin
2019-11-09 00:33:42 -08:00
parent 11ef050d58
commit 0b4baf35d5
8 changed files with 275 additions and 25 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(path: decoder.codingPath)
throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath)
}
guard let linksVal = links else {
throw JSONAPIEncodingError.missingOrMalformedLinks(path: decoder.codingPath)
throw JSONAPICodingError.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.", path: encoder.codingPath)
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 {
@@ -1,11 +1,11 @@
//
// EncodingError.swift
// JSONAPICodingError.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/7/18.
//
public enum JSONAPIEncodingError: Swift.Error {
public enum JSONAPICodingError: Swift.Error {
case typeMismatch(expected: String, found: String, path: [CodingKey])
case illegalEncoding(String, path: [CodingKey])
case illegalDecoding(String, path: [CodingKey])
@@ -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.", path: decoder.codingPath)
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.", path: encoder.codingPath)
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)
}
}
+2 -2
View File
@@ -189,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, path: decoder.codingPath)
throw JSONAPICodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath)
}
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
@@ -247,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, path: decoder.codingPath)
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)))
@@ -414,21 +414,25 @@ 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, path: decoder.codingPath)
throw JSONAPICodingError.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)
do {
attributes = try (NoAttributes() as? Description.Attributes) ??
container.decode(Description.Attributes.self, forKey: .attributes)
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 let decodingError as JSONAPIEncodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
} catch _ as EmptyObjectDecodingError {
throw ResourceObjectDecodingError(
subjectName: ResourceObjectDecodingError.entireObject,
cause: .keyNotFound,
location: .attributes
)
}
do {
@@ -438,7 +442,7 @@ public extension ResourceObject {
} catch let decodingError as DecodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
} catch let decodingError as JSONAPIEncodingError {
} catch let decodingError as JSONAPICodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
} catch _ as EmptyObjectDecodingError {
@@ -469,21 +473,23 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable {
case jsonTypeMismatch(expectedType: String, foundType: String)
}
public enum Location: Equatable {
public enum Location: String, Equatable {
case attributes
case relationships
var singular: String {
switch self {
case .attributes: return "attribute"
case .relationships: return "relationship"
}
}
}
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)
}
let typeString = String(describing: expectedType)
cause = .typeMismatch(expectedTypeName: typeString)
case .valueNotFound(_, let ctx):
(location, subjectName) = Self.context(ctx)
@@ -497,7 +503,7 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable {
}
}
init?(_ jsonAPIError: JSONAPIEncodingError) {
init?(_ jsonAPIError: JSONAPICodingError) {
switch jsonAPIError {
case .typeMismatch(expected: let expected, found: let found, path: let path):
(location, subjectName) = Self.context(path: path)
@@ -525,3 +531,21 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable {
)
}
}
extension ResourceObjectDecodingError: CustomStringConvertible {
public var description: String {
switch cause {
case .keyNotFound:
if subjectName == ResourceObjectDecodingError.entireObject {
return "\(location) object is required and missing."
}
return "'\(subjectName)' \(location.singular) is required and missing."
case .valueNotFound:
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):
return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\""
}
}
}
@@ -23,6 +23,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase {
location: .relationships
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"relationships object is required and missing."
)
}
}
@@ -39,6 +44,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase {
location: .relationships
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'required' relationship is required and missing."
)
}
}
@@ -55,6 +65,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase {
location: .relationships
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'required' relationship is not nullable but null."
)
}
}
@@ -71,6 +86,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase {
location: .relationships
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'required' relationship is not nullable but null."
)
}
}
@@ -88,13 +108,146 @@ final class ResourceObjectDecodingErrorTests: XCTestCase {
location: .relationships
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
#"'required' relationship is of JSON:API type "not_the_same" but it was expected to be "thirteenth_test_entities""#
)
}
}
func test_twoOneVsToMany_relationship() {
// TODO: write test
}
}
// MARK: - Attributes
extension ResourceObjectDecodingErrorTests {
// TODO: write tests
func test_missingAttributesObject() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity2.self,
from: entity_attributes_entirely_missing
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: ResourceObjectDecodingError.entireObject,
cause: .keyNotFound,
location: .attributes
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"attributes object is required and missing."
)
}
}
func test_required_attribute() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity2.self,
from: entity_required_attribute_is_omitted
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "required",
cause: .keyNotFound,
location: .attributes
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'required' attribute is required and missing."
)
}
}
func test_NonNullable_attribute() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity2.self,
from: entity_nonNullable_attribute_is_null
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "required",
cause: .valueNotFound,
location: .attributes
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'required' attribute is not nullable but null."
)
}
}
func test_oneTypeVsAnother_attribute() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity2.self,
from: entity_attribute_is_wrong_type
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "required",
cause: .typeMismatch(expectedTypeName: String(describing: String.self)),
location: .attributes
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'required' attribute is not a String as expected."
)
}
}
func test_oneTypeVsAnother_attribute2() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity2.self,
from: entity_attribute_is_wrong_type2
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "other",
cause: .typeMismatch(expectedTypeName: String(describing: Int.self)),
location: .attributes
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'other' attribute is not a Int as expected."
)
}
}
func test_oneTypeVsAnother_attribute3() {
XCTAssertThrowsError(try testDecoder.decode(
TestEntity2.self,
from: entity_attribute_is_wrong_type3
)) { error in
XCTAssertEqual(
error as? ResourceObjectDecodingError,
ResourceObjectDecodingError(
subjectName: "yetAnother",
cause: .typeMismatch(expectedTypeName: String(describing: Bool.self)),
location: .attributes
)
)
XCTAssertEqual(
(error as? ResourceObjectDecodingError)?.description,
"'yetAnother' attribute is not a Bool as expected."
)
}
}
}
// MARK: - Test Types
@@ -111,4 +264,19 @@ extension ResourceObjectDecodingErrorTests {
}
typealias TestEntity = BasicEntity<TestEntityType>
enum TestEntityType2: ResourceObjectDescription {
public static var jsonType: String { return "thirteenth_test_entities" }
public struct Attributes: JSONAPI.Attributes {
let required: Attribute<String>
let other: Attribute<Int>?
let yetAnother: Attribute<Bool?>?
}
typealias Relationships = NoRelationships
}
typealias TestEntity2 = BasicEntity<TestEntityType2>
}
@@ -446,6 +446,64 @@ let entity_relationships_entirely_missing = """
}
""".data(using: .utf8)!
let entity_required_attribute_is_omitted = """
{
"id": "1",
"type": "thirteenth_test_entities",
"attributes": {
}
}
""".data(using: .utf8)!
let entity_nonNullable_attribute_is_null = """
{
"id": "1",
"type": "thirteenth_test_entities",
"attributes": {
"required": null
}
}
""".data(using: .utf8)!
let entity_attribute_is_wrong_type = """
{
"id": "1",
"type": "thirteenth_test_entities",
"attributes": {
"required": 10
}
}
""".data(using: .utf8)!
let entity_attribute_is_wrong_type2 = """
{
"id": "1",
"type": "thirteenth_test_entities",
"attributes": {
"required": "hello",
"other": "world"
}
}
""".data(using: .utf8)!
let entity_attribute_is_wrong_type3 = """
{
"id": "1",
"type": "thirteenth_test_entities",
"attributes": {
"required": "hello",
"yetAnother": 101
}
}
""".data(using: .utf8)!
let entity_attributes_entirely_missing = """
{
"id": "1",
"type": "thirteenth_test_entities"
}
""".data(using: .utf8)!
let entity_unidentified = """
{
"type": "unidentified_test_entities",