diff --git a/Sources/Common/Util/StringAnyEncodable.swift b/Sources/Common/Util/StringAnyEncodable.swift index 9ea133b1f..e3ed98732 100644 --- a/Sources/Common/Util/StringAnyEncodable.swift +++ b/Sources/Common/Util/StringAnyEncodable.swift @@ -1,22 +1,31 @@ import Foundation public struct StringAnyEncodable: Encodable { + private let logger: Logger private let data: [String: AnyEncodable] - public init(_ data: [String: Any]) { - var builtValue = [String: AnyEncodable]() - for (key, value) in data { + public init(logger: Logger, _ data: [String: Any]) { + // Nested function to convert the ‘Any’ values to ‘AnyEncodable’ recursively + func encode(value: Any) -> AnyEncodable? { switch value { case let enc as Encodable: - builtValue[key] = AnyEncodable(enc) + return AnyEncodable(enc) + case let dict as [String: Any]: - builtValue[key] = AnyEncodable(StringAnyEncodable(dict)) + return AnyEncodable(StringAnyEncodable(logger: logger, dict)) + + case let list as [Any]: + // If the value is an array, recursively encode each element + return AnyEncodable(list.compactMap { encode(value: $0) }) + default: - // XXX: logger error - continue + logger.error("Tried to convert \(data) into [String: AnyEncodable] but the data type is not Encodable.") + return nil } } - self.data = builtValue + + self.logger = logger + self.data = data.compactMapValues { encode(value: $0) } } public func encode(to encoder: Encoder) throws { diff --git a/Sources/Tracking/CustomerIO.swift b/Sources/Tracking/CustomerIO.swift index e38abf942..622b9fed8 100644 --- a/Sources/Tracking/CustomerIO.swift +++ b/Sources/Tracking/CustomerIO.swift @@ -356,7 +356,10 @@ public class CustomerIO: CustomerIOInstance { name: String, data: [String: Any] ) { - automaticScreenView(name: name, data: StringAnyEncodable(data)) + guard let logger = diGraph?.logger else { + return + } + automaticScreenView(name: name, data: StringAnyEncodable(logger: logger, data)) } // Designed to be called from swizzled methods for automatic screen tracking. @@ -396,4 +399,4 @@ public class CustomerIO: CustomerIOInstance { ) { implementation?.trackMetric(deliveryID: deliveryID, event: event, deviceToken: deviceToken) } -} +} // swiftlint:disable:this file_length diff --git a/Sources/Tracking/CustomerIOImplementation.swift b/Sources/Tracking/CustomerIOImplementation.swift index cff8cfe38..e3baf5a5b 100644 --- a/Sources/Tracking/CustomerIOImplementation.swift +++ b/Sources/Tracking/CustomerIOImplementation.swift @@ -157,7 +157,7 @@ internal class CustomerIOImplementation: CustomerIOInstance { } public func identify(identifier: String, body: [String: Any]) { - identify(identifier: identifier, body: StringAnyEncodable(body)) + identify(identifier: identifier, body: StringAnyEncodable(logger: logger, body)) } public func clearIdentify() { @@ -191,11 +191,11 @@ internal class CustomerIOImplementation: CustomerIOInstance { } public func track(name: String, data: [String: Any]) { - track(name: name, data: StringAnyEncodable(data)) + track(name: name, data: StringAnyEncodable(logger: logger, data)) } public func screen(name: String, data: [String: Any]) { - screen(name: name, data: StringAnyEncodable(data)) + screen(name: name, data: StringAnyEncodable(logger: logger, data)) } public func screen( @@ -251,8 +251,7 @@ internal class CustomerIOImplementation: CustomerIOInstance { deviceAttributesProvider.getDefaultDeviceAttributes { defaultDeviceAttributes in let deviceAttributes = defaultDeviceAttributes.mergeWith(customAttributes) - let encodableBody = - StringAnyEncodable(deviceAttributes) // makes [String: Any] Encodable to use in JSON body. + let encodableBody = StringAnyEncodable(logger: self.logger, deviceAttributes) // makes [String: Any] Encodable to use in JSON body. let requestBody = RegisterDeviceRequest(device: Device( token: deviceToken, platform: deviceOsName, diff --git a/Tests/Common/Util/StringAnyEncodable.swift b/Tests/Common/Util/StringAnyEncodable.swift index 7a654b660..a451122b9 100644 --- a/Tests/Common/Util/StringAnyEncodable.swift +++ b/Tests/Common/Util/StringAnyEncodable.swift @@ -9,6 +9,7 @@ struct DummyData: Codable, Equatable { let testValue: String let dict: [String: String] let array: [Int] + let dictWithArray: [String: [String]] enum CodingKeys: String, CodingKey { case boolean @@ -16,6 +17,7 @@ struct DummyData: Codable, Equatable { case testValue case dict case array + case dictWithArray } } @@ -29,7 +31,7 @@ class StringAnyEncodableTest: UnitTest { let data = ["fooBar": Unencodable(data: 12345)] as [String: Any] - let json = StringAnyEncodable(data) + let json = StringAnyEncodable(logger: log, data) guard let actual = jsonAdapter.toJson(json) else { XCTFail("couldn't encode to JSON") @@ -44,7 +46,7 @@ class StringAnyEncodableTest: UnitTest { let data = ["fooBar": "bar"] as [String: String] - let json = StringAnyEncodable(data) + let json = StringAnyEncodable(logger: log, data) guard let actual = jsonAdapter.toJson(json) else { XCTFail("couldn't encode to JSON") @@ -59,7 +61,7 @@ class StringAnyEncodableTest: UnitTest { let data = ["fooBar": 1.2] as [String: Double] - let json = StringAnyEncodable(data) + let json = StringAnyEncodable(logger: log, data) guard let actual = jsonAdapter.toJson(json) else { XCTFail("couldn't encode to JSON") @@ -74,7 +76,7 @@ class StringAnyEncodableTest: UnitTest { let data = ["fooBar": ["bar": 1000] as [String: Int]] as [String: Any] - let json = StringAnyEncodable(data) + let json = StringAnyEncodable(logger: log, data) guard let actual = jsonAdapter.toJson(json) else { XCTFail("couldn't encode to JSON") @@ -85,17 +87,19 @@ class StringAnyEncodableTest: UnitTest { } func test_stringanyencodable_encodes_complex_data() { - let expect = DummyData(boolean: true, numeric: 1, testValue: "foo", dict: ["test": "value"], array: [1, 2, 4]) + let expect = DummyData(boolean: true, numeric: 1, testValue: "foo", dict: ["test": "value"], array: [1, 2, 4], dictWithArray: ["color": ["Red", "Green", "Blue"]]) + // React native wrap some values in AnyHashable let data = [ "testValue": "foo", "numeric": 1, "boolean": true, "array": [1, 2, 4], - "dict": ["test": "value"] as [String: Any] + "dict": ["test": "value"] as [String: Any], + "dictWithArray": ["color": ["Red", "Green", "Blue"]] as [String: [Any]] ] as [String: Any] - let json = StringAnyEncodable(data) + let json = StringAnyEncodable(logger: log, data) guard let actual = jsonAdapter.toJson(json) else { XCTFail("couldn't encode to JSON") diff --git a/Tests/Tracking/CustomerIOImplementationTest.swift b/Tests/Tracking/CustomerIOImplementationTest.swift index 1599ccc62..515428a1f 100644 --- a/Tests/Tracking/CustomerIOImplementationTest.swift +++ b/Tests/Tracking/CustomerIOImplementationTest.swift @@ -323,7 +323,7 @@ class CustomerIOImplementationTest: UnitTest { platform: "iOS", lastUsed: dateUtilStub .givenNow, - attributes: StringAnyEncodable(givenDefaultAttributes) + attributes: StringAnyEncodable(logger: log, givenDefaultAttributes) ) )) XCTAssertEqual(actualQueueTaskData?.attributesJsonString, expectedJsonString)