diff --git a/Sources/AblyLiveObjects/Utility/ExtendedJSONValue.swift b/Sources/AblyLiveObjects/Utility/ExtendedJSONValue.swift index 133fd86..1f1d133 100644 --- a/Sources/AblyLiveObjects/Utility/ExtendedJSONValue.swift +++ b/Sources/AblyLiveObjects/Utility/ExtendedJSONValue.swift @@ -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 { +/// 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 { case object([String: Self]) case array([Self]) case string(String) - case number(NSNumber) + case number(Number) case bool(Bool) case null case extra(Extra) @@ -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: @@ -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 @@ -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: @@ -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` to an `ExtendedJSONValue` using a given transformation. - func map(_ transform: @escaping (Extra) throws(Failure) -> NewExtra) throws(Failure) -> ExtendedJSONValue { + /// Converts this `ExtendedJSONValue` to an `ExtendedJSONValue` using given transformations. + func map(number transformNumber: @escaping (Number) throws(Failure) -> NewNumber, extra transformExtra: @escaping (Extra) throws(Failure) -> NewExtra) throws(Failure) -> ExtendedJSONValue { 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)) } } } diff --git a/Sources/AblyLiveObjects/Utility/JSONValue.swift b/Sources/AblyLiveObjects/Utility/JSONValue.swift index 1c6ae2e..02a71c9 100644 --- a/Sources/AblyLiveObjects/Utility/JSONValue.swift +++ b/Sources/AblyLiveObjects/Utility/JSONValue.swift @@ -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 @@ -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 { @@ -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) } } @@ -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(deserialized: jsonSerializationOutput, createExtraValue: { deserializedExtraValue in + let extended = ExtendedJSONValue(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)") }) @@ -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 }) } } @@ -226,7 +225,7 @@ internal extension [JSONValue] { // MARK: - Conversion to/from ExtendedJSONValue internal extension JSONValue { - init(extendedJSONValue: ExtendedJSONValue) { + init(extendedJSONValue: ExtendedJSONValue) { switch extendedJSONValue { case let .object(underlying): self = .object(underlying.mapValues { .init(extendedJSONValue: $0) }) @@ -243,7 +242,7 @@ internal extension JSONValue { } } - var toExtendedJSONValue: ExtendedJSONValue { + var toExtendedJSONValue: ExtendedJSONValue { switch self { case let .object(underlying): .object(underlying.mapValues(\.toExtendedJSONValue)) diff --git a/Sources/AblyLiveObjects/Utility/WireValue.swift b/Sources/AblyLiveObjects/Utility/WireValue.swift index 9f59db1..c8b75f2 100644 --- a/Sources/AblyLiveObjects/Utility/WireValue.swift +++ b/Sources/AblyLiveObjects/Utility/WireValue.swift @@ -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]) @@ -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(deserialized: pluginSupportData, createExtraValue: { deserializedExtraValue in + let extendedJSONValue = ExtendedJSONValue(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) @@ -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 @@ -176,7 +174,7 @@ internal extension WireValue { case data(Data) } - init(extendedJSONValue: ExtendedJSONValue) { + init(extendedJSONValue: ExtendedJSONValue) { switch extendedJSONValue { case let .object(underlying): self = .object(underlying.mapValues { .init(extendedJSONValue: $0) }) @@ -198,7 +196,7 @@ internal extension WireValue { } } - var toExtendedJSONValue: ExtendedJSONValue { + var toExtendedJSONValue: ExtendedJSONValue { switch self { case let .object(underlying): .object(underlying.mapValues(\.toExtendedJSONValue)) @@ -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 { @@ -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) } diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift index 77b78e1..92b79a0 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift @@ -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 { @@ -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)]), ] } diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 1ad7592..0cfce19 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -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)] = [ @@ -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", @@ -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 { @@ -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 { @@ -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 { @@ -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 { diff --git a/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift b/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift index b58cfb3..9f88051 100644 --- a/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift @@ -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": [