From 0dba0fcca3e1d251a2080930892d727d8a3720fe Mon Sep 17 00:00:00 2001 From: hborawski Date: Fri, 27 Sep 2024 10:51:41 -0700 Subject: [PATCH 1/5] Expand AnyType to handle deeply nested AnyType for beacon encoding --- .../Types/Assets/BaseAssetRegistry.swift | 19 ++++++++ ios/core/Sources/Types/Generic/AnyType.swift | 28 +++++++++++ .../Tests/Types/Generic/AnyTypeTests.swift | 47 +++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift b/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift index 04eca9fe3..25440c8ca 100644 --- a/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift +++ b/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift @@ -166,6 +166,9 @@ open class BaseAssetRegistry: PlayerRegistry where public func decode(_ value: JSValue) throws -> WrapperType.AssetType { assert(Thread.isMainThread, "decoder must be accessed from main") typealias Shim = RegistryDecodeShim + if let context = AnyTypeDecodingContext(value) { + return try context.inject(to: decoder).decode(Shim.self, from: value).asset + } return try decoder.decode(Shim.self, from: value).asset } @@ -177,6 +180,10 @@ open class BaseAssetRegistry: PlayerRegistry where */ public func decodeWrapper(_ value: JSValue) throws -> WrapperType { assert(Thread.isMainThread, "decoder must be accessed from main") + if let context = AnyTypeDecodingContext(value) { + return try context.inject(to: decoder).decode(WrapperType.self, from: value) + } + return try decoder.decode(WrapperType.self, from: value) } } @@ -190,6 +197,18 @@ public struct RegistryDecodeShim: Decodable { } } +extension AnyTypeDecodingContext { + init?(_ value: JSValue) { + guard + let obj = value.toObject(), + let data = try? JSONSerialization.data(withJSONObject: obj) + else { + return nil + } + self.init(rawData: data) + } +} + extension JSValue { var jsonDisplayString: String { do { diff --git a/ios/core/Sources/Types/Generic/AnyType.swift b/ios/core/Sources/Types/Generic/AnyType.swift index af123a8ce..405b1e3d2 100644 --- a/ios/core/Sources/Types/Generic/AnyType.swift +++ b/ios/core/Sources/Types/Generic/AnyType.swift @@ -34,6 +34,8 @@ public enum AnyType: Hashable { hasher.combine(data) case .anyDictionary(let data): hasher.combine(data as NSDictionary) + case .anyArray(let data): + hasher.combine(data as NSArray) case .unknownData: return } @@ -73,6 +75,13 @@ public enum AnyType: Hashable { */ case anyDictionary(data: [String: Any]) + /** + The underlying data was an array of varied value types + + **This requires the decoder to add `AnyTypeDecodingContext` to the decoders userInfo** + */ + case anyArray(data: [Any]) + /// The underlying data was not in a known format case unknownData } @@ -91,6 +100,7 @@ extension AnyType: Equatable { case (.numberArray(let lhv), .numberArray(let rhv)): return lhv == rhv case (.booleanArray(let lhv), .booleanArray(let rhv)): return lhv == rhv case (.anyDictionary(let lhv), .anyDictionary(let rhv)): return (lhv as NSDictionary).isEqual(to: rhv) + case (.anyArray(let lhv), .anyArray(let rhv)): return (lhv as NSArray).isEqual(to: rhv) default: return false } } @@ -139,6 +149,9 @@ extension AnyType: Decodable { if let dictionary = obj as? [String: Any] { self = .anyDictionary(data: dictionary) return + } else if let array = obj as? [Any] { + self = .anyArray(data: array) + return } } self = .unknownData @@ -154,6 +167,12 @@ struct CustomEncodable: CodingKey { self.stringValue = key if let encodable = encodable as? Encodable { self.data = encodable + } else if + let encodable, + let data = try? JSONSerialization.data(withJSONObject: encodable, options: .fragmentsAllowed), + let decoded = try? AnyTypeDecodingContext(rawData: data).inject(to: JSONDecoder()).decode(AnyType.self, from: data) + { + self.data = decoded } } var stringValue: String @@ -208,6 +227,15 @@ extension AnyType: Encodable { try keyed.encode(value, forKey: customEncodable) } } + case .anyArray(data: let array): + var indexed = encoder.unkeyedContainer() + for value in array { + let encodable = CustomEncodable(value, key: "") + if let data = encodable.data { + try indexed.encode(data) + } + } + default: try container.encodeNil() return diff --git a/ios/core/Tests/Types/Generic/AnyTypeTests.swift b/ios/core/Tests/Types/Generic/AnyTypeTests.swift index dbd6836eb..71f4d20da 100644 --- a/ios/core/Tests/Types/Generic/AnyTypeTests.swift +++ b/ios/core/Tests/Types/Generic/AnyTypeTests.swift @@ -166,6 +166,53 @@ class AnyTypeTests: XCTestCase { } } + func testAnyArray() { + let string = "[1, true]" + guard + let data = string.data(using: .utf8), + let anyType = try? AnyTypeDecodingContext(rawData: string.data(using: .utf8)!).inject(to: JSONDecoder()).decode(AnyType.self, from: data) + else { return XCTFail("could not decode") } + switch anyType { + case .anyArray(let result): + XCTAssertEqual(1, result[0] as? Int) + XCTAssertEqual(true, result[1] as? Bool) + default: + XCTFail("data was not anyArray") + } + } + + func testAnyDictionaryDataWithArray() { + let string = "{\"key2\":1,\"key\":[false]}" + guard + let data = string.data(using: .utf8), + let anyType = try? AnyTypeDecodingContext(rawData: string.data(using: .utf8)!).inject(to: JSONDecoder()).decode(AnyType.self, from: data) + else { return XCTFail("could not decode") } + switch anyType { + case .anyDictionary(let result): + XCTAssertEqual(false, (result["key"] as? [Bool])?.first) + XCTAssertEqual(1, result["key2"] as? Double) + default: + XCTFail("data was not dictionary") + } + } + + func testAnyDictionaryDataWithDeepNestedTypes() { + let string = "{\"key2\":1,\"key\":[{\"nestedKey\": \"nestedValue\"}]}" + guard + let data = string.data(using: .utf8), + let anyType = try? AnyTypeDecodingContext(rawData: string.data(using: .utf8)!).inject(to: JSONDecoder()).decode(AnyType.self, from: data) + else { return XCTFail("could not decode") } + switch anyType { + case .anyDictionary(let result): + let nestedArray = result["key"] as? [Any] + let nestedDict = nestedArray?.first as? [String: Any] + XCTAssertEqual("nestedValue", nestedDict?["nestedKey"] as? String) + XCTAssertEqual(1, result["key2"] as? Double) + default: + XCTFail("data was not dictionary") + } + } + func testUnknownData() { let string = "{\"key\":\"value\", \"key2\": 2}" guard From dadbce3edc4d39c473fbdc8c474383e8625402a6 Mon Sep 17 00:00:00 2001 From: hborawski Date: Fri, 27 Sep 2024 11:14:50 -0700 Subject: [PATCH 2/5] add encode test --- ios/core/Tests/Types/Generic/AnyTypeTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/core/Tests/Types/Generic/AnyTypeTests.swift b/ios/core/Tests/Types/Generic/AnyTypeTests.swift index 71f4d20da..489004a27 100644 --- a/ios/core/Tests/Types/Generic/AnyTypeTests.swift +++ b/ios/core/Tests/Types/Generic/AnyTypeTests.swift @@ -250,6 +250,7 @@ class AnyTypeTests: XCTestCase { XCTAssertEqual("[1,2]", doEncode(AnyType.numberArray(data: [1, 2]))) XCTAssertEqual("[false,true]", doEncode(AnyType.booleanArray(data: [false, true]))) XCTAssertEqual("{\"a\":false,\"b\":1}", doEncode(AnyType.anyDictionary(data: ["a": false, "b": 1]))) + XCTAssertEqual("{\"key\":[{\"nestedKey\":\"nestedValue\"}],\"key2\":1}", doEncode(AnyType.anyDictionary(data: ["key2": 1, "key": AnyType.anyArray(data: [["nestedKey": "nestedValue"]])]))) } func doEncode(_ data: AnyType) -> String? { From fa05b8ae507f880ee318871e317240ec1cc8b7a3 Mon Sep 17 00:00:00 2001 From: hborawski Date: Fri, 27 Sep 2024 12:04:00 -0700 Subject: [PATCH 3/5] hasher test + slight refactor for coverage --- .../Types/Assets/BaseAssetRegistry.swift | 17 +++++------------ ios/core/Tests/Types/Generic/AnyTypeTests.swift | 8 +++++--- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift b/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift index 25440c8ca..7f8eb9f8a 100644 --- a/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift +++ b/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift @@ -166,10 +166,8 @@ open class BaseAssetRegistry: PlayerRegistry where public func decode(_ value: JSValue) throws -> WrapperType.AssetType { assert(Thread.isMainThread, "decoder must be accessed from main") typealias Shim = RegistryDecodeShim - if let context = AnyTypeDecodingContext(value) { - return try context.inject(to: decoder).decode(Shim.self, from: value).asset - } - return try decoder.decode(Shim.self, from: value).asset + let localDecoder = AnyTypeDecodingContext(value).map { $0.inject(to: decoder) } ?? decoder + return try localDecoder.decode(Shim.self, from: value).asset } /** @@ -180,11 +178,8 @@ open class BaseAssetRegistry: PlayerRegistry where */ public func decodeWrapper(_ value: JSValue) throws -> WrapperType { assert(Thread.isMainThread, "decoder must be accessed from main") - if let context = AnyTypeDecodingContext(value) { - return try context.inject(to: decoder).decode(WrapperType.self, from: value) - } - - return try decoder.decode(WrapperType.self, from: value) + let localDecoder = AnyTypeDecodingContext(value).map { $0.inject(to: decoder) } ?? decoder + return try localDecoder.decode(WrapperType.self, from: value) } } @@ -202,9 +197,7 @@ extension AnyTypeDecodingContext { guard let obj = value.toObject(), let data = try? JSONSerialization.data(withJSONObject: obj) - else { - return nil - } + else { return nil } self.init(rawData: data) } } diff --git a/ios/core/Tests/Types/Generic/AnyTypeTests.swift b/ios/core/Tests/Types/Generic/AnyTypeTests.swift index 489004a27..a8879f04a 100644 --- a/ios/core/Tests/Types/Generic/AnyTypeTests.swift +++ b/ios/core/Tests/Types/Generic/AnyTypeTests.swift @@ -197,17 +197,18 @@ class AnyTypeTests: XCTestCase { } func testAnyDictionaryDataWithDeepNestedTypes() { - let string = "{\"key2\":1,\"key\":[{\"nestedKey\": \"nestedValue\"}]}" + let string = "{\"container\":{\"key2\":1,\"key\":[{\"nestedKey\": \"nestedValue\"}]}}" guard let data = string.data(using: .utf8), let anyType = try? AnyTypeDecodingContext(rawData: string.data(using: .utf8)!).inject(to: JSONDecoder()).decode(AnyType.self, from: data) else { return XCTFail("could not decode") } switch anyType { case .anyDictionary(let result): - let nestedArray = result["key"] as? [Any] + let container = result["container"] as? [String: Any] + let nestedArray = container?["key"] as? [Any] let nestedDict = nestedArray?.first as? [String: Any] XCTAssertEqual("nestedValue", nestedDict?["nestedKey"] as? String) - XCTAssertEqual(1, result["key2"] as? Double) + XCTAssertEqual(1, container?["key2"] as? Double) default: XCTFail("data was not dictionary") } @@ -279,6 +280,7 @@ class AnyTypeTests: XCTestCase { XCTAssertNotEqual(AnyType.numberArray(data: [1, 2]).hashValue, 0) XCTAssertNotEqual(AnyType.booleanArray(data: [false, true]).hashValue, 0) XCTAssertNotEqual(AnyType.anyDictionary(data: ["key": false, "key2": 1]).hashValue, 0) + XCTAssertNotEqual(AnyType.anyArray(data: [1, false]).hashValue, 0) XCTAssertNotEqual(AnyType.unknownData.hashValue, 0) } From 56bcb1cf55eb26abd455f9782f899b5c3b0ad59a Mon Sep 17 00:00:00 2001 From: hborawski Date: Fri, 27 Sep 2024 13:52:26 -0700 Subject: [PATCH 4/5] nest in another dictionary to trigger encoding case --- ios/core/Tests/Types/Generic/AnyTypeTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/core/Tests/Types/Generic/AnyTypeTests.swift b/ios/core/Tests/Types/Generic/AnyTypeTests.swift index a8879f04a..4b5daef87 100644 --- a/ios/core/Tests/Types/Generic/AnyTypeTests.swift +++ b/ios/core/Tests/Types/Generic/AnyTypeTests.swift @@ -251,7 +251,7 @@ class AnyTypeTests: XCTestCase { XCTAssertEqual("[1,2]", doEncode(AnyType.numberArray(data: [1, 2]))) XCTAssertEqual("[false,true]", doEncode(AnyType.booleanArray(data: [false, true]))) XCTAssertEqual("{\"a\":false,\"b\":1}", doEncode(AnyType.anyDictionary(data: ["a": false, "b": 1]))) - XCTAssertEqual("{\"key\":[{\"nestedKey\":\"nestedValue\"}],\"key2\":1}", doEncode(AnyType.anyDictionary(data: ["key2": 1, "key": AnyType.anyArray(data: [["nestedKey": "nestedValue"]])]))) + XCTAssertEqual("{\"container\":{\"key\":[{\"nestedKey\":\"nestedValue\"}],\"key2\":1}}", doEncode(AnyType.anyDictionary(data: ["container": AnyType.anyDictionary(data: ["key2": 1, "key": AnyType.anyArray(data: [["nestedKey": "nestedValue"]])])]))) } func doEncode(_ data: AnyType) -> String? { From ad827d6e4c4214e77f5fe6a3450f087b85fc4f26 Mon Sep 17 00:00:00 2001 From: hborawski Date: Fri, 27 Sep 2024 14:59:35 -0700 Subject: [PATCH 5/5] update test cases to actually hit new case in CustomEncodable --- ios/core/Tests/Types/Generic/AnyTypeTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ios/core/Tests/Types/Generic/AnyTypeTests.swift b/ios/core/Tests/Types/Generic/AnyTypeTests.swift index 4b5daef87..2025dbae5 100644 --- a/ios/core/Tests/Types/Generic/AnyTypeTests.swift +++ b/ios/core/Tests/Types/Generic/AnyTypeTests.swift @@ -251,7 +251,8 @@ class AnyTypeTests: XCTestCase { XCTAssertEqual("[1,2]", doEncode(AnyType.numberArray(data: [1, 2]))) XCTAssertEqual("[false,true]", doEncode(AnyType.booleanArray(data: [false, true]))) XCTAssertEqual("{\"a\":false,\"b\":1}", doEncode(AnyType.anyDictionary(data: ["a": false, "b": 1]))) - XCTAssertEqual("{\"container\":{\"key\":[{\"nestedKey\":\"nestedValue\"}],\"key2\":1}}", doEncode(AnyType.anyDictionary(data: ["container": AnyType.anyDictionary(data: ["key2": 1, "key": AnyType.anyArray(data: [["nestedKey": "nestedValue"]])])]))) + XCTAssertEqual("[1,\"a\",true]", doEncode(AnyType.anyArray(data: [1, "a", true]))) + XCTAssertEqual("{\"key\":[{\"nestedKey\":\"nestedValue\"},1,{}],\"key2\":1}", doEncode(AnyType.anyDictionary(data: ["key2": 1, "key": AnyType.anyArray(data: [["nestedKey": "nestedValue"], 1, [:] as Any])]))) } func doEncode(_ data: AnyType) -> String? { @@ -280,7 +281,7 @@ class AnyTypeTests: XCTestCase { XCTAssertNotEqual(AnyType.numberArray(data: [1, 2]).hashValue, 0) XCTAssertNotEqual(AnyType.booleanArray(data: [false, true]).hashValue, 0) XCTAssertNotEqual(AnyType.anyDictionary(data: ["key": false, "key2": 1]).hashValue, 0) - XCTAssertNotEqual(AnyType.anyArray(data: [1, false]).hashValue, 0) + XCTAssertNotEqual(AnyType.anyArray(data: [1, "a", true]).hashValue, 0) XCTAssertNotEqual(AnyType.unknownData.hashValue, 0) } @@ -297,6 +298,7 @@ class AnyTypeTests: XCTestCase { XCTAssertEqual(AnyType.numberArray(data: [1, 2]), AnyType.numberArray(data: [1, 2])) XCTAssertEqual(AnyType.booleanArray(data: [false, true]), AnyType.booleanArray(data: [false, true])) XCTAssertEqual(AnyType.anyDictionary(data: ["key": false, "key2": 1]), AnyType.anyDictionary(data: ["key": false, "key2": 1])) + XCTAssertEqual(AnyType.anyArray(data: [1, "a", true]), AnyType.anyArray(data: [1, "a", true])) XCTAssertNotEqual(AnyType.unknownData, AnyType.string(data: "test")) }