diff --git a/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift b/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift index 04eca9fe3..7f8eb9f8a 100644 --- a/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift +++ b/ios/core/Sources/Types/Assets/BaseAssetRegistry.swift @@ -166,7 +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 - 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 } /** @@ -177,7 +178,8 @@ open class BaseAssetRegistry: PlayerRegistry where */ public func decodeWrapper(_ value: JSValue) throws -> WrapperType { assert(Thread.isMainThread, "decoder must be accessed from main") - 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) } } @@ -190,6 +192,16 @@ 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..2025dbae5 100644 --- a/ios/core/Tests/Types/Generic/AnyTypeTests.swift +++ b/ios/core/Tests/Types/Generic/AnyTypeTests.swift @@ -166,6 +166,54 @@ 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 = "{\"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 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, container?["key2"] as? Double) + default: + XCTFail("data was not dictionary") + } + } + func testUnknownData() { let string = "{\"key\":\"value\", \"key2\": 2}" guard @@ -203,6 +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("[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? { @@ -231,6 +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, "a", true]).hashValue, 0) XCTAssertNotEqual(AnyType.unknownData.hashValue, 0) } @@ -247,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")) }