Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Serialization fixes #448

Merged
merged 25 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
816bd62
Adding Serialization fixes
Daeda88 Dec 19, 2023
e126519
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 2, 2024
69e4516
Ensure API stability
Daeda88 Jan 3, 2024
582532d
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 3, 2024
fae5299
Track for JS
Daeda88 Jan 3, 2024
0d252b5
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 5, 2024
a4a7150
Use named arguments for settings
Daeda88 Jan 12, 2024
0c6e22c
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 12, 2024
8305507
Use builders instead
Daeda88 Jan 12, 2024
045e127
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 12, 2024
3c9f7ff
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 19, 2024
4ad4d13
Remove Async classes + add inline for builder funs
Daeda88 Jan 22, 2024
46d7c17
Fixed Database runTransaction
Daeda88 Jan 22, 2024
bf709a3
Fixed Android crash, updated tests + cleanup
Daeda88 Jan 23, 2024
3021502
Update Readme
Daeda88 Jan 23, 2024
59b4522
Some renames + readme update
Daeda88 Jan 27, 2024
7a2ae5d
Add value class fix
Daeda88 Jan 27, 2024
2fb7f58
Made helper methods internal
Daeda88 Jan 27, 2024
0ca5675
Slight optimization of Value class tests
Daeda88 Jan 27, 2024
1995071
Small test improvement
Daeda88 Jan 29, 2024
87d4ae2
Decode polymorphic using elementName rather than index
Daeda88 Jan 29, 2024
71bcce4
Also test nested data class encoding
Daeda88 Jan 29, 2024
26d7ea9
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Mar 1, 2024
f75b240
Use Wrappers rather than Abstract classes
Daeda88 Mar 1, 2024
f8eb4fd
Use wrappes + extension methods instead of abstract class
Daeda88 Mar 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ citiesRef.where {
}
```

<h3><a href="https://kotlinlang.org/docs/reference/functions.html#named-arguments">Named arguments</a></h3>

To improve readability functions such as the Cloud Firestore data encoding/decoding use named arguments:

```kotlin
documentRef.set(data, encodeDefaults = false, serializersModule = customSerializerModule)
```

<h3><a href="https://kotlinlang.org/docs/reference/operator-overloading.html">Operator overloading</a></h3>

In cases where it makes sense, such as Firebase Functions HTTPS Callable, operator overloading is used:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,41 @@

package dev.gitlive.firebase

import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.CompositeDecoder

actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor): CompositeDecoder = when(descriptor.kind) {
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> (value as Map<*, *>).let { map ->
FirebaseClassDecoder(map.size, { map.containsKey(it) }) { desc, index ->
val elementName = desc.getElementName(index)
if (desc.kind is PolymorphicKind && elementName == "value") {
map
} else {
map[desc.getElementName(index)]
}
}
}
StructureKind.LIST ->
when(value) {
is List<*> -> value
is Map<*, *> -> value.asSequence()
.sortedBy { (it) -> it.toString().toIntOrNull() }
.map { (_, it) -> it }
.toList()
else -> error("unexpected type, got $value when expecting a list")
}
.let { FirebaseCompositeDecoder(it.size) { _, index -> it[index] } }
StructureKind.MAP -> (value as Map<*, *>).entries.toList().let {
FirebaseCompositeDecoder(it.size) { _, index -> it[index/2].run { if(index % 2 == 0) key else value } }
}
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, polymorphicIsNested: Boolean): CompositeDecoder = when (descriptor.kind) {
StructureKind.CLASS, StructureKind.OBJECT -> decodeAsMap(false)
StructureKind.LIST -> (value as? List<*>).orEmpty().let {
FirebaseCompositeDecoder(it.size, settings) { _, index -> it[index] }
}

StructureKind.MAP -> (value as? Map<*, *>).orEmpty().entries.toList().let {
FirebaseCompositeDecoder(
it.size,
settings
) { _, index -> it[index / 2].run { if (index % 2 == 0) key else value } }
}

is PolymorphicKind -> decodeAsMap(polymorphicIsNested)
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
}

actual fun getPolymorphicType(value: Any?, discriminator: String): String =
(value as Map<*,*>)[discriminator] as String
(value as? Map<*,*>).orEmpty()[discriminator] as String

private fun FirebaseDecoder.decodeAsMap(isNestedPolymorphic: Boolean): CompositeDecoder = (value as? Map<*, *>).orEmpty().let { map ->
FirebaseClassDecoder(map.size, settings, { map.containsKey(it) }) { desc, index ->
if (isNestedPolymorphic) {
if (index == 0)
map[desc.getElementName(index)]
else {
map
}
} else {
map[desc.getElementName(index)]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@

package dev.gitlive.firebase

import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlin.collections.set

actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
StructureKind.LIST -> mutableListOf<Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
.let { FirebaseCompositeEncoder(settings) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf<Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault,
.let { FirebaseCompositeEncoder(settings, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT -> encodeAsMap(descriptor)
is PolymorphicKind -> encodeAsMap(descriptor)
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
}

private fun FirebaseEncoder.encodeAsMap(descriptor: SerialDescriptor): FirebaseCompositeEncoder = mutableMapOf<Any?, Any?>()
.also { value = it }
.let {
FirebaseCompositeEncoder(
settings,
setPolymorphicType = { discriminator, type ->
it[discriminator] = type
},
set = { _, index, value -> it[descriptor.getElementName(index)] = value }
) }
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.gitlive.firebase

import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule

/**
* Settings used to configure encoding/decoding
*/
sealed class EncodeDecodeSettings {

/**
* The [SerializersModule] to use for serialization. This allows for polymorphic serialization on runtime
*/
abstract val serializersModule: SerializersModule
}

/**
* [EncodeDecodeSettings] used when encoding an object
* @property shouldEncodeElementDefault if `true` this will explicitly encode elements even if they are their default value
* @param serializersModule the [SerializersModule] to use for serialization. This allows for polymorphic serialization on runtime
*/
data class EncodeSettings(
val shouldEncodeElementDefault: Boolean = true,
override val serializersModule: SerializersModule = EmptySerializersModule(),
) : EncodeDecodeSettings()

/**
* [EncodeDecodeSettings] used when decoding an object
* @param serializersModule the [SerializersModule] to use for deserialization. This allows for polymorphic serialization on runtime
*/
data class DecodeSettings(
override val serializersModule: SerializersModule = EmptySerializersModule(),
) : EncodeDecodeSettings()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cant these be internal now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some issues with some inline funs.

But actually, I'd like to propose that instead of named arguments we try a builder approach. E.g.:

set(value) { shouldEncodeDefaults = false }

Reason im suggesting it is because its annoying as hell right now to add new settings, since you have dozens of methods that need them specified. A builder makes it a one line addition

Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ internal fun <T> FirebaseEncoder.encodePolymorphically(
value: T,
ifPolymorphic: (String) -> Unit
) {
// If serializer is not an AbstractPolymorphicSerializer or if we are encoding this as a list, we can just use the regular serializer
// This will result in calling structureEncoder for complicated structures
// For PolymorphicKind this will first encode the polymorphic discriminator as a String and the remaining StructureKind.Class as a map of key-value pairs
// This will result in a list structured like: (type, { classKey = classValue })
if (serializer !is AbstractPolymorphicSerializer<*>) {
serializer.serialize(this, value)
return
}

// When doing Polymorphic Serialization with EncodeDecodeSettings.PolymorphicStructure.MAP we will use the polymorphic serializer of the class.
val casted = serializer as AbstractPolymorphicSerializer<Any>
val baseClassDiscriminator = serializer.descriptor.classDiscriminator()
val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
Expand All @@ -32,15 +38,15 @@ internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
value: Any?,
deserializer: DeserializationStrategy<T>,
): T {
// If deserializer is not an AbstractPolymorphicSerializer or if we are decoding this from a list, we can just use the regular serializer
if (deserializer !is AbstractPolymorphicSerializer<*>) {
return deserializer.deserialize(this)
}

val casted = deserializer as AbstractPolymorphicSerializer<Any>
val discriminator = deserializer.descriptor.classDiscriminator()
val type = getPolymorphicType(value, discriminator)
val actualDeserializer = casted.findPolymorphicSerializerOrNull(
structureDecoder(deserializer.descriptor),
structureDecoder(deserializer.descriptor, false),
type
) as DeserializationStrategy<T>
return actualDeserializer.deserialize(this)
Expand All @@ -55,4 +61,3 @@ internal fun SerialDescriptor.classDiscriminator(): String {
}
return "type"
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,24 @@ import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer

@Suppress("UNCHECKED_CAST")
inline fun <reified T> decode(value: Any?): T {
inline fun <reified T> decode(value: Any?, serializersModule: SerializersModule = EmptySerializersModule()): T {
val strategy = serializer<T>()
return decode(strategy as DeserializationStrategy<T>, value)
return decode(strategy as DeserializationStrategy<T>, value, serializersModule)
}

fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?): T {
fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?, serializersModule: SerializersModule = EmptySerializersModule()): T {
require(value != null || strategy.descriptor.isNullable) { "Value was null for non-nullable type ${strategy.descriptor.serialName}" }
return FirebaseDecoder(value).decodeSerializableValue(strategy)
return FirebaseDecoder(value, DecodeSettings(serializersModule)).decodeSerializableValue(strategy)
}
expect fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor): CompositeDecoder
expect fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, polymorphicIsNested: Boolean): CompositeDecoder
expect fun getPolymorphicType(value: Any?, discriminator: String): String

class FirebaseDecoder(internal val value: Any?) : Decoder {
class FirebaseDecoder(val value: Any?, internal val settings: DecodeSettings) : Decoder {

constructor(value: Any?) : this(value, DecodeSettings())

override val serializersModule: SerializersModule
get() = EmptySerializersModule()
override val serializersModule: SerializersModule = settings.serializersModule

override fun beginStructure(descriptor: SerialDescriptor) = structureDecoder(descriptor)
override fun beginStructure(descriptor: SerialDescriptor) = structureDecoder(descriptor, true)

override fun decodeString() = decodeString(value)

Expand All @@ -59,7 +58,7 @@ class FirebaseDecoder(internal val value: Any?) : Decoder {

override fun decodeNull() = decodeNull(value)

override fun decodeInline(descriptor: SerialDescriptor) = FirebaseDecoder(value)
override fun decodeInline(descriptor: SerialDescriptor) = FirebaseDecoder(value, settings)

override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return decodeSerializableValuePolymorphic(value, deserializer)
Expand All @@ -68,26 +67,35 @@ class FirebaseDecoder(internal val value: Any?) : Decoder {

class FirebaseClassDecoder(
size: Int,
settings: DecodeSettings,
private val containsKey: (name: String) -> Boolean,
get: (descriptor: SerialDescriptor, index: Int) -> Any?
) : FirebaseCompositeDecoder(size, get) {
) : FirebaseCompositeDecoder(size, settings, get) {
private var index: Int = 0

override fun decodeSequentially() = false

override fun decodeElementIndex(descriptor: SerialDescriptor): Int =
(index until descriptor.elementsCount)
.firstOrNull { !descriptor.isElementOptional(it) || containsKey(descriptor.getElementName(it)) }
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
return (index until descriptor.elementsCount)
.firstOrNull {
!descriptor.isElementOptional(it) || containsKey(
descriptor.getElementName(
it
)
)
}
?.also { index = it + 1 }
?: DECODE_DONE
}
}

open class FirebaseCompositeDecoder(
private val size: Int,
private val get: (descriptor: SerialDescriptor, index: Int) -> Any?
internal val settings: DecodeSettings,
private val get: (descriptor: SerialDescriptor, index: Int) -> Any?,
): CompositeDecoder {

override val serializersModule = EmptySerializersModule()
override val serializersModule: SerializersModule = settings.serializersModule

override fun decodeSequentially() = true

Expand All @@ -100,21 +108,30 @@ open class FirebaseCompositeDecoder(
index: Int,
deserializer: DeserializationStrategy<T>,
previousValue: T?
) = deserializer.deserialize(FirebaseDecoder(get(descriptor, index)))
) = decodeElement(descriptor, index) {
deserializer.deserialize(FirebaseDecoder(it, settings))
}

override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = decodeBoolean(get(descriptor, index))
override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeBoolean)

override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = decodeByte(get(descriptor, index))
override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeByte)

override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = decodeChar(get(descriptor, index))
override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeChar)

override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeDouble(get(descriptor, index))
override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeDouble)

override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = decodeFloat(get(descriptor, index))
override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeFloat)

override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = decodeInt(get(descriptor, index))
override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeInt)

override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = decodeLong(get(descriptor, index))
override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeLong)

override fun <T : Any> decodeNullableSerializableElement(
descriptor: SerialDescriptor,
Expand All @@ -123,19 +140,37 @@ open class FirebaseCompositeDecoder(
previousValue: T?
): T? {
val isNullabilitySupported = deserializer.descriptor.isNullable
return if (isNullabilitySupported || decodeNotNullMark(get(descriptor, index))) decodeSerializableElement(descriptor, index, deserializer, previousValue) else decodeNull(get(descriptor, index))
return if (isNullabilitySupported || decodeElement(descriptor, index, ::decodeNotNullMark)) {
decodeSerializableElement(descriptor, index, deserializer, previousValue)
} else {
decodeElement(descriptor, index, ::decodeNull)
}
}

override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = decodeShort(get(descriptor, index))
override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeShort)

override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = decodeString(get(descriptor, index))
override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) =
decodeElement(descriptor, index, ::decodeString)

override fun endStructure(descriptor: SerialDescriptor) {}

@ExperimentalSerializationApi
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder =
FirebaseDecoder(get(descriptor, index))

decodeElement(descriptor, index) {
FirebaseDecoder(it, settings)
}

private fun <T> decodeElement(descriptor: SerialDescriptor, index: Int, decoder: (Any?) -> T): T {
return try {
decoder(get(descriptor, index))
} catch (e: Exception) {
throw SerializationException(
message = "Exception during decoding ${descriptor.serialName} ${descriptor.getElementName(index)}",
cause = e
)
}
}
}

private fun decodeString(value: Any?) = value.toString()
Expand Down Expand Up @@ -201,5 +236,3 @@ internal fun SerialDescriptor.getElementIndexOrThrow(name: String): Int {
private fun decodeNotNullMark(value: Any?) = value != null

private fun decodeNull(value: Any?) = value as Nothing?


Loading
Loading