Skip to content

Commit b846865

Browse files
committed
[ECO-5386] Implemented custom serializers for ObjectData type
1. Added JSON serializer/deserializer using ObjectDataJsonSerializer 2, Added msgpack serializer/deserializer using ObjectDataMsgpackSerializer and ObjectDataMsgpackDeserializer
1 parent 7a4873e commit b846865

3 files changed

Lines changed: 112 additions & 19 deletions

File tree

live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import com.google.gson.JsonArray
44
import com.google.gson.JsonObject
55

66
import com.fasterxml.jackson.annotation.JsonProperty
7+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
8+
import com.fasterxml.jackson.databind.annotation.JsonSerialize
9+
import com.google.gson.annotations.JsonAdapter
710
import com.google.gson.annotations.SerializedName
811

912
/**
@@ -31,19 +34,16 @@ internal enum class MapSemantics(val code: Int) {
3134
* An ObjectData represents a value in an object on a channel.
3235
* Spec: OD1
3336
*/
37+
@JsonAdapter(ObjectDataJsonSerializer::class)
38+
@JsonSerialize(using = ObjectDataMsgpackSerializer::class)
39+
@JsonDeserialize(using = ObjectDataMsgpackDeserializer::class)
3440
internal data class ObjectData(
3541
/**
3642
* A reference to another object, used to support composable object structures.
3743
* Spec: OD2a
3844
*/
3945
val objectId: String? = null,
4046

41-
/**
42-
* Can be set by the client to indicate that value in `string` or `bytes` field have an encoding.
43-
* Spec: OD2b
44-
*/
45-
val encoding: String? = null,
46-
4747
/**
4848
* String, number, boolean or binary - a concrete value of the object
4949
* Spec: OD2c
@@ -214,18 +214,13 @@ internal data class ObjectOperation(
214214
val nonce: String? = null,
215215

216216
/**
217-
* The initial value bytes for the object. These bytes should be used along with the nonce
218-
* and timestamp to create the object ID. Frontdoor will use this to verify the object ID.
219-
* After verification the bytes will be decoded into the Map or Counter objects and
220-
* the initialValue, nonce, and initialValueEncoding will be removed.
217+
* The initial value for the object, encoded as a JSON string.
218+
* This value should be used along with the nonce and timestamp to create the object ID.
219+
* Frontdoor will use this to verify the object ID. After verification, the value will be
220+
* decoded into the Map or Counter objects and the initialValue, nonce, and initialValueEncoding will be removed.
221221
* Spec: OOP3h
222222
*/
223-
val initialValue: Binary? = null,
224-
225-
/** The initial value encoding defines how the initialValue should be interpreted.
226-
* Spec: OOP3i
227-
*/
228-
val initialValueEncoding: ProtocolMessageFormat? = null
223+
val initialValue: String? = null,
229224
)
230225

231226
/**

live-objects/src/main/kotlin/io/ably/lib/objects/Serialization.kt

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
package io.ably.lib.objects
44

5+
import com.fasterxml.jackson.core.JsonGenerator
6+
import com.fasterxml.jackson.databind.DeserializationContext
57
import com.fasterxml.jackson.databind.ObjectMapper
8+
import com.fasterxml.jackson.databind.SerializerProvider
69
import com.google.gson.*
710
import org.msgpack.core.MessagePack
811
import org.msgpack.core.MessagePacker
912
import org.msgpack.core.MessageUnpacker
1013
import org.msgpack.jackson.dataformat.MessagePackFactory
14+
import java.lang.reflect.Type
15+
import java.util.*
1116

1217
// Gson instance for JSON serialization/deserialization
1318
internal val gson: Gson = GsonBuilder().create()
@@ -86,3 +91,98 @@ internal class DefaultLiveObjectSerializer : LiveObjectSerializer {
8691
return jsonArray
8792
}
8893
}
94+
95+
internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeserializer<ObjectData> {
96+
override fun serialize(src: ObjectData?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
97+
val obj = JsonObject()
98+
src?.objectId?.let { obj.addProperty("objectId", it) }
99+
100+
src?.value?.let { value ->
101+
when (val v = value.value) {
102+
is Boolean -> obj.addProperty("boolean", v)
103+
is String -> obj.addProperty("string", v)
104+
is Number -> obj.addProperty("number", v)
105+
is Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.data))
106+
// Spec: OD4c5
107+
is JsonObject, is JsonArray -> {
108+
obj.addProperty("string", v.toString())
109+
obj.addProperty("encoding", "json")
110+
}
111+
}
112+
}
113+
return obj
114+
}
115+
116+
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): ObjectData {
117+
val obj = if (json?.isJsonObject == true) json.asJsonObject else throw JsonParseException("Expected JsonObject")
118+
val objectId = if (obj.has("objectId")) obj.get("objectId").asString else null
119+
val encoding = if (obj.has("encoding")) obj.get("encoding").asString else null
120+
val value = when {
121+
obj.has("boolean") -> ObjectValue(obj.get("boolean").asBoolean)
122+
// Spec: OD5b3
123+
obj.has("string") && encoding == "json" -> {
124+
val jsonStr = obj.get("string").asString
125+
val parsed = JsonParser.parseString(jsonStr)
126+
ObjectValue(
127+
when {
128+
parsed.isJsonObject -> parsed.asJsonObject
129+
parsed.isJsonArray -> parsed.asJsonArray
130+
else -> throw JsonParseException("Invalid JSON string for encoding=json")
131+
}
132+
)
133+
}
134+
obj.has("string") -> ObjectValue(obj.get("string").asString)
135+
obj.has("number") -> ObjectValue(obj.get("number").asNumber)
136+
obj.has("bytes") -> ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
137+
else -> throw JsonParseException("ObjectData must have one of the fields: boolean, string, number, or bytes")
138+
}
139+
return ObjectData(objectId, value)
140+
}
141+
}
142+
143+
internal class ObjectDataMsgpackSerializer : com.fasterxml.jackson.databind.JsonSerializer<ObjectData>() {
144+
override fun serialize(value: ObjectData?, gen: JsonGenerator, serializers: SerializerProvider) {
145+
gen.writeStartObject()
146+
value?.objectId?.let { gen.writeStringField("objectId", it) }
147+
value?.value?.let { v ->
148+
when (val data = v.value) {
149+
is Boolean -> gen.writeBooleanField("boolean", data)
150+
is String -> gen.writeStringField("string", data)
151+
is Number -> gen.writeNumberField("number", data.toDouble())
152+
is Binary -> gen.writeBinaryField("bytes", data.data)
153+
is JsonObject, is JsonArray -> {
154+
gen.writeStringField("string", data.toString())
155+
gen.writeStringField("encoding", "json")
156+
}
157+
}
158+
}
159+
gen.writeEndObject()
160+
}
161+
}
162+
163+
internal class ObjectDataMsgpackDeserializer : com.fasterxml.jackson.databind.JsonDeserializer<ObjectData>() {
164+
override fun deserialize(p: com.fasterxml.jackson.core.JsonParser, ctxt: DeserializationContext): ObjectData {
165+
val node = p.codec.readTree<com.fasterxml.jackson.databind.JsonNode>(p)
166+
val objectId = node.get("objectId")?.asText()
167+
val encoding = node.get("encoding")?.asText()
168+
val value = when {
169+
node.has("boolean") -> ObjectValue(node.get("boolean").asBoolean())
170+
node.has("string") && encoding == "json" -> {
171+
val jsonStr = node.get("string").asText()
172+
val parsed = JsonParser.parseString(jsonStr)
173+
ObjectValue(
174+
when {
175+
parsed.isJsonObject -> parsed.asJsonObject
176+
parsed.isJsonArray -> parsed.asJsonArray
177+
else -> throw IllegalArgumentException("Invalid JSON string for encoding=json")
178+
}
179+
)
180+
}
181+
node.has("string") -> ObjectValue(node.get("string").asText())
182+
node.has("number") -> ObjectValue(node.get("number").numberValue())
183+
node.has("bytes") -> ObjectValue(Binary(node.get("bytes").binaryValue()))
184+
else -> throw IllegalArgumentException("ObjectData must have one of the fields: boolean, string, number, or bytes")
185+
}
186+
return ObjectData(objectId, value)
187+
}
188+
}

live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class ObjectMessageSizeTest {
4545
key = "mapKey", // Size: 6 bytes (UTF-8 byte length)
4646
data = ObjectData(
4747
objectId = "ref_obj", // Not counted in data size
48-
encoding = "utf-8", // Not counted in data size
4948
value = ObjectValue("sample") // Size: 6 bytes (UTF-8 byte length)
5049
) // Total ObjectData size: 6 bytes
5150
), // Total ObjectMapOp size: 6 + 6 = 12 bytes
@@ -80,8 +79,7 @@ class ObjectMessageSizeTest {
8079
), // Total ObjectCounter size: 8 bytes
8180

8281
nonce = "nonce123", // Not counted in operation size
83-
initialValue = Binary("initial".toByteArray()), // Not counted in operation size
84-
initialValueEncoding = ProtocolMessageFormat.Json // Not counted in operation size
82+
initialValue = "some-value", // Not counted in operation size
8583
), // Total ObjectOperation size: 12 + 8 + 26 + 8 = 54 bytes
8684

8785
objectState = ObjectState(

0 commit comments

Comments
 (0)