Skip to content

Commit

Permalink
Merge branch 'SerializerSealedClass'
Browse files Browse the repository at this point in the history
  • Loading branch information
janseeger committed Mar 14, 2024
2 parents da8ba51 + 114f68b commit 278aa55
Show file tree
Hide file tree
Showing 38 changed files with 237 additions and 712 deletions.
4 changes: 2 additions & 2 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ plugins {
version = versionString

android {
namespace = "de.sipgate.federmappe.realtimedb"
namespace = "de.sipgate.federmappe.common"
compileSdk = 34
defaultConfig.minSdk = 23

Expand Down Expand Up @@ -63,7 +63,7 @@ fun Properties.parseInt(key: String) = (this[key] as String).toInt()
publishing {
publications.register<MavenPublication>("release") {
groupId = "de.sipgate"
artifactId = "federmappe-realtimedb"
artifactId = "federmappe-common"
version = project.version.toString()

pom {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import de.sipgate.federmappe.firestore.serializers.DateSerializer
import de.sipgate.federmappe.firestore.serializers.UriSerializer
import de.sipgate.federmappe.common.serializers.DateSerializer
import de.sipgate.federmappe.common.serializers.UriSerializer
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import com.google.firebase.Timestamp
import de.sipgate.federmappe.common.decodeEnum
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
Expand All @@ -16,6 +14,7 @@ class ListDecoder(
private val list: ArrayDeque<Any>,
private val elementsCount: Int = 0,
override val serializersModule: SerializersModule = EmptySerializersModule(),
private val subtypeDecoder: (Any?) -> CompositeDecoder? = {null}
) : AbstractDecoder() {
private var index = 0

Expand Down Expand Up @@ -44,8 +43,9 @@ class ListDecoder(
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
val value = list.removeFirst()

if (value is Timestamp) {
return FirebaseTimestampDecoder(timestamp = value)
val decoder = subtypeDecoder(value)
if (decoder != null) {
return decoder
}

when (descriptor.kind) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import android.util.Log
import de.sipgate.federmappe.common.decodeEnum
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package de.sipgate.federmappe.common

import kotlin.reflect.KClass
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.serializerOrNull

@ExperimentalSerializationApi
@InternalSerializationApi
abstract class SealedClassWithTypeSerializer<Type : Any, DiscriminatorType>(
private val baseClass: KClass<Type>,
private val discriminatorName: String = "type"
) : KSerializer<Type> {

override val descriptor: SerialDescriptor =
buildSerialDescriptor(
"JsonContentPolymorphicSerializer<${baseClass.simpleName}>",
PolymorphicKind.SEALED
)

final override fun serialize(encoder: Encoder, value: Type) {
val actualSerializer =
encoder.serializersModule.getPolymorphic(baseClass, value)
?: value::class.serializerOrNull()
?: throwSubtypeNotRegistered(value::class, baseClass)
@Suppress("UNCHECKED_CAST")
(actualSerializer as KSerializer<Type>).serialize(encoder, value)
}

final override fun deserialize(decoder: Decoder): Type {
val type = (decoder as? TypeAwareDecoder)
?.decodeType<DiscriminatorType>(typeKey = discriminatorName)
?: throw IllegalStateException("We need to know the type to decode first!")

val actualSerializer = selectDeserializer(type) as KSerializer<Type>
return decoder.decodeSerializableValue(actualSerializer)
}

protected abstract fun selectDeserializer(element: DiscriminatorType): DeserializationStrategy<Type>

private fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing {
val subClassName = subClass.simpleName ?: "$subClass"
val scope = "in the scope of '${baseClass.simpleName}'"
throw SerializationException(
"Class '${subClassName}' is not registered for polymorphic serialization $scope.\n" +
"Mark the base class as 'sealed' or register the serializer explicitly."
)
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer

@ExperimentalSerializationApi
inline fun <reified T : Any> Map<String, Any>.toObjectWithSerializer(
serializer: KSerializer<T> = serializer<T>(),
customSerializers: SerializersModule
customSerializers: SerializersModule,
noinline subtypeDecoder: (Any?) -> CompositeDecoder? = {null}
): T = serializer.deserialize(
StringMapToObjectDecoder(
this,
ignoreUnknownProperties = true,
serializersModule = customSerializers,
subtypeDecoder = subtypeDecoder
),
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import com.google.firebase.Timestamp
import de.sipgate.federmappe.common.decodeEnum
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.AbstractDecoder
Expand All @@ -16,7 +15,8 @@ class StringMapToObjectDecoder(
private val data: Map<String, Any?>,
override val serializersModule: SerializersModule = EmptySerializersModule(),
private val ignoreUnknownProperties: Boolean = false,
) : AbstractDecoder() {
private val subtypeDecoder: (Any?) -> CompositeDecoder? = { null }
) : AbstractDecoder(), TypeAwareDecoder {
private val keysIterator = data.keys.iterator()
private var index: Int? = null
private var key: String? = null
Expand Down Expand Up @@ -59,12 +59,13 @@ class StringMapToObjectDecoder(
val value = data[key]
val valueDescriptor = descriptor.kind

if (value is Timestamp) {
return FirebaseTimestampDecoder(timestamp = value)
val decoder = subtypeDecoder(value)
if (decoder != null) {
return decoder
}

when (valueDescriptor) {
StructureKind.CLASS -> return StringMapToObjectDecoder(
StructureKind.CLASS, PolymorphicKind.SEALED -> return StringMapToObjectDecoder(
data = value as Map<String, Any>,
ignoreUnknownProperties = ignoreUnknownProperties,
serializersModule = this.serializersModule,
Expand All @@ -76,11 +77,18 @@ class StringMapToObjectDecoder(
)

StructureKind.LIST -> {
val list = (value as Iterable<Any>).toCollection(mutableListOf())
return ListDecoder(ArrayDeque(list), list.size, serializersModule)
val list = (value as Iterable<Any>)
.toCollection(mutableListOf())

return ListDecoder(
list = ArrayDeque(list),
elementsCount = list.size,
serializersModule = serializersModule,
subtypeDecoder = subtypeDecoder
)
}

else -> throw SerializationException("Given value is neither a list nor a type! value: $value, type: ${value?.let { it::class.qualifiedName } ?: "null"}")
else -> throw SerializationException("${key ?: "root"} was expected to be of type $valueDescriptor, but was $value")
}
}

Expand All @@ -91,4 +99,10 @@ class StringMapToObjectDecoder(

super.endStructure(descriptor)
}

override fun <T> decodeType(typeKey: String): T? {
@Suppress("UNCHECKED_CAST")
val currentData = (data[key] as? Map<String, Any>) ?: return null
return (currentData[typeKey] as? T)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.sipgate.federmappe.common

interface TypeAwareDecoder {
fun <T> decodeType(typeKey: String = "type"): T?
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore.serializers
package de.sipgate.federmappe.common.serializers

import java.util.Date
import kotlinx.serialization.KSerializer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore.serializers
package de.sipgate.federmappe.common.serializers

import java.net.URI
import kotlinx.serialization.ExperimentalSerializationApi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.sipgate.federmappe.common

import de.sipgate.federmappe.common.SealedTypeTests.BaseType
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import kotlinx.serialization.serializer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Test

@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
internal class CustomSerializer : SealedClassWithTypeSerializer<BaseType, String>(BaseType::class) {
override fun selectDeserializer(element: String): DeserializationStrategy<BaseType> {
return when (element) {
"A" -> BaseType.A.serializer()
"B" -> BaseType.B.serializer()
else -> throw IllegalArgumentException("unknown element $element")
}
}
}

@OptIn(ExperimentalSerializationApi::class)
internal class SealedTypeTests {

@Serializable(with = CustomSerializer::class)
sealed interface BaseType {
val type: String

@Serializable
data class A(val value: String) : BaseType {
override val type = "A"
}

@Serializable
data class B(val value: Boolean) : BaseType {
override val type = "B"
}
}

@Test
fun deserializeBasicDataClassWithBooleanFieldSetToTrue() {
// Arrange
@Serializable
data class TestClass(val a: BaseType)

val serializer = serializer<TestClass>()
val data = mapOf<String, Any?>(
"a" to mapOf(
"type" to "A",
"value" to "some string"
)
)

// Act
val result =
serializer.deserialize(
StringMapToObjectDecoder(
data,
ignoreUnknownProperties = true,
serializersModule = SerializersModule {
polymorphic(BaseType::class) {
subclass(BaseType.A::class)
subclass(BaseType.B::class)
}
}
),
)

// Assert
assertInstanceOf(TestClass::class.java, result)
assertInstanceOf(BaseType.A::class.java, result.a)
assertEquals("some string", (result.a as BaseType.A).value)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import java.util.stream.Stream
import kotlin.reflect.KClass
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Assertions.assertNotNull
Expand All @@ -12,7 +11,7 @@ import org.junit.jupiter.api.Test
@OptIn(ExperimentalSerializationApi::class)
class SubclassDecodingTests {
@Serializable
data class A(val b: String)
internal data class A(val b: String)

@Test
fun deserializeNestedDataClass() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.sipgate.federmappe.firestore
package de.sipgate.federmappe.common

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.sipgate.federmappe.common.serializers

import de.sipgate.federmappe.firestore.StringMapToObjectDecoder
import de.sipgate.federmappe.firestore.serializers.UriSerializer
import de.sipgate.federmappe.common.StringMapToObjectDecoder
import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
Expand Down
Loading

0 comments on commit 278aa55

Please sign in to comment.