Skip to content

Commit bf709a3

Browse files
committed
Fixed Android crash, updated tests + cleanup
1 parent 46d7c17 commit bf709a3

File tree

12 files changed

+309
-75
lines changed

12 files changed

+309
-75
lines changed

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor
1111
import kotlinx.serialization.encoding.CompositeDecoder
1212
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
1313
import kotlinx.serialization.encoding.Decoder
14-
import kotlinx.serialization.modules.EmptySerializersModule
1514
import kotlinx.serialization.modules.SerializersModule
1615
import kotlinx.serialization.serializer
1716

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
package dev.gitlive.firebase
66

7-
import kotlinx.serialization.*
8-
import kotlinx.serialization.descriptors.*
9-
import kotlinx.serialization.encoding.*
7+
import kotlinx.serialization.ExperimentalSerializationApi
8+
import kotlinx.serialization.SerializationStrategy
9+
import kotlinx.serialization.descriptors.SerialDescriptor
10+
import kotlinx.serialization.encoding.CompositeEncoder
11+
import kotlinx.serialization.encoding.Encoder
1012
import kotlinx.serialization.modules.SerializersModule
1113

1214
@Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("encode(strategy, value) { this.shouldEncodeElementDefault = shouldEncodeElementDefault }"))

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/reencodeTransformation.kt

+11-4
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@ package dev.gitlive.firebase
22

33
import kotlinx.serialization.KSerializer
44

5-
inline fun <reified T> reencodeTransformation(value: Any?, builder: EncodeDecodeSettingsBuilder.() -> Unit, transform: (T) -> T): Any? {
5+
inline fun <reified T> reencodeTransformation(value: Any?, builder: EncodeDecodeSettingsBuilder.() -> Unit = {}, transform: (T) -> T): Any? {
66
val encodeDecodeSettingsBuilder = EncodeDecodeSettingsBuilderImpl().apply(builder)
77
val oldValue: T = decode(value, encodeDecodeSettingsBuilder.buildDecodeSettings())
8-
return encode(transform(oldValue), encodeDecodeSettingsBuilder.buildEncodeSettings())
8+
return encode(
9+
transform(oldValue),
10+
encodeDecodeSettingsBuilder.buildEncodeSettings()
11+
)
912
}
1013

11-
inline fun <T> reencodeTransformation(strategy: KSerializer<T>, value: Any?, builder: EncodeDecodeSettingsBuilder.() -> Unit, transform: (T) -> T): Any? {
14+
inline fun <T> reencodeTransformation(strategy: KSerializer<T>, value: Any?, builder: EncodeDecodeSettingsBuilder.() -> Unit = {}, transform: (T) -> T): Any? {
1215
val encodeDecodeSettingsBuilder = EncodeDecodeSettingsBuilderImpl().apply(builder)
1316
val oldValue: T = decode(strategy, value, encodeDecodeSettingsBuilder.buildDecodeSettings())
14-
return encode(strategy, transform(oldValue), encodeDecodeSettingsBuilder.buildEncodeSettings())
17+
return encode(
18+
strategy,
19+
transform(oldValue),
20+
encodeDecodeSettingsBuilder.buildEncodeSettings()
21+
)
1522
}

firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt

+146-11
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@ package dev.gitlive.firebase
77
import kotlinx.serialization.SerialName
88
import kotlinx.serialization.Serializable
99
import kotlinx.serialization.builtins.ListSerializer
10-
import kotlin.test.Test
11-
import kotlin.test.assertEquals
12-
import kotlin.test.assertNull
13-
import dev.gitlive.firebase.nativeAssertEquals
14-
import dev.gitlive.firebase.nativeMapOf
1510
import kotlinx.serialization.builtins.MapSerializer
1611
import kotlinx.serialization.builtins.serializer
1712
import kotlinx.serialization.modules.SerializersModule
13+
import kotlin.test.Test
14+
import kotlin.test.assertEquals
1815

1916
@Serializable
2017
object TestObject {
@@ -62,7 +59,7 @@ class EncodersTest {
6259
@Test
6360
fun encodeDecodeList() {
6461
val list = listOf("One", "Two", "Three")
65-
val encoded = encode(list, shouldEncodeElementDefault = true)
62+
val encoded = encode<List<String>>(list) { shouldEncodeElementDefault = true }
6663

6764
nativeAssertEquals(nativeListOf("One", "Two", "Three"), encoded)
6865

@@ -73,7 +70,7 @@ class EncodersTest {
7370
@Test
7471
fun encodeDecodeMap() {
7572
val map = mapOf("key" to "value", "key2" to "value2", "key3" to "value3")
76-
val encoded = encode(map, shouldEncodeElementDefault = true)
73+
val encoded = encode<Map<String, String>>(map) { shouldEncodeElementDefault = true }
7774

7875
nativeAssertEquals(nativeMapOf("key" to "value", "key2" to "value2", "key3" to "value3"), encoded)
7976

@@ -83,7 +80,7 @@ class EncodersTest {
8380

8481
@Test
8582
fun encodeDecodeObject() {
86-
val encoded = encode(TestObject.serializer(), TestObject, shouldEncodeElementDefault = false)
83+
val encoded = encode(TestObject.serializer(), TestObject) { shouldEncodeElementDefault = false }
8784
nativeAssertEquals(nativeMapOf(), encoded)
8885

8986
val decoded = decode(TestObject.serializer(), encoded)
@@ -93,7 +90,7 @@ class EncodersTest {
9390
@Test
9491
fun encodeDecodeClass() {
9592
val testDataClass = TestData(mapOf("key" to "value"), mapOf(1 to 1), true)
96-
val encoded = encode(TestData.serializer(), testDataClass, shouldEncodeElementDefault = false)
93+
val encoded = encode(TestData.serializer(), testDataClass) { shouldEncodeElementDefault = false }
9794

9895
nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true), encoded)
9996

@@ -104,7 +101,7 @@ class EncodersTest {
104101
@Test
105102
fun encodeDecodeClassNullableValue() {
106103
val testDataClass = TestData(mapOf("key" to "value"), mapOf(1 to 1), true, nullableBool = true)
107-
val encoded = encode(TestData.serializer(), testDataClass, shouldEncodeElementDefault = true)
104+
val encoded = encode(TestData.serializer(), testDataClass) { shouldEncodeElementDefault = true }
108105

109106
nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to true), encoded)
110107

@@ -116,7 +113,7 @@ class EncodersTest {
116113
fun encodeDecodeGenericClass() {
117114
val innerClass = TestData(mapOf("key" to "value"), mapOf(1 to 1), true)
118115
val genericClass = GenericClass(innerClass)
119-
val encoded = encode(GenericClass.serializer(TestData.serializer()), genericClass, shouldEncodeElementDefault = true)
116+
val encoded = encode(GenericClass.serializer(TestData.serializer()), genericClass) { shouldEncodeElementDefault = true }
120117

121118
nativeAssertEquals(nativeMapOf("inner" to nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to null)), encoded)
122119

@@ -188,4 +185,142 @@ class EncodersTest {
188185
}
189186
assertEquals(nestedClass, decoded)
190187
}
188+
189+
@Test
190+
fun reencodeTransformationList() {
191+
val reencoded = reencodeTransformation<List<String>>(nativeListOf("One", "Two", "Three")) {
192+
assertEquals(listOf("One", "Two", "Three"), it)
193+
it.map { value -> "new$value" }
194+
}
195+
nativeAssertEquals(nativeListOf("newOne", "newTwo", "newThree"), reencoded)
196+
}
197+
198+
@Test
199+
fun reencodeTransformationMap() {
200+
val reencoded = reencodeTransformation<Map<String, String>>(nativeMapOf("key" to "value", "key2" to "value2", "key3" to "value3")) {
201+
assertEquals(mapOf("key" to "value", "key2" to "value2", "key3" to "value3"), it)
202+
it.mapValues { (_, value) -> "new-$value" }
203+
}
204+
205+
nativeAssertEquals(nativeMapOf("key" to "new-value", "key2" to "new-value2", "key3" to "new-value3"), reencoded)
206+
}
207+
208+
@Test
209+
fun reencodeTransformationObject() {
210+
val reencoded = reencodeTransformation<TestObject>(nativeMapOf(), { shouldEncodeElementDefault = false }) {
211+
assertEquals(TestObject, it)
212+
it
213+
}
214+
nativeAssertEquals(nativeMapOf(), reencoded)
215+
}
216+
217+
@Test
218+
fun reencodeTransformationClass() {
219+
val reencoded = reencodeTransformation<TestData>(
220+
nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to true),
221+
{ shouldEncodeElementDefault = false }
222+
) {
223+
assertEquals(TestData(mapOf("key" to "value"), mapOf(1 to 1), bool = true, nullableBool = true), it)
224+
it.copy(map = mapOf("newKey" to "newValue"), nullableBool = null)
225+
}
226+
227+
nativeAssertEquals(nativeMapOf("map" to nativeMapOf("newKey" to "newValue"), "otherMap" to nativeMapOf(1 to 1), "bool" to true), reencoded)
228+
}
229+
230+
@Test
231+
fun reencodeTransformationNullableValue() {
232+
val reencoded = reencodeTransformation<TestData?>(
233+
nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to true),
234+
{ shouldEncodeElementDefault = false }
235+
) {
236+
assertEquals(TestData(mapOf("key" to "value"), mapOf(1 to 1), bool = true, nullableBool = true), it)
237+
null
238+
}
239+
240+
nativeAssertEquals(null, reencoded)
241+
}
242+
243+
@Test
244+
fun reencodeTransformationGenericClass() {
245+
val reencoded = reencodeTransformation(
246+
GenericClass.serializer(TestData.serializer()),
247+
nativeMapOf("inner" to nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to false)),
248+
{ shouldEncodeElementDefault = false }
249+
) {
250+
assertEquals(
251+
GenericClass(TestData(mapOf("key" to "value"), mapOf(1 to 1), bool = true, nullableBool = false)),
252+
it
253+
)
254+
GenericClass(it.inner.copy(map = mapOf("newKey" to "newValue"), nullableBool = null))
255+
}
256+
257+
nativeAssertEquals(nativeMapOf("inner" to nativeMapOf("map" to nativeMapOf("newKey" to "newValue"), "otherMap" to nativeMapOf(1 to 1), "bool" to true)), reencoded)
258+
}
259+
260+
@Test
261+
fun reencodeTransformationSealedClass() {
262+
val reencoded = reencodeTransformation(SealedClass.serializer(), nativeMapOf("type" to "test", "value" to "value")) {
263+
assertEquals(SealedClass.Test("value"), it)
264+
SealedClass.Test("newTest")
265+
}
266+
267+
nativeAssertEquals(nativeMapOf("type" to "test", "value" to "newTest"), reencoded)
268+
}
269+
270+
@Test
271+
fun reencodeTransformationPolymorphicClass() {
272+
val module = SerializersModule {
273+
polymorphic(AbstractClass::class, ImplementedClass::class, ImplementedClass.serializer())
274+
}
275+
276+
val reencoded = reencodeTransformation(
277+
AbstractClass.serializer(),
278+
nativeMapOf("type" to "implemented", "value" to "value", "otherValue" to true),
279+
builder = {
280+
serializersModule = module
281+
}
282+
) {
283+
assertEquals(ImplementedClass("value", true), it)
284+
ImplementedClass("new-${it.value}", false)
285+
}
286+
287+
nativeAssertEquals(nativeMapOf("type" to "implemented", "value" to "new-value", "otherValue" to false), reencoded)
288+
}
289+
290+
@Test
291+
fun reencodeTransformationNestedClass() {
292+
val module = SerializersModule {
293+
polymorphic(AbstractClass::class, ImplementedClass::class, ImplementedClass.serializer())
294+
}
295+
296+
val sealedClass: SealedClass = SealedClass.Test("value")
297+
val abstractClass: AbstractClass = ImplementedClass("value", true)
298+
val nestedClass = NestedClass(sealedClass, abstractClass, listOf(sealedClass), listOf(abstractClass), mapOf(sealedClass to sealedClass), mapOf(abstractClass to abstractClass))
299+
val encoded = encode(NestedClass.serializer(), nestedClass) {
300+
shouldEncodeElementDefault = true
301+
serializersModule = module
302+
}
303+
304+
val reencoded = reencodeTransformation(NestedClass.serializer(), encoded, builder = {
305+
shouldEncodeElementDefault = true
306+
serializersModule = module
307+
}) {
308+
assertEquals(nestedClass, it)
309+
it.copy(sealed = SealedClass.Test("newValue"))
310+
}
311+
312+
val sealedEncoded = nativeMapOf("type" to "test", "value" to "value")
313+
val abstractEncoded = nativeMapOf("type" to "implemented", "value" to "value", "otherValue" to true)
314+
nativeAssertEquals(
315+
nativeMapOf(
316+
"sealed" to nativeMapOf("type" to "test", "value" to "newValue"),
317+
"abstract" to abstractEncoded,
318+
"sealedList" to nativeListOf(sealedEncoded),
319+
"abstractList" to nativeListOf(abstractEncoded),
320+
"sealedMap" to nativeMapOf(sealedEncoded to sealedEncoded),
321+
"abstractMap" to nativeMapOf(abstractEncoded to abstractEncoded)
322+
),
323+
reencoded
324+
)
325+
}
191326
}

firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt

+44-16
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,36 @@
55
package dev.gitlive.firebase.database
66

77
import com.google.android.gms.tasks.Task
8-
import com.google.firebase.database.*
9-
import dev.gitlive.firebase.*
8+
import com.google.firebase.database.ChildEventListener
9+
import com.google.firebase.database.DatabaseError
10+
import com.google.firebase.database.Logger
11+
import com.google.firebase.database.MutableData
12+
import com.google.firebase.database.Transaction
13+
import com.google.firebase.database.ValueEventListener
14+
import dev.gitlive.firebase.DecodeSettings
15+
import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
16+
import dev.gitlive.firebase.Firebase
17+
import dev.gitlive.firebase.FirebaseApp
1018
import dev.gitlive.firebase.database.ChildEvent.Type
1119
import dev.gitlive.firebase.database.FirebaseDatabase.Companion.FirebaseDatabase
20+
import dev.gitlive.firebase.decode
21+
import dev.gitlive.firebase.reencodeTransformation
1222
import kotlinx.coroutines.CompletableDeferred
1323
import kotlinx.coroutines.channels.awaitClose
1424
import kotlinx.coroutines.channels.trySendBlocking
15-
import kotlinx.coroutines.flow.*
25+
import kotlinx.coroutines.flow.Flow
26+
import kotlinx.coroutines.flow.callbackFlow
27+
import kotlinx.coroutines.flow.debounce
28+
import kotlinx.coroutines.flow.filterNot
29+
import kotlinx.coroutines.flow.first
30+
import kotlinx.coroutines.flow.flow
31+
import kotlinx.coroutines.flow.map
32+
import kotlinx.coroutines.flow.merge
1633
import kotlinx.coroutines.tasks.await
1734
import kotlinx.serialization.DeserializationStrategy
35+
import kotlinx.serialization.ExperimentalSerializationApi
1836
import kotlinx.serialization.KSerializer
19-
import java.util.*
37+
import java.util.WeakHashMap
2038
import kotlin.time.Duration.Companion.seconds
2139

2240
suspend fun <T> Task<T>.awaitWhileOnline(database: FirebaseDatabase): T =
@@ -118,18 +136,18 @@ actual open class Query internal actual constructor(
118136

119137
actual val valueEvents: Flow<DataSnapshot>
120138
get() = callbackFlow {
121-
val listener = object : ValueEventListener {
122-
override fun onDataChange(snapshot: com.google.firebase.database.DataSnapshot) {
123-
trySendBlocking(DataSnapshot(snapshot, persistenceEnabled))
124-
}
139+
val listener = object : ValueEventListener {
140+
override fun onDataChange(snapshot: com.google.firebase.database.DataSnapshot) {
141+
trySendBlocking(DataSnapshot(snapshot, persistenceEnabled))
142+
}
125143

126-
override fun onCancelled(error: com.google.firebase.database.DatabaseError) {
127-
close(error.toException())
144+
override fun onCancelled(error: com.google.firebase.database.DatabaseError) {
145+
close(error.toException())
146+
}
128147
}
148+
android.addValueEventListener(listener)
149+
awaitClose { android.removeEventListener(listener) }
129150
}
130-
android.addValueEventListener(listener)
131-
awaitClose { android.removeEventListener(listener) }
132-
}
133151

134152
actual fun childEvents(vararg types: Type): Flow<ChildEvent> = callbackFlow {
135153
val listener = object : ChildEventListener {
@@ -192,12 +210,22 @@ actual class DatabaseReference internal constructor(
192210
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
193211
.run { Unit }
194212

213+
@OptIn(ExperimentalSerializationApi::class)
195214
actual suspend fun <T> runTransaction(strategy: KSerializer<T>, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit, transactionUpdate: (currentData: T) -> T): DataSnapshot {
196215
val deferred = CompletableDeferred<DataSnapshot>()
197216
android.runTransaction(object : Transaction.Handler {
198217

199218
override fun doTransaction(currentData: MutableData): Transaction.Result {
200-
currentData.value = reencodeTransformation(strategy, currentData.value, buildSettings, transactionUpdate)
219+
val valueToReencode = currentData.value
220+
// Value may be null initially, so only reencode if this is allowed
221+
if (strategy.descriptor.isNullable || valueToReencode != null) {
222+
currentData.value = reencodeTransformation(
223+
strategy,
224+
valueToReencode,
225+
buildSettings,
226+
transactionUpdate
227+
)
228+
}
201229
return Transaction.success(currentData)
202230
}
203231

@@ -257,8 +285,8 @@ actual class OnDisconnect internal constructor(
257285
.run { Unit }
258286

259287
override suspend fun setValue(encodedValue: Any?) = android.setValue(encodedValue)
260-
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
261-
.run { Unit }
288+
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
289+
.run { Unit }
262290

263291
override suspend fun updateEncodedChildren(encodedUpdate: Map<String, Any?>) =
264292
android.updateChildren(encodedUpdate)

firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
99
import dev.gitlive.firebase.EncodeSettings
1010
import dev.gitlive.firebase.Firebase
1111
import dev.gitlive.firebase.FirebaseApp
12-
import dev.gitlive.firebase.database.ChildEvent.Type.*
12+
import dev.gitlive.firebase.database.ChildEvent.Type.ADDED
13+
import dev.gitlive.firebase.database.ChildEvent.Type.CHANGED
14+
import dev.gitlive.firebase.database.ChildEvent.Type.MOVED
15+
import dev.gitlive.firebase.database.ChildEvent.Type.REMOVED
1316
import dev.gitlive.firebase.encode
1417
import kotlinx.coroutines.flow.Flow
1518
import kotlinx.serialization.DeserializationStrategy

0 commit comments

Comments
 (0)