Skip to content

Commit

Permalink
🐛✨ fix firestore timestamp decoding by adding a DataNormalizer
Browse files Browse the repository at this point in the history
  • Loading branch information
JBokMan committed Aug 9, 2024
1 parent 33f168f commit fbe679f
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.sipgate.federmappe.common

import de.sipgate.federmappe.common.decoder.DataNormalizer
import de.sipgate.federmappe.common.decoder.StringMapToObjectDecoder
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
Expand All @@ -11,10 +12,12 @@ inline fun <reified T : Any> Map<String, Any>.toObjectWithSerializer(
serializer: KSerializer<T> = serializer<T>(),
customSerializers: SerializersModule = DefaultSerializersModule,
ignoreUnknownProperties: Boolean = true,
dataNormalizer: DataNormalizer,
): T = serializer.deserialize(
StringMapToObjectDecoder(
this,
ignoreUnknownProperties = ignoreUnknownProperties,
serializersModule = customSerializers
serializersModule = customSerializers,
dataNormalizer = dataNormalizer
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.sipgate.federmappe.common.decoder

interface DataNormalizer {
fun normalize(data: Map<String, Any?>): Map<String, Any?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ListDecoder(
private val list: ArrayDeque<Any>,
private val elementsCount: Int = 0,
override val serializersModule: SerializersModule = EmptySerializersModule(),
private val dataNormalizer: DataNormalizer
) : AbstractDecoder() {
private var index = 0

Expand Down Expand Up @@ -49,11 +50,17 @@ class ListDecoder(
data = value as Map<String, Any>,
ignoreUnknownProperties = true,
serializersModule = this.serializersModule,
dataNormalizer = dataNormalizer
)

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

else -> throw SerializationException("Type is not a list ${descriptor.serialName}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class MapDecoder(
private val map: Map<String, Any?>,
override val serializersModule: SerializersModule = EmptySerializersModule(),
private val ignoreUnknownProperties: Boolean = false,
private val dataNormalizer: DataNormalizer
) : AbstractDecoder() {
private val flattenedData =
map.entries.fold(emptyList<Any?>()) { acc, (key, value) ->
Expand Down Expand Up @@ -81,16 +82,23 @@ class MapDecoder(
data = value as Map<String, Any>,
ignoreUnknownProperties = ignoreUnknownProperties,
serializersModule = this.serializersModule,
dataNormalizer = dataNormalizer
)

StructureKind.MAP -> return MapDecoder(
map = value as Map<String, Any>,
ignoreUnknownProperties = ignoreUnknownProperties,
dataNormalizer = dataNormalizer
)

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

else -> throw SerializationException("Given value is neither a list nor a type $value")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import kotlin.collections.toCollection
class StringMapToObjectDecoder(
private val data: Map<String, Any?>,
override val serializersModule: SerializersModule = EmptySerializersModule(),
private val ignoreUnknownProperties: Boolean = false
private val ignoreUnknownProperties: Boolean = false,
private val dataNormalizer: DataNormalizer
) : AbstractDecoder(), TypeAwareDecoder {
private val keysIterator = data.sortByPrio().keys.iterator()
private var index: Int? = null
Expand Down Expand Up @@ -64,25 +65,29 @@ class StringMapToObjectDecoder(
return this
}

val value = data[key]
val normalizedData = dataNormalizer.normalize(data)
val value = normalizedData[key]
val valueDescriptor = descriptor.kind

when (valueDescriptor) {
StructureKind.CLASS -> return StringMapToObjectDecoder(
data = value as Map<String, Any>,
ignoreUnknownProperties = ignoreUnknownProperties,
serializersModule = this.serializersModule,
dataNormalizer = dataNormalizer
)

PolymorphicKind.SEALED -> return StringMapToObjectDecoder(
data = value as Map<String, Any>,
ignoreUnknownProperties = ignoreUnknownProperties,
serializersModule = this.serializersModule,
dataNormalizer = dataNormalizer
)

StructureKind.MAP -> return MapDecoder(
map = value as Map<String, Any>,
ignoreUnknownProperties = ignoreUnknownProperties,
dataNormalizer = dataNormalizer
)

StructureKind.LIST -> {
Expand All @@ -92,7 +97,8 @@ class StringMapToObjectDecoder(
return ListDecoder(
list = ArrayDeque(list),
elementsCount = list.size,
serializersModule = serializersModule
serializersModule = serializersModule,
dataNormalizer = dataNormalizer
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ package de.sipgate.federmappe.firestore
import com.google.firebase.firestore.DocumentSnapshot
import de.sipgate.federmappe.common.DefaultSerializersModule
import de.sipgate.federmappe.common.ErrorHandler
import de.sipgate.federmappe.common.decoder.DataNormalizer
import de.sipgate.federmappe.common.toObjectWithSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.modules.SerializersModule

@ExperimentalSerializationApi
inline fun <reified T : Any> DocumentSnapshot.toObject(
customSerializers: SerializersModule = DefaultSerializersModule,
errorHandler: ErrorHandler<T> = { throw it }
errorHandler: ErrorHandler<T> = { throw it },
dataNormalizer: DataNormalizer = FirestoreTimestampDataNormalizer()
): T? = try {
data?.toObjectWithSerializer<T>(customSerializers = customSerializers)
data?.normalizeStringMap()?.toObjectWithSerializer<T>(
customSerializers = customSerializers,
dataNormalizer = dataNormalizer
)
} catch (ex: Throwable) {
errorHandler(ex)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.sipgate.federmappe.firestore

import de.sipgate.federmappe.common.decoder.DataNormalizer

class FirestoreTimestampDataNormalizer : DataNormalizer {
override fun normalize(data: Map<String, Any?>): Map<String, Any?> =
data.normalizeStringMapNullable()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.firebase.Timestamp
import com.google.firebase.firestore.QuerySnapshot
import de.sipgate.federmappe.common.DefaultSerializersModule
import de.sipgate.federmappe.common.ErrorHandler
import de.sipgate.federmappe.common.decoder.DataNormalizer
import de.sipgate.federmappe.common.toObjectWithSerializer
import de.sipgate.federmappe.firestore.types.toDecodableTimestamp
import kotlinx.serialization.ExperimentalSerializationApi
Expand All @@ -12,12 +13,16 @@ import kotlinx.serialization.modules.SerializersModule
@ExperimentalSerializationApi
inline fun <reified T : Any> QuerySnapshot.toObjects(
customSerializers: SerializersModule = DefaultSerializersModule,
errorHandler: ErrorHandler<T> = { throw it }
errorHandler: ErrorHandler<T> = { throw it },
dataNormalizer: DataNormalizer = FirestoreTimestampDataNormalizer()
): List<T?> = map { documentSnapshot ->
try {
documentSnapshot.data
.normalizeStringMap()
.toObjectWithSerializer<T>(customSerializers = customSerializers)
.toObjectWithSerializer<T>(
customSerializers = customSerializers,
dataNormalizer = dataNormalizer
)
} catch (ex: Throwable) {
errorHandler(ex)
}
Expand All @@ -29,3 +34,10 @@ fun Map<String, Any>.normalizeStringMap(): Map<String, Any> = mapValues {
else -> value
}
}

fun Map<String, Any?>.normalizeStringMapNullable(): Map<String, Any?> = mapValues {
when (val value = it.value) {
is Timestamp -> value.toDecodableTimestamp()
else -> value
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.sipgate.federmappe.firestore.types.de.sipgate.federmappe.firestore

import com.google.firebase.Timestamp
import de.sipgate.federmappe.common.decoder.StringMapToObjectDecoder
import de.sipgate.federmappe.firestore.FirestoreTimestampDataNormalizer
import de.sipgate.federmappe.firestore.types.toDecodableTimestamp
import kotlinx.datetime.Instant
import kotlinx.datetime.serializers.InstantComponentSerializer
import kotlinx.datetime.toJavaInstant
import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import kotlinx.serialization.serializer
import java.util.Date
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs

class FirestoreTimestampToDecodableTimestampTest {

@Test
fun timestampWithNanosecondPrecisionIsConvertedSuccessfully() {
val expectedInstant = Instant.fromEpochSeconds(1716823455, 854)

val timestamp = Timestamp(expectedInstant.toJavaInstant())

val result = timestamp.toDecodableTimestamp()
assertEquals(expectedInstant.epochSeconds, result["epochSeconds"])
assertEquals(expectedInstant.nanosecondsOfSecond.toLong(), result["nanosecondsOfSecond"])
}

@Test
fun timestampWithSecondPrecisionIsConvertedSuccessfully() {
val expectedDate = Date.from(Instant.fromEpochSeconds(1716823455).toJavaInstant())
val expectedEpochSeconds = expectedDate.time / 1000

val timestamp = Timestamp(expectedDate)

val result = timestamp.toDecodableTimestamp()

assertEquals(expectedEpochSeconds, result["epochSeconds"])
assertEquals(0L, result["nanosecondsOfSecond"])
}

@OptIn(ExperimentalSerializationApi::class)
@Test
fun firestoreTimestampIsDecodedCorrectly() {
// Arrange
val expectedInstant = Instant.fromEpochSeconds(1716823455)
val expectedDate = Date.from(expectedInstant.toJavaInstant())
val timestamp = Timestamp(expectedDate)

@Serializable
data class MockLocalDataClass(
@Contextual
val createdAt: Instant
)

val serializer = serializer<MockLocalDataClass>()

val data = mapOf<String, Any?>("createdAt" to timestamp)

// Act
val result =
serializer.deserialize(
StringMapToObjectDecoder(
data = data,
serializersModule = SerializersModule { contextual(InstantComponentSerializer) },
dataNormalizer = FirestoreTimestampDataNormalizer()
),
)

// Assert
assertEquals(expectedInstant, result.createdAt)
assertIs<MockLocalDataClass>(result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package de.sipgate.federmappe.realtimedb

import com.google.firebase.database.DataSnapshot
import de.sipgate.federmappe.common.ErrorHandler
import de.sipgate.federmappe.common.decoder.DataNormalizer
import de.sipgate.federmappe.common.toObjectWithSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.modules.EmptySerializersModule
Expand All @@ -12,14 +13,16 @@ import kotlinx.serialization.serializer
inline fun <reified T : Any> DataSnapshot.toObject(
customSerializers: SerializersModule = EmptySerializersModule(),
ignoreUnknownProperties: Boolean = false,
crossinline errorHandler: ErrorHandler<T> = { throw it }
crossinline errorHandler: ErrorHandler<T> = { throw it },
dataNormalizer: DataNormalizer = DummyDataNormalizer()
): T? = try {
toObjectMap()
.unwrapRoot()
.toObjectWithSerializer(
serializer = serializer(),
customSerializers = customSerializers,
ignoreUnknownProperties = ignoreUnknownProperties
ignoreUnknownProperties = ignoreUnknownProperties,
dataNormalizer = dataNormalizer
)
} catch (ex: Throwable) {
errorHandler(ex)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.sipgate.federmappe.realtimedb

import de.sipgate.federmappe.common.decoder.DataNormalizer

class DummyDataNormalizer : DataNormalizer {
override fun normalize(data: Map<String, Any?>): Map<String, Any?> = data
}

0 comments on commit fbe679f

Please sign in to comment.