From 31d0d76beb1fb69b70aea728e3695f451381ce96 Mon Sep 17 00:00:00 2001 From: allinelara Date: Wed, 13 Mar 2024 15:23:51 +0100 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=9A=A7=20serializer=20selead=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/build.gradle.kts | 22 ++++---- .../common/serializers/UriSerializer.kt | 2 +- .../common/serializers/UriSerializerTest.kt | 1 - firestore/build.gradle.kts | 18 +++---- .../firestore/DefaultSerializersModule.kt | 2 +- realtimedb/build.gradle.kts | 18 +++---- .../DataSnapshotPolymorphicSerializer.kt | 51 ++++++++++++++++++ .../realtimedb/PolymorphicDecoder.kt | 52 +++++++++++++++++++ .../federmappe/realtimedb/SnapshotDecoder.kt | 13 ++++- 9 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt create mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 3a004e6..2f5354e 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -5,13 +5,13 @@ plugins { alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kotlin.serialization) `maven-publish` - signing + // signing } version = versionString android { - namespace = "de.sipgate.federmappe.realtimedb" + namespace = "de.sipgate.federmappe.common" compileSdk = 34 defaultConfig.minSdk = 23 @@ -53,7 +53,7 @@ val Project.versionString: String return versionProperties.inputStream().use { inputStream -> Properties().run { load(inputStream) - "${parseInt("majorVersion")}.${parseInt("minorVersion")}.${parseInt("patchVersion")}" + "${parseInt("majorVersion")}.${parseInt("minorVersion")}.${parseInt("patchVersion")}-SNAPSHOT" } } } @@ -63,7 +63,7 @@ fun Properties.parseInt(key: String) = (this[key] as String).toInt() publishing { publications.register("release") { groupId = "de.sipgate" - artifactId = "federmappe-realtimedb" + artifactId = "federmappe-common" version = project.version.toString() pom { @@ -99,10 +99,10 @@ publishing { } } } - -signing { - val signingKey: String? by project - val signingPassword: String? by project - useInMemoryPgpKeys(signingKey, signingPassword) - sign(publishing.publications) -} +// +//signing { +// val signingKey: String? by project +// val signingPassword: String? by project +// useInMemoryPgpKeys(signingKey, signingPassword) +// sign(publishing.publications) +//} diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/serializers/UriSerializer.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/serializers/UriSerializer.kt index 1a358f0..3dd884c 100644 --- a/common/src/main/kotlin/de/sipgate/federmappe/common/serializers/UriSerializer.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/serializers/UriSerializer.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore.serializers +package de.sipgate.federmappe.common.serializers import java.net.URI import kotlinx.serialization.ExperimentalSerializationApi diff --git a/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt index 69e25e8..4d12040 100644 --- a/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt @@ -1,7 +1,6 @@ package de.sipgate.federmappe.common.serializers import de.sipgate.federmappe.firestore.StringMapToObjectDecoder -import de.sipgate.federmappe.firestore.serializers.UriSerializer import kotlinx.serialization.Contextual import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index b07b723..be54350 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -5,7 +5,7 @@ plugins { alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kotlin.serialization) `maven-publish` - signing + // signing } version = versionString @@ -31,7 +31,7 @@ android { } dependencies { - implementation(project(":common")) + api(project(":common")) compileOnly(libs.kotlinx.serialization) implementation(libs.kotlinx.datetime) compileOnly(libs.firebase.firestore) @@ -53,7 +53,7 @@ val Project.versionString: String return versionProperties.inputStream().use { inputStream -> Properties().run { load(inputStream) - "${parseInt("majorVersion")}.${parseInt("minorVersion")}.${parseInt("patchVersion")}" + "${parseInt("majorVersion")}.${parseInt("minorVersion")}.${parseInt("patchVersion")}-SNAPSHOT" } } } @@ -100,9 +100,9 @@ publishing { } } -signing { - val signingKey: String? by project - val signingPassword: String? by project - useInMemoryPgpKeys(signingKey, signingPassword) - sign(publishing.publications) -} +//signing { +// val signingKey: String? by project +// val signingPassword: String? by project +// useInMemoryPgpKeys(signingKey, signingPassword) +// sign(publishing.publications) +//} diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/DefaultSerializersModule.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/DefaultSerializersModule.kt index c55759c..6b04766 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/DefaultSerializersModule.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/DefaultSerializersModule.kt @@ -1,7 +1,7 @@ package de.sipgate.federmappe.firestore import de.sipgate.federmappe.firestore.serializers.DateSerializer -import de.sipgate.federmappe.firestore.serializers.UriSerializer +import de.sipgate.federmappe.common.serializers.UriSerializer import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual diff --git a/realtimedb/build.gradle.kts b/realtimedb/build.gradle.kts index 9880236..ad14918 100644 --- a/realtimedb/build.gradle.kts +++ b/realtimedb/build.gradle.kts @@ -5,7 +5,7 @@ plugins { alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kotlin.serialization) `maven-publish` - signing + // signing } version = versionString @@ -31,7 +31,7 @@ android { } dependencies { - implementation(project(":common")) + api(project(":common")) compileOnly(libs.kotlinx.serialization) implementation(libs.kotlinx.datetime) compileOnly(libs.firebase.database) @@ -56,7 +56,7 @@ val Project.versionString: String return versionProperties.inputStream().use { inputStream -> Properties().run { load(inputStream) - "${parseInt("majorVersion")}.${parseInt("minorVersion")}.${parseInt("patchVersion")}" + "${parseInt("majorVersion")}.${parseInt("minorVersion")}.${parseInt("patchVersion")}-SNAPSHOT" } } } @@ -103,9 +103,9 @@ publishing { } } -signing { - val signingKey: String? by project - val signingPassword: String? by project - useInMemoryPgpKeys(signingKey, signingPassword) - sign(publishing.publications) -} +//signing { +// val signingKey: String? by project +// val signingPassword: String? by project +// useInMemoryPgpKeys(signingKey, signingPassword) +// sign(publishing.publications) +//} diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt new file mode 100644 index 0000000..ad7842b --- /dev/null +++ b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt @@ -0,0 +1,51 @@ +package de.sipgate.federmappe.realtimedb + +import com.google.firebase.database.DataSnapshot +import kotlinx.serialization.DeserializationStrategy +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 +import kotlin.reflect.KClass + +@InternalSerializationApi +abstract class DataSnapshotPolymorphicSerializer(private val baseClass: KClass) : + KSerializer { + + override val descriptor: SerialDescriptor = + buildSerialDescriptor("DataSnapshotPolymorphicSerializer<${baseClass.simpleName}>", PolymorphicKind.SEALED) + + final override fun serialize(encoder: Encoder, value: T) { + val actualSerializer = + encoder.serializersModule.getPolymorphic(baseClass, value) + ?: value::class.serializerOrNull() + ?: throwSubtypeNotRegistered(value::class, baseClass) + @Suppress("UNCHECKED_CAST") + (actualSerializer as KSerializer).serialize(encoder, value) + } + + final override fun deserialize(decoder: Decoder): T { + val input = decoder as PolymorphicDecoder + val tree = input.decodeValue() as DataSnapshot + + @Suppress("UNCHECKED_CAST") + val actualSerializer = selectDeserializer(tree) as KSerializer + return input.decodeSerializableValue(actualSerializer) + } + + protected abstract fun selectDeserializer(element: DataSnapshot): DeserializationStrategy + + 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.") + } + + } diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt new file mode 100644 index 0000000..22cc3ff --- /dev/null +++ b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt @@ -0,0 +1,52 @@ +package de.sipgate.federmappe.realtimedb + +import com.google.firebase.database.DataSnapshot +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +class PolymorphicDecoder( + private val type: String, + private val dataSnapshot: DataSnapshot, + override val serializersModule: SerializersModule = EmptySerializersModule(), + private val ignoreUnknownProperties: Boolean = false, +) : AbstractDecoder() { + + private var state = DecodeState.Empty + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + + return when (state) { + DecodeState.Empty -> { + state = DecodeState.TypeInfo + 0 + } + + DecodeState.TypeInfo -> { + state = DecodeState.Data + 1 + } + + DecodeState.Data, DecodeState.None -> { + state = DecodeState.None + CompositeDecoder.DECODE_DONE + } + } + } + + override fun decodeString(): String { + return if (state == DecodeState.TypeInfo) type else super.decodeString() + } + + override fun decodeValue(): Any { + return if (state == DecodeState.Data) dataSnapshot else super.decodeValue() + } +} + +enum class DecodeState { + Empty, + None, + TypeInfo, + Data +} diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt index c5387c0..bb4a0cf 100644 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt +++ b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt @@ -2,9 +2,12 @@ package de.sipgate.federmappe.realtimedb import com.google.firebase.database.DataSnapshot import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Polymorphic import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.capturedKClass import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.modules.SerializersModule @@ -13,7 +16,7 @@ import kotlinx.serialization.modules.SerializersModule class SnapshotDecoder( private val dataSnapshot: DataSnapshot, private val ignoreUnknownProperties: Boolean, - override val serializersModule: SerializersModule + override val serializersModule: SerializersModule, ) : AbstractDecoder() { private val keysIterator = dataSnapshot.children.mapNotNull { it.key }.iterator() @@ -55,6 +58,14 @@ class SnapshotDecoder( serializersModule = this.serializersModule, ) } + PolymorphicKind.SEALED -> { + return PolymorphicDecoder( + dataSnapshot = value, + ignoreUnknownProperties = ignoreUnknownProperties, + serializersModule = this.serializersModule, + type = descriptor.serialName + ) + } StructureKind.MAP -> { return MapDecoder( list = value.children.map { it }, From d4672881a77f877c72a971857db18b0c4f2e25a8 Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Wed, 13 Mar 2024 19:29:23 +0100 Subject: [PATCH 2/9] allow direct access to the decoded value at index --- .../federmappe/realtimedb/SnapshotDecoder.kt | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt index bb4a0cf..a7a814d 100644 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt +++ b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt @@ -1,13 +1,12 @@ package de.sipgate.federmappe.realtimedb import com.google.firebase.database.DataSnapshot +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Polymorphic import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.descriptors.capturedKClass import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.modules.SerializersModule @@ -58,6 +57,7 @@ class SnapshotDecoder( serializersModule = this.serializersModule, ) } + PolymorphicKind.SEALED -> { return PolymorphicDecoder( dataSnapshot = value, @@ -66,6 +66,7 @@ class SnapshotDecoder( type = descriptor.serialName ) } + StructureKind.MAP -> { return MapDecoder( list = value.children.map { it }, @@ -73,10 +74,12 @@ class SnapshotDecoder( serializersModule = serializersModule, ) } + StructureKind.LIST -> { val list = value.children.map { it } return ListDecoder(ArrayDeque(list), list.size, serializersModule) } + else -> throw SerializationException( "Given value is neither a list nor a type! value: $value, type: ${value::class.qualifiedName}" ) @@ -106,4 +109,17 @@ class SnapshotDecoder( override fun decodeValue(): Any { return dataSnapshot.child(key!!).value!! } + + fun decodeValue(descriptor: SerialDescriptor, index: Int): Any { + val key = descriptor.getElementName(index) + return dataSnapshot.child(key).value!! + } + +// override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { +// return deserializer.deserialize(SnapshotDecoder( +// dataSnapshot= decodeValue() as DataSnapshot, +// ignoreUnknownProperties = ignoreUnknownProperties, +// serializersModule = serializersModule +// )) +// } } From b9d6ca4447140254b63f81dfb410470308bed09d Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 13:17:51 +0100 Subject: [PATCH 3/9] Extract Firestore-specific timestamp handling out of StringMaptoObjectDecoder --- .../federmappe/firestore/DocumentSnapshotExt.kt | 6 +++++- .../de/sipgate/federmappe/firestore/ListDecoder.kt | 7 ++++--- .../sipgate/federmappe/firestore/QuerySnapshotExt.kt | 10 +++++++--- .../de/sipgate/federmappe/firestore/StringMapExt.kt | 6 +++++- .../federmappe/firestore/StringMapToObjectDecoder.kt | 9 +++++---- .../java/de/sipgate/federmappe/firestore/DateTests.kt | 3 +++ .../firestore/serializers/DateSerializerTest.kt | 9 +++++++-- 7 files changed, 36 insertions(+), 14 deletions(-) diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt index 5e8f9e1..9097b83 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt @@ -1,5 +1,6 @@ package de.sipgate.federmappe.firestore +import com.google.firebase.Timestamp import com.google.firebase.firestore.DocumentSnapshot import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.modules.SerializersModule @@ -9,7 +10,10 @@ inline fun DocumentSnapshot.toObject( customSerializers: SerializersModule = DefaultSerializersModule, errorHandler: (Throwable) -> T? = { throw it }): T? = try { - data?.toObjectWithSerializer(customSerializers = customSerializers) + data?.toObjectWithSerializer( + customSerializers = customSerializers, + subtypeDecoder = { (it as? Timestamp)?.let(::FirebaseTimestampDecoder) } + ) } catch (ex: Throwable) { errorHandler(ex) } diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/ListDecoder.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/ListDecoder.kt index 25718bc..8040a7d 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/ListDecoder.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/ListDecoder.kt @@ -1,6 +1,5 @@ package de.sipgate.federmappe.firestore -import com.google.firebase.Timestamp import de.sipgate.federmappe.common.decodeEnum import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException @@ -16,6 +15,7 @@ class ListDecoder( private val list: ArrayDeque, private val elementsCount: Int = 0, override val serializersModule: SerializersModule = EmptySerializersModule(), + private val subtypeDecoder: (Any?) -> CompositeDecoder? = {null} ) : AbstractDecoder() { private var index = 0 @@ -44,8 +44,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) { diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt index 9053240..947f85f 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt @@ -1,5 +1,6 @@ package de.sipgate.federmappe.firestore +import com.google.firebase.Timestamp import com.google.firebase.firestore.QuerySnapshot import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.modules.SerializersModule @@ -7,10 +8,13 @@ import kotlinx.serialization.modules.SerializersModule @ExperimentalSerializationApi inline fun QuerySnapshot.toObject( customSerializers: SerializersModule = DefaultSerializersModule, - errorHandler: (Throwable) -> T? = { throw it }): List = - map { + errorHandler: (Throwable) -> T? = { throw it } +): List = + map { documentSnapshot -> try { - it.data.toObjectWithSerializer(customSerializers = customSerializers) + documentSnapshot.data.toObjectWithSerializer( + customSerializers = customSerializers, + subtypeDecoder = { (it as? Timestamp)?.let(::FirebaseTimestampDecoder) }) } catch (ex: Throwable) { errorHandler(ex) } diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapExt.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapExt.kt index 8edf2f9..85fa953 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapExt.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapExt.kt @@ -1,18 +1,22 @@ package de.sipgate.federmappe.firestore +import com.google.firebase.Timestamp 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 Map.toObjectWithSerializer( serializer: KSerializer = serializer(), - customSerializers: SerializersModule + customSerializers: SerializersModule, + noinline subtypeDecoder: (Any?) -> CompositeDecoder? = {null} ): T = serializer.deserialize( StringMapToObjectDecoder( this, ignoreUnknownProperties = true, serializersModule = customSerializers, + subtypeDecoder = subtypeDecoder ), ) diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapToObjectDecoder.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapToObjectDecoder.kt index 865f411..d5f896c 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapToObjectDecoder.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapToObjectDecoder.kt @@ -1,6 +1,5 @@ package de.sipgate.federmappe.firestore -import com.google.firebase.Timestamp import de.sipgate.federmappe.common.decodeEnum import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException @@ -16,6 +15,7 @@ class StringMapToObjectDecoder( private val data: Map, override val serializersModule: SerializersModule = EmptySerializersModule(), private val ignoreUnknownProperties: Boolean = false, + private val subtypeDecoder: (Any?) -> CompositeDecoder? = {null} ) : AbstractDecoder() { private val keysIterator = data.keys.iterator() private var index: Int? = null @@ -59,8 +59,9 @@ 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) { @@ -77,7 +78,7 @@ class StringMapToObjectDecoder( StructureKind.LIST -> { val list = (value as Iterable).toCollection(mutableListOf()) - return ListDecoder(ArrayDeque(list), list.size, serializersModule) + return ListDecoder(ArrayDeque(list), list.size, serializersModule, subtypeDecoder) } else -> throw SerializationException("Given value is neither a list nor a type! value: $value, type: ${value?.let { it::class.qualifiedName } ?: "null"}") diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt b/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt index bb9228a..2d31e63 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt +++ b/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt @@ -36,6 +36,7 @@ class DateTests { data, ignoreUnknownProperties = true, serializersModule = SerializersModule { contextual(DateSerializer) }, + subtypeDecoder = { (it as? Timestamp)?.let(::FirebaseTimestampDecoder) } ), ) @@ -64,6 +65,7 @@ class DateTests { StringMapToObjectDecoder( data, ignoreUnknownProperties = true, + subtypeDecoder = { (it as? Timestamp)?.let(::FirebaseTimestampDecoder) } ), ) @@ -93,6 +95,7 @@ class DateTests { data, ignoreUnknownProperties = true, serializersModule = SerializersModule { contextual(DateSerializer) }, + subtypeDecoder = { (it as? Timestamp)?.let(::FirebaseTimestampDecoder) } ), ) diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt b/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt index 143b0dd..7771279 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt +++ b/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt @@ -1,6 +1,7 @@ package de.sipgate.federmappe.firestore.serializers import com.google.firebase.Timestamp +import de.sipgate.federmappe.firestore.FirebaseTimestampDecoder import de.sipgate.federmappe.firestore.StringMapToObjectDecoder import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -13,8 +14,9 @@ import java.util.Date class DateSerializerTest { private val epochSeconds = 1707833611L * 1000 private val nanos = 801 / 1000000 + /* Nanos will be ignored, because java.util.Date isn't precise enough. */ - private val date = Date( epochSeconds + nanos) + private val date = Date(epochSeconds + nanos) @OptIn(ExperimentalSerializationApi::class) @Test @@ -25,7 +27,10 @@ class DateSerializerTest { val serializer = serializer() val data = mapOf("a" to Timestamp(1707833611, 801)) - val result = serializer.deserialize(StringMapToObjectDecoder(data)) + val result = serializer.deserialize( + StringMapToObjectDecoder(data, + subtypeDecoder = { (it as? Timestamp)?.let(::FirebaseTimestampDecoder) }) + ) // Assert Assertions.assertEquals(date, result.a) From 6be7e66861e878ef5c32bd77140fe2bbea0b26ea Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 13:38:39 +0100 Subject: [PATCH 4/9] Move generic classes to Common --- .../federmappe/common}/DefaultSerializersModule.kt | 4 ++-- .../de/sipgate/federmappe/common}/ListDecoder.kt | 3 +-- .../de/sipgate/federmappe/common}/MapDecoder.kt | 3 +-- .../de/sipgate/federmappe/common}/StringMapExt.kt | 3 +-- .../federmappe/common}/StringMapToObjectDecoder.kt | 13 +++++++++---- .../common}/serializers/DateSerializer.kt | 2 +- .../de/sipgate/federmappe/common}/BooleanTests.kt | 2 +- .../de/sipgate/federmappe/common}/EnumListTests.kt | 2 +- .../de/sipgate/federmappe/common}/EnumTests.kt | 2 +- .../federmappe/common}/FloatingPointNumberTests.kt | 2 +- .../sipgate/federmappe/common}/StringListTests.kt | 2 +- .../federmappe/common}/StringMapDecoderTest.kt | 2 +- .../de/sipgate/federmappe/common}/StringTests.kt | 2 +- .../federmappe/common}/SubclassDecodingTests.kt | 3 +-- .../sipgate/federmappe/common}/WholeNumberTests.kt | 2 +- .../common/serializers/UriSerializerTest.kt | 2 +- .../federmappe/firestore/DocumentSnapshotExt.kt | 2 ++ .../federmappe/firestore/QuerySnapshotExt.kt | 2 ++ .../de/sipgate/federmappe/firestore/DateTests.kt | 4 +++- .../firestore/serializers/DateSerializerTest.kt | 3 ++- 20 files changed, 34 insertions(+), 26 deletions(-) rename {firestore/src/main/java/de/sipgate/federmappe/firestore => common/src/main/kotlin/de/sipgate/federmappe/common}/DefaultSerializersModule.kt (72%) rename {firestore/src/main/java/de/sipgate/federmappe/firestore => common/src/main/kotlin/de/sipgate/federmappe/common}/ListDecoder.kt (96%) rename {firestore/src/main/java/de/sipgate/federmappe/firestore => common/src/main/kotlin/de/sipgate/federmappe/common}/MapDecoder.kt (97%) rename {firestore/src/main/java/de/sipgate/federmappe/firestore => common/src/main/kotlin/de/sipgate/federmappe/common}/StringMapExt.kt (90%) rename {firestore/src/main/java/de/sipgate/federmappe/firestore => common/src/main/kotlin/de/sipgate/federmappe/common}/StringMapToObjectDecoder.kt (89%) rename {firestore/src/main/java/de/sipgate/federmappe/firestore => common/src/main/kotlin/de/sipgate/federmappe/common}/serializers/DateSerializer.kt (95%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/BooleanTests.kt (99%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/EnumListTests.kt (98%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/EnumTests.kt (98%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/FloatingPointNumberTests.kt (99%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/StringListTests.kt (98%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/StringMapDecoderTest.kt (99%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/StringTests.kt (98%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/SubclassDecodingTests.kt (93%) rename {firestore/src/test/java/de/sipgate/federmappe/firestore => common/src/test/kotlin/de/sipgate/federmappe/common}/WholeNumberTests.kt (99%) diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/DefaultSerializersModule.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/DefaultSerializersModule.kt similarity index 72% rename from firestore/src/main/java/de/sipgate/federmappe/firestore/DefaultSerializersModule.kt rename to common/src/main/kotlin/de/sipgate/federmappe/common/DefaultSerializersModule.kt index 6b04766..6395fe3 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/DefaultSerializersModule.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/DefaultSerializersModule.kt @@ -1,6 +1,6 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common -import de.sipgate.federmappe.firestore.serializers.DateSerializer +import de.sipgate.federmappe.common.serializers.DateSerializer import de.sipgate.federmappe.common.serializers.UriSerializer import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/ListDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/ListDecoder.kt similarity index 96% rename from firestore/src/main/java/de/sipgate/federmappe/firestore/ListDecoder.kt rename to common/src/main/kotlin/de/sipgate/federmappe/common/ListDecoder.kt index 8040a7d..45b9665 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/ListDecoder.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/ListDecoder.kt @@ -1,6 +1,5 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common -import de.sipgate.federmappe.common.decodeEnum import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/MapDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/MapDecoder.kt similarity index 97% rename from firestore/src/main/java/de/sipgate/federmappe/firestore/MapDecoder.kt rename to common/src/main/kotlin/de/sipgate/federmappe/common/MapDecoder.kt index 324ef1d..ab15538 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/MapDecoder.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/MapDecoder.kt @@ -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 diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapExt.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapExt.kt similarity index 90% rename from firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapExt.kt rename to common/src/main/kotlin/de/sipgate/federmappe/common/StringMapExt.kt index 85fa953..dae9b8e 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapExt.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapExt.kt @@ -1,6 +1,5 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common -import com.google.firebase.Timestamp import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.encoding.CompositeDecoder diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapToObjectDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt similarity index 89% rename from firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapToObjectDecoder.kt rename to common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt index d5f896c..f27a21e 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/StringMapToObjectDecoder.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt @@ -1,6 +1,5 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common -import de.sipgate.federmappe.common.decodeEnum import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor @@ -77,8 +76,14 @@ class StringMapToObjectDecoder( ) StructureKind.LIST -> { - val list = (value as Iterable).toCollection(mutableListOf()) - return ListDecoder(ArrayDeque(list), list.size, serializersModule, subtypeDecoder) + val list = (value as Iterable) + .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"}") diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/serializers/DateSerializer.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/serializers/DateSerializer.kt similarity index 95% rename from firestore/src/main/java/de/sipgate/federmappe/firestore/serializers/DateSerializer.kt rename to common/src/main/kotlin/de/sipgate/federmappe/common/serializers/DateSerializer.kt index 81581f9..f753f18 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/serializers/DateSerializer.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/serializers/DateSerializer.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore.serializers +package de.sipgate.federmappe.common.serializers import java.util.Date import kotlinx.serialization.KSerializer diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/BooleanTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/BooleanTests.kt similarity index 99% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/BooleanTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/BooleanTests.kt index 07b060d..c65f381 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/BooleanTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/BooleanTests.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/EnumListTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/EnumListTests.kt similarity index 98% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/EnumListTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/EnumListTests.kt index c8f6852..cd322d3 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/EnumListTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/EnumListTests.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/EnumTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/EnumTests.kt similarity index 98% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/EnumTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/EnumTests.kt index 831e5de..cffe79d 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/EnumTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/EnumTests.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/FloatingPointNumberTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/FloatingPointNumberTests.kt similarity index 99% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/FloatingPointNumberTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/FloatingPointNumberTests.kt index 044bb6e..b843ccc 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/FloatingPointNumberTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/FloatingPointNumberTests.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/StringListTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/StringListTests.kt similarity index 98% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/StringListTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/StringListTests.kt index 4d39a42..175ca4e 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/StringListTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/StringListTests.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/StringMapDecoderTest.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/StringMapDecoderTest.kt similarity index 99% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/StringMapDecoderTest.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/StringMapDecoderTest.kt index 50840f7..680f456 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/StringMapDecoderTest.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/StringMapDecoderTest.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import java.util.stream.Stream import kotlin.reflect.KClass diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/StringTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/StringTests.kt similarity index 98% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/StringTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/StringTests.kt index 55e1753..f9fdd90 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/StringTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/StringTests.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/SubclassDecodingTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt similarity index 93% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/SubclassDecodingTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt index 3785c44..4fefb14 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/SubclassDecodingTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt @@ -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 diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/WholeNumberTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/WholeNumberTests.kt similarity index 99% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/WholeNumberTests.kt rename to common/src/test/kotlin/de/sipgate/federmappe/common/WholeNumberTests.kt index 5140790..e984307 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/WholeNumberTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/WholeNumberTests.kt @@ -1,4 +1,4 @@ -package de.sipgate.federmappe.firestore +package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt index 4d12040..d450c5b 100644 --- a/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/serializers/UriSerializerTest.kt @@ -1,6 +1,6 @@ package de.sipgate.federmappe.common.serializers -import de.sipgate.federmappe.firestore.StringMapToObjectDecoder +import de.sipgate.federmappe.common.StringMapToObjectDecoder import kotlinx.serialization.Contextual import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt index 9097b83..97a851d 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/DocumentSnapshotExt.kt @@ -2,6 +2,8 @@ package de.sipgate.federmappe.firestore import com.google.firebase.Timestamp import com.google.firebase.firestore.DocumentSnapshot +import de.sipgate.federmappe.common.DefaultSerializersModule +import de.sipgate.federmappe.common.toObjectWithSerializer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.modules.SerializersModule diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt index 947f85f..8192645 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt @@ -2,6 +2,8 @@ package de.sipgate.federmappe.firestore import com.google.firebase.Timestamp import com.google.firebase.firestore.QuerySnapshot +import de.sipgate.federmappe.common.DefaultSerializersModule +import de.sipgate.federmappe.common.toObjectWithSerializer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.modules.SerializersModule diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt b/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt index 2d31e63..efa43e6 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt +++ b/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt @@ -1,7 +1,8 @@ package de.sipgate.federmappe.firestore import com.google.firebase.Timestamp -import de.sipgate.federmappe.firestore.serializers.DateSerializer +import de.sipgate.federmappe.common.StringMapToObjectDecoder +import de.sipgate.federmappe.common.serializers.DateSerializer import java.util.Date import java.util.GregorianCalendar import kotlinx.serialization.Contextual @@ -157,6 +158,7 @@ class DateTests { data, ignoreUnknownProperties = true, serializersModule = SerializersModule { contextual(DateSerializer) }, + subtypeDecoder = { (it as? Timestamp)?.let(::FirebaseTimestampDecoder) } ), ) diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt b/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt index 7771279..c7b0b03 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt +++ b/firestore/src/test/java/de/sipgate/federmappe/firestore/serializers/DateSerializerTest.kt @@ -2,7 +2,8 @@ package de.sipgate.federmappe.firestore.serializers import com.google.firebase.Timestamp import de.sipgate.federmappe.firestore.FirebaseTimestampDecoder -import de.sipgate.federmappe.firestore.StringMapToObjectDecoder +import de.sipgate.federmappe.common.StringMapToObjectDecoder +import de.sipgate.federmappe.common.serializers.DateSerializer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.serializer From f182af667efbf027e1b889a9a4cd932fbed5f259 Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 13:39:05 +0100 Subject: [PATCH 5/9] Rename FirebaseTimestampTests --- .../firestore/{DateTests.kt => FirebaseTimestampTests.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename firestore/src/test/java/de/sipgate/federmappe/firestore/{DateTests.kt => FirebaseTimestampTests.kt} (99%) diff --git a/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt b/firestore/src/test/java/de/sipgate/federmappe/firestore/FirebaseTimestampTests.kt similarity index 99% rename from firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt rename to firestore/src/test/java/de/sipgate/federmappe/firestore/FirebaseTimestampTests.kt index efa43e6..f91a94c 100644 --- a/firestore/src/test/java/de/sipgate/federmappe/firestore/DateTests.kt +++ b/firestore/src/test/java/de/sipgate/federmappe/firestore/FirebaseTimestampTests.kt @@ -15,7 +15,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @OptIn(ExperimentalSerializationApi::class) -class DateTests { +class FirebaseTimestampTests { @Test fun deserializeBasicDataClassWithDateFieldSetToFirstDayOfYear2000() { // Arrange From fec291893c0a59bdfc6e3a55273c794ba5a8e007 Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 13:45:20 +0100 Subject: [PATCH 6/9] Reuse generic StringMapToObjectDecoder for RealtimeDB type decoding --- .../federmappe/realtimedb/DataSnapshotExt.kt | 16 ++- .../DataSnapshotPolymorphicSerializer.kt | 51 ------- .../federmappe/realtimedb/ListDecoder.kt | 61 --------- .../federmappe/realtimedb/MapDecoder.kt | 92 ------------- .../realtimedb/PolymorphicDecoder.kt | 52 -------- .../realtimedb/SharedEnumDecoder.kt | 23 ---- .../federmappe/realtimedb/SnapshotDecoder.kt | 125 ------------------ .../realtimedb/BooleanDecodingTest.kt | 77 ----------- .../federmappe/realtimedb/EnumDecodingTest.kt | 71 ---------- .../realtimedb/FractionalNumberTests.kt | 60 --------- .../federmappe/realtimedb/StringTests.kt | 62 --------- .../realtimedb/SubclassDecodingTests.kt | 51 ------- .../federmappe/realtimedb/WholeNumberTests.kt | 60 --------- 13 files changed, 9 insertions(+), 792 deletions(-) delete mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt delete mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/ListDecoder.kt delete mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/MapDecoder.kt delete mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt delete mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SharedEnumDecoder.kt delete mode 100644 realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SnapshotDecoder.kt delete mode 100644 realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/BooleanDecodingTest.kt delete mode 100644 realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/EnumDecodingTest.kt delete mode 100644 realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/FractionalNumberTests.kt delete mode 100644 realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/StringTests.kt delete mode 100644 realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/SubclassDecodingTests.kt delete mode 100644 realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/WholeNumberTests.kt diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotExt.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotExt.kt index c772fb8..5306ecd 100644 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotExt.kt +++ b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotExt.kt @@ -1,6 +1,7 @@ package de.sipgate.federmappe.realtimedb import com.google.firebase.database.DataSnapshot +import de.sipgate.federmappe.common.StringMapToObjectDecoder import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.modules.EmptySerializersModule @@ -21,10 +22,11 @@ inline fun DataSnapshot.toObject( inline fun DataSnapshot.toObjectWithSerializer( serializer: KSerializer = serializer(), customSerializers: SerializersModule = EmptySerializersModule() -): T = serializer.deserialize( - SnapshotDecoder( - this, - ignoreUnknownProperties = true, - serializersModule = customSerializers, - ), -) +): T = serializer.deserialize(StringMapToObjectDecoder(toObjectMap().unwrapRoot(), customSerializers)) + +@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") +fun DataSnapshot.toObjectMap(): Pair = + (key ?: "root") to (if (hasChildren()) children.associate { it.toObjectMap() } else value)!! + +@Suppress("UNCHECKED_CAST") +fun Pair.unwrapRoot() = second as Map diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt deleted file mode 100644 index ad7842b..0000000 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/DataSnapshotPolymorphicSerializer.kt +++ /dev/null @@ -1,51 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import kotlinx.serialization.DeserializationStrategy -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 -import kotlin.reflect.KClass - -@InternalSerializationApi -abstract class DataSnapshotPolymorphicSerializer(private val baseClass: KClass) : - KSerializer { - - override val descriptor: SerialDescriptor = - buildSerialDescriptor("DataSnapshotPolymorphicSerializer<${baseClass.simpleName}>", PolymorphicKind.SEALED) - - final override fun serialize(encoder: Encoder, value: T) { - val actualSerializer = - encoder.serializersModule.getPolymorphic(baseClass, value) - ?: value::class.serializerOrNull() - ?: throwSubtypeNotRegistered(value::class, baseClass) - @Suppress("UNCHECKED_CAST") - (actualSerializer as KSerializer).serialize(encoder, value) - } - - final override fun deserialize(decoder: Decoder): T { - val input = decoder as PolymorphicDecoder - val tree = input.decodeValue() as DataSnapshot - - @Suppress("UNCHECKED_CAST") - val actualSerializer = selectDeserializer(tree) as KSerializer - return input.decodeSerializableValue(actualSerializer) - } - - protected abstract fun selectDeserializer(element: DataSnapshot): DeserializationStrategy - - 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.") - } - - } diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/ListDecoder.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/ListDecoder.kt deleted file mode 100644 index d4e0bc0..0000000 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/ListDecoder.kt +++ /dev/null @@ -1,61 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -@ExperimentalSerializationApi -class ListDecoder( - private val list: ArrayDeque, - private val elementsCount: Int = 0, - override val serializersModule: SerializersModule = EmptySerializersModule(), -) : AbstractDecoder() { - private var index = 0 - - override fun decodeSequentially(): Boolean = true - - override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = elementsCount - - override fun decodeValue(): Any = list.removeFirst().value!! - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int = - when (index) { - elementsCount -> CompositeDecoder.DECODE_DONE - else -> index++ - } - - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = - decodeEnum(enumDescriptor, ::decodeValue) - - override fun decodeNotNullMark(): Boolean = - when { - list.firstOrNull()?.value != null -> true - else -> false.also { list.removeFirst() } - } - - @Suppress("UNCHECKED_CAST") - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - val value = list.removeFirst() - - when (descriptor.kind) { - StructureKind.CLASS -> - return SnapshotDecoder( - dataSnapshot = value, - ignoreUnknownProperties = true, - serializersModule = this.serializersModule, - ) - StructureKind.LIST -> { - val subList = (value as Iterable).toCollection(mutableListOf()) - return ListDecoder(ArrayDeque(subList), subList.size, serializersModule) - } - else -> {} - } - - return ListDecoder(list, descriptor.elementsCount) - } -} diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/MapDecoder.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/MapDecoder.kt deleted file mode 100644 index 11d73c1..0000000 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/MapDecoder.kt +++ /dev/null @@ -1,92 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import android.util.Log -import com.google.firebase.database.DataSnapshot -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -@ExperimentalSerializationApi -class MapDecoder( - private val list: List, - override val serializersModule: SerializersModule = EmptySerializersModule(), - private val ignoreUnknownProperties: Boolean = false, -) : AbstractDecoder() { - private val keysIterator = list.iterator() - private var index: Int = -2 - - private val skippedValues = mutableSetOf() - - override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = list.size - - override fun decodeValue(): Any = list[index]!!.value!! - - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = - decodeEnum(enumDescriptor, ::decodeValue) - - override fun decodeNotNullMark(): Boolean = list[index] != null - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - while (keysIterator.hasNext()) { - val nextKey = keysIterator.next() - if (index >= 0) { - if (index % 2 == 0) { - index += 1 - return index - } - } - val nextIndex = - if (descriptor.kind == StructureKind.MAP) { - list.indexOf(nextKey) - } else { - descriptor.getElementIndex(nextKey!!.key!!) - } - if (nextIndex == CompositeDecoder.UNKNOWN_NAME) { - Log.w("MapDecoder", "encountered unknown key while decoding") - skippedValues.add(nextKey!!.key!!) - continue - } - - index = nextIndex - return nextIndex - } - - return CompositeDecoder.DECODE_DONE - } - - @Suppress("UNCHECKED_CAST") - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - val value = - if (index % 2 == 0) { - list[index + 1] - } else { - list[index] - }!! - - when (descriptor.kind) { - StructureKind.CLASS -> { - return SnapshotDecoder( - dataSnapshot = value, - ignoreUnknownProperties = ignoreUnknownProperties, - serializersModule = serializersModule, - ) - } - StructureKind.MAP -> { - return MapDecoder( - list = value.children.map { it }, - ignoreUnknownProperties = ignoreUnknownProperties, - ) - } - StructureKind.LIST -> { - val list = value.children.map { it } - return ListDecoder(ArrayDeque(list), list.size, serializersModule) - } - else -> throw SerializationException("Given value is neither a list nor a type $value") - } - } -} diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt deleted file mode 100644 index 22cc3ff..0000000 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/PolymorphicDecoder.kt +++ /dev/null @@ -1,52 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -class PolymorphicDecoder( - private val type: String, - private val dataSnapshot: DataSnapshot, - override val serializersModule: SerializersModule = EmptySerializersModule(), - private val ignoreUnknownProperties: Boolean = false, -) : AbstractDecoder() { - - private var state = DecodeState.Empty - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - - return when (state) { - DecodeState.Empty -> { - state = DecodeState.TypeInfo - 0 - } - - DecodeState.TypeInfo -> { - state = DecodeState.Data - 1 - } - - DecodeState.Data, DecodeState.None -> { - state = DecodeState.None - CompositeDecoder.DECODE_DONE - } - } - } - - override fun decodeString(): String { - return if (state == DecodeState.TypeInfo) type else super.decodeString() - } - - override fun decodeValue(): Any { - return if (state == DecodeState.Data) dataSnapshot else super.decodeValue() - } -} - -enum class DecodeState { - Empty, - None, - TypeInfo, - Data -} diff --git a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SharedEnumDecoder.kt b/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SharedEnumDecoder.kt deleted file mode 100644 index c61fea8..0000000 --- a/realtimedb/src/main/kotlin/de/sipgate/federmappe/realtimedb/SharedEnumDecoder.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor - -@ExperimentalSerializationApi -fun decodeEnum( - enumDescriptor: SerialDescriptor, - decodeValue: () -> Any, -): Int { - val decodedValue = decodeValue() - val enumNamespace = enumDescriptor.serialName - - for (i in 0..() - - override fun decodeSequentially() = false - - override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = dataSnapshot.children.count() - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - while (keysIterator.hasNext()) { - val nextKey = keysIterator.next().also { key = it } - - val nextIndex = descriptor.getElementIndex(nextKey) - if (nextIndex == CompositeDecoder.UNKNOWN_NAME) { - skippedValues.add(nextKey) - continue - } - - return nextIndex.also { index = it } - } - - return CompositeDecoder.DECODE_DONE - } - - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - if (key == null) return this - - val value = dataSnapshot.child(key!!) - val valueDescriptor = descriptor.kind - when (valueDescriptor) { - StructureKind.CLASS -> { - return SnapshotDecoder( - dataSnapshot = value, - ignoreUnknownProperties = ignoreUnknownProperties, - serializersModule = this.serializersModule, - ) - } - - PolymorphicKind.SEALED -> { - return PolymorphicDecoder( - dataSnapshot = value, - ignoreUnknownProperties = ignoreUnknownProperties, - serializersModule = this.serializersModule, - type = descriptor.serialName - ) - } - - StructureKind.MAP -> { - return MapDecoder( - list = value.children.map { it }, - ignoreUnknownProperties = ignoreUnknownProperties, - serializersModule = serializersModule, - ) - } - - StructureKind.LIST -> { - val list = value.children.map { it } - return ListDecoder(ArrayDeque(list), list.size, serializersModule) - } - - else -> throw SerializationException( - "Given value is neither a list nor a type! value: $value, type: ${value::class.qualifiedName}" - ) - } - } - - override fun endStructure(descriptor: SerialDescriptor) { - if (skippedValues.isNotEmpty() && !ignoreUnknownProperties) { - throw SerializationException("found unhandled properties: $skippedValues") - } - - super.endStructure(descriptor) - } - - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { - return decodeEnum(enumDescriptor, ::decodeValue) - } - - override fun decodeInt(): Int { - return (decodeValue() as Long).toInt() - } - - override fun decodeFloat(): Float { - return (decodeValue() as Double).toFloat() - } - - override fun decodeValue(): Any { - return dataSnapshot.child(key!!).value!! - } - - fun decodeValue(descriptor: SerialDescriptor, index: Int): Any { - val key = descriptor.getElementName(index) - return dataSnapshot.child(key).value!! - } - -// override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { -// return deserializer.deserialize(SnapshotDecoder( -// dataSnapshot= decodeValue() as DataSnapshot, -// ignoreUnknownProperties = ignoreUnknownProperties, -// serializersModule = serializersModule -// )) -// } -} diff --git a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/BooleanDecodingTest.kt b/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/BooleanDecodingTest.kt deleted file mode 100644 index eafc6bc..0000000 --- a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/BooleanDecodingTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test - -@OptIn(ExperimentalSerializationApi::class) -class BooleanDecodingTest { - @Test - fun nonNullBooleanTest() { - @Serializable - data class Asdf(val someValue: Boolean) - - val expectedProperty = mockk { - every { value } returns true - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - Assertions.assertNotNull(output) - Assertions.assertTrue(output.someValue) - } - - @Test - fun nullableBooleanTest() { - @Serializable - data class Asdf(val someValue: Boolean? = null) - - val expectedProperty = mockk { - every { value } returns true - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - Assertions.assertNotNull(output) - Assertions.assertTrue(output.someValue!!) - } - - @Test - fun nullableBooleanDefaultsToNullTest() { - @Serializable - data class Asdf(val someValue: Boolean? = null) - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns emptyList() - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - Assertions.assertNotNull(output) - Assertions.assertNull(output.someValue) - } -} diff --git a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/EnumDecodingTest.kt b/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/EnumDecodingTest.kt deleted file mode 100644 index 084295c..0000000 --- a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/EnumDecodingTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -@OptIn(ExperimentalSerializationApi::class) -class EnumDecodingTest { - enum class SomeEnum { - A, B - } - - @Test - fun simpleEnumDecodingTest() { - @Serializable - data class Asdf(val someValue: SomeEnum) - - val expectedProperty = mockk { - every { value } returns "A" - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - assertNotNull(output) - assertEquals(SomeEnum.A, output.someValue) - } - - enum class CustomEnum { - @SerialName("avalue") A, - @SerialName("bvalue") B - } - - @Test - fun customEnumDecodingTest() { - @Serializable - data class Asdf(val someValue: CustomEnum) - - val expectedProperty = mockk { - every { value } returns "avalue" - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - assertNotNull(output) - assertEquals(CustomEnum.A, output.someValue) - } -} diff --git a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/FractionalNumberTests.kt b/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/FractionalNumberTests.kt deleted file mode 100644 index 9f33294..0000000 --- a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/FractionalNumberTests.kt +++ /dev/null @@ -1,60 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test - -@OptIn(ExperimentalSerializationApi::class) -class FractionalNumberTests { - @Test - fun floatDecodingTest() { - @Serializable - data class Asdf(val someValue: Float) - - val expectedProperty = mockk { - every { value } returns 1.5 - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - Assertions.assertNotNull(output) - Assertions.assertEquals(1.5f, output.someValue) - } - - @Test - fun doubleDecodingTest() { - @Serializable - data class Asdf(val someValue: Double) - - val expectedProperty = mockk { - every { value } returns 1.5 - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - Assertions.assertNotNull(output) - Assertions.assertEquals(1.5, output.someValue) - } -} diff --git a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/StringTests.kt b/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/StringTests.kt deleted file mode 100644 index d7f6e32..0000000 --- a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/StringTests.kt +++ /dev/null @@ -1,62 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Test - -@OptIn(ExperimentalSerializationApi::class) -class StringTests { - @Test - fun testStringWithMatchingName() { - @Serializable - data class Asdf(val someValue: String) - - val expectedProperty = mockk { - every { value } returns "asdf" - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - assertNotNull(output) - assertEquals("asdf", output.someValue) - } - - @Test - fun testStringWithOverriddenName() { - @Serializable - data class Asdf(@SerialName("overriddenName") val someValue: String) - - val expectedProperty = mockk { - every { value } returns "asdf" - every { key } returns "overriddenName" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("overriddenName") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - assertNotNull(output) - assertEquals("asdf", output.someValue) - } -} diff --git a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/SubclassDecodingTests.kt b/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/SubclassDecodingTests.kt deleted file mode 100644 index af1c09c..0000000 --- a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/SubclassDecodingTests.kt +++ /dev/null @@ -1,51 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertInstanceOf -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test - -@Disabled -@OptIn(ExperimentalSerializationApi::class) -class SubclassDecodingTests { - - @Serializable - data class A(val b: String) - - @Test - fun nestedDataStructureDecodingTest() { - @Serializable - data class TestClass(val a: A) - - val bPropertyMock = mockk { - every { value } returns "Some String" - every { key } returns "b" - every { children } returns emptyList() - } - - val aPropertyMock = mockk { - every { value } returns null - every { key } returns "a" - every { children } returns emptyList() - } - - val testClassMock = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(aPropertyMock) - every { child("a") } returns aPropertyMock - } - - val result = testClassMock.toObjectWithSerializer(TestClass.serializer()) - - assertInstanceOf(TestClass::class.java, result) - assertNotNull(result.a) - assertEquals("Some String", result.a.b) - } -} diff --git a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/WholeNumberTests.kt b/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/WholeNumberTests.kt deleted file mode 100644 index dc8fdee..0000000 --- a/realtimedb/src/test/kotlin/de/sipgate/federmappe/realtimedb/WholeNumberTests.kt +++ /dev/null @@ -1,60 +0,0 @@ -package de.sipgate.federmappe.realtimedb - -import com.google.firebase.database.DataSnapshot -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test - -@OptIn(ExperimentalSerializationApi::class) -class WholeNumberTests { - @Test - fun integerDecodingTest() { - @Serializable - data class Asdf(val someValue: Int) - - val expectedProperty = mockk { - every { value } returns 1234L - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - Assertions.assertNotNull(output) - Assertions.assertEquals(1234, output.someValue) - } - - @Test - fun longDecodingTest() { - @Serializable - data class Asdf(val someValue: Long) - - val expectedProperty = mockk { - every { value } returns 1234L - every { key } returns "someValue" - every { children } returns emptyList() - } - - val data = mockk { - every { key } returns null - every { value } returns null - every { children } returns listOf(expectedProperty) - every { child("someValue") } returns expectedProperty - } - - val output = data.toObjectWithSerializer(Asdf.serializer()) - - Assertions.assertNotNull(output) - Assertions.assertEquals(1234L, output.someValue) - } -} From eb5244b2a0faa8cd4f2379b1b9b2beb31845159d Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 15:03:17 +0100 Subject: [PATCH 7/9] Add failing sealed class test --- .../federmappe/common/SealedTypeTests.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt diff --git a/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt new file mode 100644 index 0000000..c0b3149 --- /dev/null +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt @@ -0,0 +1,61 @@ +package de.sipgate.federmappe.common + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Polymorphic +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +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(ExperimentalSerializationApi::class) +class SealedTypeTests { + + enum class TypeDecl { + @SerialName("A_TYPE") A, + @SerialName("B_TYPE") B + } + + @Serializable + sealed interface BaseType { + val type: TypeDecl + + @Serializable + data class A(val value: String): BaseType { + override val type = TypeDecl.A + } + + @Serializable + data class B(val value: Boolean): BaseType { + override val type = TypeDecl.B + } + } + + @Test + fun deserializeBasicDataClassWithBooleanFieldSetToTrue() { + // Arrange + @Serializable + data class TestClass(val a: BaseType) + + val serializer = serializer() + val data = mapOf("a" to mapOf( + "type" to "A_TYPE", + "value" to "some string" + )) + + // Act + val result = + serializer.deserialize( + StringMapToObjectDecoder( + data, + ignoreUnknownProperties = true, + ), + ) + + // Assert + assertInstanceOf(TestClass::class.java, result) + assertInstanceOf(BaseType.A::class.java, result.a) + assertEquals("some string", (result.a as BaseType.A).value) + } +} From b8f735e8af471550c809bec6f6e1e2871ed5719c Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 15:06:19 +0100 Subject: [PATCH 8/9] Improve log statement --- .../sipgate/federmappe/common/StringMapToObjectDecoder.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt index f27a21e..17d8e02 100644 --- a/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt @@ -14,7 +14,7 @@ class StringMapToObjectDecoder( private val data: Map, override val serializersModule: SerializersModule = EmptySerializersModule(), private val ignoreUnknownProperties: Boolean = false, - private val subtypeDecoder: (Any?) -> CompositeDecoder? = {null} + private val subtypeDecoder: (Any?) -> CompositeDecoder? = { null } ) : AbstractDecoder() { private val keysIterator = data.keys.iterator() private var index: Int? = null @@ -83,10 +83,11 @@ class StringMapToObjectDecoder( list = ArrayDeque(list), elementsCount = list.size, serializersModule = serializersModule, - subtypeDecoder = subtypeDecoder) + 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") } } From 3882a9ebe73709e2fe0d7c1bc27fa98194d9e6f5 Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 15:37:26 +0100 Subject: [PATCH 9/9] Add generic sealed class support Currently only String type properties are supported. Enums will follow. --- .../common/SealedClassWithTypeSerializer.kt | 57 +++++++++++++++++++ .../common/StringMapToObjectDecoder.kt | 11 +++- .../federmappe/common/TypeAwareDecoder.kt | 5 ++ .../federmappe/common/SealedTypeTests.kt | 54 ++++++++++++------ .../common/SubclassDecodingTests.kt | 2 +- 5 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt create mode 100644 common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt new file mode 100644 index 0000000..5fea69b --- /dev/null +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt @@ -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( + private val baseClass: KClass, + private val discriminatorName: String = "type" +) : KSerializer { + + 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).serialize(encoder, value) + } + + final override fun deserialize(decoder: Decoder): Type { + val type = (decoder as? TypeAwareDecoder) + ?.decodeType(typeKey = discriminatorName) + ?: throw IllegalStateException("We need to know the type to decode first!") + + val actualSerializer = selectDeserializer(type) as KSerializer + return decoder.decodeSerializableValue(actualSerializer) + } + + protected abstract fun selectDeserializer(element: DiscriminatorType): DeserializationStrategy + + 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." + ) + } +} diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt index 17d8e02..df3c374 100644 --- a/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt @@ -2,6 +2,7 @@ package de.sipgate.federmappe.common 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 @@ -15,7 +16,7 @@ class StringMapToObjectDecoder( override val serializersModule: SerializersModule = EmptySerializersModule(), private val ignoreUnknownProperties: Boolean = false, private val subtypeDecoder: (Any?) -> CompositeDecoder? = { null } -) : AbstractDecoder() { +) : AbstractDecoder(), TypeAwareDecoder { private val keysIterator = data.keys.iterator() private var index: Int? = null private var key: String? = null @@ -64,7 +65,7 @@ class StringMapToObjectDecoder( } when (valueDescriptor) { - StructureKind.CLASS -> return StringMapToObjectDecoder( + StructureKind.CLASS, PolymorphicKind.SEALED -> return StringMapToObjectDecoder( data = value as Map, ignoreUnknownProperties = ignoreUnknownProperties, serializersModule = this.serializersModule, @@ -98,4 +99,10 @@ class StringMapToObjectDecoder( super.endStructure(descriptor) } + + override fun decodeType(typeKey: String): T? { + @Suppress("UNCHECKED_CAST") + val currentData = (data[key] as? Map) ?: return null + return (currentData[typeKey] as? T) + } } diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt new file mode 100644 index 0000000..6018093 --- /dev/null +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt @@ -0,0 +1,5 @@ +package de.sipgate.federmappe.common + +interface TypeAwareDecoder { + fun decodeType(typeKey: String = "type"): T? +} diff --git a/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt index c0b3149..a4a5443 100644 --- a/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt @@ -1,34 +1,44 @@ package de.sipgate.federmappe.common +import de.sipgate.federmappe.common.SealedTypeTests.BaseType +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Polymorphic -import kotlinx.serialization.SerialName +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(ExperimentalSerializationApi::class) -class SealedTypeTests { - - enum class TypeDecl { - @SerialName("A_TYPE") A, - @SerialName("B_TYPE") B +@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) +internal class CustomSerializer : SealedClassWithTypeSerializer(BaseType::class) { + override fun selectDeserializer(element: String): DeserializationStrategy { + return when (element) { + "A" -> BaseType.A.serializer() + "B" -> BaseType.B.serializer() + else -> throw IllegalArgumentException("unknown element $element") + } } +} - @Serializable +@OptIn(ExperimentalSerializationApi::class) +internal class SealedTypeTests { + + @Serializable(with = CustomSerializer::class) sealed interface BaseType { - val type: TypeDecl + val type: String @Serializable - data class A(val value: String): BaseType { - override val type = TypeDecl.A + data class A(val value: String) : BaseType { + override val type = "A" } @Serializable - data class B(val value: Boolean): BaseType { - override val type = TypeDecl.B + data class B(val value: Boolean) : BaseType { + override val type = "B" } } @@ -39,10 +49,12 @@ class SealedTypeTests { data class TestClass(val a: BaseType) val serializer = serializer() - val data = mapOf("a" to mapOf( - "type" to "A_TYPE", - "value" to "some string" - )) + val data = mapOf( + "a" to mapOf( + "type" to "A", + "value" to "some string" + ) + ) // Act val result = @@ -50,6 +62,12 @@ class SealedTypeTests { StringMapToObjectDecoder( data, ignoreUnknownProperties = true, + serializersModule = SerializersModule { + polymorphic(BaseType::class) { + subclass(BaseType.A::class) + subclass(BaseType.B::class) + } + } ), ) diff --git a/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt index 4fefb14..403c029 100644 --- a/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt @@ -11,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() {