Skip to content

Commit c919b36

Browse files
Add JSON objects and arrays to allowed LiveMap values
Public API based on [1] at 6d43429. [1] ably/ably-js#2052
1 parent 5fc3ed3 commit c919b36

5 files changed

Lines changed: 84 additions & 24 deletions

File tree

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -716,9 +716,13 @@ internal final class InternalDefaultLiveMap: Sendable {
716716
switch string {
717717
case let .string(string):
718718
return .primitive(.string(string))
719-
case .json:
720-
// TODO: Understand how to handle JSON values (https://github.com/ably/specification/pull/333/files#r2164561055)
721-
notYetImplemented()
719+
case let .json(objectOrArray):
720+
switch objectOrArray {
721+
case let .array(array):
722+
return .primitive(.jsonArray(array))
723+
case let .object(object):
724+
return .primitive(.jsonObject(object))
725+
}
722726
}
723727
}
724728

Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ internal enum InternalLiveMapValue: Sendable, Equatable {
5252
primitiveValue?.dataValue
5353
}
5454

55+
/// If this `InternalLiveMapValue` has case `primitive` with a JSON array value, this returns that value. Else, it returns `nil`.
56+
internal var jsonArrayValue: [JSONValue]? {
57+
primitiveValue?.jsonArrayValue
58+
}
59+
60+
/// If this `InternalLiveMapValue` has case `primitive` with a JSON object value, this returns that value. Else, it returns `nil`.
61+
internal var jsonObjectValue: [String: JSONValue]? {
62+
primitiveValue?.jsonObjectValue
63+
}
64+
5565
// MARK: - Equatable Implementation
5666

5767
public static func == (lhs: InternalLiveMapValue, rhs: InternalLiveMapValue) -> Bool {

Sources/AblyLiveObjects/Public/PublicTypes.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public protocol BatchContextLiveMap: AnyObject, Sendable {
175175
/// Mirrors the ``LiveMap/get(key:)`` method and returns the value associated with a key in the map.
176176
///
177177
/// - Parameter key: The key to retrieve the value for.
178-
/// - Returns: A ``LiveObject``, a primitive type (string, number, boolean, or binary data) or `nil` if the key doesn't exist in a map or the associated ``LiveObject`` has been deleted. Always `nil` if this map object is deleted.
178+
/// - Returns: A ``LiveObject``, a primitive type (string, number, boolean, JSON-serializable object or array ,or binary data) or `nil` if the key doesn't exist in a map or the associated ``LiveObject`` has been deleted. Always `nil` if this map object is deleted.
179179
func get(key: String) -> LiveMapValue?
180180

181181
/// Returns the number of key-value pairs in the map.
@@ -226,14 +226,14 @@ public protocol BatchContextLiveCounter: AnyObject, Sendable {
226226
/// Conflicts in a LiveMap are automatically resolved with last-write-wins (LWW) semantics,
227227
/// meaning that if two clients update the same key in the map, the update with the most recent timestamp wins.
228228
///
229-
/// Keys must be strings. Values can be another ``LiveObject``, or a primitive type, such as a string, number, boolean, or binary data (see ``PrimitiveObjectValue``).
229+
/// Keys must be strings. Values can be another ``LiveObject``, or a primitive type, such as a string, number, boolean, JSON-serializable object or array, or binary data (see ``PrimitiveObjectValue``).
230230
public protocol LiveMap: LiveObject where Update == LiveMapUpdate {
231231
/// Returns the value associated with a given key. Returns `nil` if the key doesn't exist in a map or if the associated ``LiveObject`` has been deleted.
232232
///
233233
/// Always returns `nil` if this map object is deleted.
234234
///
235235
/// - Parameter key: The key to retrieve the value for.
236-
/// - Returns: A ``LiveObject``, a primitive type (string, number, boolean, or binary data) or `nil` if the key doesn't exist in a map or the associated ``LiveObject`` has been deleted. Always `nil` if this map object is deleted.
236+
/// - Returns: A ``LiveObject``, a primitive type (string, number, boolean, JSON-serializable object or array, or binary data) or `nil` if the key doesn't exist in a map or the associated ``LiveObject`` has been deleted. Always `nil` if this map object is deleted.
237237
func get(key: String) throws(ARTErrorInfo) -> LiveMapValue?
238238

239239
/// Returns the number of key-value pairs in the map.
@@ -291,6 +291,8 @@ public enum PrimitiveObjectValue: Sendable, Equatable {
291291
case number(Double)
292292
case bool(Bool)
293293
case data(Data)
294+
case jsonArray([JSONValue])
295+
case jsonObject([String: JSONValue])
294296

295297
// MARK: - Convenience getters for associated values
296298

@@ -325,6 +327,22 @@ public enum PrimitiveObjectValue: Sendable, Equatable {
325327
}
326328
return nil
327329
}
330+
331+
/// If this `PrimitiveObjectValue` has case `jsonArray`, this returns the associated value. Else, it returns `nil`.
332+
public var jsonArrayValue: [JSONValue]? {
333+
if case let .jsonArray(value) = self {
334+
return value
335+
}
336+
return nil
337+
}
338+
339+
/// If this `PrimitiveObjectValue` has case `jsonObject`, this returns the associated value. Else, it returns `nil`.
340+
public var jsonObjectValue: [String: JSONValue]? {
341+
if case let .jsonObject(value) = self {
342+
return value
343+
}
344+
return nil
345+
}
328346
}
329347

330348
/// The `LiveCounter` class represents a counter that can be incremented or decremented and is synchronized across clients in realtime.

Sources/AblyLiveObjects/Utility/JSONValue.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Foundation
2525
/// ```
2626
///
2727
/// > Note: To write a `JSONValue` that corresponds to the `null` JSON value, you must explicitly write `.null`. `JSONValue` deliberately does not implement the `ExpressibleByNilLiteral` protocol in order to avoid confusion between a value of type `JSONValue?` and a `JSONValue` with case `.null`.
28-
internal indirect enum JSONValue: Sendable, Equatable {
28+
public indirect enum JSONValue: Sendable, Equatable {
2929
case object([String: JSONValue])
3030
case array([JSONValue])
3131
case string(String)
@@ -36,7 +36,7 @@ internal indirect enum JSONValue: Sendable, Equatable {
3636
// MARK: - Convenience getters for associated values
3737

3838
/// If this `JSONValue` has case `object`, this returns the associated value. Else, it returns `nil`.
39-
internal var objectValue: [String: JSONValue]? {
39+
public var objectValue: [String: JSONValue]? {
4040
if case let .object(objectValue) = self {
4141
objectValue
4242
} else {
@@ -45,7 +45,7 @@ internal indirect enum JSONValue: Sendable, Equatable {
4545
}
4646

4747
/// If this `JSONValue` has case `array`, this returns the associated value. Else, it returns `nil`.
48-
internal var arrayValue: [JSONValue]? {
48+
public var arrayValue: [JSONValue]? {
4949
if case let .array(arrayValue) = self {
5050
arrayValue
5151
} else {
@@ -54,7 +54,7 @@ internal indirect enum JSONValue: Sendable, Equatable {
5454
}
5555

5656
/// If this `JSONValue` has case `string`, this returns the associated value. Else, it returns `nil`.
57-
internal var stringValue: String? {
57+
public var stringValue: String? {
5858
if case let .string(stringValue) = self {
5959
stringValue
6060
} else {
@@ -63,7 +63,7 @@ internal indirect enum JSONValue: Sendable, Equatable {
6363
}
6464

6565
/// If this `JSONValue` has case `number`, this returns the associated value. Else, it returns `nil`.
66-
internal var numberValue: NSNumber? {
66+
public var numberValue: NSNumber? {
6767
if case let .number(numberValue) = self {
6868
numberValue
6969
} else {
@@ -72,7 +72,7 @@ internal indirect enum JSONValue: Sendable, Equatable {
7272
}
7373

7474
/// If this `JSONValue` has case `bool`, this returns the associated value. Else, it returns `nil`.
75-
internal var boolValue: Bool? {
75+
public var boolValue: Bool? {
7676
if case let .bool(boolValue) = self {
7777
boolValue
7878
} else {
@@ -81,7 +81,7 @@ internal indirect enum JSONValue: Sendable, Equatable {
8181
}
8282

8383
/// Returns true if and only if this `JSONValue` has case `null`.
84-
internal var isNull: Bool {
84+
public var isNull: Bool {
8585
if case .null = self {
8686
true
8787
} else {
@@ -91,37 +91,37 @@ internal indirect enum JSONValue: Sendable, Equatable {
9191
}
9292

9393
extension JSONValue: ExpressibleByDictionaryLiteral {
94-
internal init(dictionaryLiteral elements: (String, JSONValue)...) {
94+
public init(dictionaryLiteral elements: (String, JSONValue)...) {
9595
self = .object(.init(uniqueKeysWithValues: elements))
9696
}
9797
}
9898

9999
extension JSONValue: ExpressibleByArrayLiteral {
100-
internal init(arrayLiteral elements: JSONValue...) {
100+
public init(arrayLiteral elements: JSONValue...) {
101101
self = .array(elements)
102102
}
103103
}
104104

105105
extension JSONValue: ExpressibleByStringLiteral {
106-
internal init(stringLiteral value: String) {
106+
public init(stringLiteral value: String) {
107107
self = .string(value)
108108
}
109109
}
110110

111111
extension JSONValue: ExpressibleByIntegerLiteral {
112-
internal init(integerLiteral value: Int) {
112+
public init(integerLiteral value: Int) {
113113
self = .number(value as NSNumber)
114114
}
115115
}
116116

117117
extension JSONValue: ExpressibleByFloatLiteral {
118-
internal init(floatLiteral value: Double) {
118+
public init(floatLiteral value: Double) {
119119
self = .number(value as NSNumber)
120120
}
121121
}
122122

123123
extension JSONValue: ExpressibleByBooleanLiteral {
124-
internal init(booleanLiteral value: Bool) {
124+
public init(booleanLiteral value: Bool) {
125125
self = .bool(value)
126126
}
127127
}

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ struct InternalDefaultLiveMapTests {
8181
#expect(result?.numberValue == 123.456)
8282
}
8383

84-
// @spec RTLM5d2e
84+
// @specOneOf(1/3) RTLM5d2e - When `string` is a string
8585
@Test
8686
func returnsStringValue() throws {
8787
let logger = TestLogger()
@@ -92,6 +92,28 @@ struct InternalDefaultLiveMapTests {
9292
#expect(result?.stringValue == "test")
9393
}
9494

95+
// @specOneOf(2/3) RTLM5d2e - When `string` is a JSON array
96+
@Test
97+
func returnsJSONArrayValue() throws {
98+
let logger = TestLogger()
99+
let entry = TestFactories.internalMapEntry(data: ObjectData(string: .json(.array(["foo"]))))
100+
let coreSDK = MockCoreSDK(channelState: .attaching)
101+
let map = InternalDefaultLiveMap(testsOnly_data: ["key": entry], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
102+
let result = try map.get(key: "key", coreSDK: coreSDK, delegate: MockLiveMapObjectPoolDelegate())
103+
#expect(result?.jsonArrayValue == ["foo"])
104+
}
105+
106+
// @specOneOf(3/3) RTLM5d2e - When `string` is a JSON object
107+
@Test
108+
func returnsJSONObjectValue() throws {
109+
let logger = TestLogger()
110+
let entry = TestFactories.internalMapEntry(data: ObjectData(string: .json(.object(["foo": "bar"]))))
111+
let coreSDK = MockCoreSDK(channelState: .attaching)
112+
let map = InternalDefaultLiveMap(testsOnly_data: ["key": entry], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
113+
let result = try map.get(key: "key", coreSDK: coreSDK, delegate: MockLiveMapObjectPoolDelegate())
114+
#expect(result?.jsonObjectValue == ["foo": "bar"])
115+
}
116+
95117
// @spec RTLM5d2f1
96118
@Test
97119
func returnsNilWhenReferencedObjectDoesNotExist() throws {
@@ -389,6 +411,8 @@ struct InternalDefaultLiveMapTests {
389411
"bytes": TestFactories.internalMapEntry(data: ObjectData(bytes: Data([0x01, 0x02, 0x03]))), // RTLM5d2c
390412
"number": TestFactories.internalMapEntry(data: ObjectData(number: NSNumber(value: 42))), // RTLM5d2d
391413
"string": TestFactories.internalMapEntry(data: ObjectData(string: .string("hello"))), // RTLM5d2e
414+
"jsonArray": TestFactories.internalMapEntry(data: ObjectData(string: .json(.array(["foo"])))), // RTLM5d2e
415+
"jsonObject": TestFactories.internalMapEntry(data: ObjectData(string: .json(.object(["foo": "bar"])))), // RTLM5d2e
392416
"mapRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "map:ref@123")), // RTLM5d2f2
393417
"counterRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "counter:ref@456")), // RTLM5d2f2
394418
],
@@ -403,23 +427,27 @@ struct InternalDefaultLiveMapTests {
403427
let keys = try map.keys(coreSDK: coreSDK, delegate: delegate)
404428
let values = try map.values(coreSDK: coreSDK, delegate: delegate)
405429

406-
#expect(size == 6)
407-
#expect(entries.count == 6)
408-
#expect(keys.count == 6)
409-
#expect(values.count == 6)
430+
#expect(size == 8)
431+
#expect(entries.count == 8)
432+
#expect(keys.count == 8)
433+
#expect(values.count == 8)
410434

411435
// Verify the correct values are returned by `entries`
412436
let booleanEntry = entries.first { $0.key == "boolean" } // RTLM5d2b
413437
let bytesEntry = entries.first { $0.key == "bytes" } // RTLM5d2c
414438
let numberEntry = entries.first { $0.key == "number" } // RTLM5d2d
415439
let stringEntry = entries.first { $0.key == "string" } // RTLM5d2e
440+
let jsonArrayEntry = entries.first { $0.key == "jsonArray" } // RTLM5d2e
441+
let jsonObjectEntry = entries.first { $0.key == "jsonObject" } // RTLM5d2e
416442
let mapRefEntry = entries.first { $0.key == "mapRef" } // RTLM5d2f2
417443
let counterRefEntry = entries.first { $0.key == "counterRef" } // RTLM5d2f2
418444

419445
#expect(booleanEntry?.value.boolValue == true) // RTLM5d2b
420446
#expect(bytesEntry?.value.dataValue == Data([0x01, 0x02, 0x03])) // RTLM5d2c
421447
#expect(numberEntry?.value.numberValue == 42) // RTLM5d2d
422448
#expect(stringEntry?.value.stringValue == "hello") // RTLM5d2e
449+
#expect(jsonArrayEntry?.value.jsonArrayValue == ["foo"]) // RTLM5d2e
450+
#expect(jsonObjectEntry?.value.jsonObjectValue == ["foo": "bar"]) // RTLM5d2e
423451
#expect(mapRefEntry?.value.liveMapValue as AnyObject === referencedMap as AnyObject) // RTLM5d2f2
424452
#expect(counterRefEntry?.value.liveCounterValue as AnyObject === referencedCounter as AnyObject) // RTLM5d2f2
425453
}

0 commit comments

Comments
 (0)