Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -692,12 +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 .json:
// TODO: Understand how to handle JSON values (https://github.com/ably/specification/pull/333/files#r2164561055)
notYetImplemented()
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))
}
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 13 additions & 34 deletions Sources/AblyLiveObjects/Protocol/ObjectMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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:
Expand All @@ -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,
)
}
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand All @@ -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)
}
Expand All @@ -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
}
Expand Down
24 changes: 21 additions & 3 deletions Sources/AblyLiveObjects/Public/PublicTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
26 changes: 13 additions & 13 deletions Sources/AblyLiveObjects/Utility/JSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
}
Expand Down
10 changes: 5 additions & 5 deletions Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ struct TestFactories {
entry: mapEntry(
tombstone: tombstone,
timeserial: timeserial,
data: ObjectData(string: .string(value)),
data: ObjectData(string: value),
),
)
}
Expand All @@ -448,7 +448,7 @@ struct TestFactories {
entry: internalMapEntry(
tombstone: tombstone,
timeserial: timeserial,
data: ObjectData(string: .string(value)),
data: ObjectData(string: value),
),
)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -567,7 +567,7 @@ struct TestFactories {
objectId: objectId,
mapOp: ObjectsMapOp(
key: key,
data: ObjectData(string: .string(value)),
data: ObjectData(string: value),
),
),
serial: serial,
Expand Down Expand Up @@ -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)
}
Expand Down
Loading