Skip to content

Commit

Permalink
iOS: Expand AnyType to handle deeply nested AnyType for beacon encodi…
Browse files Browse the repository at this point in the history
…ng (#519)

* Expand AnyType to handle deeply nested AnyType for beacon encoding

* add encode test

* hasher test + slight refactor for coverage

* nest in another dictionary to trigger encoding case

* update test cases to actually hit new case in CustomEncodable
  • Loading branch information
hborawski authored Oct 1, 2024
1 parent c0f7345 commit 9a3514c
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 2 deletions.
16 changes: 14 additions & 2 deletions ios/core/Sources/Types/Assets/BaseAssetRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ open class BaseAssetRegistry<WrapperType>: PlayerRegistry where
public func decode(_ value: JSValue) throws -> WrapperType.AssetType {
assert(Thread.isMainThread, "decoder must be accessed from main")
typealias Shim = RegistryDecodeShim<WrapperType.AssetType>
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
}

/**
Expand All @@ -177,7 +178,8 @@ open class BaseAssetRegistry<WrapperType>: 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)
}
}

Expand All @@ -190,6 +192,16 @@ public struct RegistryDecodeShim<Asset>: 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 {
Expand Down
28 changes: 28 additions & 0 deletions ios/core/Sources/Types/Generic/AnyType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions ios/core/Tests/Types/Generic/AnyTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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)
}

Expand All @@ -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"))
}

Expand Down

0 comments on commit 9a3514c

Please sign in to comment.