// // SparseFieldEncoderTests.swift // // // Created by Mathew Polzin on 8/5/19. // import XCTest @testable import JSONAPI import Foundation class SparseFieldEncoderTests: XCTestCase { func test_AccurateCodingPath() { let encoder = JSONEncoder() XCTAssertThrowsError(try encoder.encode(Wrapper())) do { let _ = try encoder.encode(Wrapper()) } catch let err as Wrapper.OuterFail.FailError { print(err.path) XCTAssertEqual(err.path.first as? Wrapper.OuterFail.CodingKeys, Wrapper.OuterFail.CodingKeys.inner) } catch { XCTFail("received unexpected error during test") } } func test_SkipsOmittedFields() { let encoder = JSONEncoder() // does not throw because we omit the field that would have failed XCTAssertNoThrow(try encoder.encode(Wrapper(fields: []))) } func test_EverythingArsenal_allOn() { let encoder = JSONEncoder() let allThingsOn = try! encoder.encode(EverythingWrapper(fields: EverythingWrapper.EverythingWrapped.CodingKeys.allCases)) let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, options: []) as! [String: Any] XCTAssertNil(allThingsOnDeserialized["omittable"]) XCTAssertNotNil(allThingsOnDeserialized["nullable"] as? NSNull) XCTAssertEqual(allThingsOnDeserialized["bool"] as? Bool, true) XCTAssertEqual(allThingsOnDeserialized["double"] as? Double, 10.5) XCTAssertEqual(allThingsOnDeserialized["string"] as? String, "hello") XCTAssertEqual(allThingsOnDeserialized["float"] as? Float, 1.2) XCTAssertEqual(allThingsOnDeserialized["int"] as? Int, 3) XCTAssertEqual(allThingsOnDeserialized["int8"] as? Int8, 4) XCTAssertEqual(allThingsOnDeserialized["int16"] as? Int16, 5) XCTAssertEqual(allThingsOnDeserialized["int32"] as? Int32, 6) XCTAssertEqual(allThingsOnDeserialized["int64"] as? Int64, 7) XCTAssertEqual(allThingsOnDeserialized["uint"] as? UInt, 8) XCTAssertEqual(allThingsOnDeserialized["uint8"] as? UInt8, 9) XCTAssertEqual(allThingsOnDeserialized["uint16"] as? UInt16, 10) XCTAssertEqual(allThingsOnDeserialized["uint32"] as? UInt32, 11) XCTAssertEqual(allThingsOnDeserialized["uint64"] as? UInt64, 12) XCTAssertEqual(allThingsOnDeserialized["nested"] as? String, "world") } func test_EverythingArsenal_allOff() { let encoder = JSONEncoder() let allThingsOn = try! encoder.encode(EverythingWrapper(fields: [])) let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, options: []) as! [String: Any] XCTAssertNil(allThingsOnDeserialized["omittable"]) XCTAssertNil(allThingsOnDeserialized["nullable"]) XCTAssertNil(allThingsOnDeserialized["bool"]) XCTAssertNil(allThingsOnDeserialized["double"]) XCTAssertNil(allThingsOnDeserialized["string"]) XCTAssertNil(allThingsOnDeserialized["float"]) XCTAssertNil(allThingsOnDeserialized["int"]) XCTAssertNil(allThingsOnDeserialized["int8"]) XCTAssertNil(allThingsOnDeserialized["int16"]) XCTAssertNil(allThingsOnDeserialized["int32"]) XCTAssertNil(allThingsOnDeserialized["int64"]) XCTAssertNil(allThingsOnDeserialized["uint"]) XCTAssertNil(allThingsOnDeserialized["uint8"]) XCTAssertNil(allThingsOnDeserialized["uint16"]) XCTAssertNil(allThingsOnDeserialized["uint32"]) XCTAssertNil(allThingsOnDeserialized["uint64"]) 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 { struct Wrapper: Encodable { let fields: [OuterFail.CodingKeys] init(fields: [OuterFail.CodingKeys] = OuterFail.CodingKeys.allCases) { self.fields = fields } func encode(to encoder: Encoder) throws { let sparseEncoder = SparseFieldEncoder(wrapping: encoder, encoding: fields) try OuterFail(inner: .init()).encode(to: sparseEncoder) } struct OuterFail: Encodable { let inner: InnerFail public enum CodingKeys: String, Equatable, CaseIterable, CodingKey { case inner } struct InnerFail: Encodable { func encode(to encoder: Encoder) throws { throw FailError(path: encoder.codingPath) } } struct FailError: Swift.Error { let path: [CodingKey] } } } struct EverythingWrapper: Encodable { let fields: [EverythingWrapped.CodingKeys] func encode(to encoder: Encoder) throws { let sparseEncoder = SparseFieldEncoder(wrapping: encoder, encoding: fields) try EverythingWrapped(omittable: nil, nullable: .init(value: nil), bool: true, double: 10.5, string: "hello", float: 1.2, int: 3, int8: 4, int16: 5, int32: 6, int64: 7, uint: 8, uint8: 9, uint16: 10, uint32: 11, uint64: 12, nested: .init(value: "world")) .encode(to: sparseEncoder) } struct EverythingWrapped: Encodable { let omittable: Int? let nullable: Attribute let bool: Bool let double: Double let string: String let float: Float let int: Int let int8: Int8 let int16: Int16 let int32: Int32 let int64: Int64 let uint: UInt let uint8: UInt8 let uint16: UInt16 let uint32: UInt32 let uint64: UInt64 let nested: Attribute enum CodingKeys: String, Equatable, CaseIterable, CodingKey { case omittable case nullable case bool case double case string case float case int case int8 case int16 case int32 case int64 case uint case uint8 case uint16 case uint32 case uint64 case nested } } } 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 } } } }