mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
got some attribute cases added and tested. added some descriptions (custom string convertible)
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(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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user