diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index fc03c4e..6d6ca43 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -401,6 +401,18 @@ extension ToManyRelationship: Codable { links = try container.decode(LinksType.self, forKey: .links) } + let hasData = container.contains(.data) + let canHaveNoDataInRelationships: Bool + if let relatableType = Relatable.self as? ResourceObjectWithOptionalDataInRelationships.Type { + canHaveNoDataInRelationships = relatableType.canHaveNoDataInRelationships + } else { + canHaveNoDataInRelationships = false + } + guard hasData || !canHaveNoDataInRelationships else { + idsWithMeta = [] + return + } + var identifiers: UnkeyedDecodingContainer do { identifiers = try container.nestedUnkeyedContainer(forKey: .data) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 77d367e..42eb345 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -53,11 +53,25 @@ public protocol JSONTyped { /// A `ResourceObjectProxyDescription` is an `ResourceObjectDescription` /// without Codable conformance. -public protocol ResourceObjectProxyDescription: JSONTyped { +public protocol ResourceObjectProxyDescription: JSONTyped, ResourceObjectWithOptionalDataInRelationships { associatedtype Attributes: Equatable associatedtype Relationships: Equatable } +/// A flagging protocol for `ResourceObjectProxyDescription` objects. +/// Indicates an object with varying behavior when it's being decoded from resources and the `data` key is missing. +public protocol ResourceObjectWithOptionalDataInRelationships { + /// A Boolean flag indicating that instances while decoding from relationships can be decoded without `data` + /// key and have only links and meta information. + /// + /// Default value: `false`. + static var canHaveNoDataInRelationships: Bool { get } +} + +extension ResourceObjectWithOptionalDataInRelationships { + public static var canHaveNoDataInRelationships: Bool { false } +} + /// A `ResourceObjectDescription` describes a JSON API /// Resource Object. The Resource Object /// itself is encoded and decoded as an @@ -244,6 +258,12 @@ public extension ResourceObject where EntityRawIdType: CreatableRawIdType { } } +// Conformance to the protocol so we can access the `canHaveNoDataInRelationships` flag from +// `ToManyRelationship` in type-erasured `Relatable`. +extension ResourceObject: ResourceObjectWithOptionalDataInRelationships where Description: ResourceObjectWithOptionalDataInRelationships { + public static var canHaveNoDataInRelationships: Bool { Description.canHaveNoDataInRelationships } +} + // MARK: - Attribute Access public extension ResourceObjectProxy { // MARK: Dynaminc Member Keypath Lookup diff --git a/Tests/JSONAPITests/Relationships/RelationshipTests.swift b/Tests/JSONAPITests/Relationships/RelationshipTests.swift index 4a9a5fe..3decfae 100644 --- a/Tests/JSONAPITests/Relationships/RelationshipTests.swift +++ b/Tests/JSONAPITests/Relationships/RelationshipTests.swift @@ -235,6 +235,36 @@ extension RelationshipTests { test_DecodeEncodeEquality(type: ToManyWithMetaAndLinks.self, data: to_many_relationship_with_meta_and_links) } + + func test_ToManyRelationshipWithMetaNoDataOmittable() { + TestEntityType1.canHaveNoDataInRelationships = true + + let relationship = decoded(type: ToManyWithMeta.self, + data: to_many_relationship_with_meta_no_data) + + XCTAssertEqual(relationship.ids, []) + XCTAssertEqual(relationship.meta.a, "hello") + + TestEntityType1.canHaveNoDataInRelationships = false + } + + func test_ToManyRelationshipWithMetaNoDataNotOmittable() { + TestEntityType1.canHaveNoDataInRelationships = false + + XCTAssertThrowsError( + try decodedThrows(type: ToManyWithMeta.self, + data: to_many_relationship_with_meta_no_data) + ) { error in + let oldLinuxFoundationMsg = "The operation could not be completed. (SwiftError error 0.)" + let newLinuxFoundationMsg = "The operation could not be completed. The data is missing." + let newDesirableMsg = "The data couldn’t be read because it is missing." + XCTAssert( + error.localizedDescription == newDesirableMsg + || error.localizedDescription == newLinuxFoundationMsg + || error.localizedDescription == oldLinuxFoundationMsg + ) + } + } } // MARK: Nullable @@ -277,12 +307,14 @@ extension RelationshipTests { // MARK: - Test types extension RelationshipTests { - enum TestEntityType1: ResourceObjectDescription { + enum TestEntityType1: ResourceObjectDescription, ResourceObjectWithOptionalDataInRelationships { typealias Attributes = NoAttributes typealias Relationships = NoRelationships public static var jsonType: String { return "test_entity1" } + + static var canHaveNoDataInRelationships: Bool = false } typealias TestEntity1 = BasicEntity diff --git a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift index 434667f..18bd179 100644 --- a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift +++ b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift @@ -241,3 +241,11 @@ let to_many_relationship_type_mismatch = """ ] } """.data(using: .utf8)! + +let to_many_relationship_with_meta_no_data = """ +{ + "meta": { + "a": "hello" + } +} +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift index 75a93e7..2b59137 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift @@ -24,6 +24,10 @@ func decoded(type: T.Type, data: Data) -> T { return try! testDecoder.decode(T.self, from: data) } +func decodedThrows(type: T.Type, data: Data) throws -> T { + return try testDecoder.decode(T.self, from: data) +} + func encoded(value: T) -> Data { return try! testEncoder.encode(value) }