Skip to content

Commit 32b1a36

Browse files
committed
[ECO-5426] Updated ObjectValue to have compile time type safety instead of
checking type at runtime
1 parent 4b084f8 commit 32b1a36

6 files changed

Lines changed: 119 additions & 123 deletions

File tree

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

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.ably.lib.objects
22

3-
import com.google.gson.JsonArray
43
import com.google.gson.JsonObject
54

65
import com.google.gson.annotations.JsonAdapter
@@ -52,28 +51,18 @@ internal data class ObjectData(
5251

5352
/**
5453
* Represents a value that can be a String, Number, Boolean, Binary, JsonObject or JsonArray.
55-
* Performs a type check on initialization.
54+
* Provides compile-time type safety through sealed class pattern.
5655
* Spec: OD2c
5756
*/
58-
internal data class ObjectValue(
59-
/**
60-
* The concrete value of the object. Can be a String, Number, Boolean, Binary, JsonObject or JsonArray.
61-
* Spec: OD2c
62-
*/
63-
val value: Any,
64-
) {
65-
init {
66-
require(
67-
value is String ||
68-
value is Number ||
69-
value is Boolean ||
70-
value is Binary ||
71-
value is JsonObject ||
72-
value is JsonArray
73-
) {
74-
"value must be String, Number, Boolean, Binary, JsonObject or JsonArray"
75-
}
76-
}
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()
7766
}
7867

7968
/**
@@ -444,13 +433,12 @@ private fun ObjectData.size(): Int {
444433
* Spec: OD3*
445434
*/
446435
private fun ObjectValue.size(): Int {
447-
return when (value) {
448-
is Boolean -> 1 // Spec: OD3b
449-
is Binary -> value.size() // Spec: OD3c
450-
is Number -> 8 // Spec: OD3d
451-
is String -> value.byteSize // Spec: OD3e
452-
is JsonObject, is JsonArray -> value.toString().byteSize // Spec: OD3e
453-
else -> 0 // Spec: OD3f
436+
return when (this) {
437+
is ObjectValue.Boolean -> 1 // Spec: OD3b
438+
is ObjectValue.Binary -> value.size() // Spec: OD3c
439+
is ObjectValue.Number -> 8 // Spec: OD3d
440+
is ObjectValue.String -> value.byteSize // Spec: OD3e
441+
is ObjectValue.JsonObject, is ObjectValue.JsonArray -> value.toString().byteSize // Spec: OD3e
454442
}
455443
}
456444

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeseri
4646
val obj = JsonObject()
4747
src.objectId?.let { obj.addProperty("objectId", it) }
4848

49-
src.value?.let { value ->
50-
when (val v = value.value) {
51-
is Boolean -> obj.addProperty("boolean", v)
52-
is String -> obj.addProperty("string", v)
53-
is Number -> obj.addProperty("number", v.toDouble())
54-
is Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.data))
55-
is JsonObject, is JsonArray -> obj.addProperty("json", v.toString()) // Spec: OD4c5
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
5656
}
5757
}
5858
return obj
@@ -62,11 +62,18 @@ internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeseri
6262
val obj = if (json.isJsonObject) json.asJsonObject else throw JsonParseException("Expected JsonObject")
6363
val objectId = if (obj.has("objectId")) obj.get("objectId").asString else null
6464
val value = when {
65-
obj.has("boolean") -> ObjectValue(obj.get("boolean").asBoolean)
66-
obj.has("string") -> ObjectValue(obj.get("string").asString)
67-
obj.has("number") -> ObjectValue(obj.get("number").asDouble)
68-
obj.has("bytes") -> ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
69-
obj.has("json") -> ObjectValue(JsonParser.parseString(obj.get("json").asString))
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+
}
7077
else -> {
7178
if (objectId != null)
7279
null

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

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -625,28 +625,32 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) {
625625
packer.packString(objectId)
626626
}
627627

628-
if (value != null) {
629-
when (val v = value.value) {
630-
is Boolean -> {
628+
value?.let { v ->
629+
when (v) {
630+
is ObjectValue.Boolean -> {
631631
packer.packString("boolean")
632-
packer.packBoolean(v)
632+
packer.packBoolean(v.value)
633633
}
634-
is String -> {
634+
is ObjectValue.String -> {
635635
packer.packString("string")
636-
packer.packString(v)
636+
packer.packString(v.value)
637637
}
638-
is Number -> {
638+
is ObjectValue.Number -> {
639639
packer.packString("number")
640-
packer.packDouble(v.toDouble())
640+
packer.packDouble(v.value.toDouble())
641641
}
642-
is Binary -> {
642+
is ObjectValue.Binary -> {
643643
packer.packString("bytes")
644-
packer.packBinaryHeader(v.data.size)
645-
packer.writePayload(v.data)
644+
packer.packBinaryHeader(v.value.data.size)
645+
packer.writePayload(v.value.data)
646646
}
647-
is JsonObject, is JsonArray -> {
647+
is ObjectValue.JsonObject -> {
648648
packer.packString("json")
649-
packer.packString(v.toString())
649+
packer.packString(v.value.toString())
650+
}
651+
is ObjectValue.JsonArray -> {
652+
packer.packString("json")
653+
packer.packString(v.value.toString())
650654
}
651655
}
652656
}
@@ -671,26 +675,24 @@ private fun readObjectData(unpacker: MessageUnpacker): ObjectData {
671675

672676
when (fieldName) {
673677
"objectId" -> objectId = unpacker.unpackString()
674-
"boolean" -> value = ObjectValue(unpacker.unpackBoolean())
675-
"string" -> value = ObjectValue(unpacker.unpackString())
676-
"number" -> value = ObjectValue(unpacker.unpackDouble())
678+
"boolean" -> value = ObjectValue.Boolean(unpacker.unpackBoolean())
679+
"string" -> value = ObjectValue.String(unpacker.unpackString())
680+
"number" -> value = ObjectValue.Number(unpacker.unpackDouble())
677681
"bytes" -> {
678682
val size = unpacker.unpackBinaryHeader()
679683
val bytes = ByteArray(size)
680684
unpacker.readPayload(bytes)
681-
value = ObjectValue(Binary(bytes))
685+
value = ObjectValue.Binary(Binary(bytes))
682686
}
683687
"json" -> {
684688
val jsonString = unpacker.unpackString()
685689
val parsed = JsonParser.parseString(jsonString)
686-
value = ObjectValue(
687-
when {
688-
parsed.isJsonObject -> parsed.asJsonObject
689-
parsed.isJsonArray -> parsed.asJsonArray
690-
else ->
691-
throw ablyException("Invalid JSON string for json field", ErrorCode.MapValueDataTypeUnsupported, HttpStatusCode.InternalServerError)
692-
}
693-
)
690+
value = when {
691+
parsed.isJsonObject -> ObjectValue.JsonObject(parsed.asJsonObject)
692+
parsed.isJsonArray -> ObjectValue.JsonArray(parsed.asJsonArray)
693+
else ->
694+
throw ablyException("Invalid JSON string for json field", ErrorCode.MapValueDataTypeUnsupported, HttpStatusCode.InternalServerError)
695+
}
694696
}
695697
else -> unpacker.skipValue()
696698
}

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import kotlinx.coroutines.test.runTest
1818
import org.junit.Test
1919
import kotlin.test.assertEquals
2020
import kotlin.test.assertFailsWith
21-
import kotlin.text.toByteArray
2221

2322
class ObjectMessageSizeTest {
2423

@@ -47,7 +46,7 @@ class ObjectMessageSizeTest {
4746
key = "mapKey", // Size: 6 bytes (UTF-8 byte length)
4847
data = ObjectData(
4948
objectId = "ref_obj", // Not counted in data size
50-
value = ObjectValue("sample") // Size: 6 bytes (UTF-8 byte length)
49+
value = ObjectValue.String("sample") // Size: 6 bytes (UTF-8 byte length)
5150
) // Total ObjectData size: 6 bytes
5251
), // Total ObjectMapOp size: 6 + 6 = 12 bytes
5352

@@ -64,12 +63,12 @@ class ObjectMessageSizeTest {
6463
tombstone = false, // Not counted in entry size
6564
timeserial = "ts_123", // Not counted in entry size
6665
data = ObjectData(
67-
value = ObjectValue("value1") // Size: 6 bytes
66+
value = ObjectValue.String("value1") // Size: 6 bytes
6867
) // ObjectMapEntry size: 6 bytes
6968
), // Total for this entry: 6 (key) + 6 (entry) = 12 bytes
7069
"entry2" to ObjectMapEntry( // Key size: 6 bytes
7170
data = ObjectData(
72-
value = ObjectValue(42) // Size: 8 bytes (number)
71+
value = ObjectValue.Number(42) // Size: 8 bytes (number)
7372
) // ObjectMapEntry size: 8 bytes
7473
) // Total for this entry: 6 (key) + 8 (entry) = 14 bytes
7574
) // Total entries size: 12 + 14 = 26 bytes
@@ -96,7 +95,7 @@ class ObjectMessageSizeTest {
9695
mapOp = ObjectMapOp(
9796
key = "createKey", // Size: 9 bytes
9897
data = ObjectData(
99-
value = ObjectValue("createValue") // Size: 11 bytes
98+
value = ObjectValue.String("createValue") // Size: 11 bytes
10099
) // ObjectData size: 11 bytes
101100
) // ObjectMapOp size: 9 + 11 = 20 bytes
102101
), // Total createOp size: 20 bytes
@@ -106,7 +105,7 @@ class ObjectMessageSizeTest {
106105
entries = mapOf(
107106
"stateKey" to ObjectMapEntry( // Key size: 8 bytes
108107
data = ObjectData(
109-
value = ObjectValue("stateValue") // Size: 10 bytes
108+
value = ObjectValue.String("stateValue") // Size: 10 bytes
110109
) // ObjectMapEntry size: 10 bytes
111110
) // Total: 8 + 10 = 18 bytes
112111
)
@@ -139,7 +138,7 @@ class ObjectMessageSizeTest {
139138
mapOp = ObjectMapOp(
140139
key = "",
141140
data = ObjectData(
142-
value = ObjectValue("你😊") // 你 -> 3 bytes, 😊 -> 4 bytes
141+
value = ObjectValue.String("你😊") // 你 -> 3 bytes, 😊 -> 4 bytes
143142
),
144143
),
145144
)

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ import io.ably.lib.objects.ObjectMessage
99
import io.ably.lib.objects.ObjectState
1010
import io.ably.lib.objects.ObjectValue
1111

12-
internal val dummyObjectDataStringValue = ObjectData(objectId = "object-id", ObjectValue("dummy string"))
12+
internal val dummyObjectDataStringValue = ObjectData(objectId = "object-id", ObjectValue.String("dummy string"))
1313

14-
internal val dummyBinaryObjectValue = ObjectData(objectId = "object-id", ObjectValue(Binary(byteArrayOf(1, 2, 3))))
14+
internal val dummyBinaryObjectValue = ObjectData(objectId = "object-id", ObjectValue.Binary(Binary(byteArrayOf(1, 2, 3))))
1515

16-
internal val dummyNumberObjectValue = ObjectData(objectId = "object-id", ObjectValue(42.0))
16+
internal val dummyNumberObjectValue = ObjectData(objectId = "object-id", ObjectValue.Number(42.0))
1717

18-
internal val dummyBooleanObjectValue = ObjectData(objectId = "object-id", ObjectValue(true))
18+
internal val dummyBooleanObjectValue = ObjectData(objectId = "object-id", ObjectValue.Boolean(true))
1919

2020
val dummyJsonObject = JsonObject().apply { addProperty("foo", "bar") }
21-
internal val dummyJsonObjectValue = ObjectData(objectId = "object-id", ObjectValue(dummyJsonObject))
21+
internal val dummyJsonObjectValue = ObjectData(objectId = "object-id", ObjectValue.JsonObject(dummyJsonObject))
2222

2323
val dummyJsonArray = JsonArray().apply { add(1); add(2); add(3) }
24-
internal val dummyJsonArrayValue = ObjectData(objectId = "object-id", ObjectValue(dummyJsonArray))
24+
internal val dummyJsonArrayValue = ObjectData(objectId = "object-id", ObjectValue.JsonArray(dummyJsonArray))
2525

2626
internal val dummyObjectMapEntry = ObjectMapEntry(
2727
tombstone = false,

0 commit comments

Comments
 (0)