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
41 changes: 17 additions & 24 deletions Sources/AblyLiveObjects/Utility/ExtendedJSONValue.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Foundation

/// Like ``JSONValue``, but provides an additional case named `extra`, which allows you to support additional types of data. It's used as a common base for the implementations of ``JSONValue`` and ``WireValue``, and for converting between them.
internal indirect enum ExtendedJSONValue<Extra> {
/// Like ``JSONValue``, but provides a flexible `number` case and an additional case named `extra`, which allows you to support additional types of data. It's used as a common base for the implementations of ``JSONValue`` and ``WireValue``, and for converting between them.
internal indirect enum ExtendedJSONValue<Number, Extra> {
case object([String: Self])
case array([Self])
case string(String)
case number(NSNumber)
case number(Number)
case bool(Bool)
case null
case extra(Extra)
Expand All @@ -17,12 +17,12 @@ internal extension ExtendedJSONValue {
/// Creates an `ExtendedJSONValue` from an object.
///
/// The rules for what `deserialized` will accept are the same as those of `JSONValue.init(jsonSerializationOutput)`, with one addition: any nonsupported values are passed to the `createExtraValue` function, and the result of this function will be used to create an `ExtendedJSONValue` of case `.extra`.
init(deserialized: Any, createExtraValue: (Any) -> Extra) {
init(deserialized: Any, createNumberValue: (NSNumber) -> Number, createExtraValue: (Any) -> Extra) {
switch deserialized {
case let dictionary as [String: Any]:
self = .object(dictionary.mapValues { .init(deserialized: $0, createExtraValue: createExtraValue) })
self = .object(dictionary.mapValues { .init(deserialized: $0, createNumberValue: createNumberValue, createExtraValue: createExtraValue) })
case let array as [Any]:
self = .array(array.map { .init(deserialized: $0, createExtraValue: createExtraValue) })
self = .array(array.map { .init(deserialized: $0, createNumberValue: createNumberValue, createExtraValue: createExtraValue) })
case let string as String:
self = .string(string)
case let number as NSNumber:
Expand All @@ -32,7 +32,7 @@ internal extension ExtendedJSONValue {
} else if number === kCFBooleanFalse {
self = .bool(false)
} else {
self = .number(number)
self = .number(createNumberValue(number))
}
case is NSNull:
self = .null
Expand All @@ -44,16 +44,16 @@ internal extension ExtendedJSONValue {
/// Converts an `ExtendedJSONValue` to an object.
///
/// The contract for what this will return are the same as those of `JSONValue.toJSONSerializationInputElement`, with one addition: any values in the input of case `.extra` will be passed to the `serializeExtraValue` function, and the result of this function call will be inserted into the output object.
func serialized(serializeExtraValue: (Extra) -> Any) -> Any {
func serialized(serializeNumberValue: (Number) -> Any, serializeExtraValue: (Extra) -> Any) -> Any {
switch self {
case let .object(underlying):
underlying.mapValues { $0.serialized(serializeExtraValue: serializeExtraValue) }
underlying.mapValues { $0.serialized(serializeNumberValue: serializeNumberValue, serializeExtraValue: serializeExtraValue) }
case let .array(underlying):
underlying.map { $0.serialized(serializeExtraValue: serializeExtraValue) }
underlying.map { $0.serialized(serializeNumberValue: serializeNumberValue, serializeExtraValue: serializeExtraValue) }
case let .string(underlying):
underlying
case let .number(underlying):
underlying
serializeNumberValue(underlying)
case let .bool(underlying):
underlying
case .null:
Expand All @@ -64,37 +64,30 @@ internal extension ExtendedJSONValue {
}
}

internal extension ExtendedJSONValue where Extra == Never {
var serialized: Any {
// swiftlint:disable:next trailing_closure
serialized(serializeExtraValue: { _ in })
}
}

// MARK: - Transforming the extra data

internal extension ExtendedJSONValue {
/// Converts this `ExtendedJSONValue<Extra>` to an `ExtendedJSONValue<NewExtra>` using a given transformation.
func map<NewExtra, Failure>(_ transform: @escaping (Extra) throws(Failure) -> NewExtra) throws(Failure) -> ExtendedJSONValue<NewExtra> {
/// Converts this `ExtendedJSONValue<Number, Extra>` to an `ExtendedJSONValue<NewNumber, NewExtra>` using given transformations.
func map<NewNumber, NewExtra, Failure>(number transformNumber: @escaping (Number) throws(Failure) -> NewNumber, extra transformExtra: @escaping (Extra) throws(Failure) -> NewExtra) throws(Failure) -> ExtendedJSONValue<NewNumber, NewExtra> {
switch self {
case let .object(underlying):
try .object(underlying.ablyLiveObjects_mapValuesWithTypedThrow { value throws(Failure) in
try value.map(transform)
try value.map(number: transformNumber, extra: transformExtra)
})
case let .array(underlying):
try .array(underlying.map { element throws(Failure) in
try element.map(transform)
try element.map(number: transformNumber, extra: transformExtra)
})
case let .string(underlying):
.string(underlying)
case let .number(underlying):
.number(underlying)
try .number(transformNumber(underlying))
case let .bool(underlying):
.bool(underlying)
case .null:
.null
case let .extra(extra):
try .extra(transform(extra))
try .extra(transformExtra(extra))
}
}
}
17 changes: 8 additions & 9 deletions Sources/AblyLiveObjects/Utility/JSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public indirect enum JSONValue: Sendable, Equatable {
case object([String: JSONValue])
case array([JSONValue])
case string(String)
case number(NSNumber)
case number(Double)
case bool(Bool)
case null

Expand Down Expand Up @@ -63,7 +63,7 @@ public indirect enum JSONValue: Sendable, Equatable {
}

/// If this `JSONValue` has case `number`, this returns the associated value. Else, it returns `nil`.
public var numberValue: NSNumber? {
public var numberValue: Double? {
if case let .number(numberValue) = self {
numberValue
} else {
Expand Down Expand Up @@ -110,13 +110,13 @@ extension JSONValue: ExpressibleByStringLiteral {

extension JSONValue: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .number(value as NSNumber)
self = .number(Double(value))
}
}

extension JSONValue: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = .number(value as NSNumber)
self = .number(value)
}
}

Expand All @@ -136,8 +136,7 @@ internal extension JSONValue {
/// - The result of serializing an array or dictionary using `JSONSerialization`
/// - Some nested element of the result of serializing such an array or dictionary
init(jsonSerializationOutput: Any) {
// swiftlint:disable:next trailing_closure
let extended = ExtendedJSONValue<Never>(deserialized: jsonSerializationOutput, createExtraValue: { deserializedExtraValue in
let extended = ExtendedJSONValue<Double, Never>(deserialized: jsonSerializationOutput, createNumberValue: { $0.doubleValue }, createExtraValue: { deserializedExtraValue in
// JSONSerialization is not conforming to our assumptions; our assumptions are probably wrong. Either way, bring this loudly to our attention instead of trying to carry on
preconditionFailure("JSONValue(jsonSerializationOutput:) was given unsupported value \(deserializedExtraValue)")
})
Expand All @@ -152,7 +151,7 @@ internal extension JSONValue {
/// - All cases: An object which we can put inside an array or dictionary that we ask `JSONSerialization` to serialize
/// - Additionally, if case `object` or `array`: An object which we can ask `JSONSerialization` to serialize
var toJSONSerializationInputElement: Any {
toExtendedJSONValue.serialized
toExtendedJSONValue.serialized(serializeNumberValue: { $0 as NSNumber }, serializeExtraValue: { _ in })
}
}

Expand Down Expand Up @@ -226,7 +225,7 @@ internal extension [JSONValue] {
// MARK: - Conversion to/from ExtendedJSONValue

internal extension JSONValue {
init(extendedJSONValue: ExtendedJSONValue<Never>) {
init(extendedJSONValue: ExtendedJSONValue<Double, Never>) {
switch extendedJSONValue {
case let .object(underlying):
self = .object(underlying.mapValues { .init(extendedJSONValue: $0) })
Expand All @@ -243,7 +242,7 @@ internal extension JSONValue {
}
}

var toExtendedJSONValue: ExtendedJSONValue<Never> {
var toExtendedJSONValue: ExtendedJSONValue<Double, Never> {
switch self {
case let .object(underlying):
.object(underlying.mapValues(\.toExtendedJSONValue))
Expand Down
23 changes: 12 additions & 11 deletions Sources/AblyLiveObjects/Utility/WireValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation

/// A wire value that can be represents the kinds of data that we expect to find inside a deserialized wire object received from `_AblyPluginSupportPrivate`, or which we may put inside a serialized wire object that we send to `_AblyPluginSupportPrivate`.
///
/// Its cases are a superset of those of ``JSONValue``, adding a further `data` case for binary data (we expect to be able to send and receive binary data in the case where ably-cocoa is using the MessagePack format).
/// Its cases are a superset of those of ``JSONValue``, adding a further `data` case for binary data (we expect to be able to send and receive binary data in the case where ably-cocoa is using the MessagePack format). Also, its `number` case is `NSNumber` instead of `Double`, to allow us to communicate to ably-cocoa's MessagePack encoder that it should encode certain values (e.g. enums) as integers, not doubles.
internal indirect enum WireValue: Sendable, Equatable {
case object([String: WireValue])
case array([WireValue])
Expand Down Expand Up @@ -122,8 +122,7 @@ internal extension WireValue {
///
/// Specifically, `pluginSupportData` can be a value that was passed to `LiveObjectsPlugin.decodeObjectMessage:…`.
init(pluginSupportData: Any) {
// swiftlint:disable:next trailing_closure
let extendedJSONValue = ExtendedJSONValue<ExtraValue>(deserialized: pluginSupportData, createExtraValue: { deserializedExtraValue in
let extendedJSONValue = ExtendedJSONValue<NSNumber, ExtraValue>(deserialized: pluginSupportData, createNumberValue: { $0 }, createExtraValue: { deserializedExtraValue in
// We support binary data (used for MessagePack format) in addition to JSON values
if let data = deserializedExtraValue as? Data {
return .data(data)
Expand All @@ -150,8 +149,7 @@ internal extension WireValue {
///
/// Used by `[String: WireValue].toPluginSupportDataDictionary`.
var toPluginSupportData: Any {
// swiftlint:disable:next trailing_closure
toExtendedJSONValue.serialized(serializeExtraValue: { extendedValue in
toExtendedJSONValue.serialized(serializeNumberValue: { $0 }, serializeExtraValue: { extendedValue in
switch extendedValue {
case let .data(data):
data
Expand All @@ -176,7 +174,7 @@ internal extension WireValue {
case data(Data)
}

init(extendedJSONValue: ExtendedJSONValue<ExtraValue>) {
init(extendedJSONValue: ExtendedJSONValue<NSNumber, ExtraValue>) {
switch extendedJSONValue {
case let .object(underlying):
self = .object(underlying.mapValues { .init(extendedJSONValue: $0) })
Expand All @@ -198,7 +196,7 @@ internal extension WireValue {
}
}

var toExtendedJSONValue: ExtendedJSONValue<ExtraValue> {
var toExtendedJSONValue: ExtendedJSONValue<NSNumber, ExtraValue> {
switch self {
case let .object(underlying):
.object(underlying.mapValues(\.toExtendedJSONValue))
Expand All @@ -223,8 +221,9 @@ internal extension WireValue {
internal extension WireValue {
/// Converts a `JSONValue` to its corresponding `WireValue`.
init(jsonValue: JSONValue) {
// swiftlint:disable:next array_init
self.init(extendedJSONValue: jsonValue.toExtendedJSONValue.map { (extra: Never) in extra })
self.init(extendedJSONValue: jsonValue.toExtendedJSONValue.map(number: { (number: Double) -> NSNumber in
number as NSNumber
}, extra: { (extra: Never) in extra }))
}

enum ConversionError: Error {
Expand All @@ -236,12 +235,14 @@ internal extension WireValue {
/// - Throws: `ConversionError.dataCannotBeConvertedToJSONValue` if `WireValue` represents binary data.
var toJSONValue: JSONValue {
get throws(ARTErrorInfo) {
let neverExtended = try toExtendedJSONValue.map { extra throws(ARTErrorInfo) -> Never in
let neverExtended = try toExtendedJSONValue.map(number: { (number: NSNumber) throws(ARTErrorInfo) -> Double in
number.doubleValue
}, extra: { (extra: ExtraValue) throws(ARTErrorInfo) -> Never in
switch extra {
case .data:
throw ConversionError.dataCannotBeConvertedToJSONValue.toARTErrorInfo()
}
}
})

return .init(extendedJSONValue: neverExtended)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ final class ObjectsHelper: Sendable {
]

if let number {
opBody["data"] = .object(["number": .number(NSNumber(value: number))])
opBody["data"] = .object(["number": .number(number)])
}

if let objectId {
Expand All @@ -467,7 +467,7 @@ final class ObjectsHelper: Sendable {
[
"operation": .string(Actions.counterInc.stringValue),
"objectId": .string(objectId),
"data": .object(["number": .number(NSNumber(value: number))]),
"data": .object(["number": .number(number)]),
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ private let objectsFixturesChannel = "objects_fixtures"

// MARK: - Top-level fixtures (ported from JS objects.test.js)

// The value of JS's `Number.MAX_SAFE_INTEGER` — the maximum integer that a `Double` can represent exactly.
private let maxSafeInteger = Double((1 << 53) - 1)

// Primitive key data fixture used across multiple test scenarios
// liveMapValue field contains the value as LiveMapValue for use in map operations
private let primitiveKeyData: [(key: String, data: [String: JSONValue], liveMapValue: LiveMapValue)] = [
Expand All @@ -156,13 +159,13 @@ private let primitiveKeyData: [(key: String, data: [String: JSONValue], liveMapV
),
(
key: "maxSafeIntegerKey",
data: ["number": .number(.init(value: Int.max))],
liveMapValue: .number(Double(Int.max))
data: ["number": .number(maxSafeInteger)],
liveMapValue: .number(maxSafeInteger)
),
(
key: "negativeMaxSafeIntegerKey",
data: ["number": .number(.init(value: -Int.max))],
liveMapValue: .number(-Double(Int.max))
data: ["number": .number(-maxSafeInteger)],
liveMapValue: .number(-maxSafeInteger)
),
(
key: "numberKey",
Expand Down Expand Up @@ -854,7 +857,7 @@ private struct ObjectsIntegrationTests {
let expectedData = Data(base64Encoded: bytesString)
#expect(try mapObj.get(key: key)?.dataValue == expectedData, "Check map \"\(mapKey)\" has correct value for \"\(key)\" key")
} else if let numberValue = data["number"]?.numberValue {
#expect(try mapObj.get(key: key)?.numberValue == numberValue.doubleValue, "Check map \"\(mapKey)\" has correct value for \"\(key)\" key")
#expect(try mapObj.get(key: key)?.numberValue == numberValue, "Check map \"\(mapKey)\" has correct value for \"\(key)\" key")
} else if let stringValue = data["string"]?.stringValue {
#expect(try mapObj.get(key: key)?.stringValue == stringValue, "Check map \"\(mapKey)\" has correct value for \"\(key)\" key")
} else if let boolValue = data["boolean"]?.boolValue {
Expand Down Expand Up @@ -1070,7 +1073,7 @@ private struct ObjectsIntegrationTests {
let expectedData = Data(base64Encoded: bytesString)
#expect(try mapValue.get(key: "value")?.dataValue == expectedData, "Check root has correct value for \"\(keyData.key)\" key after MAP_SET op")
} else if let numberValue = keyData.data["number"]?.numberValue {
#expect(try mapValue.get(key: "value")?.numberValue == numberValue.doubleValue, "Check root has correct value for \"\(keyData.key)\" key after MAP_SET op")
#expect(try mapValue.get(key: "value")?.numberValue == numberValue, "Check root has correct value for \"\(keyData.key)\" key after MAP_SET op")
} else if let stringValue = keyData.data["string"]?.stringValue {
#expect(try mapValue.get(key: "value")?.stringValue == stringValue, "Check root has correct value for \"\(keyData.key)\" key after MAP_SET op")
} else if let boolValue = keyData.data["boolean"]?.boolValue {
Expand Down Expand Up @@ -2047,7 +2050,7 @@ private struct ObjectsIntegrationTests {
}
} else if let numberValue = keyData.data["number"] {
if case let .number(expectedNumber) = numberValue {
#expect(try #require(root.get(key: keyData.key)?.numberValue) == expectedNumber.doubleValue, "Check root has correct value for \"\(keyData.key)\" key after OBJECT_SYNC has ended and buffered operations are applied")
#expect(try #require(root.get(key: keyData.key)?.numberValue) == expectedNumber, "Check root has correct value for \"\(keyData.key)\" key after OBJECT_SYNC has ended and buffered operations are applied")
}
} else if let boolValue = keyData.data["boolean"] {
if case let .bool(expectedBool) = boolValue {
Expand Down Expand Up @@ -2347,7 +2350,7 @@ private struct ObjectsIntegrationTests {
}
} else if let numberValue = keyData.data["number"] {
if case let .number(expectedNumber) = numberValue {
#expect(try #require(root.get(key: keyData.key)?.numberValue) == expectedNumber.doubleValue, "Check root has correct value for \"\(keyData.key)\" key after OBJECT_SYNC has ended and buffered operations are applied")
#expect(try #require(root.get(key: keyData.key)?.numberValue) == expectedNumber, "Check root has correct value for \"\(keyData.key)\" key after OBJECT_SYNC has ended and buffered operations are applied")
}
} else if let boolValue = keyData.data["boolean"] {
if case let .bool(expectedBool) = boolValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct ObjectCreationHelpersTests {
#expect(deserializedInitialValue == [
"map": [
// RTO11f4a
"semantics": .number(ObjectsMapSemantics.lww.rawValue as NSNumber),
"semantics": .number(Double(ObjectsMapSemantics.lww.rawValue)),
"entries": [
// RTO11f4c1a
"mapRef": [
Expand Down