diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index af964a7..7fefbec 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -15,10 +15,12 @@ public protocol Relationships: Codable & Equatable {} /// properties of any types that are JSON encodable. public protocol Attributes: Codable & Equatable {} +public typealias SparsableCodingKey = CodingKey & Equatable + /// Attributes containing publicly accessible and `Equatable` /// CodingKeys are required to support Sparse Fieldsets. public protocol SparsableAttributes: Attributes { - associatedtype CodingKeys: CodingKey & Equatable + associatedtype CodingKeys: SparsableCodingKey } /// Can be used as `Relationships` Type for Entities that do not diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 2aa8229..5f9c18b 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -926,6 +926,217 @@ extension DocumentTests { } } +// MARK: Sparse Fieldset Documents + +extension DocumentTests { + func test_sparsePrimaryResource() { + let primaryResource = Book(attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: []), + meta: .none, + links: .none) + .sparse(with: [.pageCount]) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .none, + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100) + + XCTAssertNotNil(deserializedObj1["relationships"]) + } + + func test_sparsePrimaryResourceOptionalAndNil() { + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: nil), + includes: .none, + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + XCTAssertNotNil(deserializedObj?["data"] as? NSNull) + } + + func test_sparseIncludeFullPrimaryResource() { + let bookInclude = Book(id: "444", + attributes: .init(pageCount: 113), + relationships: .init(author: "1234", + series: ["443"]), + meta: .none, + links: .none) + .sparse(with: []) + + let primaryResource = Book(id: "443", + attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: ["444"]), + meta: .none, + links: .none) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + Include1, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .init(values: [.init(bookInclude)]), + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100) + + XCTAssertNotNil(deserializedObj1["relationships"]) + + guard let deserializedIncludes = deserializedObj?["included"] as? [Any], + let deserializedObj2 = deserializedIncludes.first as? [String: Any] else { + XCTFail("Expected to deserialize one incude object") + return + } + + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj2["relationships"]) + } + + func test_sparseIncludeSparsePrimaryResource() { + let bookInclude = Book(id: "444", + attributes: .init(pageCount: 113), + relationships: .init(author: "1234", + series: ["443"]), + meta: .none, + links: .none) + .sparse(with: []) + + let primaryResource = Book(id: "443", + attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: ["444"]), + meta: .none, + links: .none) + .sparse(with: []) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + Include1, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .init(values: [.init(bookInclude)]), + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj1["relationships"]) + + guard let deserializedIncludes = deserializedObj?["included"] as? [Any], + let deserializedObj2 = deserializedIncludes.first as? [String: Any] else { + XCTFail("Expected to deserialize one incude object") + return + } + + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj2["relationships"]) + } +} + // MARK: Poly PrimaryResource Tests extension DocumentTests { func test_singleDocument_PolyPrimaryResource() { @@ -1115,6 +1326,25 @@ extension DocumentTests { typealias Article = BasicEntity + enum BookType: ResourceObjectDescription { + static var jsonType: String { return "books" } + + struct Attributes: JSONAPI.SparsableAttributes { + let pageCount: Attribute + + enum CodingKeys: String, SparsableCodingKey { + case pageCount + } + } + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + let series: ToManyRelationship + } + } + + typealias Book = BasicEntity + struct TestPageMetadata: JSONAPI.Meta { let total: Int let limit: Int diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 2254efe..4272083 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -12,15 +12,15 @@ class IncludedTests: XCTestCase { func test_zeroIncludes() { let includes = decoded(type: Includes.self, data: two_same_type_includes) - + XCTAssertEqual(includes.count, 0) } func test_zeroIncludes_encode() { - XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes.self, - data: two_same_type_includes))) - } - + XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes.self, + data: two_same_type_includes))) + } + func test_OneInclude() { let includes = decoded(type: Includes>.self, data: one_include) @@ -32,7 +32,7 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: one_include) } - + func test_TwoSameIncludes() { let includes = decoded(type: Includes>.self, data: two_same_type_includes) @@ -57,7 +57,7 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: two_different_type_includes) } - + func test_ThreeDifferentIncludes() { let includes = decoded(type: Includes>.self, data: three_different_type_includes) @@ -71,11 +71,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: three_different_type_includes) } - + func test_FourDifferentIncludes() { let includes = decoded(type: Includes>.self, data: four_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity4.self].count, 1) @@ -86,11 +86,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: four_different_type_includes) } - + func test_FiveDifferentIncludes() { let includes = decoded(type: Includes>.self, data: five_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity3.self].count, 1) @@ -102,11 +102,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: five_different_type_includes) } - + func test_SixDifferentIncludes() { let includes = decoded(type: Includes>.self, data: six_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity3.self].count, 1)