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

Fix OnDisconnect failing to updateChildren #494

Merged
merged 15 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,21 @@ package dev.gitlive.firebase
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import java.lang.IllegalArgumentException
import kotlin.collections.set

actual data class EncodedObject(actual val raw: Map<String, Any?>) : Map<String, Any?> by raw {
actual companion object {
actual val emptyEncodedObject: EncodedObject = EncodedObject(emptyMap())
}
}

@PublishedApi
internal actual fun List<Pair<String, Any?>>.asEncodedObject() = EncodedObject(toMap())

@PublishedApi
internal actual fun Any.asNativeMap(): Map<*, *>? = this as? Map<*, *>

actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
StructureKind.LIST -> mutableListOf<Any?>()
.also { value = it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.modules.SerializersModule

/**
* Platform specific object for storing encoded data that can be used for methods that explicitly require an object.
* This is essentially a [Map] of [String] and [Any]? (as represented by [raw]) but since [encode] gives a platform specific value, this method wraps that.
*
* Created using [encodeAsObject]
*/
expect class EncodedObject {
companion object {
val emptyEncodedObject: EncodedObject
}

val raw: Map<String, Any?>
}

@Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("encode(strategy, value) { encodeDefaults = shouldEncodeElementDefault }"))
fun <T> encode(strategy: SerializationStrategy<T>, value: T, shouldEncodeElementDefault: Boolean): Any? = encode(strategy, value) {
this.encodeDefaults = shouldEncodeElementDefault
Expand All @@ -31,6 +45,21 @@ inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean): An
inline fun <reified T> encode(value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) =
encode(value, EncodeSettings.BuilderImpl().apply(buildSettings).buildEncodeSettings())

inline fun <T : Any> encodeAsObject(strategy: SerializationStrategy<T>, value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}): EncodedObject {
if (value is Map<*, *> && value.keys.any { it !is String }) {
throw IllegalArgumentException("$value is a Map containing non-String keys. Must be of the form Map<String, Any?>")
}
val encoded = encode(strategy, value, buildSettings) ?: throw IllegalArgumentException("$value was encoded as null. Must be of the form Map<String, Any?>")
return encoded.asNativeMap()?.asEncodedObject() ?: throw IllegalArgumentException("$value was encoded as ${encoded::class}. Must be of the form Map<String, Any?>")
}
inline fun <reified T : Any> encodeAsObject(value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}): EncodedObject {
if (value is Map<*, *> && value.keys.any { it !is String }) {
throw IllegalArgumentException("$value is a Map containing non-String keys. Must be of the form Map<String, Any?>")
}
val encoded = encode(value, buildSettings) ?: throw IllegalArgumentException("$value was encoded as null. Must be of the form Map<String, Any?>")
return encoded.asNativeMap()?.asEncodedObject() ?: throw IllegalArgumentException("$value was encoded as ${encoded::class}. Must be of the form Map<String, Any?>")
}

@PublishedApi
internal inline fun <reified T> encode(value: T, encodeSettings: EncodeSettings): Any? = value?.let {
FirebaseEncoder(encodeSettings).apply {
Expand All @@ -45,6 +74,21 @@ internal inline fun <reified T> encode(value: T, encodeSettings: EncodeSettings)
}.value
}

@PublishedApi
expect internal fun Any.asNativeMap(): Map<*, *>?

@PublishedApi
internal fun Map<*, *>.asEncodedObject(): EncodedObject = map { (key, value) ->
if (key is String) {
key to value
} else {
throw IllegalArgumentException("Expected a String key but received $key")
}
}.asEncodedObject()

@PublishedApi
internal expect fun List<Pair<String, Any?>>.asEncodedObject(): EncodedObject

/**
* An extension which which serializer to use for value. Handy in updating fields by name or path
* where using annotation is not possible
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlin.jvm.JvmInline
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull

@Serializable
object TestObject {
Expand Down Expand Up @@ -71,6 +74,17 @@ data class NestedClass(

class EncodersTest {

@Test
fun encodeDecodePrimaryTypes() {
assertEncode(true)
assertEncode(42)
assertEncode(8.toShort())
assertEncode(Int.MAX_VALUE.toLong() + 3)
assertEncode(0x03F)
assertEncode(3.33)
assertEncode(6.65f)
assertEncode("Test")
}
@Test
fun encodeDecodeList() {
val list = listOf("One", "Two", "Three")
Expand All @@ -82,6 +96,17 @@ class EncodersTest {
assertEquals(listOf("One", "Two", "Three"), decoded)
}

@Test
fun encodeDecodeNullableList() {
val list = listOf("One", "Two", null)
val encoded = encode<List<String?>>(list) { encodeDefaults = true }

nativeAssertEquals(nativeListOf("One", "Two", null), encoded)

val decoded = decode(ListSerializer(String.serializer().nullable), encoded)
assertEquals(listOf("One", "Two", null), decoded)
}

@Test
fun encodeDecodeMap() {
val map = mapOf("key" to "value", "key2" to "value2", "key3" to "value3")
Expand All @@ -93,6 +118,17 @@ class EncodersTest {
assertEquals(mapOf("key" to "value", "key2" to "value2", "key3" to "value3"), decoded)
}

@Test
fun encodeDecodeNullableMap() {
val map = mapOf("key" to "value", "key2" to "value2", "key3" to null)
val encoded = encode<Map<String, String?>>(map) { encodeDefaults = true }

nativeAssertEquals(nativeMapOf("key" to "value", "key2" to "value2", "key3" to null), encoded)

val decoded = decode(MapSerializer(String.serializer(), String.serializer().nullable), encoded)
assertEquals(mapOf("key" to "value", "key2" to "value2", "key3" to null), decoded)
}

@Test
fun encodeDecodeObject() {
val encoded = encode(TestObject.serializer(), TestObject) { encodeDefaults = false }
Expand Down Expand Up @@ -380,4 +416,39 @@ class EncodersTest {
reencoded
)
}

@Test
fun encodeAsObject() {
val testDataClass = TestData(mapOf("key" to "value"), mapOf(1 to 1), true, null, ValueClass(42))
val encodedObject = encodeAsObject(TestData.serializer(), testDataClass) { encodeDefaults = false }

nativeAssertEquals(mapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "valueClass" to 42), encodedObject.raw)

val testMap = mapOf("one" to 1, "two" to null, "three" to false)
assertEquals(testMap, encodeAsObject(testMap).raw)

assertEquals(emptyMap(), encodeAsObject(TestObject).raw)

assertFailsWith<IllegalArgumentException> { encodeAsObject(true) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(42) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(8.toShort()) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(Int.MAX_VALUE.toLong() + 3) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(0x03F) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(3.33) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(6.65f) }
assertFailsWith<IllegalArgumentException> { encodeAsObject("Test") }
assertFailsWith<IllegalArgumentException> { encodeAsObject(ValueClass(2)) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(mapOf(1 to "one")) }
assertFailsWith<IllegalArgumentException> { encodeAsObject(listOf("one")) }
}

private inline fun <reified T> assertEncode(value: T) {
val encoded = encode(value)
assertEquals(value, encoded)
assertEquals(value, decode<T>(encoded))

val nullableEncoded = encode<T?>(null)
assertNull(nullableEncoded)
assertNull(decode<T?>(nullableEncoded))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlin.collections.set

actual data class EncodedObject(actual val raw: Map<String, Any?>) : Map<Any?, Any?> by raw.mapKeys({ (key, _) -> key as? Any }) {
actual companion object {
actual val emptyEncodedObject: EncodedObject = EncodedObject(emptyMap())
}
}

@PublishedApi
internal actual fun List<Pair<String, Any?>>.asEncodedObject() = EncodedObject(toMap())

@PublishedApi
internal actual fun Any.asNativeMap(): Map<*, *>? = this as? Map<*, *>

actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
StructureKind.LIST -> encodeAsList()
StructureKind.MAP -> mutableListOf<Any?>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,47 @@ package dev.gitlive.firebase
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlin.js.Json
import kotlin.js.json

actual data class EncodedObject(private val keyValues: List<Pair<String, Any?>>) {
actual companion object {
actual val emptyEncodedObject: EncodedObject = EncodedObject(emptyList())
}

actual val raw get() = keyValues.toMap()
val json get() = json(*keyValues.toTypedArray())
}

@PublishedApi
internal actual fun List<Pair<String, Any?>>.asEncodedObject() = EncodedObject(this)

@PublishedApi
internal actual fun Any.asNativeMap(): Map<*, *>? {
val json = when (this) {
is Number -> null
is Boolean -> null
is String -> null
is Map<*, *> -> {
if (keys.all { it is String }) {
this as Json
} else {
null
}
}
is Collection<*> -> null
is Array<*> -> null
else -> {
this as Json
}
} ?: return null
val mutableMap = mutableMapOf<String, Any?>()
for (key in js("Object").keys(json)) {
mutableMap[key] = json[key]
}
return mutableMap.toMap()
}

actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
StructureKind.LIST -> encodeAsList(descriptor)
StructureKind.MAP -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.google.firebase.database.Transaction
import com.google.firebase.database.ValueEventListener
import dev.gitlive.firebase.DecodeSettings
import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
import dev.gitlive.firebase.EncodedObject
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.database.ChildEvent.Type
Expand Down Expand Up @@ -87,6 +88,10 @@ actual class FirebaseDatabase private constructor(val android: com.google.fireba

actual fun useEmulator(host: String, port: Int) =
android.useEmulator(host, port)

actual fun goOffline() = android.goOffline()

actual fun goOnline() = android.goOnline()
}

internal actual open class NativeQuery(
Expand Down Expand Up @@ -201,9 +206,8 @@ internal actual class NativeDatabaseReference internal constructor(
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
.run { Unit }

@Suppress("UNCHECKED_CAST")
actual suspend fun updateEncodedChildren(encodedUpdate: Any?) =
android.updateChildren(encodedUpdate as Map<String, Any?>)
actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) =
android.updateChildren(encodedUpdate)
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
.run { Unit }

Expand Down Expand Up @@ -249,7 +253,6 @@ internal actual class NativeDatabaseReference internal constructor(

val DatabaseReference.android get() = nativeReference.android

@Suppress("UNCHECKED_CAST")
actual class DataSnapshot internal constructor(
val android: com.google.firebase.database.DataSnapshot,
private val persistenceEnabled: Boolean
Expand Down Expand Up @@ -293,7 +296,7 @@ internal actual class NativeOnDisconnect internal constructor(
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
.run { Unit }

actual suspend fun updateEncodedChildren(encodedUpdate: Map<String, Any?>) =
actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) =
android.updateChildren(encodedUpdate)
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
.run { Unit }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ package dev.gitlive.firebase.database
import dev.gitlive.firebase.DecodeSettings
import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
import dev.gitlive.firebase.EncodeSettings
import dev.gitlive.firebase.EncodedObject
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.database.ChildEvent.Type.ADDED
import dev.gitlive.firebase.database.ChildEvent.Type.CHANGED
import dev.gitlive.firebase.database.ChildEvent.Type.MOVED
import dev.gitlive.firebase.database.ChildEvent.Type.REMOVED
import dev.gitlive.firebase.encode
import dev.gitlive.firebase.encodeAsObject
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
Expand All @@ -37,6 +39,10 @@ expect class FirebaseDatabase {
fun setPersistenceEnabled(enabled: Boolean)
fun setLoggingEnabled(enabled: Boolean)
fun useEmulator(host: String, port: Int)

fun goOffline()

fun goOnline()
}

data class ChildEvent internal constructor(
Expand Down Expand Up @@ -78,7 +84,7 @@ internal expect class NativeDatabaseReference : NativeQuery {
val key: String?
fun push(): NativeDatabaseReference
suspend fun setValueEncoded(encodedValue: Any?)
suspend fun updateEncodedChildren(encodedUpdate: Any?)
suspend fun updateEncodedChildren(encodedUpdate: EncodedObject)
Comment on lines 88 to +89
Copy link
Member

Choose a reason for hiding this comment

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

are these methods suppose to be part of the public API? They don't have an equivalent methods in the official SDK, I'm assuming we don't have any documentation for them either, and personally I have no idea what 'encoded' means here?

fun child(path: String): NativeDatabaseReference
fun onDisconnect(): NativeOnDisconnect

Expand Down Expand Up @@ -114,7 +120,7 @@ class DatabaseReference internal constructor(@PublishedApi internal val nativeRe
this.encodeDefaults = encodeDefaults
}
suspend inline fun updateChildren(update: Map<String, Any?>, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = nativeReference.updateEncodedChildren(
encode(update, buildSettings))
encodeAsObject(update, buildSettings))

suspend fun removeValue() = nativeReference.removeValue()

Expand All @@ -140,7 +146,7 @@ internal expect class NativeOnDisconnect {
suspend fun removeValue()
suspend fun cancel()
suspend fun setValue(encodedValue: Any?)
suspend fun updateEncodedChildren(encodedUpdate: Map<String, Any?>)
suspend fun updateEncodedChildren(encodedUpdate: EncodedObject)
}

class OnDisconnect internal constructor(@PublishedApi internal val native: NativeOnDisconnect) {
Expand All @@ -156,7 +162,9 @@ class OnDisconnect internal constructor(@PublishedApi internal val native: Nativ
setValue(strategy, value) { this.encodeDefaults = encodeDefaults }
suspend inline fun <T> setValue(strategy: SerializationStrategy<T>, value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = setValue(encode(strategy, value, buildSettings))

suspend inline fun updateChildren(update: Map<String, Any?>, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncodedChildren(update.mapValues { (_, it) -> encode(it, buildSettings) })
suspend inline fun updateChildren(update: Map<String, Any?>, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncodedChildren(
encodeAsObject(update, buildSettings)
)
@Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("updateChildren(update) { this.encodeDefaults = encodeDefaults }"))
suspend fun updateChildren(update: Map<String, Any?>, encodeDefaults: Boolean) = updateChildren(update) {
this.encodeDefaults = encodeDefaults
Expand Down
Loading
Loading