Skip to content

Commit 7a6d815

Browse files
committed
Add polymorphic serialization
1 parent 99a83a1 commit 7a6d815

File tree

11 files changed

+163
-21
lines changed

11 files changed

+163
-21
lines changed

firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_decoders.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decode
2020
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index/2].run { if(index % 2 == 0) key else value } }
2121
}
2222
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
23-
}
23+
}
24+
25+
actual fun getPolymorphicType(value: Any?, discriminator: String): String =
26+
(value as Map<*,*>)[discriminator] as String

firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@
44

55
package dev.gitlive.firebase
66

7-
import kotlinx.serialization.descriptors.PolymorphicKind
8-
import kotlinx.serialization.encoding.CompositeEncoder
97
import kotlinx.serialization.descriptors.SerialDescriptor
108
import kotlinx.serialization.descriptors.StructureKind
119
import kotlin.collections.set
1210

13-
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
11+
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1412
StructureKind.LIST -> mutableListOf<Any?>()
1513
.also { value = it }
1614
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
1715
StructureKind.MAP -> mutableListOf<Any?>()
1816
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
19-
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> mutableMapOf<Any?, Any?>()
17+
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
2018
.also { value = it }
21-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
19+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
20+
setPolymorphicType = { discriminator, type ->
21+
it[discriminator] = type
22+
},
23+
set = { _, index, value -> it[descriptor.getElementName(index)] = value }
24+
) }
2225
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
2326
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dev.gitlive.firebase
2+
3+
import kotlinx.serialization.InheritableSerialInfo
4+
5+
@InheritableSerialInfo
6+
@Target(AnnotationTarget.CLASS)
7+
annotation class FirebaseClassDiscriminator(val discriminator: String)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package dev.gitlive.firebase
2+
3+
import kotlinx.serialization.DeserializationStrategy
4+
import kotlinx.serialization.SerializationStrategy
5+
import kotlinx.serialization.descriptors.SerialDescriptor
6+
import kotlinx.serialization.encoding.CompositeDecoder
7+
import kotlinx.serialization.findPolymorphicSerializer
8+
import kotlinx.serialization.internal.AbstractPolymorphicSerializer
9+
10+
11+
@Suppress("UNCHECKED_CAST")
12+
internal fun <T> FirebaseEncoder.encodePolymorphically(
13+
serializer: SerializationStrategy<T>,
14+
value: T,
15+
ifPolymorphic: (String) -> Unit
16+
) {
17+
if (serializer !is AbstractPolymorphicSerializer<*>) {
18+
serializer.serialize(this, value)
19+
return
20+
}
21+
val casted = serializer as AbstractPolymorphicSerializer<Any>
22+
val baseClassDiscriminator = serializer.descriptor.classDiscriminator()
23+
val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
24+
// validateIfSealed(casted, actualSerializer, baseClassDiscriminator)
25+
// checkKind(actualSerializer.descriptor.kind)
26+
ifPolymorphic(baseClassDiscriminator)
27+
actualSerializer.serialize(this, value)
28+
}
29+
30+
31+
32+
@Suppress("UNCHECKED_CAST")
33+
internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
34+
value: Any?,
35+
decodeDouble: (value: Any?) -> Double?,
36+
deserializer: DeserializationStrategy<T>,
37+
): T {
38+
if (deserializer !is AbstractPolymorphicSerializer<*>) {
39+
return deserializer.deserialize(this)
40+
}
41+
42+
val casted = deserializer as AbstractPolymorphicSerializer<Any>
43+
val discriminator = deserializer.descriptor.classDiscriminator()
44+
val type = getPolymorphicType(value, discriminator)
45+
val actualDeserializer = casted.findPolymorphicSerializerOrNull(
46+
structureDecoder(deserializer.descriptor, decodeDouble),
47+
type
48+
) as DeserializationStrategy<T>
49+
return actualDeserializer.deserialize(this)
50+
}
51+
52+
53+
//private fun validateIfSealed(
54+
// serializer: SerializationStrategy<*>,
55+
// actualSerializer: SerializationStrategy<Any>,
56+
// classDiscriminator: String
57+
//) {
58+
// if (serializer !is SealedClassSerializer<*>) return
59+
// @Suppress("DEPRECATION_ERROR")
60+
// if (classDiscriminator in actualSerializer.descriptor.jsonCachedSerialNames()) {
61+
// val baseName = serializer.descriptor.serialName
62+
// val actualName = actualSerializer.descriptor.serialName
63+
// error(
64+
// "Sealed class '$actualName' cannot be serialized as base class '$baseName' because" +
65+
// " it has property name that conflicts with class discriminator '$classDiscriminator'. " +
66+
// "You can either change class discriminator with FirebaseClassDiscriminator annotation or " +
67+
// "rename property with @SerialName annotation"
68+
// )
69+
// }
70+
//}
71+
72+
//internal fun checkKind(kind: SerialKind) {
73+
// if (kind is SerialKind.ENUM) error("Enums cannot be serialized polymorphically with 'type' parameter. You can use 'JsonBuilder.useArrayPolymorphism' instead")
74+
// if (kind is PrimitiveKind) error("Primitives cannot be serialized polymorphically with 'type' parameter. You can use 'JsonBuilder.useArrayPolymorphism' instead")
75+
// if (kind is PolymorphicKind) error("Actual serializer for polymorphic cannot be polymorphic itself")
76+
//}
77+
78+
internal fun SerialDescriptor.classDiscriminator(): String {
79+
// Plain loop is faster than allocation of Sequence or ArrayList
80+
// We can rely on the fact that only one FirebaseClassDiscriminator is present —
81+
// compiler plugin checked that.
82+
for (annotation in annotations) {
83+
if (annotation is FirebaseClassDiscriminator) return annotation.discriminator
84+
}
85+
return "type"
86+
}
87+

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?, decodeDouble:
2525
require(value != null || strategy.descriptor.isNullable) { "Value was null for non-nullable type ${strategy.descriptor.serialName}" }
2626
return FirebaseDecoder(value, decodeDouble).decodeSerializableValue(strategy)
2727
}
28-
2928
expect fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder
29+
expect fun getPolymorphicType(value: Any?, discriminator: String): String
3030

3131
class FirebaseDecoder(internal val value: Any?, private val decodeDouble: (value: Any?) -> Double?) : Decoder {
3232

@@ -59,8 +59,11 @@ class FirebaseDecoder(internal val value: Any?, private val decodeDouble: (value
5959

6060
override fun decodeNull() = decodeNull(value)
6161

62-
@ExperimentalSerializationApi
6362
override fun decodeInline(inlineDescriptor: SerialDescriptor) = FirebaseDecoder(value, decodeDouble)
63+
64+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
65+
return decodeSerializableValuePolymorphic(value, decodeDouble, deserializer)
66+
}
6467
}
6568

6669
class FirebaseClassDecoder(
@@ -80,7 +83,7 @@ class FirebaseClassDecoder(
8083
?: DECODE_DONE
8184
}
8285

83-
open class FirebaseCompositeDecoder constructor(
86+
open class FirebaseCompositeDecoder(
8487
private val decodeDouble: (value: Any?) -> Double?,
8588
private val size: Int,
8689
private val get: (descriptor: SerialDescriptor, index: Int) -> Any?
@@ -134,6 +137,7 @@ open class FirebaseCompositeDecoder constructor(
134137
@ExperimentalSerializationApi
135138
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder =
136139
FirebaseDecoder(get(descriptor, index), decodeDouble)
140+
137141
}
138142

139143
private fun decodeString(value: Any?) = value.toString()

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean, pos
1616
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply { encodeSerializableValue(it.firebaseSerializer(), it) }.value
1717
}
1818

19-
expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder
19+
expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder
2020

2121
class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positiveInfinity: Any) : TimestampEncoder(positiveInfinity), Encoder {
2222

2323
var value: Any? = null
2424

2525
override val serializersModule = EmptySerializersModule
26-
override fun beginStructure(descriptor: SerialDescriptor) = structureEncoder(descriptor)
26+
private var polymorphicDiscriminator: String? = null
27+
28+
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
29+
val encoder = structureEncoder(descriptor)
30+
if (polymorphicDiscriminator != null) {
31+
encoder.encodePolymorphicClassDiscriminator(polymorphicDiscriminator!!, descriptor.serialName)
32+
polymorphicDiscriminator = null
33+
}
34+
return encoder
35+
}
2736

2837
override fun encodeBoolean(value: Boolean) {
2938
this.value = value
@@ -73,9 +82,14 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
7382
this.value = value
7483
}
7584

76-
@ExperimentalSerializationApi
7785
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
7886
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
87+
88+
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
89+
encodePolymorphically(serializer, value) {
90+
polymorphicDiscriminator = it
91+
}
92+
}
7993
}
8094

8195
abstract class TimestampEncoder(internal val positiveInfinity: Any) {
@@ -89,7 +103,8 @@ open class FirebaseCompositeEncoder constructor(
89103
private val shouldEncodeElementDefault: Boolean,
90104
positiveInfinity: Any,
91105
private val end: () -> Unit = {},
92-
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit
106+
private val setPolymorphicType: (String, String) -> Unit = { _, _ -> },
107+
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit,
93108
): TimestampEncoder(positiveInfinity), CompositeEncoder {
94109

95110
override val serializersModule = EmptySerializersModule
@@ -153,6 +168,9 @@ open class FirebaseCompositeEncoder constructor(
153168
@ExperimentalSerializationApi
154169
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder =
155170
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
156-
}
157171

172+
fun encodePolymorphicClassDiscriminator(discriminator: String, type: String) {
173+
setPolymorphicType(discriminator, type)
174+
}
175+
}
158176

firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package dev.gitlive.firebase
77
import kotlinx.serialization.SerialName
88
import kotlinx.serialization.Serializable
99
import kotlinx.serialization.builtins.ListSerializer
10+
import kotlinx.serialization.json.JsonClassDiscriminator
1011
import kotlin.test.Test
1112
import kotlin.test.assertEquals
1213
import kotlin.test.assertNull
@@ -48,7 +49,7 @@ class EncodersTest {
4849
@Test
4950
fun encodeSealedClass() {
5051
val encoded = encode<TestSealed>(TestSealed.serializer(), TestSealed.ChildClass(mapOf("key" to "value"), true), shouldEncodeElementDefault = true)
51-
nativeAssertEquals(nativeMapOf("type" to "child", "value" to nativeMapOf("map" to nativeMapOf("key" to "value"), "bool" to true)), encoded)
52+
nativeAssertEquals(nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true), encoded)
5253
}
5354

5455
@Test
@@ -71,7 +72,7 @@ class EncodersTest {
7172

7273
@Test
7374
fun decodeSealedClass() {
74-
val decoded = decode(TestSealed.serializer(), nativeMapOf("type" to "child", "value" to nativeMapOf("map" to nativeMapOf("key" to "value"), "bool" to true)))
75+
val decoded = decode(TestSealed.serializer(), nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true))
7576
assertEquals(TestSealed.ChildClass(mapOf("key" to "value"), true), decoded)
7677
}
7778
}

firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_decoders.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decode
2020
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index/2].run { if(index % 2 == 0) key else value } }
2121
}
2222
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
23-
}
23+
}
24+
25+
actual fun getPolymorphicType(value: Any?, discriminator: String): String =
26+
(value as Map<*,*>)[discriminator] as String

firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ import kotlinx.serialization.descriptors.SerialDescriptor
1010
import kotlinx.serialization.descriptors.StructureKind
1111
import kotlin.collections.set
1212

13-
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
13+
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1414
StructureKind.LIST -> mutableListOf<Any?>()
1515
.also { value = it }
1616
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
1717
StructureKind.MAP -> mutableListOf<Any?>()
1818
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
1919
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> mutableMapOf<Any?, Any?>()
2020
.also { value = it }
21-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
21+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
22+
setPolymorphicType = { discriminator, type ->
23+
it[discriminator] = type
24+
},
25+
set = { _, index, value -> it[descriptor.getElementName(index)] = value }
26+
) }
2227
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
2328
}

firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_decoders.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decode
2525
}
2626
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
2727
}
28+
29+
@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
30+
actual fun getPolymorphicType(value: Any?, discriminator: String): String =
31+
(value as Json)[discriminator] as String

0 commit comments

Comments
 (0)