From 6ba217f5531374cec2c9daec50553ff6fd2c6c29 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 08:27:22 -0700 Subject: [PATCH] More sparse field encoder tests --- .../SparseFields/SparseFieldEncoder.swift | 21 ++- .../SparseFieldEncoderTests.swift | 158 ++++++++++++++++++ 2 files changed, 174 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index e5985c8..a377fc9 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -50,7 +50,8 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo self.allowedKeys = allowedKeys } - private func shouldAllow(key: Key) -> Bool { + /// Ask the container whether the given key should be encoded. + public func shouldAllow(key: Key) -> Bool { if let key = key as? SparseKey { return allowedKeys.contains(key) } @@ -157,6 +158,9 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { guard shouldAllow(key: key) else { return KeyedEncodingContainer( + // TODO: not needed by JSONAPI library, but for completeness could + // add an EmptyObjectEncoder that can be returned here so that + // at least nothing gets encoded within the nested container SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, forKey: key), encoding: []) @@ -172,7 +176,9 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { guard shouldAllow(key: key) else { - // TODO: Seems like this might not work as expected... maybe need an empty unkeyed container + // TODO: not needed by JSONAPI library, but for completeness could + // add an EmptyObjectEncoder that can be returned here so that + // at least nothing gets encoded within the nested container return wrappedContainer.nestedUnkeyedContainer(forKey: key) } @@ -180,14 +186,19 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo } public mutating func superEncoder() -> Encoder { - return wrappedContainer.superEncoder() + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(), + encoding: allowedKeys) } public mutating func superEncoder(forKey key: Key) -> Encoder { guard shouldAllow(key: key) else { - return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: [SparseKey]()) + // NOTE: We are creating a sparse field encoder with no allowed keys + // here because the given key should not be allowed. + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), + encoding: [SparseKey]()) } - return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: allowedKeys) + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), + encoding: allowedKeys) } } diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index e6b3a9d..f0abce2 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -85,6 +85,73 @@ class SparseFieldEncoderTests: XCTestCase { XCTAssertNil(allThingsOnDeserialized["nested"]) XCTAssertEqual(allThingsOnDeserialized.count, 0) } + + func test_NilEncode() { + let encoder = JSONEncoder() + + let nilOn = try! encoder.encode(NilWrapper(fields: [.hello])) + let nilOff = try! encoder.encode(NilWrapper(fields: [])) + + let nilOnDeserialized = try! JSONSerialization.jsonObject(with: nilOn, + options: []) as! [String: Any] + + let nilOffDeserialized = try! JSONSerialization.jsonObject(with: nilOff, + options: []) as! [String: Any] + + XCTAssertEqual(nilOnDeserialized.count, 1) + XCTAssertNotNil(nilOnDeserialized["hello"] as? NSNull) + XCTAssertEqual(nilOffDeserialized.count, 0) + } + + func test_NestedContainers() { + let encoder = JSONEncoder() + + let nestedOn = try! encoder.encode(NestedWrapper(fields: [.hello, .world])) + let nestedOff = try! encoder.encode(NestedWrapper(fields: [])) + + let nestedOnDeserialized = try! JSONSerialization.jsonObject(with: nestedOn, + options: []) as! [String: Any] + let nestedOffDeserialized = try! JSONSerialization.jsonObject(with: nestedOff, + options: []) as! [String: Any] + + XCTAssertEqual(nestedOnDeserialized.count, 2) + XCTAssertEqual((nestedOnDeserialized["hello"] as? [String: Bool])?["nestedKey"], true) + XCTAssertEqual((nestedOnDeserialized["world"] as? [Bool])?.first, false) + + // NOTE: When a nested container is explicitly requested, + // the best we can do to omit the field is to encode + // nothing _within_ the nested container. + XCTAssertEqual(nestedOffDeserialized.count, 2) + // TODO: The container currently does not encode empty object + // for the keyed nested container but I think it should. + XCTAssertEqual((nestedOffDeserialized["hello"] as? [String: Bool])?.count, 1) + // TODO: The container currently does not encode empty array + // for the unkeyed nested container but I think it should. + XCTAssertEqual((nestedOffDeserialized["world"] as? [Bool])?.count, 1) + } + + func test_SuperEncoderIsStillSparse() { + let encoder = JSONEncoder() + + let superOn = try! encoder.encode(SuperWrapper(fields: [.hello, .world])) + let superOff = try! encoder.encode(SuperWrapper(fields: [])) + + let superOnDeserialized = try! JSONSerialization.jsonObject(with: superOn, + options: []) as! [String: Any] + let superOffDeserialized = try! JSONSerialization.jsonObject(with: superOff, + options: []) as! [String: Any] + + XCTAssertEqual(superOnDeserialized.count, 2) + XCTAssertEqual((superOnDeserialized["hello"] as? [String: Bool])?["hello"], true) + XCTAssertEqual((superOnDeserialized["super"] as? [String: Bool])?["world"], false) + + // NOTE: When explicitly requesting a super encoder + // the best we can do is tell the super encoder only + // to encode the same keys + XCTAssertEqual(superOffDeserialized.count, 2) + XCTAssertEqual((superOffDeserialized["hello"] as? [String: Bool])?.count, 0) + XCTAssertEqual((superOffDeserialized["super"] as? [String: Bool])?.count, 0) + } } extension SparseFieldEncoderTests { @@ -189,4 +256,95 @@ extension SparseFieldEncoderTests { } } } + + struct NilWrapper: Encodable { + let fields: [NilWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try NilWrapped() + .encode(to: sparseEncoder) + } + + struct NilWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeNil(forKey: .hello) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + } + } + } + + struct NestedWrapper: Encodable { + let fields: [NestedWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try NestedWrapped() + .encode(to: sparseEncoder) + } + + struct NestedWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + var nestedContainer1 = container.nestedContainer(keyedBy: NestedKeys.self, forKey: .hello) + + var nestedContainer2 = container.nestedUnkeyedContainer(forKey: .world) + + try nestedContainer1.encode(true, forKey: .nestedKey) + try nestedContainer2.encode(false) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + case world + } + + enum NestedKeys: String, CodingKey { + case nestedKey + } + } + } + + struct SuperWrapper: Encodable { + let fields: [SuperWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try SuperWrapped() + .encode(to: sparseEncoder) + } + + struct SuperWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let superEncoder1 = container.superEncoder(forKey: .hello) + + let superEncoder2 = container.superEncoder() + + var container1 = superEncoder1.container(keyedBy: CodingKeys.self) + var container2 = superEncoder2.container(keyedBy: CodingKeys.self) + + try container1.encode(true, forKey: .hello) + try container2.encode(false, forKey: .world) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + case world + } + } + } }