Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS: Expand AnyType to handle deeply nested AnyType for beacon encoding #519

Merged
merged 5 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
Copy link
Contributor Author

@hborawski hborawski Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.fragmentsAllowed lets us encode String here as a top level type in JSON (normally JSON would need to be [] or {} as the top level type), as we dont know what the value type will be

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 @@
}
}

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")

Check warning on line 180 in ios/core/Tests/Types/Generic/AnyTypeTests.swift

View check run for this annotation

Codecov / codecov/patch

ios/core/Tests/Types/Generic/AnyTypeTests.swift#L180

Added line #L180 was not covered by tests
}
}

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")

Check warning on line 195 in ios/core/Tests/Types/Generic/AnyTypeTests.swift

View check run for this annotation

Codecov / codecov/patch

ios/core/Tests/Types/Generic/AnyTypeTests.swift#L195

Added line #L195 was not covered by tests
}
}

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")

Check warning on line 213 in ios/core/Tests/Types/Generic/AnyTypeTests.swift

View check run for this annotation

Codecov / codecov/patch

ios/core/Tests/Types/Generic/AnyTypeTests.swift#L213

Added line #L213 was not covered by tests
}
}

func testUnknownData() {
let string = "{\"key\":\"value\", \"key2\": 2}"
guard
Expand Down Expand Up @@ -203,6 +251,8 @@
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 @@
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 @@
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