Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: allow absent data in ToManyRelationship #110

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Sources/JSONAPI/Resource/Relationship.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ extension Optional: JSONAPIIdentifiable, OptionalRelatable, JSONTyped where Wrap
}

// MARK: Codable
private enum ResourceLinkageCodingKeys: String, CodingKey {
enum ResourceLinkageCodingKeys: String, CodingKey {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for test_ToManyRelationshipWithMetaNoDataNotOmittable to check the exact thrown error.

case data = "data"
case meta = "meta"
case links = "links"
Expand Down Expand Up @@ -401,6 +401,16 @@ extension ToManyRelationship: Codable {
links = try container.decode(LinksType.self, forKey: .links)
}

let hasData = container.contains(.data)
var canHaveNoDataInRelationships: Bool = false
if let relatableType = Relatable.self as? ResourceObjectWithOptionalDataInRelationships.Type {
canHaveNoDataInRelationships = relatableType.canHaveNoDataInRelationships
}
guard hasData || !canHaveNoDataInRelationships else {
idsWithMeta = []
return
}

var identifiers: UnkeyedDecodingContainer
do {
identifiers = try container.nestedUnkeyedContainer(forKey: .data)
Expand Down
22 changes: 21 additions & 1 deletion Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 35 additions & 2 deletions Tests/JSONAPITests/Relationships/RelationshipTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

import XCTest
import JSONAPI
@testable import JSONAPI

class RelationshipTests: XCTestCase {

Expand Down Expand Up @@ -235,6 +235,37 @@ 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

do {
_ = try decodedThrows(type: ToManyWithMeta.self,
data: to_many_relationship_with_meta_no_data)
XCTFail("Expected decoding to fail.")
} catch let error as DecodingError {
switch error {
case .keyNotFound(ResourceLinkageCodingKeys.data, _):
break
default:
XCTFail("Expected error to be DecodingError.keyNotFound(.data), but got \(error)")
}
} catch {
XCTFail("Expected to have DecodingError.keyNotFound(.data), but got \(error)")
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep the coding keys private, not do an @testable import, and test the description of the error instead of the exact coding key it outputs. This is just a preference of mine, I don't think there's a right or wrong way to go about testing this.

The test could use the XCTAssertThrowsError to clean up a bit of the XCTFail work done above and it would look something like:

    XCTAssertThrowsError(
      try decodedThrows(type: ToManyWithMeta.self,
                        data: to_many_relationship_with_meta_no_data)
    ) { error in 
      XCTAssertEqual(error.localizedDescription, "The data couldn’t be read because it is missing.")
    }

}
}

// MARK: Nullable
Expand Down Expand Up @@ -277,12 +308,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<TestEntityType1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)!
4 changes: 4 additions & 0 deletions Tests/JSONAPITests/Test Helpers/EncodeDecode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ func decoded<T: Decodable>(type: T.Type, data: Data) -> T {
return try! testDecoder.decode(T.self, from: data)
}

func decodedThrows<T: Decodable>(type: T.Type, data: Data) throws -> T {
return try testDecoder.decode(T.self, from: data)
}

func encoded<T: Encodable>(value: T) -> Data {
return try! testEncoder.encode(value)
}
Expand Down