Skip to content

Commit ffcce5c

Browse files
committed
[AIT-208] feat: update LiveObjects messages fields to support protocol v6
- Expose typed fields on public `ObjectData` (`boolean`, `bytes`, `number`, `string`, `json`) - Remove previous field names on `ObjectData`
1 parent f1ae6ee commit ffcce5c

16 files changed

Lines changed: 261 additions & 339 deletions

File tree

liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -161,19 +161,4 @@ internal fun ObjectsAdapter.throwIfEchoMessagesDisabled() {
161161
}
162162
}
163163

164-
internal class Binary(val data: ByteArray) {
165-
override fun equals(other: Any?): Boolean {
166-
if (this === other) return true
167-
if (other !is Binary) return false
168-
return data.contentEquals(other.data)
169-
}
170-
171-
override fun hashCode(): Int {
172-
return data.contentHashCode()
173-
}
174-
}
175-
176-
internal fun Binary.size(): Int {
177-
return data.size
178-
}
179164

liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

Lines changed: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package io.ably.lib.objects
22

3+
import com.google.gson.JsonElement
34
import com.google.gson.JsonObject
45

56
import com.google.gson.annotations.JsonAdapter
67
import com.google.gson.annotations.SerializedName
78
import io.ably.lib.objects.serialization.ObjectDataJsonSerializer
89
import io.ably.lib.objects.serialization.gson
10+
import java.util.Base64
911

1012
/**
1113
* An enum class representing the different actions that can be performed on an object.
@@ -42,28 +44,21 @@ internal data class ObjectData(
4244
*/
4345
val objectId: String? = null,
4446

45-
/**
46-
* String, number, boolean or binary - a concrete value of the object
47-
* Spec: OD2c
48-
*/
49-
val value: ObjectValue? = null,
50-
)
47+
/** String value. Spec: OD2c */
48+
val string: String? = null,
5149

52-
/**
53-
* Represents a value that can be a String, Number, Boolean, Binary, JsonObject or JsonArray.
54-
* Provides compile-time type safety through sealed class pattern.
55-
* Spec: OD2c
56-
*/
57-
internal sealed class ObjectValue {
58-
abstract val value: Any
59-
60-
data class String(override val value: kotlin.String) : ObjectValue()
61-
data class Number(override val value: kotlin.Number) : ObjectValue()
62-
data class Boolean(override val value: kotlin.Boolean) : ObjectValue()
63-
data class Binary(override val value: io.ably.lib.objects.Binary) : ObjectValue()
64-
data class JsonObject(override val value: com.google.gson.JsonObject) : ObjectValue()
65-
data class JsonArray(override val value: com.google.gson.JsonArray) : ObjectValue()
66-
}
50+
/** Numeric value. Spec: OD2c */
51+
val number: Double? = null,
52+
53+
/** Boolean value. Spec: OD2c */
54+
val boolean: Boolean? = null,
55+
56+
/** Binary value encoded as a base64 string. Spec: OD2c */
57+
val bytes: String? = null,
58+
59+
/** JSON object or array value. Spec: OD2c */
60+
val json: JsonElement? = null,
61+
)
6762

6863
/**
6964
* Payload for MAP_CREATE operation.
@@ -390,7 +385,7 @@ internal data class ObjectMessage(
390385
* Spec: OM3
391386
*/
392387
internal fun ObjectMessage.size(): Int {
393-
val clientIdSize = clientId?.length ?: 0 // Spec: OM3f
388+
val clientIdSize = clientId?.byteSize ?: 0 // Spec: OM3f
394389
val operationSize = operation?.size() ?: 0 // Spec: OM3b, OOP4
395390
val objectStateSize = objectState?.size() ?: 0 // Spec: OM3c, OST3
396391
val extrasSize = extras?.let { gson.toJson(it).length } ?: 0 // Spec: OM3d
@@ -432,21 +427,21 @@ private fun ObjectState.size(): Int {
432427
* Calculates the size of a MapCreate payload in bytes.
433428
*/
434429
private fun MapCreate.size(): Int {
435-
return entries.entries.sumOf { it.key.length + it.value.size() }
430+
return entries.entries.sumOf { it.key.byteSize + it.value.size() }
436431
}
437432

438433
/**
439434
* Calculates the size of a MapSet payload in bytes.
440435
*/
441436
private fun MapSet.size(): Int {
442-
return key.length + value.size()
437+
return key.byteSize + value.size()
443438
}
444439

445440
/**
446441
* Calculates the size of a MapRemove payload in bytes.
447442
*/
448443
private fun MapRemove.size(): Int {
449-
return key.length
444+
return key.byteSize
450445
}
451446

452447
/**
@@ -467,14 +462,14 @@ private fun CounterInc.size(): Int {
467462
* Calculates the size of a MapCreateWithObjectId payload in bytes.
468463
*/
469464
private fun MapCreateWithObjectId.size(): Int {
470-
return initialValue.length + nonce.length
465+
return initialValue.byteSize + nonce.byteSize
471466
}
472467

473468
/**
474469
* Calculates the size of a CounterCreateWithObjectId payload in bytes.
475470
*/
476471
private fun CounterCreateWithObjectId.size(): Int {
477-
return initialValue.length + nonce.length
472+
return initialValue.byteSize + nonce.byteSize
478473
}
479474

480475
/**
@@ -513,23 +508,19 @@ private fun ObjectsMapEntry.size(): Int {
513508
* Spec: OD3
514509
*/
515510
private fun ObjectData.size(): Int {
516-
return value?.size() ?: 0 // Spec: OD3f
517-
}
518-
519-
/**
520-
* Calculates the size of an ObjectValue in bytes.
521-
* Spec: OD3*
522-
*/
523-
private fun ObjectValue.size(): Int {
524-
return when (this) {
525-
is ObjectValue.Boolean -> 1 // Spec: OD3b
526-
is ObjectValue.Binary -> value.size() // Spec: OD3c
527-
is ObjectValue.Number -> 8 // Spec: OD3d
528-
is ObjectValue.String -> value.byteSize // Spec: OD3e
529-
is ObjectValue.JsonObject, is ObjectValue.JsonArray -> value.toString().byteSize // Spec: OD3e
530-
}
511+
string?.let { return it.byteSize } // Spec: OD3e
512+
number?.let { return 8 } // Spec: OD3d
513+
boolean?.let { return 1 } // Spec: OD3b
514+
bytes?.let { return Base64.getDecoder().decode(it).size } // Spec: OD3c
515+
json?.let { return it.toString().byteSize } // Spec: OD3e
516+
return 0
531517
}
532518

533519
internal fun ObjectData?.isInvalid(): Boolean {
534-
return this?.objectId.isNullOrEmpty() && this?.value == null
520+
return this?.objectId.isNullOrEmpty() &&
521+
this?.string == null &&
522+
this?.number == null &&
523+
this?.boolean == null &&
524+
this?.bytes == null &&
525+
this?.json == null
535526
}
Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
package io.ably.lib.objects.serialization
22

33
import com.google.gson.*
4-
import io.ably.lib.objects.Binary
54
import io.ably.lib.objects.ObjectsMapSemantics
65
import io.ably.lib.objects.ObjectData
76
import io.ably.lib.objects.ObjectMessage
87
import io.ably.lib.objects.ObjectOperationAction
9-
import io.ably.lib.objects.ObjectValue
108
import java.lang.reflect.Type
11-
import java.util.*
129
import kotlin.enums.EnumEntries
1310

1411
// Gson instance for JSON serialization/deserialization
@@ -45,42 +42,26 @@ internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeseri
4542
override fun serialize(src: ObjectData, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
4643
val obj = JsonObject()
4744
src.objectId?.let { obj.addProperty("objectId", it) }
48-
49-
src.value?.let { v ->
50-
when (v) {
51-
is ObjectValue.Boolean -> obj.addProperty("boolean", v.value)
52-
is ObjectValue.String -> obj.addProperty("string", v.value)
53-
is ObjectValue.Number -> obj.addProperty("number", v.value.toDouble())
54-
is ObjectValue.Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.value.data))
55-
is ObjectValue.JsonObject, is ObjectValue.JsonArray -> obj.addProperty("json", v.value.toString()) // Spec: OD4c5
56-
}
57-
}
45+
src.string?.let { obj.addProperty("string", it) }
46+
src.number?.let { obj.addProperty("number", it) }
47+
src.boolean?.let { obj.addProperty("boolean", it) }
48+
src.bytes?.let { obj.addProperty("bytes", it) }
49+
src.json?.let { obj.addProperty("json", it.toString()) } // Spec: OD4c5
5850
return obj
5951
}
6052

6153
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): ObjectData {
6254
val obj = if (json.isJsonObject) json.asJsonObject else throw JsonParseException("Expected JsonObject")
6355
val objectId = if (obj.has("objectId")) obj.get("objectId").asString else null
64-
val value = when {
65-
obj.has("boolean") -> ObjectValue.Boolean(obj.get("boolean").asBoolean)
66-
obj.has("string") -> ObjectValue.String(obj.get("string").asString)
67-
obj.has("number") -> ObjectValue.Number(obj.get("number").asDouble)
68-
obj.has("bytes") -> ObjectValue.Binary(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
69-
obj.has("json") -> {
70-
val jsonElement = JsonParser.parseString(obj.get("json").asString)
71-
when {
72-
jsonElement.isJsonObject -> ObjectValue.JsonObject(jsonElement.asJsonObject)
73-
jsonElement.isJsonArray -> ObjectValue.JsonArray(jsonElement.asJsonArray)
74-
else -> throw JsonParseException("Invalid JSON structure")
75-
}
76-
}
77-
else -> {
78-
if (objectId != null)
79-
null
80-
else
81-
throw JsonParseException("Since objectId is not present, at least one of the value fields must be present")
82-
}
56+
val string = if (obj.has("string")) obj.get("string").asString else null
57+
val number = if (obj.has("number")) obj.get("number").asDouble else null
58+
val boolean = if (obj.has("boolean")) obj.get("boolean").asBoolean else null
59+
val bytes = if (obj.has("bytes")) obj.get("bytes").asString else null
60+
val json = if (obj.has("json")) JsonParser.parseString(obj.get("json").asString) else null
61+
62+
if (objectId == null && string == null && number == null && boolean == null && bytes == null && json == null) {
63+
throw JsonParseException("Since objectId is not present, at least one of the value fields must be present")
8364
}
84-
return ObjectData(objectId, value)
65+
return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json)
8566
}
8667
}

liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package io.ably.lib.objects.serialization
22

3+
import com.google.gson.JsonElement
34
import com.google.gson.JsonObject
45
import com.google.gson.JsonParser
56
import io.ably.lib.objects.*
6-
import io.ably.lib.objects.Binary
77
import io.ably.lib.objects.CounterCreate
88
import io.ably.lib.objects.CounterCreateWithObjectId
99
import io.ably.lib.objects.CounterInc
@@ -22,7 +22,7 @@ import io.ably.lib.objects.ObjectMessage
2222
import io.ably.lib.objects.ObjectOperation
2323
import io.ably.lib.objects.ObjectOperationAction
2424
import io.ably.lib.objects.ObjectState
25-
import io.ably.lib.objects.ObjectValue
25+
import java.util.Base64
2626
import io.ably.lib.util.Serialisation
2727
import org.msgpack.core.MessageFormat
2828
import org.msgpack.core.MessagePacker
@@ -806,9 +806,11 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) {
806806
var fieldCount = 0
807807

808808
if (objectId != null) fieldCount++
809-
value?.let {
810-
fieldCount++
811-
}
809+
if (string != null) fieldCount++
810+
if (number != null) fieldCount++
811+
if (boolean != null) fieldCount++
812+
if (bytes != null) fieldCount++
813+
if (json != null) fieldCount++
812814

813815
packer.packMapHeader(fieldCount)
814816

@@ -817,34 +819,31 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) {
817819
packer.packString(objectId)
818820
}
819821

820-
value?.let { v ->
821-
when (v) {
822-
is ObjectValue.Boolean -> {
823-
packer.packString("boolean")
824-
packer.packBoolean(v.value)
825-
}
826-
is ObjectValue.String -> {
827-
packer.packString("string")
828-
packer.packString(v.value)
829-
}
830-
is ObjectValue.Number -> {
831-
packer.packString("number")
832-
packer.packDouble(v.value.toDouble())
833-
}
834-
is ObjectValue.Binary -> {
835-
packer.packString("bytes")
836-
packer.packBinaryHeader(v.value.data.size)
837-
packer.writePayload(v.value.data)
838-
}
839-
is ObjectValue.JsonObject -> {
840-
packer.packString("json")
841-
packer.packString(v.value.toString())
842-
}
843-
is ObjectValue.JsonArray -> {
844-
packer.packString("json")
845-
packer.packString(v.value.toString())
846-
}
847-
}
822+
if (string != null) {
823+
packer.packString("string")
824+
packer.packString(string)
825+
}
826+
827+
if (number != null) {
828+
packer.packString("number")
829+
packer.packDouble(number)
830+
}
831+
832+
if (boolean != null) {
833+
packer.packString("boolean")
834+
packer.packBoolean(boolean)
835+
}
836+
837+
if (bytes != null) {
838+
val rawBytes = Base64.getDecoder().decode(bytes)
839+
packer.packString("bytes")
840+
packer.packBinaryHeader(rawBytes.size)
841+
packer.writePayload(rawBytes)
842+
}
843+
844+
if (json != null) {
845+
packer.packString("json")
846+
packer.packString(json.toString())
848847
}
849848
}
850849

@@ -854,7 +853,11 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) {
854853
private fun readObjectData(unpacker: MessageUnpacker): ObjectData {
855854
val fieldCount = unpacker.unpackMapHeader()
856855
var objectId: String? = null
857-
var value: ObjectValue? = null
856+
var string: String? = null
857+
var number: Double? = null
858+
var boolean: Boolean? = null
859+
var bytes: String? = null
860+
var json: JsonElement? = null
858861

859862
for (i in 0 until fieldCount) {
860863
val fieldName = unpacker.unpackString().intern()
@@ -867,28 +870,19 @@ private fun readObjectData(unpacker: MessageUnpacker): ObjectData {
867870

868871
when (fieldName) {
869872
"objectId" -> objectId = unpacker.unpackString()
870-
"boolean" -> value = ObjectValue.Boolean(unpacker.unpackBoolean())
871-
"string" -> value = ObjectValue.String(unpacker.unpackString())
872-
"number" -> value = ObjectValue.Number(unpacker.unpackDouble())
873+
"string" -> string = unpacker.unpackString()
874+
"number" -> number = unpacker.unpackDouble()
875+
"boolean" -> boolean = unpacker.unpackBoolean()
873876
"bytes" -> {
874877
val size = unpacker.unpackBinaryHeader()
875-
val bytes = ByteArray(size)
876-
unpacker.readPayload(bytes)
877-
value = ObjectValue.Binary(Binary(bytes))
878-
}
879-
"json" -> {
880-
val jsonString = unpacker.unpackString()
881-
val parsed = JsonParser.parseString(jsonString)
882-
value = when {
883-
parsed.isJsonObject -> ObjectValue.JsonObject(parsed.asJsonObject)
884-
parsed.isJsonArray -> ObjectValue.JsonArray(parsed.asJsonArray)
885-
else ->
886-
throw ablyException("Invalid JSON string for json field", ErrorCode.MapValueDataTypeUnsupported, HttpStatusCode.InternalServerError)
887-
}
878+
val rawBytes = ByteArray(size)
879+
unpacker.readPayload(rawBytes)
880+
bytes = Base64.getEncoder().encodeToString(rawBytes)
888881
}
882+
"json" -> json = JsonParser.parseString(unpacker.unpackString())
889883
else -> unpacker.skipValue()
890884
}
891885
}
892886

893-
return ObjectData(objectId = objectId, value = value)
887+
return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json)
894888
}

0 commit comments

Comments
 (0)