From cb9a11cca5a18529d46c49b98a6250c41f7ba0a7 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 30 Jul 2025 09:47:02 +0100 Subject: [PATCH 1/4] Add JSON objects and arrays to allowed LiveMap values Public API based on [1] at 6d43429. [1] https://github.com/ably/ably-js/pull/2052 --- .../Internal/InternalDefaultLiveMap.swift | 10 +++-- .../Internal/InternalLiveMapValue.swift | 10 +++++ .../AblyLiveObjects/Public/PublicTypes.swift | 24 ++++++++++-- .../AblyLiveObjects/Utility/JSONValue.swift | 26 ++++++------- .../InternalDefaultLiveMapTests.swift | 38 ++++++++++++++++--- 5 files changed, 84 insertions(+), 24 deletions(-) diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift index 3790b57d..9676fe0d 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift @@ -695,9 +695,13 @@ internal final class InternalDefaultLiveMap: Sendable { switch string { case let .string(string): return .primitive(.string(string)) - case .json: - // TODO: Understand how to handle JSON values (https://github.com/ably/specification/pull/333/files#r2164561055) - notYetImplemented() + case let .json(objectOrArray): + switch objectOrArray { + case let .array(array): + return .primitive(.jsonArray(array)) + case let .object(object): + return .primitive(.jsonObject(object)) + } } } diff --git a/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift b/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift index 95b36fe3..0d6cb79b 100644 --- a/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift +++ b/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift @@ -52,6 +52,16 @@ internal enum InternalLiveMapValue: Sendable, Equatable { primitiveValue?.dataValue } + /// If this `InternalLiveMapValue` has case `primitive` with a JSON array value, this returns that value. Else, it returns `nil`. + internal var jsonArrayValue: [JSONValue]? { + primitiveValue?.jsonArrayValue + } + + /// If this `InternalLiveMapValue` has case `primitive` with a JSON object value, this returns that value. Else, it returns `nil`. + internal var jsonObjectValue: [String: JSONValue]? { + primitiveValue?.jsonObjectValue + } + // MARK: - Equatable Implementation internal static func == (lhs: InternalLiveMapValue, rhs: InternalLiveMapValue) -> Bool { diff --git a/Sources/AblyLiveObjects/Public/PublicTypes.swift b/Sources/AblyLiveObjects/Public/PublicTypes.swift index 14019129..be64bcac 100644 --- a/Sources/AblyLiveObjects/Public/PublicTypes.swift +++ b/Sources/AblyLiveObjects/Public/PublicTypes.swift @@ -175,7 +175,7 @@ public protocol BatchContextLiveMap: AnyObject, Sendable { /// Mirrors the ``LiveMap/get(key:)`` method and returns the value associated with a key in the map. /// /// - Parameter key: The key to retrieve the value for. - /// - 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. + /// - 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. func get(key: String) -> LiveMapValue? /// Returns the number of key-value pairs in the map. @@ -226,14 +226,14 @@ 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, 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 (see ``PrimitiveObjectValue``). 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. /// /// Always returns `nil` if this map object is deleted. /// /// - Parameter key: The key to retrieve the value for. - /// - 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. + /// - 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. func get(key: String) throws(ARTErrorInfo) -> LiveMapValue? /// Returns the number of key-value pairs in the map. @@ -291,6 +291,8 @@ public enum PrimitiveObjectValue: Sendable, Equatable { case number(Double) case bool(Bool) case data(Data) + case jsonArray([JSONValue]) + case jsonObject([String: JSONValue]) // MARK: - Convenience getters for associated values @@ -325,6 +327,22 @@ public enum PrimitiveObjectValue: Sendable, Equatable { } 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. diff --git a/Sources/AblyLiveObjects/Utility/JSONValue.swift b/Sources/AblyLiveObjects/Utility/JSONValue.swift index d03b86e5..3030dcaa 100644 --- a/Sources/AblyLiveObjects/Utility/JSONValue.swift +++ b/Sources/AblyLiveObjects/Utility/JSONValue.swift @@ -25,7 +25,7 @@ import Foundation /// ``` /// /// > 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`. -internal indirect enum JSONValue: Sendable, Equatable { +public indirect enum JSONValue: Sendable, Equatable { case object([String: JSONValue]) case array([JSONValue]) case string(String) @@ -36,7 +36,7 @@ internal indirect enum JSONValue: Sendable, Equatable { // MARK: - Convenience getters for associated values /// If this `JSONValue` has case `object`, this returns the associated value. Else, it returns `nil`. - internal var objectValue: [String: JSONValue]? { + public var objectValue: [String: JSONValue]? { if case let .object(objectValue) = self { objectValue } else { @@ -45,7 +45,7 @@ internal indirect enum JSONValue: Sendable, Equatable { } /// If this `JSONValue` has case `array`, this returns the associated value. Else, it returns `nil`. - internal var arrayValue: [JSONValue]? { + public var arrayValue: [JSONValue]? { if case let .array(arrayValue) = self { arrayValue } else { @@ -54,7 +54,7 @@ internal indirect enum JSONValue: Sendable, Equatable { } /// If this `JSONValue` has case `string`, this returns the associated value. Else, it returns `nil`. - internal var stringValue: String? { + public var stringValue: String? { if case let .string(stringValue) = self { stringValue } else { @@ -63,7 +63,7 @@ internal indirect enum JSONValue: Sendable, Equatable { } /// If this `JSONValue` has case `number`, this returns the associated value. Else, it returns `nil`. - internal var numberValue: NSNumber? { + public var numberValue: NSNumber? { if case let .number(numberValue) = self { numberValue } else { @@ -72,7 +72,7 @@ internal indirect enum JSONValue: Sendable, Equatable { } /// If this `JSONValue` has case `bool`, this returns the associated value. Else, it returns `nil`. - internal var boolValue: Bool? { + public var boolValue: Bool? { if case let .bool(boolValue) = self { boolValue } else { @@ -81,7 +81,7 @@ internal indirect enum JSONValue: Sendable, Equatable { } /// Returns true if and only if this `JSONValue` has case `null`. - internal var isNull: Bool { + public var isNull: Bool { if case .null = self { true } else { @@ -91,37 +91,37 @@ internal indirect enum JSONValue: Sendable, Equatable { } extension JSONValue: ExpressibleByDictionaryLiteral { - internal init(dictionaryLiteral elements: (String, JSONValue)...) { + public init(dictionaryLiteral elements: (String, JSONValue)...) { self = .object(.init(uniqueKeysWithValues: elements)) } } extension JSONValue: ExpressibleByArrayLiteral { - internal init(arrayLiteral elements: JSONValue...) { + public init(arrayLiteral elements: JSONValue...) { self = .array(elements) } } extension JSONValue: ExpressibleByStringLiteral { - internal init(stringLiteral value: String) { + public init(stringLiteral value: String) { self = .string(value) } } extension JSONValue: ExpressibleByIntegerLiteral { - internal init(integerLiteral value: Int) { + public init(integerLiteral value: Int) { self = .number(value as NSNumber) } } extension JSONValue: ExpressibleByFloatLiteral { - internal init(floatLiteral value: Double) { + public init(floatLiteral value: Double) { self = .number(value as NSNumber) } } extension JSONValue: ExpressibleByBooleanLiteral { - internal init(booleanLiteral value: Bool) { + public init(booleanLiteral value: Bool) { self = .bool(value) } } diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift index 4a67d2e4..528fea67 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift @@ -81,7 +81,7 @@ struct InternalDefaultLiveMapTests { #expect(result?.numberValue == 123.456) } - // @spec RTLM5d2e + // @specOneOf(1/3) RTLM5d2e - When `string` is a string @Test func returnsStringValue() throws { let logger = TestLogger() @@ -92,6 +92,28 @@ struct InternalDefaultLiveMapTests { #expect(result?.stringValue == "test") } + // @specOneOf(2/3) RTLM5d2e - When `string` is a JSON array + @Test + func returnsJSONArrayValue() throws { + let logger = TestLogger() + let entry = TestFactories.internalMapEntry(data: ObjectData(string: .json(.array(["foo"])))) + let coreSDK = MockCoreSDK(channelState: .attaching) + let map = InternalDefaultLiveMap(testsOnly_data: ["key": entry], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) + let result = try map.get(key: "key", coreSDK: coreSDK, delegate: MockLiveMapObjectPoolDelegate()) + #expect(result?.jsonArrayValue == ["foo"]) + } + + // @specOneOf(3/3) RTLM5d2e - When `string` is a JSON object + @Test + func returnsJSONObjectValue() throws { + let logger = TestLogger() + let entry = TestFactories.internalMapEntry(data: ObjectData(string: .json(.object(["foo": "bar"])))) + let coreSDK = MockCoreSDK(channelState: .attaching) + let map = InternalDefaultLiveMap(testsOnly_data: ["key": entry], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) + let result = try map.get(key: "key", coreSDK: coreSDK, delegate: MockLiveMapObjectPoolDelegate()) + #expect(result?.jsonObjectValue == ["foo": "bar"]) + } + // @spec RTLM5d2f1 @Test func returnsNilWhenReferencedObjectDoesNotExist() throws { @@ -389,6 +411,8 @@ struct InternalDefaultLiveMapTests { "bytes": TestFactories.internalMapEntry(data: ObjectData(bytes: Data([0x01, 0x02, 0x03]))), // RTLM5d2c "number": TestFactories.internalMapEntry(data: ObjectData(number: NSNumber(value: 42))), // RTLM5d2d "string": TestFactories.internalMapEntry(data: ObjectData(string: .string("hello"))), // RTLM5d2e + "jsonArray": TestFactories.internalMapEntry(data: ObjectData(string: .json(.array(["foo"])))), // RTLM5d2e + "jsonObject": TestFactories.internalMapEntry(data: ObjectData(string: .json(.object(["foo": "bar"])))), // RTLM5d2e "mapRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "map:ref@123")), // RTLM5d2f2 "counterRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "counter:ref@456")), // RTLM5d2f2 ], @@ -403,16 +427,18 @@ struct InternalDefaultLiveMapTests { let keys = try map.keys(coreSDK: coreSDK, delegate: delegate) let values = try map.values(coreSDK: coreSDK, delegate: delegate) - #expect(size == 6) - #expect(entries.count == 6) - #expect(keys.count == 6) - #expect(values.count == 6) + #expect(size == 8) + #expect(entries.count == 8) + #expect(keys.count == 8) + #expect(values.count == 8) // Verify the correct values are returned by `entries` let booleanEntry = entries.first { $0.key == "boolean" } // RTLM5d2b let bytesEntry = entries.first { $0.key == "bytes" } // RTLM5d2c let numberEntry = entries.first { $0.key == "number" } // RTLM5d2d let stringEntry = entries.first { $0.key == "string" } // RTLM5d2e + let jsonArrayEntry = entries.first { $0.key == "jsonArray" } // RTLM5d2e + let jsonObjectEntry = entries.first { $0.key == "jsonObject" } // RTLM5d2e let mapRefEntry = entries.first { $0.key == "mapRef" } // RTLM5d2f2 let counterRefEntry = entries.first { $0.key == "counterRef" } // RTLM5d2f2 @@ -420,6 +446,8 @@ struct InternalDefaultLiveMapTests { #expect(bytesEntry?.value.dataValue == Data([0x01, 0x02, 0x03])) // RTLM5d2c #expect(numberEntry?.value.numberValue == 42) // RTLM5d2d #expect(stringEntry?.value.stringValue == "hello") // RTLM5d2e + #expect(jsonArrayEntry?.value.jsonArrayValue == ["foo"]) // RTLM5d2e + #expect(jsonObjectEntry?.value.jsonObjectValue == ["foo": "bar"]) // RTLM5d2e #expect(mapRefEntry?.value.liveMapValue as AnyObject === referencedMap as AnyObject) // RTLM5d2f2 #expect(counterRefEntry?.value.liveCounterValue as AnyObject === referencedCounter as AnyObject) // RTLM5d2f2 } From 31894f88c4b690e83bc9649e3ffea6ed7e6534fd Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 30 Jul 2025 11:34:26 +0100 Subject: [PATCH 2/4] Remove incorrect comment --- Tests/AblyLiveObjectsTests/ObjectMessageTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift index 12182b65..ccf83095 100644 --- a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift @@ -328,7 +328,7 @@ struct ObjectMessageTests { } struct JSONTests { - // @specOneOf(1/3) OD5b1 - This spec point is a bit weirdly worded, but here we're testing the case where `encoding` is not set and hence OD5a2 does not apply to the `string` property + // @specOneOf(1/3) OD5b1 @Test func boolean() throws { let wireData = WireObjectData(boolean: true) From 562f7b6918f90314c7555af023bb300c6b9858f4 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 30 Jul 2025 11:35:08 +0100 Subject: [PATCH 3/4] Fix typo in comment --- Tests/AblyLiveObjectsTests/ObjectMessageTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift index ccf83095..ff357d6d 100644 --- a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift @@ -355,7 +355,7 @@ struct ObjectMessageTests { #expect(objectData.string == nil) } - // @specOneOf(3/3) OD5b1 - This spec point is a bit weirdly worded, but here we're testing the case where `encoding` is not set and hence OD5a3 does not apply to the `string` property + // @specOneOf(3/3) OD5b1 - This spec point is a bit weirdly worded, but here we're testing the case where `encoding` is not set and hence OD5b3 does not apply to the `string` property @Test func string() throws { let testString = "hello world" From 40343829db4b8c7c731e6ebac7b415c84499f586 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 30 Jul 2025 10:30:40 +0100 Subject: [PATCH 4/4] Update the way that JSON map values are encoded on wire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements the change described in [1]. It has not yet been specified — have created #46 for adding specification point references later. (We need to implement it now because Realtime has removed support for the previous wire representation.) Resolves #44. [1] https://ably.atlassian.net/wiki/spaces/LOB/pages/4203380843/LODR-040+ObjectData+JSON+encoding+of+object+and+array+values --- .../Internal/InternalDefaultLiveMap.swift | 20 +-- .../Protocol/ObjectMessage.swift | 47 ++---- .../Protocol/WireObjectMessage.swift | 12 +- .../Helpers/TestFactories.swift | 10 +- .../InternalDefaultLiveMapTests.swift | 50 +++--- .../ObjectMessageTests.swift | 150 +++++++----------- .../ObjectsPoolTests.swift | 4 +- .../WireObjectMessageTests.swift | 6 - 8 files changed, 122 insertions(+), 177 deletions(-) diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift index 9676fe0d..78033b51 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift @@ -692,16 +692,16 @@ internal final class InternalDefaultLiveMap: Sendable { // RTLM5d2e: If ObjectsMapEntry.data.string exists, return it if let string = entry.data.string { - switch string { - case let .string(string): - return .primitive(.string(string)) - case let .json(objectOrArray): - switch objectOrArray { - case let .array(array): - return .primitive(.jsonArray(array)) - case let .object(object): - return .primitive(.jsonObject(object)) - } + return .primitive(.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)) + case let .object(object): + return .primitive(.jsonObject(object)) } } diff --git a/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift b/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift index caf21cd7..a9adea8e 100644 --- a/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift +++ b/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift @@ -56,18 +56,12 @@ internal struct ObjectOperation { } internal struct ObjectData { - /// The values that the `string` property might hold, before being encoded per OD4 or after being decoded per OD5. - internal enum StringPropertyContent { - case string(String) - case json(JSONObjectOrArray) - } - internal var objectId: String? // OD2a - internal var encoding: String? // OD2b internal var boolean: Bool? // OD2c internal var bytes: Data? // OD2d internal var number: NSNumber? // OD2e - internal var string: StringPropertyContent? // OD2f + internal var string: String? // OD2f + internal var json: JSONObjectOrArray? // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) } internal struct ObjectsMapOp { @@ -264,9 +258,9 @@ internal extension ObjectData { format: AblyPlugin.EncodingFormat ) throws(InternalError) { objectId = wireObjectData.objectId - encoding = wireObjectData.encoding boolean = wireObjectData.boolean number = wireObjectData.number + string = wireObjectData.string // OD5: Decode data based on format switch format { @@ -300,16 +294,12 @@ internal extension ObjectData { } } - if let wireString = wireObjectData.string { - // OD5a2, OD5b3: If ObjectData.encoding is set to "json", the ObjectData.string content is decoded by parsing the string as JSON - if wireObjectData.encoding == "json" { - let jsonValue = try JSONObjectOrArray(jsonString: wireString) - string = .json(jsonValue) - } else { - string = .string(wireString) - } + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + if let wireJson = wireObjectData.json { + let jsonValue = try JSONObjectOrArray(jsonString: wireJson) + json = jsonValue } else { - string = nil + json = nil } } @@ -334,20 +324,6 @@ internal extension ObjectData { nil } - // OD4c4: A string payload is encoded as a MessagePack string type, and the result is set on the ObjectData.string attribute - // OD4d4: A string payload is represented as a JSON string and set on the ObjectData.string attribute - let (wireString, wireEncoding): (String?, String?) = if let stringContent = string { - switch stringContent { - case let .string(str): - (str, nil) - case let .json(jsonValue): - // OD4c5, OD4d5: A payload consisting of a JSON-encodable object or array is stringified as a JSON object or array, represented as a JSON string and the result is set on the ObjectData.string attribute. The ObjectData.encoding attribute is then set to "json" - (jsonValue.toJSONString, "json") - } - } else { - (nil, nil) - } - let wireNumber: NSNumber? = if let number { switch format { case .json: @@ -363,11 +339,14 @@ internal extension ObjectData { return .init( objectId: objectId, - encoding: wireEncoding, boolean: boolean, bytes: wireBytes, number: wireNumber, - string: wireString, + // OD4c4: A string payload is encoded as a MessagePack string type, and the result is set on the ObjectData.string attribute + // OD4d4: A string payload is represented as a JSON string and set on the ObjectData.string attribute + string: string, + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + json: json?.toJSONString, ) } } diff --git a/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift b/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift index 163cde4d..b0d7514c 100644 --- a/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift +++ b/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift @@ -475,30 +475,30 @@ extension WireObjectsMapEntry: WireObjectCodable { internal struct WireObjectData { internal var objectId: String? // OD2a - internal var encoding: String? // OD2b internal var boolean: Bool? // OD2c internal var bytes: StringOrData? // OD2d internal var number: NSNumber? // OD2e internal var string: String? // OD2f + internal var json: String? // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) } extension WireObjectData: WireObjectCodable { internal enum WireKey: String { case objectId - case encoding case boolean case bytes case number case string + case json } internal init(wireObject: [String: WireValue]) throws(InternalError) { objectId = try wireObject.optionalStringValueForKey(WireKey.objectId.rawValue) - encoding = try wireObject.optionalStringValueForKey(WireKey.encoding.rawValue) boolean = try wireObject.optionalBoolValueForKey(WireKey.boolean.rawValue) bytes = try wireObject.optionalDecodableValueForKey(WireKey.bytes.rawValue) number = try wireObject.optionalNumberValueForKey(WireKey.number.rawValue) string = try wireObject.optionalStringValueForKey(WireKey.string.rawValue) + json = try wireObject.optionalStringValueForKey(WireKey.json.rawValue) } internal var toWireObject: [String: WireValue] { @@ -507,9 +507,6 @@ extension WireObjectData: WireObjectCodable { if let objectId { result[WireKey.objectId.rawValue] = .string(objectId) } - if let encoding { - result[WireKey.encoding.rawValue] = .string(encoding) - } if let boolean { result[WireKey.boolean.rawValue] = .bool(boolean) } @@ -522,6 +519,9 @@ extension WireObjectData: WireObjectCodable { if let string { result[WireKey.string.rawValue] = .string(string) } + if let json { + result[WireKey.json.rawValue] = .string(json) + } return result } diff --git a/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift b/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift index 362e24ed..dd7cc38d 100644 --- a/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift +++ b/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift @@ -429,7 +429,7 @@ struct TestFactories { entry: mapEntry( tombstone: tombstone, timeserial: timeserial, - data: ObjectData(string: .string(value)), + data: ObjectData(string: value), ), ) } @@ -448,7 +448,7 @@ struct TestFactories { entry: internalMapEntry( tombstone: tombstone, timeserial: timeserial, - data: ObjectData(string: .string(value)), + data: ObjectData(string: value), ), ) } @@ -539,7 +539,7 @@ struct TestFactories { entries: [String: String] = ["key1": "value1", "key2": "value2"], ) -> ObjectsMap { let mapEntries = entries.mapValues { value in - mapEntry(data: ObjectData(string: .string(value))) + mapEntry(data: ObjectData(string: value)) } return objectsMap(entries: mapEntries) } @@ -567,7 +567,7 @@ struct TestFactories { objectId: objectId, mapOp: ObjectsMapOp( key: key, - data: ObjectData(string: .string(value)), + data: ObjectData(string: value), ), ), serial: serial, @@ -676,7 +676,7 @@ struct TestFactories { entries: [String: String] = ["key1": "value1", "key2": "value2"], ) -> InboundObjectMessage { let mapEntries = entries.mapValues { value in - mapEntry(data: ObjectData(string: .string(value))) + mapEntry(data: ObjectData(string: value)) } return rootObjectMessage(entries: mapEntries) } diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift index 528fea67..80e22a98 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift @@ -81,33 +81,35 @@ struct InternalDefaultLiveMapTests { #expect(result?.numberValue == 123.456) } - // @specOneOf(1/3) RTLM5d2e - When `string` is a string + // @spec RTLM5d2e @Test func returnsStringValue() throws { let logger = TestLogger() - let entry = TestFactories.internalMapEntry(data: ObjectData(string: .string("test"))) + let entry = TestFactories.internalMapEntry(data: ObjectData(string: "test")) let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap(testsOnly_data: ["key": entry], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let result = try map.get(key: "key", coreSDK: coreSDK, delegate: MockLiveMapObjectPoolDelegate()) #expect(result?.stringValue == "test") } - // @specOneOf(2/3) RTLM5d2e - When `string` is a JSON array + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // Tests when `json` is a JSON array @Test func returnsJSONArrayValue() throws { let logger = TestLogger() - let entry = TestFactories.internalMapEntry(data: ObjectData(string: .json(.array(["foo"])))) + let entry = TestFactories.internalMapEntry(data: ObjectData(json: .array(["foo"]))) let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap(testsOnly_data: ["key": entry], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let result = try map.get(key: "key", coreSDK: coreSDK, delegate: MockLiveMapObjectPoolDelegate()) #expect(result?.jsonArrayValue == ["foo"]) } - // @specOneOf(3/3) RTLM5d2e - When `string` is a JSON object + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // Tests when `json` is a JSON object @Test func returnsJSONObjectValue() throws { let logger = TestLogger() - let entry = TestFactories.internalMapEntry(data: ObjectData(string: .json(.object(["foo": "bar"])))) + let entry = TestFactories.internalMapEntry(data: ObjectData(json: .object(["foo": "bar"]))) let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap(testsOnly_data: ["key": entry], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let result = try map.get(key: "key", coreSDK: coreSDK, delegate: MockLiveMapObjectPoolDelegate()) @@ -316,11 +318,11 @@ struct InternalDefaultLiveMapTests { let map = InternalDefaultLiveMap( testsOnly_data: [ // tombstone is nil, so not considered tombstoned - "active1": TestFactories.internalMapEntry(data: ObjectData(string: .string("value1"))), + "active1": TestFactories.internalMapEntry(data: ObjectData(string: "value1")), // tombstone is false, so not considered tombstoned[ - "active2": TestFactories.internalMapEntry(tombstone: false, data: ObjectData(string: .string("value2"))), - "tombstoned": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: .string("tombstoned"))), - "tombstoned2": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: .string("tombstoned2"))), + "active2": TestFactories.internalMapEntry(tombstone: false, data: ObjectData(string: "value2")), + "tombstoned": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: "tombstoned")), + "tombstoned2": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: "tombstoned2")), ], objectID: "arbitrary", logger: logger, @@ -362,9 +364,9 @@ struct InternalDefaultLiveMapTests { let delegate = MockLiveMapObjectPoolDelegate() let map = InternalDefaultLiveMap( testsOnly_data: [ - "key1": TestFactories.internalMapEntry(data: ObjectData(string: .string("value1"))), - "key2": TestFactories.internalMapEntry(data: ObjectData(string: .string("value2"))), - "key3": TestFactories.internalMapEntry(data: ObjectData(string: .string("value3"))), + "key1": TestFactories.internalMapEntry(data: ObjectData(string: "value1")), + "key2": TestFactories.internalMapEntry(data: ObjectData(string: "value2")), + "key3": TestFactories.internalMapEntry(data: ObjectData(string: "value3")), ], objectID: "arbitrary", logger: logger, @@ -410,9 +412,9 @@ struct InternalDefaultLiveMapTests { "boolean": TestFactories.internalMapEntry(data: ObjectData(boolean: true)), // RTLM5d2b "bytes": TestFactories.internalMapEntry(data: ObjectData(bytes: Data([0x01, 0x02, 0x03]))), // RTLM5d2c "number": TestFactories.internalMapEntry(data: ObjectData(number: NSNumber(value: 42))), // RTLM5d2d - "string": TestFactories.internalMapEntry(data: ObjectData(string: .string("hello"))), // RTLM5d2e - "jsonArray": TestFactories.internalMapEntry(data: ObjectData(string: .json(.array(["foo"])))), // RTLM5d2e - "jsonObject": TestFactories.internalMapEntry(data: ObjectData(string: .json(.object(["foo": "bar"])))), // RTLM5d2e + "string": TestFactories.internalMapEntry(data: ObjectData(string: "hello")), // RTLM5d2e + "jsonArray": TestFactories.internalMapEntry(data: ObjectData(json: .array(["foo"]))), // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + "jsonObject": TestFactories.internalMapEntry(data: ObjectData(json: .object(["foo": "bar"]))), // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) "mapRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "map:ref@123")), // RTLM5d2f2 "counterRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "counter:ref@456")), // RTLM5d2f2 ], @@ -465,7 +467,7 @@ struct InternalDefaultLiveMapTests { let delegate = MockLiveMapObjectPoolDelegate() let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap( - testsOnly_data: ["key1": TestFactories.internalMapEntry(timeserial: "ts2", data: ObjectData(string: .string("existing")))], + testsOnly_data: ["key1": TestFactories.internalMapEntry(timeserial: "ts2", data: ObjectData(string: "existing"))], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, @@ -505,7 +507,7 @@ struct InternalDefaultLiveMapTests { let delegate = MockLiveMapObjectPoolDelegate() let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap( - testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: true, timeserial: "ts1", data: ObjectData(string: .string("existing")))], + testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: true, timeserial: "ts1", data: ObjectData(string: "existing"))], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, @@ -677,7 +679,7 @@ struct InternalDefaultLiveMapTests { let delegate = MockLiveMapObjectPoolDelegate() let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap( - testsOnly_data: ["key1": TestFactories.internalMapEntry(timeserial: "ts2", data: ObjectData(string: .string("existing")))], + testsOnly_data: ["key1": TestFactories.internalMapEntry(timeserial: "ts2", data: ObjectData(string: "existing"))], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, @@ -703,7 +705,7 @@ struct InternalDefaultLiveMapTests { let delegate = MockLiveMapObjectPoolDelegate() let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap( - testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: false, timeserial: "ts1", data: ObjectData(string: .string("existing")))], + testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: false, timeserial: "ts1", data: ObjectData(string: "existing"))], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, @@ -828,7 +830,7 @@ struct InternalDefaultLiveMapTests { let delegate = MockLiveMapObjectPoolDelegate() let coreSDK = MockCoreSDK(channelState: .attaching) let map = InternalDefaultLiveMap( - testsOnly_data: ["key1": TestFactories.internalMapEntry(timeserial: entrySerial, data: ObjectData(string: .string("existing")))], + testsOnly_data: ["key1": TestFactories.internalMapEntry(timeserial: entrySerial, data: ObjectData(string: "existing"))], objectID: "arbitrary", logger: logger, userCallbackQueue: .main, @@ -839,7 +841,7 @@ struct InternalDefaultLiveMapTests { _ = map.testsOnly_applyMapSetOperation( key: "key1", operationTimeserial: operationSerial, - operationData: ObjectData(string: .string("new")), + operationData: ObjectData(string: "new"), objectsPool: &pool, ) @@ -1049,7 +1051,7 @@ struct InternalDefaultLiveMapTests { let operation = TestFactories.objectOperation( action: .known(.mapSet), - mapOp: ObjectsMapOp(key: "key1", data: ObjectData(string: .string("new"))), + mapOp: ObjectsMapOp(key: "key1", data: ObjectData(string: "new")), ) // Apply operation with serial "ts1" which is lexicographically less than existing "ts2" and thus will be applied per RTLO4a (this is a non-pathological case of RTOL4a, that spec point being fully tested elsewhere) @@ -1136,7 +1138,7 @@ struct InternalDefaultLiveMapTests { let operation = TestFactories.objectOperation( action: .known(.mapSet), - mapOp: ObjectsMapOp(key: "key1", data: ObjectData(string: .string("new"))), + mapOp: ObjectsMapOp(key: "key1", data: ObjectData(string: "new")), ) // Apply MAP_SET operation diff --git a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift index ff357d6d..a973dea7 100644 --- a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift @@ -10,7 +10,6 @@ struct ObjectMessageTests { struct EncodingTests { struct MessagePackTests { // @spec OD4c1 - // @specOneOf(1/8) OD4b @Test func boolean() { let objectData = ObjectData(boolean: true) @@ -21,12 +20,10 @@ struct ObjectMessageTests { #expect(wireData.bytes == nil) #expect(wireData.number == nil) #expect(wireData.string == nil) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } // @spec OD4c2 - // @specOneOf(2/8) OD4b @Test func binary() { let testData = Data([1, 2, 3, 4]) @@ -43,12 +40,10 @@ struct ObjectMessageTests { } #expect(wireData.number == nil) #expect(wireData.string == nil) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } // @spec OD4c3 - // @specOneOf(3/8) OD4b @Test(arguments: [15, 42.0]) func number(testNumber: NSNumber) throws { let objectData = ObjectData(number: testNumber) @@ -63,16 +58,14 @@ struct ObjectMessageTests { #expect(number == testNumber) #expect(wireData.number == testNumber) #expect(wireData.string == nil) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } // @spec OD4c4 - // @specOneOf(4/8) OD4b @Test func string() { let testString = "hello world" - let objectData = ObjectData(string: .string(testString)) + let objectData = ObjectData(string: testString) let wireData = objectData.toWire(format: .messagePack) // OD4c4: A string payload is encoded as a MessagePack string type, and the result is set on the ObjectData.string attribute @@ -80,32 +73,29 @@ struct ObjectMessageTests { #expect(wireData.bytes == nil) #expect(wireData.number == nil) #expect(wireData.string == testString) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } - // @spec OD4c5 + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) @Test(arguments: [ // We intentionally use a single-element object so that we get a stable encoding to JSON (jsonObjectOrArray: ["key": "value"] as JSONObjectOrArray, expectedJSONString: #"{"key":"value"}"#), (jsonObjectOrArray: [123, "hello world"] as JSONObjectOrArray, expectedJSONString: #"[123,"hello world"]"#), ]) func json(jsonObjectOrArray: JSONObjectOrArray, expectedJSONString: String) { - let objectData = ObjectData(string: .json(jsonObjectOrArray)) + let objectData = ObjectData(json: jsonObjectOrArray) let wireData = objectData.toWire(format: .messagePack) - // OD4c5: A payload consisting of a JSON-encodable object or array is stringified as a JSON object or array, represented as a JSON string and the result is set on the ObjectData.string attribute. The ObjectData.encoding attribute is then set to "json" #expect(wireData.boolean == nil) #expect(wireData.bytes == nil) #expect(wireData.number == nil) - #expect(wireData.string == expectedJSONString) - #expect(wireData.encoding == "json") + #expect(wireData.string == nil) + #expect(wireData.json == expectedJSONString) } } struct JSONTests { // @spec OD4d1 - // @specOneOf(5/8) OD4b @Test func boolean() { let objectData = ObjectData(boolean: true) @@ -116,12 +106,10 @@ struct ObjectMessageTests { #expect(wireData.bytes == nil) #expect(wireData.number == nil) #expect(wireData.string == nil) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } // @spec OD4d2 - // @specOneOf(6/8) OD4b @Test func binary() { let testData = Data([1, 2, 3, 4]) @@ -138,12 +126,10 @@ struct ObjectMessageTests { } #expect(wireData.number == nil) #expect(wireData.string == nil) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } // @spec OD4d3 - // @specOneOf(7/8) OD4b @Test func number() { let testNumber = NSNumber(value: 42) @@ -155,16 +141,14 @@ struct ObjectMessageTests { #expect(wireData.bytes == nil) #expect(wireData.number == testNumber) #expect(wireData.string == nil) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } // @spec OD4d4 - // @specOneOf(8/8) OD4b @Test func string() { let testString = "hello world" - let objectData = ObjectData(string: .string(testString)) + let objectData = ObjectData(string: testString) let wireData = objectData.toWire(format: .json) // OD4d4: A string payload is represented as a JSON string and set on the ObjectData.string attribute @@ -172,26 +156,24 @@ struct ObjectMessageTests { #expect(wireData.bytes == nil) #expect(wireData.number == nil) #expect(wireData.string == testString) - // OD4b: ObjectData.encoding must be left unset unless specified otherwise by the payload encoding procedure in OD4c and OD4d - #expect(wireData.encoding == nil) + #expect(wireData.json == nil) } - // @spec OD4d5 + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) @Test(arguments: [ // We intentionally use a single-element object so that we get a stable encoding to JSON (jsonObjectOrArray: ["key": "value"] as JSONObjectOrArray, expectedJSONString: #"{"key":"value"}"#), (jsonObjectOrArray: [123, "hello world"] as JSONObjectOrArray, expectedJSONString: #"[123,"hello world"]"#), ]) func json(jsonObjectOrArray: JSONObjectOrArray, expectedJSONString: String) { - let objectData = ObjectData(string: .json(jsonObjectOrArray)) + let objectData = ObjectData(json: jsonObjectOrArray) let wireData = objectData.toWire(format: .json) - // OD4d5: A payload consisting of a JSON-encodable object or array is stringified as a JSON object or array, represented as a JSON string and the result is set on the ObjectData.string attribute. The ObjectData.encoding attribute is then set to "json" #expect(wireData.boolean == nil) #expect(wireData.bytes == nil) #expect(wireData.number == nil) - #expect(wireData.string == expectedJSONString) - #expect(wireData.encoding == "json") + #expect(wireData.string == nil) + #expect(wireData.json == expectedJSONString) } } } @@ -209,6 +191,7 @@ struct ObjectMessageTests { #expect(objectData.bytes == nil) #expect(objectData.number == nil) #expect(objectData.string == nil) + #expect(objectData.json == nil) } // @specOneOf(2/5) OD5a1 @@ -223,6 +206,7 @@ struct ObjectMessageTests { #expect(objectData.bytes == testData) #expect(objectData.number == nil) #expect(objectData.string == nil) + #expect(objectData.json == nil) } // @specOneOf(3/5) OD5a1 - The spec isn't clear about what's meant to happen if you get string data in the `bytes` field; I'm choosing to ignore it but I think it's a bit moot - shouldn't happen. The only reason I'm considering it here is because of our slightly weird WireObjectData.bytes type which is typed as a string or data; might be good to at some point figure out how to rule out the string case earlier when using MessagePack, but it's not a big issue @@ -238,6 +222,7 @@ struct ObjectMessageTests { #expect(objectData.bytes == nil) #expect(objectData.number == nil) #expect(objectData.string == nil) + #expect(objectData.json == nil) } // @specOneOf(4/5) OD5a1 @@ -252,9 +237,10 @@ struct ObjectMessageTests { #expect(objectData.bytes == nil) #expect(objectData.number == testNumber) #expect(objectData.string == nil) + #expect(objectData.json == nil) } - // @specOneOf(5/5) OD5a1 - This spec point is a bit weirdly worded, but here we're testing the case where `encoding` is not set and hence OD5a2 does not apply to the `string` property + // @specOneOf(5/5) OD5a1 @Test func string() throws { let testString = "hello world" @@ -265,38 +251,31 @@ struct ObjectMessageTests { #expect(objectData.boolean == nil) #expect(objectData.bytes == nil) #expect(objectData.number == nil) - switch objectData.string { - case let .string(str): - #expect(str == testString) - default: - Issue.record("Expected .string case") - } + #expect(objectData.string == testString) + #expect(objectData.json == nil) } - // @specOneOf(1/3) OD5a2 + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) @Test func json() throws { let jsonString = "{\"key\":\"value\",\"number\":123}" - let wireData = WireObjectData(encoding: "json", string: jsonString) + let wireData = WireObjectData(json: jsonString) let objectData = try ObjectData(wireObjectData: wireData, format: .messagePack) - // OD5a2: If ObjectData.encoding is set to "json", the ObjectData.string content is decoded by parsing the string as JSON + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) #expect(objectData.boolean == nil) #expect(objectData.bytes == nil) #expect(objectData.number == nil) - switch objectData.string { - case let .json(jsonValue): - #expect(jsonValue == ["key": "value", "number": 123]) - default: - Issue.record("Expected .json case") - } + #expect(objectData.string == nil) + #expect(objectData.json == ["key": "value", "number": 123]) } - // @specOneOf(2/3) OD5a2 - The spec doesn't say what to do if JSON parsing fails; I'm choosing to treat it as an error + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // The spec doesn't say what to do if JSON parsing fails; I'm choosing to treat it as an error @Test func json_invalidJson() { let invalidJsonString = "invalid json" - let wireData = WireObjectData(encoding: "json", string: invalidJsonString) + let wireData = WireObjectData(json: invalidJsonString) // Should throw when JSON parsing fails, even in MessagePack format #expect(throws: InternalError.self) { @@ -304,7 +283,8 @@ struct ObjectMessageTests { } } - // @specOneOf(3/3) OD5a2 - The spec doesn't say what to do if given serialized JSON that contains a non-object-or-array value; I'm choosing to treat it as an error + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // The spec doesn't say what to do if given serialized JSON that contains a non-object-or-array value; I'm choosing to treat it as an error @Test(arguments: [ // string "\"hello world\"", @@ -318,7 +298,7 @@ struct ObjectMessageTests { "null", ]) func json_validJsonButNotObjectOrArray(jsonString: String) { - let wireData = WireObjectData(encoding: "json", string: jsonString) + let wireData = WireObjectData(json: jsonString) // Should throw when JSON is valid but not an object or array #expect(throws: InternalError.self) { @@ -339,6 +319,7 @@ struct ObjectMessageTests { #expect(objectData.bytes == nil) #expect(objectData.number == nil) #expect(objectData.string == nil) + #expect(objectData.json == nil) } // @specOneOf(2/3) OD5b1 @@ -353,9 +334,10 @@ struct ObjectMessageTests { #expect(objectData.bytes == nil) #expect(objectData.number == testNumber) #expect(objectData.string == nil) + #expect(objectData.json == nil) } - // @specOneOf(3/3) OD5b1 - This spec point is a bit weirdly worded, but here we're testing the case where `encoding` is not set and hence OD5b3 does not apply to the `string` property + // @specOneOf(3/3) OD5b1 @Test func string() throws { let testString = "hello world" @@ -366,12 +348,8 @@ struct ObjectMessageTests { #expect(objectData.boolean == nil) #expect(objectData.bytes == nil) #expect(objectData.number == nil) - switch objectData.string { - case let .string(str): - #expect(str == testString) - default: - Issue.record("Expected .string case") - } + #expect(objectData.string == testString) + #expect(objectData.json == nil) } // @specOneOf(1/2) OB5b2 @@ -387,6 +365,7 @@ struct ObjectMessageTests { #expect(objectData.bytes == testData) #expect(objectData.number == nil) #expect(objectData.string == nil) + #expect(objectData.json == nil) } // @specOneOf(2/2) OB5b2 - The spec doesn't say what to do if Base64 decoding fails; we're choosing to treat it as an error @@ -401,30 +380,26 @@ struct ObjectMessageTests { } } - // @specOneOf(1/3) OD5b3 + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) @Test func json() throws { let jsonString = "{\"key\":\"value\",\"number\":123}" - let wireData = WireObjectData(encoding: "json", string: jsonString) + let wireData = WireObjectData(json: jsonString) let objectData = try ObjectData(wireObjectData: wireData, format: .json) - // OD5b3: If ObjectData.encoding is set to "json", the ObjectData.string content is decoded by parsing the string as JSON #expect(objectData.boolean == nil) #expect(objectData.bytes == nil) #expect(objectData.number == nil) - switch objectData.string { - case let .json(jsonValue): - #expect(jsonValue == ["key": "value", "number": 123]) - default: - Issue.record("Expected .json case") - } + #expect(objectData.string == nil) + #expect(objectData.json == ["key": "value", "number": 123]) } - // @specOneOf(2/3) OD5b3 - The spec doesn't say what to do if JSON parsing fails; I'm choosing to treat it as an error + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // The spec doesn't say what to do if JSON parsing fails; I'm choosing to treat it as an error @Test func json_invalidJson() { let invalidJsonString = "invalid json" - let wireData = WireObjectData(encoding: "json", string: invalidJsonString) + let wireData = WireObjectData(json: invalidJsonString) // Should throw when JSON parsing fails #expect(throws: InternalError.self) { @@ -432,7 +407,8 @@ struct ObjectMessageTests { } } - // @specOneOf(3/3) OD5b3 - The spec doesn't say what to do if given serialized JSON that contains a non-object-or-array value; I'm choosing to treat it as an error + // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // The spec doesn't say what to do if given serialized JSON that contains a non-object-or-array value; I'm choosing to treat it as an error @Test(arguments: [ // string "\"hello world\"", @@ -446,7 +422,7 @@ struct ObjectMessageTests { "null", ]) func json_validJsonButNotObjectOrArray(jsonString: String) { - let wireData = WireObjectData(encoding: "json", string: jsonString) + let wireData = WireObjectData(json: jsonString) // Should throw when JSON is valid but not an object or array #expect(throws: InternalError.self) { @@ -467,9 +443,9 @@ struct ObjectMessageTests { ObjectData(boolean: true), ObjectData(bytes: Data([1, 2, 3, 4])), ObjectData(number: NSNumber(value: 42)), - ObjectData(string: .string("hello world")), - ObjectData(string: .json(["key": "value", "number": 123])), - ObjectData(string: .json([123, "hello world"])), + ObjectData(string: "hello world"), + ObjectData(json: .object(["key": "value", "number": 123])), + ObjectData(json: .array([123, "hello world"])), ]) func roundTrip(formatRawValue: EncodingFormat.RawValue, originalData: ObjectData) throws { let format = try #require(EncodingFormat(rawValue: formatRawValue)) @@ -485,17 +461,11 @@ struct ObjectMessageTests { // Compare number values #expect(decodedData.number == originalData.number) - // Compare string values, handling both .string and .json cases - switch (decodedData.string, originalData.string) { - case (.none, .none): - break - case let (.string(decoded), .string(original)): - #expect(decoded == original) - case let (.json(decoded), .json(original)): - #expect(decoded == original) - default: - Issue.record("String cases did not match") - } + // Compare string values + #expect(decodedData.string == originalData.string) + + // Compare JSON values + #expect(decodedData.json == originalData.json) } } } diff --git a/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift b/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift index 2609d9f4..89514d5f 100644 --- a/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift @@ -300,7 +300,7 @@ struct ObjectsPoolTests { createOp: TestFactories.mapCreateOperation(objectId: "map:existing@1", entries: [ "createOpKey": TestFactories.stringMapEntry(value: "bar").entry, ]), - entries: ["updated": TestFactories.mapEntry(data: ObjectData(string: .string("updated")))], + entries: ["updated": TestFactories.mapEntry(data: ObjectData(string: "updated"))], ), // Update existing counter TestFactories.counterObjectState( @@ -313,7 +313,7 @@ struct ObjectsPoolTests { TestFactories.mapObjectState( objectId: "map:new@1", siteTimeserials: ["site3": "ts3"], - entries: ["new": TestFactories.mapEntry(data: ObjectData(string: .string("new")))], + entries: ["new": TestFactories.mapEntry(data: ObjectData(string: "new"))], ), // Create new counter TestFactories.counterObjectState( diff --git a/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift b/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift index c5503f29..7b63b9ba 100644 --- a/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift +++ b/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift @@ -360,14 +360,12 @@ enum WireObjectMessageTests { func decodesAllFields() throws { let json: [String: WireValue] = [ "objectId": "obj1", - "encoding": "utf8", "boolean": true, "number": 42, "string": "value1", ] let data = try WireObjectData(wireObject: json) #expect(data.objectId == "obj1") - #expect(data.encoding == "utf8") #expect(data.boolean == true) #expect(data.number == 42) #expect(data.string == "value1") @@ -378,7 +376,6 @@ enum WireObjectMessageTests { let json: [String: WireValue] = [:] let data = try WireObjectData(wireObject: json) #expect(data.objectId == nil) - #expect(data.encoding == nil) #expect(data.boolean == nil) #expect(data.bytes == nil) #expect(data.number == nil) @@ -389,7 +386,6 @@ enum WireObjectMessageTests { func encodesAllFields() { let data = WireObjectData( objectId: "obj1", - encoding: "utf8", boolean: true, bytes: nil, number: 42, @@ -398,7 +394,6 @@ enum WireObjectMessageTests { let wire = data.toWireObject #expect(wire == [ "objectId": "obj1", - "encoding": "utf8", "boolean": true, "number": 42, "string": "value1", @@ -409,7 +404,6 @@ enum WireObjectMessageTests { func encodesWithOptionalFieldsNil() { let data = WireObjectData( objectId: nil, - encoding: nil, boolean: nil, bytes: nil, number: nil,