Skip to content

Commit c4f91d5

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 c4f91d5

File tree

5 files changed

+263
-26
lines changed

5 files changed

+263
-26
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+
* 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/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.readObjectMessage(): 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().readObjectMessage() }
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+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package io.ably.lib.objects.unit.fixtures
2+
3+
import com.google.gson.JsonArray
4+
import com.google.gson.JsonObject
5+
import io.ably.lib.objects.*
6+
import io.ably.lib.objects.Binary
7+
import io.ably.lib.objects.ObjectData
8+
import io.ably.lib.objects.ObjectMessage
9+
import io.ably.lib.objects.ObjectState
10+
import io.ably.lib.objects.ObjectValue
11+
12+
internal val dummyObjectDataStringValue = ObjectData(objectId = "object-id", ObjectValue("dummy string"))
13+
14+
internal val dummyBinaryObjectValue = ObjectData(objectId = "object-id", ObjectValue(Binary(byteArrayOf(1, 2, 3))))
15+
16+
internal val dummyNumberObjectValue = ObjectData(objectId = "object-id", ObjectValue(42.0))
17+
18+
internal val dummyBooleanObjectValue = ObjectData(objectId = "object-id", ObjectValue(true))
19+
20+
val dummyJsonObject = JsonObject().apply { addProperty("foo", "bar") }
21+
internal val dummyJsonObjectValue = ObjectData(objectId = "object-id", ObjectValue(dummyJsonObject))
22+
23+
val dummyJsonArray = JsonArray().apply { add(1); add(2); add(3) }
24+
internal val dummyJsonArrayValue = ObjectData(objectId = "object-id", ObjectValue(dummyJsonArray))
25+
26+
internal val dummyObjectMapEntry = ObjectMapEntry(
27+
tombstone = false,
28+
timeserial = "dummy-timeserial",
29+
data = dummyObjectDataStringValue
30+
)
31+
32+
internal val dummyObjectMap = ObjectMap(
33+
semantics = MapSemantics.LWW,
34+
entries = mapOf("dummy-key" to dummyObjectMapEntry)
35+
)
36+
37+
internal val dummyObjectCounter = ObjectCounter(
38+
count = 123.0
39+
)
40+
41+
internal val dummyObjectMapOp = ObjectMapOp(
42+
key = "dummy-key",
43+
data = dummyObjectDataStringValue
44+
)
45+
46+
internal val dummyObjectCounterOp = ObjectCounterOp(
47+
amount = 10.0
48+
)
49+
50+
internal val dummyObjectOperation = ObjectOperation(
51+
action = ObjectOperationAction.MapCreate,
52+
objectId = "dummy-object-id",
53+
mapOp = dummyObjectMapOp,
54+
counterOp = dummyObjectCounterOp,
55+
map = dummyObjectMap,
56+
counter = dummyObjectCounter,
57+
nonce = "dummy-nonce",
58+
initialValue = "{\"foo\":\"bar\"}"
59+
)
60+
61+
internal val dummyObjectState = ObjectState(
62+
objectId = "dummy-object-id",
63+
siteTimeserials = mapOf("site1" to "serial1"),
64+
tombstone = false,
65+
createOp = dummyObjectOperation,
66+
map = dummyObjectMap,
67+
counter = dummyObjectCounter
68+
)
69+
70+
internal val dummyObjectMessage = ObjectMessage(
71+
id = "dummy-id",
72+
timestamp = 1234567890L,
73+
clientId = "dummy-client-id",
74+
connectionId = "dummy-connection-id",
75+
extras = mapOf("meta" to "data"),
76+
operation = dummyObjectOperation,
77+
objectState = dummyObjectState,
78+
serial = "dummy-serial",
79+
siteCode = "dummy-site-code"
80+
)
81+
82+
internal fun dummyObjectMessageWithStringData(): ObjectMessage {
83+
return dummyObjectMessage
84+
}
85+
86+
internal fun dummyObjectMessageWithBinaryData(): ObjectMessage {
87+
val binaryObjectMapEntry = dummyObjectMapEntry.copy(data = dummyBinaryObjectValue)
88+
val binaryObjectMap = dummyObjectMap.copy(entries = mapOf("dummy-key" to binaryObjectMapEntry))
89+
val binaryObjectMapOp = dummyObjectMapOp.copy(data = dummyBinaryObjectValue)
90+
val binaryObjectOperation = dummyObjectOperation.copy(
91+
mapOp = binaryObjectMapOp,
92+
map = binaryObjectMap
93+
)
94+
val binaryObjectState = dummyObjectState.copy(
95+
map = binaryObjectMap,
96+
createOp = binaryObjectOperation
97+
)
98+
return dummyObjectMessage.copy(
99+
operation = binaryObjectOperation,
100+
objectState = binaryObjectState
101+
)
102+
}
103+
104+
internal fun dummyObjectMessageWithNumberData(): ObjectMessage {
105+
val numberObjectMapEntry = dummyObjectMapEntry.copy(data = dummyNumberObjectValue)
106+
val numberObjectMap = dummyObjectMap.copy(entries = mapOf("dummy-key" to numberObjectMapEntry))
107+
val numberObjectMapOp = dummyObjectMapOp.copy(data = dummyNumberObjectValue)
108+
val numberObjectOperation = dummyObjectOperation.copy(
109+
mapOp = numberObjectMapOp,
110+
map = numberObjectMap
111+
)
112+
val numberObjectState = dummyObjectState.copy(
113+
map = numberObjectMap,
114+
createOp = numberObjectOperation
115+
)
116+
return dummyObjectMessage.copy(
117+
operation = numberObjectOperation,
118+
objectState = numberObjectState
119+
)
120+
}
121+
122+
internal fun dummyObjectMessageWithBooleanData(): ObjectMessage {
123+
val booleanObjectMapEntry = dummyObjectMapEntry.copy(data = dummyBooleanObjectValue)
124+
val booleanObjectMap = dummyObjectMap.copy(entries = mapOf("dummy-key" to booleanObjectMapEntry))
125+
val booleanObjectMapOp = dummyObjectMapOp.copy(data = dummyBooleanObjectValue)
126+
val booleanObjectOperation = dummyObjectOperation.copy(
127+
mapOp = booleanObjectMapOp,
128+
map = booleanObjectMap
129+
)
130+
val booleanObjectState = dummyObjectState.copy(
131+
map = booleanObjectMap,
132+
createOp = booleanObjectOperation
133+
)
134+
return dummyObjectMessage.copy(
135+
operation = booleanObjectOperation,
136+
objectState = booleanObjectState
137+
)
138+
}
139+
140+
internal fun dummyObjectMessageWithJsonObjectData(): ObjectMessage {
141+
val jsonObjectMapEntry = dummyObjectMapEntry.copy(data = dummyJsonObjectValue)
142+
val jsonObjectMap = dummyObjectMap.copy(entries = mapOf("dummy-key" to jsonObjectMapEntry))
143+
val jsonObjectMapOp = dummyObjectMapOp.copy(data = dummyJsonObjectValue)
144+
val jsonObjectOperation = dummyObjectOperation.copy(
145+
mapOp = jsonObjectMapOp,
146+
map = jsonObjectMap
147+
)
148+
val jsonObjectState = dummyObjectState.copy(
149+
map = jsonObjectMap,
150+
createOp = jsonObjectOperation
151+
)
152+
return dummyObjectMessage.copy(
153+
operation = jsonObjectOperation,
154+
objectState = jsonObjectState
155+
)
156+
}
157+
158+
internal fun dummyObjectMessageWithJsonArrayData(): ObjectMessage {
159+
val jsonArrayMapEntry = dummyObjectMapEntry.copy(data = dummyJsonArrayValue)
160+
val jsonArrayMap = dummyObjectMap.copy(entries = mapOf("dummy-key" to jsonArrayMapEntry))
161+
val jsonArrayMapOp = dummyObjectMapOp.copy(data = dummyJsonArrayValue)
162+
val jsonArrayOperation = dummyObjectOperation.copy(
163+
mapOp = jsonArrayMapOp,
164+
map = jsonArrayMap
165+
)
166+
val jsonArrayState = dummyObjectState.copy(
167+
map = jsonArrayMap,
168+
createOp = jsonArrayOperation
169+
)
170+
return dummyObjectMessage.copy(
171+
operation = jsonArrayOperation,
172+
objectState = jsonArrayState
173+
)
174+
}

0 commit comments

Comments
 (0)