From 9bae7f365819abed85816ef62c1f4904b8cf7bce Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 14 Aug 2025 16:26:57 +0100 Subject: [PATCH 1/3] Remove PrimitiveObjectValue I copied this type blindly from JS in ce8c022, but in JS it doesn't introduce an annoying extra layer of indirection at the point of usage like it does here. Get rid of it. --- .../Internal/InternalDefaultLiveMap.swift | 12 +- .../Internal/InternalLiveMapValue.swift | 132 +++++++++------- .../InternalLiveMapValue+ToPublic.swift | 14 +- .../AblyLiveObjects/Public/PublicTypes.swift | 141 ++++++------------ .../AblyLiveObjectsTests.swift | 14 +- .../InternalDefaultLiveMapTests.swift | 16 +- .../InternalDefaultRealtimeObjectsTests.swift | 8 +- .../ObjectsIntegrationTests.swift | 50 +++---- .../ObjectCreationHelpersTests.swift | 12 +- 9 files changed, 194 insertions(+), 205 deletions(-) diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift index 53321db9..312affaa 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift @@ -887,31 +887,31 @@ internal final class InternalDefaultLiveMap: Sendable { // RTLM5d2b: If ObjectsMapEntry.data.boolean exists, return it if let boolean = entry.data?.boolean { - return .primitive(.bool(boolean)) + return .bool(boolean) } // RTLM5d2c: If ObjectsMapEntry.data.bytes exists, return it if let bytes = entry.data?.bytes { - return .primitive(.data(bytes)) + return .data(bytes) } // RTLM5d2d: If ObjectsMapEntry.data.number exists, return it if let number = entry.data?.number { - return .primitive(.number(number.doubleValue)) + return .number(number.doubleValue) } // RTLM5d2e: If ObjectsMapEntry.data.string exists, return it if let string = entry.data?.string { - return .primitive(.string(string)) + return .string(string) } // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) if let json = entry.data?.json { switch json { case let .array(array): - return .primitive(.jsonArray(array)) + return .jsonArray(array) case let .object(object): - return .primitive(.jsonObject(object)) + return .jsonObject(object) } } diff --git a/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift b/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift index 657a37f3..9ebeca85 100644 --- a/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift +++ b/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift @@ -2,7 +2,12 @@ import Foundation /// Same as the public ``LiveMapValue`` type but with associated values of internal type. internal enum InternalLiveMapValue: Sendable, Equatable { - case primitive(PrimitiveObjectValue) + case string(String) + case number(Double) + case bool(Bool) + case data(Data) + case jsonArray([JSONValue]) + case jsonObject([String: JSONValue]) case liveMap(InternalDefaultLiveMap) case liveCounter(InternalDefaultLiveCounter) @@ -13,8 +18,18 @@ internal enum InternalLiveMapValue: Sendable, Equatable { /// Needed in order to access the internals of user-provided LiveObject-valued LiveMap entries to extract their object ID. internal init(liveMapValue: LiveMapValue) { switch liveMapValue { - case let .primitive(primitiveValue): - self = .primitive(primitiveValue) + case let .string(value): + self = .string(value) + case let .number(value): + self = .number(value) + case let .bool(value): + self = .bool(value) + case let .data(value): + self = .data(value) + case let .jsonArray(value): + self = .jsonArray(value) + case let .jsonObject(value): + self = .jsonObject(value) case let .liveMap(publicLiveMap): guard let publicDefaultLiveMap = publicLiveMap as? PublicDefaultLiveMap else { // TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/37 @@ -36,21 +51,18 @@ internal enum InternalLiveMapValue: Sendable, Equatable { internal var toObjectData: ObjectData { // RTO11f4c1: Create an ObjectsMapEntry for the current value switch self { - case let .primitive(primitiveValue): - switch primitiveValue { - case let .bool(value): - .init(boolean: value) - case let .data(value): - .init(bytes: value) - case let .number(value): - .init(number: NSNumber(value: value)) - case let .string(value): - .init(string: value) - case let .jsonArray(value): - .init(json: .array(value)) - case let .jsonObject(value): - .init(json: .object(value)) - } + case let .bool(value): + .init(boolean: value) + case let .data(value): + .init(bytes: value) + case let .number(value): + .init(number: NSNumber(value: value)) + case let .string(value): + .init(string: value) + case let .jsonArray(value): + .init(json: .array(value)) + case let .jsonObject(value): + .init(json: .object(value)) case let .liveMap(liveMap): // RTO11f4c1a: If the value is of type LiveMap, set ObjectsMapEntry.data.objectId to the objectId of that object .init(objectId: liveMap.objectID) @@ -62,14 +74,6 @@ internal enum InternalLiveMapValue: Sendable, Equatable { // MARK: - Convenience getters for associated values - /// If this `InternalLiveMapValue` has case `primitive`, this returns the associated value. Else, it returns `nil`. - internal var primitiveValue: PrimitiveObjectValue? { - if case let .primitive(value) = self { - return value - } - return nil - } - /// If this `InternalLiveMapValue` has case `liveMap`, this returns the associated value. Else, it returns `nil`. internal var liveMapValue: InternalDefaultLiveMap? { if case let .liveMap(value) = self { @@ -86,54 +90,76 @@ internal enum InternalLiveMapValue: Sendable, Equatable { return nil } - /// If this `InternalLiveMapValue` has case `primitive` with a string value, this returns that value. Else, it returns `nil`. + /// If this `InternalLiveMapValue` has case `string`, this returns that value. Else, it returns `nil`. internal var stringValue: String? { - primitiveValue?.stringValue + if case let .string(value) = self { + return value + } + return nil } - /// If this `InternalLiveMapValue` has case `primitive` with a number value, this returns that value. Else, it returns `nil`. + /// If this `InternalLiveMapValue` has case `number`, this returns that value. Else, it returns `nil`. internal var numberValue: Double? { - primitiveValue?.numberValue + if case let .number(value) = self { + return value + } + return nil } - /// If this `InternalLiveMapValue` has case `primitive` with a boolean value, this returns that value. Else, it returns `nil`. + /// If this `InternalLiveMapValue` has case `bool`, this returns that value. Else, it returns `nil`. internal var boolValue: Bool? { - primitiveValue?.boolValue + if case let .bool(value) = self { + return value + } + return nil } - /// If this `InternalLiveMapValue` has case `primitive` with a data value, this returns that value. Else, it returns `nil`. + /// If this `InternalLiveMapValue` has case `data`, this returns that value. Else, it returns `nil`. internal var dataValue: Data? { - primitiveValue?.dataValue + if case let .data(value) = self { + return value + } + return nil } - /// If this `InternalLiveMapValue` has case `primitive` with a JSON array value, this returns that value. Else, it returns `nil`. + /// If this `InternalLiveMapValue` has case `jsonArray`, this returns that value. Else, it returns `nil`. internal var jsonArrayValue: [JSONValue]? { - primitiveValue?.jsonArrayValue + if case let .jsonArray(value) = self { + return value + } + return nil } - /// If this `InternalLiveMapValue` has case `primitive` with a JSON object value, this returns that value. Else, it returns `nil`. + /// If this `InternalLiveMapValue` has case `jsonObject`, this returns that value. Else, it returns `nil`. internal var jsonObjectValue: [String: JSONValue]? { - primitiveValue?.jsonObjectValue + if case let .jsonObject(value) = self { + return value + } + return nil } // MARK: - Equatable Implementation internal static func == (lhs: InternalLiveMapValue, rhs: InternalLiveMapValue) -> Bool { - switch lhs { - case let .primitive(lhsValue): - if case let .primitive(rhsValue) = rhs, lhsValue == rhsValue { - return true - } - case let .liveMap(lhsMap): - if case let .liveMap(rhsMap) = rhs, lhsMap === rhsMap { - return true - } - case let .liveCounter(lhsCounter): - if case let .liveCounter(rhsCounter) = rhs, lhsCounter === rhsCounter { - return true - } + switch (lhs, rhs) { + case let (.string(lhsValue), .string(rhsValue)): + lhsValue == rhsValue + case let (.number(lhsValue), .number(rhsValue)): + lhsValue == rhsValue + case let (.bool(lhsValue), .bool(rhsValue)): + lhsValue == rhsValue + case let (.data(lhsValue), .data(rhsValue)): + lhsValue == rhsValue + case let (.jsonArray(lhsValue), .jsonArray(rhsValue)): + lhsValue == rhsValue + case let (.jsonObject(lhsValue), .jsonObject(rhsValue)): + lhsValue == rhsValue + case let (.liveMap(lhsMap), .liveMap(rhsMap)): + lhsMap === rhsMap + case let (.liveCounter(lhsCounter), .liveCounter(rhsCounter)): + lhsCounter === rhsCounter + default: + false } - - return false } } diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift index e5b599d0..bd88d2fb 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift @@ -20,8 +20,18 @@ internal extension InternalLiveMapValue { /// Fetches the cached public object that wraps this `InternalLiveMapValue`'s associated value, creating a new public object if there isn't already one. func toPublic(creationArgs: PublicValueCreationArgs) -> LiveMapValue { switch self { - case let .primitive(primitive): - .primitive(primitive) + case let .string(value): + .string(value) + case let .number(value): + .number(value) + case let .bool(value): + .bool(value) + case let .data(value): + .data(value) + case let .jsonArray(value): + .jsonArray(value) + case let .jsonObject(value): + .jsonObject(value) case let .liveMap(internalLiveMap): .liveMap( PublicObjectsStore.shared.getOrCreateMap( diff --git a/Sources/AblyLiveObjects/Public/PublicTypes.swift b/Sources/AblyLiveObjects/Public/PublicTypes.swift index be64bcac..de0201c7 100644 --- a/Sources/AblyLiveObjects/Public/PublicTypes.swift +++ b/Sources/AblyLiveObjects/Public/PublicTypes.swift @@ -82,22 +82,19 @@ public protocol RealtimeObjects: Sendable { } /// Represents the type of data stored for a given key in a ``LiveMap``. -/// It may be a primitive value (``PrimitiveObjectValue``), or another ``LiveObject``. +/// It may be a primitive value (string, number, boolean, binary data, JSON array, or JSON object), or another ``LiveObject``. public enum LiveMapValue: Sendable, Equatable { - case primitive(PrimitiveObjectValue) + case string(String) + case number(Double) + case bool(Bool) + case data(Data) + case jsonArray([JSONValue]) + case jsonObject([String: JSONValue]) case liveMap(any LiveMap) case liveCounter(any LiveCounter) // MARK: - Convenience getters for associated values - /// If this `LiveMapValue` has case `primitive`, this returns the associated value. Else, it returns `nil`. - public var primitiveValue: PrimitiveObjectValue? { - if case let .primitive(value) = self { - return value - } - return nil - } - /// If this `LiveMapValue` has case `liveMap`, this returns the associated value. Else, it returns `nil`. public var liveMapValue: (any LiveMap)? { if case let .liveMap(value) = self { @@ -114,45 +111,61 @@ public enum LiveMapValue: Sendable, Equatable { return nil } - /// If this `LiveMapValue` has case `primitive` with a string value, this returns that value. Else, it returns `nil`. + /// If this `LiveMapValue` has case `string`, this returns the associated value. Else, it returns `nil`. public var stringValue: String? { - primitiveValue?.stringValue + if case let .string(value) = self { + return value + } + return nil } - /// If this `LiveMapValue` has case `primitive` with a number value, this returns that value. Else, it returns `nil`. + /// If this `LiveMapValue` has case `number`, this returns the associated value. Else, it returns `nil`. public var numberValue: Double? { - primitiveValue?.numberValue + if case let .number(value) = self { + return value + } + return nil } - /// If this `LiveMapValue` has case `primitive` with a boolean value, this returns that value. Else, it returns `nil`. + /// If this `LiveMapValue` has case `bool`, this returns the associated value. Else, it returns `nil`. public var boolValue: Bool? { - primitiveValue?.boolValue + if case let .bool(value) = self { + return value + } + return nil } - /// If this `LiveMapValue` has case `primitive` with a data value, this returns that value. Else, it returns `nil`. + /// If this `LiveMapValue` has case `data`, this returns the associated value. Else, it returns `nil`. public var dataValue: Data? { - primitiveValue?.dataValue + if case let .data(value) = self { + return value + } + return nil } // MARK: - Equatable Implementation public static func == (lhs: LiveMapValue, rhs: LiveMapValue) -> Bool { - switch lhs { - case let .primitive(lhsValue): - if case let .primitive(rhsValue) = rhs, lhsValue == rhsValue { - return true - } - case let .liveMap(lhsMap): - if case let .liveMap(rhsMap) = rhs, lhsMap === rhsMap { - return true - } - case let .liveCounter(lhsCounter): - if case let .liveCounter(rhsCounter) = rhs, lhsCounter === rhsCounter { - return true - } + switch (lhs, rhs) { + case let (.string(lhsValue), .string(rhsValue)): + lhsValue == rhsValue + case let (.number(lhsValue), .number(rhsValue)): + lhsValue == rhsValue + case let (.bool(lhsValue), .bool(rhsValue)): + lhsValue == rhsValue + case let (.data(lhsValue), .data(rhsValue)): + lhsValue == rhsValue + case let (.jsonArray(lhsValue), .jsonArray(rhsValue)): + lhsValue == rhsValue + case let (.jsonObject(lhsValue), .jsonObject(rhsValue)): + lhsValue == rhsValue + case let (.liveMap(lhsMap), .liveMap(rhsMap)): + lhsMap === rhsMap + case let (.liveCounter(lhsCounter), .liveCounter(rhsCounter)): + lhsCounter === rhsCounter + default: + false } - - return false } } @@ -226,7 +239,7 @@ public protocol BatchContextLiveCounter: AnyObject, Sendable { /// Conflicts in a LiveMap are automatically resolved with last-write-wins (LWW) semantics, /// meaning that if two clients update the same key in the map, the update with the most recent timestamp wins. /// -/// 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``). +/// 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. public protocol LiveMap: LiveObject where Update == LiveMapUpdate { /// 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. /// @@ -285,66 +298,6 @@ public protocol LiveMapUpdate: Sendable { var update: [String: LiveMapUpdateAction] { get } } -/// Represents a primitive value that can be stored in a ``LiveMap``. -public enum PrimitiveObjectValue: Sendable, Equatable { - case string(String) - case number(Double) - case bool(Bool) - case data(Data) - case jsonArray([JSONValue]) - case jsonObject([String: JSONValue]) - - // MARK: - Convenience getters for associated values - - /// If this `PrimitiveObjectValue` has case `string`, this returns the associated value. Else, it returns `nil`. - public var stringValue: String? { - if case let .string(value) = self { - return value - } - return nil - } - - /// If this `PrimitiveObjectValue` has case `number`, this returns the associated value. Else, it returns `nil`. - public var numberValue: Double? { - if case let .number(value) = self { - return value - } - return nil - } - - /// If this `PrimitiveObjectValue` has case `bool`, this returns the associated value. Else, it returns `nil`. - public var boolValue: Bool? { - if case let .bool(value) = self { - return value - } - return nil - } - - /// If this `PrimitiveObjectValue` has case `data`, this returns the associated value. Else, it returns `nil`. - public var dataValue: Data? { - if case let .data(value) = self { - return value - } - return nil - } - - /// If this `PrimitiveObjectValue` has case `jsonArray`, this returns the associated value. Else, it returns `nil`. - public var jsonArrayValue: [JSONValue]? { - if case let .jsonArray(value) = self { - return value - } - return nil - } - - /// If this `PrimitiveObjectValue` has case `jsonObject`, this returns the associated value. Else, it returns `nil`. - public var jsonObjectValue: [String: JSONValue]? { - if case let .jsonObject(value) = self { - return value - } - return nil - } -} - /// The `LiveCounter` class represents a counter that can be incremented or decremented and is synchronized across clients in realtime. public protocol LiveCounter: LiveObject where Update == LiveCounterUpdate { /// Returns the current value of the counter. diff --git a/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift b/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift index c9cb540b..d5bd807a 100644 --- a/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift +++ b/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift @@ -139,13 +139,13 @@ struct AblyLiveObjectsTests { // Create a map and check its initial entries let map = try await channel.objects.createMap(entries: [ - "boolKey": .primitive(.bool(true)), - "numberKey": .primitive(.number(10)), + "boolKey": .bool(true), + "numberKey": .number(10), ]) #expect( try Dictionary(uniqueKeysWithValues: map.entries) == [ - "boolKey": .primitive(.bool(true)), - "numberKey": .primitive(.number(10)), + "boolKey": .bool(true), + "numberKey": .number(10), ], ) let mapSubscription = try map.updates() @@ -162,8 +162,8 @@ struct AblyLiveObjectsTests { #expect(mapUpdate.update == ["counterKey": .updated]) #expect( try Dictionary(uniqueKeysWithValues: map.entries) == [ - "boolKey": .primitive(.bool(true)), - "numberKey": .primitive(.number(10)), + "boolKey": .bool(true), + "numberKey": .number(10), "counterKey": .liveCounter(counter), ], ) @@ -180,7 +180,7 @@ struct AblyLiveObjectsTests { #expect(mapRemoveUpdate.update == ["boolKey": .removed]) #expect( try Dictionary(uniqueKeysWithValues: map.entries) == [ - "numberKey": .primitive(.number(10)), + "numberKey": .number(10), "counterKey": .liveCounter(counter), ], ) diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift index daa2f503..9d982e99 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift @@ -1237,7 +1237,7 @@ struct InternalDefaultLiveMapTests { let coreSDK = MockCoreSDK(channelState: channelState) await #expect { - try await map.set(key: "test", value: .primitive(.string("value")), coreSDK: coreSDK) + try await map.set(key: "test", value: .string("value"), coreSDK: coreSDK) } throws: { error in guard let errorInfo = error as? ARTErrorInfo else { return false @@ -1264,16 +1264,16 @@ struct InternalDefaultLiveMapTests { (value: .liveMap(.createZeroValued(objectID: "map:test@123", logger: TestLogger(), userCallbackQueue: .main, clock: MockSimpleClock())), expectedData: .init(objectId: "map:test@123")), (value: .liveCounter(.createZeroValued(objectID: "map:test@123", logger: TestLogger(), userCallbackQueue: .main, clock: MockSimpleClock())), expectedData: .init(objectId: "map:test@123")), // RTLM20e5b - (value: .primitive(.jsonArray(["test"])), expectedData: .init(json: .array(["test"]))), - (value: .primitive(.jsonObject(["foo": "bar"])), expectedData: .init(json: .object(["foo": "bar"]))), + (value: .jsonArray(["test"]), expectedData: .init(json: .array(["test"]))), + (value: .jsonObject(["foo": "bar"]), expectedData: .init(json: .object(["foo": "bar"]))), // RTLM20e5c - (value: .primitive(.string("test")), expectedData: .init(string: "test")), + (value: .string("test"), expectedData: .init(string: "test")), // RTLM20e5d - (value: .primitive(.number(42.5)), expectedData: .init(number: NSNumber(value: 42.5))), + (value: .number(42.5), expectedData: .init(number: NSNumber(value: 42.5))), // RTLM20e5e - (value: .primitive(.bool(true)), expectedData: .init(boolean: true)), + (value: .bool(true), expectedData: .init(boolean: true)), // RTLM20e5f - (value: .primitive(.data(Data([0x01, 0x02]))), expectedData: .init(bytes: Data([0x01, 0x02]))), + (value: .data(Data([0x01, 0x02])), expectedData: .init(bytes: Data([0x01, 0x02]))), ] as [(value: InternalLiveMapValue, expectedData: ObjectData)]) func publishesCorrectObjectMessageForDifferentValueTypes(value: InternalLiveMapValue, expectedData: ObjectData) async throws { let logger = TestLogger() @@ -1317,7 +1317,7 @@ struct InternalDefaultLiveMapTests { } await #expect { - try await map.set(key: "testKey", value: .primitive(.string("testValue")), coreSDK: coreSDK) + try await map.set(key: "testKey", value: .string("testValue"), coreSDK: coreSDK) } throws: { error in guard let errorInfo = error as? ARTErrorInfo else { return false diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift index fee10e3c..b4bedee9 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift @@ -1048,7 +1048,7 @@ struct InternalDefaultRealtimeObjectsTests { func throwsIfChannelIsInInvalidState(channelState: ARTRealtimeChannelState) async throws { let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects() let coreSDK = MockCoreSDK(channelState: channelState) - let entries: [String: InternalLiveMapValue] = ["testKey": .primitive(.string("testValue"))] + let entries: [String: InternalLiveMapValue] = ["testKey": .string("testValue")] await #expect { _ = try await realtimeObjects.createMap(entries: entries, coreSDK: coreSDK) @@ -1078,7 +1078,7 @@ struct InternalDefaultRealtimeObjectsTests { // Call createMap let returnedMap = try await realtimeObjects.createMap( entries: [ - "stringKey": .primitive(.string("stringValue")), + "stringKey": .string("stringValue"), ], coreSDK: coreSDK, ) @@ -1098,7 +1098,7 @@ struct InternalDefaultRealtimeObjectsTests { ]) // Verify initial value was merged per RTO11h3a - #expect(returnedMap.testsOnly_data == ["stringKey": .init(data: .init(string: "stringValue"))]) + #expect(returnedMap.testsOnly_data == ["stringKey": InternalObjectsMapEntry(data: ObjectData(string: "stringValue"))]) // Verify object was added to pool per RTO11h3b #expect(realtimeObjects.testsOnly_objectsPool.entries[objectID]?.mapValue === returnedMap) @@ -1156,7 +1156,7 @@ struct InternalDefaultRealtimeObjectsTests { } // Call createMap - the publishHandler will create the object with the generated ID - let result = try await realtimeObjects.createMap(entries: ["testKey": .primitive(.string("testValue"))], coreSDK: coreSDK) + let result = try await realtimeObjects.createMap(entries: ["testKey": .string("testValue")], coreSDK: coreSDK) // Verify ObjectMessage was published #expect(publishedMessages.count == 1) diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 7f0e984e..03457d6e 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -137,52 +137,52 @@ private let primitiveKeyData: [(key: String, data: [String: JSONValue], liveMapV ( key: "stringKey", data: ["string": .string("stringValue")], - liveMapValue: .primitive(.string("stringValue")) + liveMapValue: .string("stringValue") ), ( key: "emptyStringKey", data: ["string": .string("")], - liveMapValue: .primitive(.string("")) + liveMapValue: .string("") ), ( key: "bytesKey", data: ["bytes": .string("eyJwcm9kdWN0SWQiOiAiMDAxIiwgInByb2R1Y3ROYW1lIjogImNhciJ9")], - liveMapValue: .primitive(.data(Data(base64Encoded: "eyJwcm9kdWN0SWQiOiAiMDAxIiwgInByb2R1Y3ROYW1lIjogImNhciJ9")!)) + liveMapValue: .data(Data(base64Encoded: "eyJwcm9kdWN0SWQiOiAiMDAxIiwgInByb2R1Y3ROYW1lIjogImNhciJ9")!) ), ( key: "emptyBytesKey", data: ["bytes": .string("")], - liveMapValue: .primitive(.data(Data(base64Encoded: "")!)) + liveMapValue: .data(Data(base64Encoded: "")!) ), ( key: "maxSafeIntegerKey", data: ["number": .number(.init(value: Int.max))], - liveMapValue: .primitive(.number(Double(Int.max))) + liveMapValue: .number(Double(Int.max)) ), ( key: "negativeMaxSafeIntegerKey", data: ["number": .number(.init(value: -Int.max))], - liveMapValue: .primitive(.number(-Double(Int.max))) + liveMapValue: .number(-Double(Int.max)) ), ( key: "numberKey", data: ["number": .number(1)], - liveMapValue: .primitive(.number(1)) + liveMapValue: .number(1) ), ( key: "zeroKey", data: ["number": .number(0)], - liveMapValue: .primitive(.number(0)) + liveMapValue: .number(0) ), ( key: "trueKey", data: ["boolean": .bool(true)], - liveMapValue: .primitive(.bool(true)) + liveMapValue: .bool(true) ), ( key: "falseKey", data: ["boolean": .bool(false)], - liveMapValue: .primitive(.bool(false)) + liveMapValue: .bool(false) ), ] @@ -422,7 +422,7 @@ private struct ObjectsIntegrationTests { } // MAP_CREATE - let map = try await objects.createMap(entries: ["shouldStay": .primitive(.string("foo")), "shouldDelete": .primitive(.string("bar"))]) + let map = try await objects.createMap(entries: ["shouldStay": .string("foo"), "shouldDelete": .string("bar")]) // COUNTER_CREATE let counter = try await objects.createCounter(count: 1) @@ -449,7 +449,7 @@ private struct ObjectsIntegrationTests { } // Perform the operations and await the promise - async let setAnotherKeyPromise: Void = map.set(key: "anotherKey", value: .primitive(.string("baz"))) + async let setAnotherKeyPromise: Void = map.set(key: "anotherKey", value: .string("baz")) async let removeKeyPromise: Void = map.remove(key: "shouldDelete") async let incrementPromise: Void = counter.increment(amount: 10) _ = try await (setAnotherKeyPromise, removeKeyPromise, incrementPromise, operationsAppliedPromise) @@ -2555,16 +2555,16 @@ private struct ObjectsIntegrationTests { let actualValue = try #require(try root.get(key: keyData.key)) switch keyData.liveMapValue { - case let .primitive(.data(expectedData)): + case let .data(expectedData): let actualData = try #require(actualValue.dataValue) #expect(actualData == expectedData, "Check root has correct value for \"\(keyData.key)\" key after LiveMap.set call") - case let .primitive(.string(expectedString)): + case let .string(expectedString): let actualString = try #require(actualValue.stringValue) #expect(actualString == expectedString, "Check root has correct value for \"\(keyData.key)\" key after LiveMap.set call") - case let .primitive(.number(expectedNumber)): + case let .number(expectedNumber): let actualNumber = try #require(actualValue.numberValue) #expect(actualNumber == expectedNumber, "Check root has correct value for \"\(keyData.key)\" key after LiveMap.set call") - case let .primitive(.bool(expectedBool)): + case let .bool(expectedBool): let actualBool = try #require(actualValue.boolValue as Bool?) #expect(actualBool == expectedBool, "Check root has correct value for \"\(keyData.key)\" key after LiveMap.set call") default: @@ -2947,19 +2947,19 @@ private struct ObjectsIntegrationTests { let actualValue = try map.get(key: key) switch expectedValue { - case let .primitive(.data(expectedData)): + case let .data(expectedData): let actualData = try #require(actualValue?.dataValue) #expect(actualData == expectedData, "Check map #\(i + 1) has correct value for \"\(key)\" key") - case let .primitive(.string(expectedString)): + case let .string(expectedString): let actualString = try #require(actualValue?.stringValue) #expect(actualString == expectedString, "Check map #\(i + 1) has correct value for \"\(key)\" key") - case let .primitive(.number(expectedNumber)): + case let .number(expectedNumber): let actualNumber = try #require(actualValue?.numberValue) #expect(actualNumber == expectedNumber, "Check map #\(i + 1) has correct value for \"\(key)\" key") - case let .primitive(.bool(expectedBool)): + case let .bool(expectedBool): let actualBool = try #require(actualValue?.boolValue as Bool?) #expect(actualBool == expectedBool, "Check map #\(i + 1) has correct value for \"\(key)\" key") - case .primitive(.jsonArray), .primitive(.jsonObject): + case .jsonArray, .jsonObject: Issue.record("JSON array/object primitives not expected in test data") case .liveCounter, .liveMap: Issue.record("Nested objects not expected in primitive test data") @@ -3032,7 +3032,7 @@ private struct ObjectsIntegrationTests { async let mapCreatedPromise: Void = waitForMapKeyUpdate(mapCreatedPromiseUpdates, "map") let counter = try await objects.createCounter() - let map = try await objects.createMap(entries: ["foo": .primitive(.string("bar")), "baz": .liveCounter(counter)]) + let map = try await objects.createMap(entries: ["foo": .string("bar"), "baz": .liveCounter(counter)]) try await root.set(key: "map", value: .liveMap(map)) _ = await mapCreatedPromise @@ -3058,7 +3058,7 @@ private struct ObjectsIntegrationTests { internallyTypedObjects.testsOnly_overridePublish(with: { _ in }) // prevent publishing of ops to realtime so we guarantee that the initial value doesn't come from a CREATE op - let map = try await objects.createMap(entries: ["foo": .primitive(.string("bar"))]) + let map = try await objects.createMap(entries: ["foo": .string("bar")]) #expect(try #require(map.get(key: "foo")?.stringValue) == "bar", "Check map has expected initial value") }, ), @@ -3103,7 +3103,7 @@ private struct ObjectsIntegrationTests { } }) - let map = try await objects.createMap(entries: ["foo": .primitive(.string("bar"))]) + let map = try await objects.createMap(entries: ["foo": .string("bar")]) // Map should be created with forged initial value instead of the actual one #expect(try map.get(key: "foo") == nil, "Check key \"foo\" was not set on a map client-side") @@ -3127,7 +3127,7 @@ private struct ObjectsIntegrationTests { }) // Create map locally, should have an initial value set - let map = try await objects.createMap(entries: ["foo": .primitive(.string("bar"))]) + let map = try await objects.createMap(entries: ["foo": .string("bar")]) let internalMap = try #require(map as? PublicDefaultLiveMap) let mapId = internalMap.proxied.objectID diff --git a/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift b/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift index 126b9426..d51efec3 100644 --- a/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift @@ -36,16 +36,16 @@ struct ObjectCreationHelpersTests { // RTO11f4c1a "counterRef": .liveCounter(referencedCounter), // RTO11f4c1b - "jsonArrayKey": .primitive(.jsonArray([.string("arrayItem1"), .string("arrayItem2")])), - "jsonObjectKey": .primitive(.jsonObject(["nestedKey": .string("nestedValue")])), + "jsonArrayKey": .jsonArray([.string("arrayItem1"), .string("arrayItem2")]), + "jsonObjectKey": .jsonObject(["nestedKey": .string("nestedValue")]), // RTO11f4c1c - "stringKey": .primitive(.string("stringValue")), + "stringKey": .string("stringValue"), // RTO11f4c1d - "numberKey": .primitive(.number(42.5)), + "numberKey": .number(42.5), // RTO11f4c1e - "booleanKey": .primitive(.bool(true)), + "booleanKey": .bool(true), // RTO11f4c1f - "dataKey": .primitive(.data(Data([0x01, 0x02, 0x03]))), + "dataKey": .data(Data([0x01, 0x02, 0x03])), ], timestamp: timestamp, ) From 59045db3b2ec526986e061839b738b7590c1600b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 14 Aug 2025 16:34:03 +0100 Subject: [PATCH 2/3] Add missing JSON getters to LiveMapValue Missed this in cb9a11c. --- Sources/AblyLiveObjects/Public/PublicTypes.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/AblyLiveObjects/Public/PublicTypes.swift b/Sources/AblyLiveObjects/Public/PublicTypes.swift index de0201c7..4d2abb33 100644 --- a/Sources/AblyLiveObjects/Public/PublicTypes.swift +++ b/Sources/AblyLiveObjects/Public/PublicTypes.swift @@ -143,6 +143,22 @@ public enum LiveMapValue: Sendable, Equatable { return nil } + /// If this `LiveMapValue` has case `jsonArray`, this returns the associated value. Else, it returns `nil`. + public var jsonArrayValue: [JSONValue]? { + if case let .jsonArray(value) = self { + return value + } + return nil + } + + /// If this `LiveMapValue` has case `jsonObject`, this returns the associated value. Else, it returns `nil`. + public var jsonObjectValue: [String: JSONValue]? { + if case let .jsonObject(value) = self { + return value + } + return nil + } + // MARK: - Equatable Implementation public static func == (lhs: LiveMapValue, rhs: LiveMapValue) -> Bool { From 714988dbd12c2e749f0ee2950a7150630245eab3 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 14 Aug 2025 16:45:25 +0100 Subject: [PATCH 3/3] Add ExpressibleBy*Literal conformance to LiveMapValue. The conversion to use literal syntax was done by Cursor; I've vaguely satisfied myself that it's caught most of the usages but there may be some it's missed. Resolves #65. --- .../AblyLiveObjects/Public/PublicTypes.swift | 59 +++++++++++++++++++ .../AblyLiveObjectsTests.swift | 14 ++--- .../ObjectsIntegrationTests.swift | 24 ++++---- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/Sources/AblyLiveObjects/Public/PublicTypes.swift b/Sources/AblyLiveObjects/Public/PublicTypes.swift index 4d2abb33..6e79b290 100644 --- a/Sources/AblyLiveObjects/Public/PublicTypes.swift +++ b/Sources/AblyLiveObjects/Public/PublicTypes.swift @@ -83,6 +83,27 @@ public protocol RealtimeObjects: Sendable { /// Represents the type of data stored for a given key in a ``LiveMap``. /// It may be a primitive value (string, number, boolean, binary data, JSON array, or JSON object), or another ``LiveObject``. +/// +/// `LiveMapValue` implements Swift's `ExpressibleBy*Literal` protocols. This, in combination with `JSONValue`'s conformance to these protocols, allows you to write type-safe map values using familiar syntax. For example: +/// +/// ```swift +/// let map = try await channel.objects.createMap(entries: [ +/// "someStringKey": "someString", +/// "someIntegerKey": 123, +/// "someFloatKey": 123.456, +/// "someTrueKey": true, +/// "someFalseKey": false, +/// "someJSONObjectKey": [ +/// "someNestedJSONObjectKey": [ +/// "someOtherKey": "someOtherValue", +/// ], +/// ], +/// "someJSONArrayKey": [ +/// "foo", +/// 42, +/// ], +/// ]) +/// ``` public enum LiveMapValue: Sendable, Equatable { case string(String) case number(Double) @@ -185,6 +206,44 @@ public enum LiveMapValue: Sendable, Equatable { } } +// MARK: - ExpressibleBy*Literal conformances + +extension LiveMapValue: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, JSONValue)...) { + self = .jsonObject(.init(uniqueKeysWithValues: elements)) + } +} + +extension LiveMapValue: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: JSONValue...) { + self = .jsonArray(elements) + } +} + +extension LiveMapValue: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension LiveMapValue: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .number(Double(value)) + } +} + +extension LiveMapValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self = .number(value) + } +} + +extension LiveMapValue: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Bool) { + self = .bool(value) + } +} + /// Object returned from an `on` call, allowing the listener provided in that call to be deregistered. public protocol OnObjectsEventResponse: Sendable { /// Deregisters the listener passed to the `on` call. diff --git a/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift b/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift index d5bd807a..e71959de 100644 --- a/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift +++ b/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift @@ -139,13 +139,13 @@ struct AblyLiveObjectsTests { // Create a map and check its initial entries let map = try await channel.objects.createMap(entries: [ - "boolKey": .bool(true), - "numberKey": .number(10), + "boolKey": true, + "numberKey": 10, ]) #expect( try Dictionary(uniqueKeysWithValues: map.entries) == [ - "boolKey": .bool(true), - "numberKey": .number(10), + "boolKey": true, + "numberKey": 10, ], ) let mapSubscription = try map.updates() @@ -162,8 +162,8 @@ struct AblyLiveObjectsTests { #expect(mapUpdate.update == ["counterKey": .updated]) #expect( try Dictionary(uniqueKeysWithValues: map.entries) == [ - "boolKey": .bool(true), - "numberKey": .number(10), + "boolKey": true, + "numberKey": 10, "counterKey": .liveCounter(counter), ], ) @@ -180,7 +180,7 @@ struct AblyLiveObjectsTests { #expect(mapRemoveUpdate.update == ["boolKey": .removed]) #expect( try Dictionary(uniqueKeysWithValues: map.entries) == [ - "numberKey": .number(10), + "numberKey": 10, "counterKey": .liveCounter(counter), ], ) diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 03457d6e..016f06c0 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -137,12 +137,12 @@ private let primitiveKeyData: [(key: String, data: [String: JSONValue], liveMapV ( key: "stringKey", data: ["string": .string("stringValue")], - liveMapValue: .string("stringValue") + liveMapValue: "stringValue" ), ( key: "emptyStringKey", data: ["string": .string("")], - liveMapValue: .string("") + liveMapValue: "" ), ( key: "bytesKey", @@ -167,22 +167,22 @@ private let primitiveKeyData: [(key: String, data: [String: JSONValue], liveMapV ( key: "numberKey", data: ["number": .number(1)], - liveMapValue: .number(1) + liveMapValue: 1 ), ( key: "zeroKey", data: ["number": .number(0)], - liveMapValue: .number(0) + liveMapValue: 0 ), ( key: "trueKey", data: ["boolean": .bool(true)], - liveMapValue: .bool(true) + liveMapValue: true ), ( key: "falseKey", data: ["boolean": .bool(false)], - liveMapValue: .bool(false) + liveMapValue: false ), ] @@ -422,7 +422,7 @@ private struct ObjectsIntegrationTests { } // MAP_CREATE - let map = try await objects.createMap(entries: ["shouldStay": .string("foo"), "shouldDelete": .string("bar")]) + let map = try await objects.createMap(entries: ["shouldStay": "foo", "shouldDelete": "bar"]) // COUNTER_CREATE let counter = try await objects.createCounter(count: 1) @@ -449,7 +449,7 @@ private struct ObjectsIntegrationTests { } // Perform the operations and await the promise - async let setAnotherKeyPromise: Void = map.set(key: "anotherKey", value: .string("baz")) + async let setAnotherKeyPromise: Void = map.set(key: "anotherKey", value: "baz") async let removeKeyPromise: Void = map.remove(key: "shouldDelete") async let incrementPromise: Void = counter.increment(amount: 10) _ = try await (setAnotherKeyPromise, removeKeyPromise, incrementPromise, operationsAppliedPromise) @@ -3032,7 +3032,7 @@ private struct ObjectsIntegrationTests { async let mapCreatedPromise: Void = waitForMapKeyUpdate(mapCreatedPromiseUpdates, "map") let counter = try await objects.createCounter() - let map = try await objects.createMap(entries: ["foo": .string("bar"), "baz": .liveCounter(counter)]) + let map = try await objects.createMap(entries: ["foo": "bar", "baz": .liveCounter(counter)]) try await root.set(key: "map", value: .liveMap(map)) _ = await mapCreatedPromise @@ -3058,7 +3058,7 @@ private struct ObjectsIntegrationTests { internallyTypedObjects.testsOnly_overridePublish(with: { _ in }) // prevent publishing of ops to realtime so we guarantee that the initial value doesn't come from a CREATE op - let map = try await objects.createMap(entries: ["foo": .string("bar")]) + let map = try await objects.createMap(entries: ["foo": "bar"]) #expect(try #require(map.get(key: "foo")?.stringValue) == "bar", "Check map has expected initial value") }, ), @@ -3103,7 +3103,7 @@ private struct ObjectsIntegrationTests { } }) - let map = try await objects.createMap(entries: ["foo": .string("bar")]) + let map = try await objects.createMap(entries: ["foo": "bar"]) // Map should be created with forged initial value instead of the actual one #expect(try map.get(key: "foo") == nil, "Check key \"foo\" was not set on a map client-side") @@ -3127,7 +3127,7 @@ private struct ObjectsIntegrationTests { }) // Create map locally, should have an initial value set - let map = try await objects.createMap(entries: ["foo": .string("bar")]) + let map = try await objects.createMap(entries: ["foo": "bar"]) let internalMap = try #require(map as? PublicDefaultLiveMap) let mapId = internalMap.proxied.objectID