Skip to content

Commit 86c746e

Browse files
committed
[ECO-5386] Implemented unit tests for ObjectMesssage json/msgpack serialization
1. Created ObjectMessageFixtures to represent dummy data in various formats 2. Added jackson-param dependency to fix jackson deserialization issue 3. Marked javaParameters as true on compilerOptions
1 parent b846865 commit 86c746e

File tree

7 files changed

+277
-32
lines changed

7 files changed

+277
-32
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ turbine = "1.2.0"
2626
ktor = "3.1.3"
2727
jetbrains-annoations = "26.0.2"
2828
jackson-msgpack = "0.8.11" # Compatible with msgpack-core 0.8.11
29+
jackson-param = "2.9.0" # Compatible with jackson-msgpack
2930

3031
[libraries]
3132
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
@@ -56,6 +57,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
5657
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
5758
jetbrains = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annoations" }
5859
jackson-msgpack = { group = "org.msgpack", name = "jackson-dataformat-msgpack", version.ref = "jackson-msgpack" }
60+
jackson-parameter-names = { group = "com.fasterxml.jackson.module", name = "jackson-module-parameter-names", version.ref = "jackson-param" }
5961

6062
[bundles]
6163
common = ["msgpack", "vcdiff-core"]

live-objects/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ dependencies {
1414
implementation(libs.bundles.common)
1515
implementation(libs.coroutine.core)
1616
implementation(libs.jackson.msgpack)
17+
implementation(libs.jackson.parameter.names) // Add this
18+
1719

1820
testImplementation(kotlin("test"))
1921
testImplementation(libs.bundles.kotlin.tests)
@@ -45,4 +47,12 @@ tasks.register<Test>("runLiveObjectIntegrationTests") {
4547

4648
kotlin {
4749
explicitApi()
50+
51+
/**
52+
* Enables Jackson to map JSON property names to constructor parameters without use of @JsonProperty.
53+
* Adds metadata params to bytecode class. Approach is completely binary-compatible with consumers of the library.
54+
*/
55+
compilerOptions {
56+
javaParameters = true
57+
}
4858
}

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,19 @@ internal data class ObjectOperation(
214214
val nonce: String? = null,
215215

216216
/**
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.
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.
221221
* Spec: OOP3h
222222
*/
223-
val initialValue: String? = null,
223+
val initialValue: Binary? = null,
224+
225+
/** The initial value encoding defines how the initialValue should be interpreted.
226+
* Spec: OOP3i
227+
*/
228+
@Deprecated("Will be removed in the future, initialValue will be json string")
229+
val initialValueEncoding: ProtocolMessageFormat? = null
224230
)
225231

226232
/**

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

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
package io.ably.lib.objects
44

5+
import com.fasterxml.jackson.annotation.JsonCreator
56
import com.fasterxml.jackson.core.JsonGenerator
67
import com.fasterxml.jackson.databind.DeserializationContext
78
import com.fasterxml.jackson.databind.ObjectMapper
89
import com.fasterxml.jackson.databind.SerializerProvider
10+
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
911
import com.google.gson.*
1012
import org.msgpack.core.MessagePack
1113
import org.msgpack.core.MessagePacker
1214
import org.msgpack.core.MessageUnpacker
1315
import org.msgpack.jackson.dataformat.MessagePackFactory
16+
import org.msgpack.value.ImmutableMapValue
1417
import java.lang.reflect.Type
1518
import java.util.*
1619

@@ -19,7 +22,10 @@ internal val gson: Gson = GsonBuilder().create()
1922

2023
// Jackson ObjectMapper for MessagePack serialization (respects @JsonProperty annotations)
2124
// Caches type metadata and serializers for ObjectMessage class after first use, so next time it's super fast 🚀
22-
private val msgpackMapper = ObjectMapper(MessagePackFactory())
25+
// https://github.com/FasterXML/jackson-modules-java8/tree/3.x/parameter-names
26+
private val msgpackMapper = ObjectMapper(MessagePackFactory()).apply {
27+
registerModule(ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
28+
}
2329

2430
internal fun ObjectMessage.toJsonObject(): JsonObject {
2531
return gson.toJsonTree(this).asJsonObject
@@ -30,29 +36,15 @@ internal fun JsonObject.toObjectMessage(): ObjectMessage {
3036
}
3137

3238
internal fun ObjectMessage.writeTo(packer: MessagePacker) {
33-
// Jackson automatically creates the correct msgpack map structure
34-
val msgpackBytes = msgpackMapper.writeValueAsBytes(this)
35-
36-
// Parse the msgpack bytes to get the structured value
37-
val tempUnpacker = MessagePack.newDefaultUnpacker(msgpackBytes)
38-
val msgpackValue = tempUnpacker.unpackValue()
39-
tempUnpacker.close()
40-
41-
// Pack the structured value using the provided packer
42-
packer.packValue(msgpackValue)
39+
val msgpackBytes = msgpackMapper.writeValueAsBytes(this) // returns correct msgpack map structure
40+
packer.writePayload(msgpackBytes)
4341
}
4442

45-
internal fun MessageUnpacker.readObjectMessage(): ObjectMessage {
46-
// Read the msgpack value from the unpacker
47-
val msgpackValue = this.unpackValue()
48-
49-
// Convert the msgpack value back to bytes
50-
val tempPacker = MessagePack.newDefaultBufferPacker()
51-
tempPacker.packValue(msgpackValue)
52-
val msgpackBytes = tempPacker.toByteArray()
53-
tempPacker.close()
54-
55-
// Let Jackson deserialize the msgpack bytes back to ObjectMessage
43+
internal fun ImmutableMapValue.toObjectMessage(): ObjectMessage {
44+
val msgpackBytes = MessagePack.newDefaultBufferPacker().use { packer ->
45+
packer.packValue(this)
46+
packer.toByteArray()
47+
}
5648
return msgpackMapper.readValue(msgpackBytes, ObjectMessage::class.java)
5749
}
5850

@@ -66,7 +58,7 @@ internal class DefaultLiveObjectSerializer : LiveObjectSerializer {
6658

6759
override fun readMsgpackArray(unpacker: MessageUnpacker): Array<Any> {
6860
val objectMessagesCount = unpacker.unpackArrayHeader()
69-
return Array(objectMessagesCount) { unpacker.readObjectMessage() }
61+
return Array(objectMessagesCount) { unpacker.unpackValue().asMapValue().toObjectMessage() }
7062
}
7163

7264
override fun writeMsgpackArray(objects: Array<out Any>?, packer: MessagePacker) {
@@ -101,7 +93,7 @@ internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeseri
10193
when (val v = value.value) {
10294
is Boolean -> obj.addProperty("boolean", v)
10395
is String -> obj.addProperty("string", v)
104-
is Number -> obj.addProperty("number", v)
96+
is Number -> obj.addProperty("number", v.toDouble())
10597
is Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.data))
10698
// Spec: OD4c5
10799
is JsonObject, is JsonArray -> {
@@ -132,7 +124,7 @@ internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeseri
132124
)
133125
}
134126
obj.has("string") -> ObjectValue(obj.get("string").asString)
135-
obj.has("number") -> ObjectValue(obj.get("number").asNumber)
127+
obj.has("number") -> ObjectValue(obj.get("number").asDouble)
136128
obj.has("bytes") -> ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
137129
else -> throw JsonParseException("ObjectData must have one of the fields: boolean, string, number, or bytes")
138130
}
@@ -179,7 +171,7 @@ internal class ObjectDataMsgpackDeserializer : com.fasterxml.jackson.databind.Js
179171
)
180172
}
181173
node.has("string") -> ObjectValue(node.get("string").asText())
182-
node.has("number") -> ObjectValue(node.get("number").numberValue())
174+
node.has("number") -> ObjectValue(node.get("number").doubleValue())
183175
node.has("bytes") -> ObjectValue(Binary(node.get("bytes").binaryValue()))
184176
else -> throw IllegalArgumentException("ObjectData must have one of the fields: boolean, string, number, or bytes")
185177
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.ably.lib.objects.unit
2+
3+
import io.ably.lib.objects.unit.fixtures.*
4+
import io.ably.lib.types.ProtocolMessage
5+
import io.ably.lib.types.ProtocolSerializer
6+
import kotlinx.coroutines.test.runTest
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Test
9+
import kotlin.test.assertNotNull
10+
11+
class ObjectMessageSerializationTest {
12+
13+
private val objectMessages = arrayOf(
14+
dummyObjectMessageWithStringData(),
15+
dummyObjectMessageWithBinaryData(),
16+
dummyObjectMessageWithNumberData(),
17+
dummyObjectMessageWithBooleanData(),
18+
dummyObjectMessageWithJsonObjectData(),
19+
dummyObjectMessageWithJsonArrayData()
20+
)
21+
22+
@Test
23+
fun testObjectMessageMsgPackSerialization() = runTest {
24+
val protocolMessage = ProtocolMessage()
25+
protocolMessage.action = ProtocolMessage.Action.`object`
26+
protocolMessage.state = objectMessages
27+
28+
// Serialize the ProtocolMessage containing ObjectMessages to MsgPack format
29+
val serializedProtoMsg = ProtocolSerializer.writeMsgpack(protocolMessage)
30+
assertNotNull(serializedProtoMsg)
31+
32+
// Deserialize back to ProtocolMessage
33+
val deserializedProtoMsg = ProtocolSerializer.readMsgpack(serializedProtoMsg)
34+
assertNotNull(deserializedProtoMsg)
35+
36+
deserializedProtoMsg.state.zip(objectMessages).forEach { (actual, expected) ->
37+
assertEquals(expected, actual as? io.ably.lib.objects.ObjectMessage)
38+
}
39+
}
40+
41+
@Test
42+
fun testObjectMessageJsonSerialization() = runTest {
43+
val protocolMessage = ProtocolMessage()
44+
protocolMessage.action = ProtocolMessage.Action.`object`
45+
protocolMessage.state = objectMessages
46+
47+
// Serialize the ProtocolMessage containing ObjectMessages to MsgPack format
48+
val serializedProtoMsg = ProtocolSerializer.writeJSON(protocolMessage).toString(Charsets.UTF_8)
49+
assertNotNull(serializedProtoMsg)
50+
51+
// Deserialize back to ProtocolMessage
52+
val deserializedProtoMsg = ProtocolSerializer.fromJSON(serializedProtoMsg)
53+
assertNotNull(deserializedProtoMsg)
54+
55+
deserializedProtoMsg.state.zip(objectMessages).forEach { (actual, expected) ->
56+
assertEquals(expected, (actual as? io.ably.lib.objects.ObjectMessage))
57+
}
58+
}
59+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import io.ably.lib.objects.ensureMessageSizeWithinLimit
1111
import io.ably.lib.objects.size
1212
import io.ably.lib.transport.Defaults
1313
import io.ably.lib.types.AblyException
14+
import io.ktor.utils.io.core.*
1415
import io.mockk.every
1516
import io.mockk.mockk
1617
import kotlinx.coroutines.test.runTest
1718
import org.junit.Test
1819
import kotlin.test.assertEquals
1920
import kotlin.test.assertFailsWith
21+
import kotlin.text.toByteArray
2022

2123
class ObjectMessageSizeTest {
2224

@@ -79,7 +81,7 @@ class ObjectMessageSizeTest {
7981
), // Total ObjectCounter size: 8 bytes
8082

8183
nonce = "nonce123", // Not counted in operation size
82-
initialValue = "some-value", // Not counted in operation size
84+
initialValue = Binary("some-value".toByteArray()), // Not counted in operation size
8385
), // Total ObjectOperation size: 12 + 8 + 26 + 8 = 54 bytes
8486

8587
objectState = ObjectState(

0 commit comments

Comments
 (0)