Skip to content

Commit 194e802

Browse files
committed
EncodedObject to make encoding more robust
1 parent 06dd193 commit 194e802

File tree

14 files changed

+404
-302
lines changed

14 files changed

+404
-302
lines changed

firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,26 @@ package dev.gitlive.firebase
77
import kotlinx.serialization.descriptors.PolymorphicKind
88
import kotlinx.serialization.descriptors.SerialDescriptor
99
import kotlinx.serialization.descriptors.StructureKind
10+
import java.lang.IllegalArgumentException
1011
import kotlin.collections.set
1112

13+
actual data class EncodedObject(
14+
val android: Map<String, Any?>
15+
)
16+
17+
actual val emptyEncodedObject: EncodedObject = EncodedObject(emptyMap())
18+
19+
@PublishedApi
20+
internal actual fun Map<*, *>.asEncodedObject() = EncodedObject(
21+
map { (key, value) ->
22+
if (key is String) {
23+
key to value
24+
} else {
25+
throw IllegalArgumentException("Expected a String key but received $key")
26+
}
27+
}.toMap()
28+
)
29+
1230
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1331
StructureKind.LIST -> mutableListOf<Any?>()
1432
.also { value = it }

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import kotlinx.serialization.encoding.CompositeEncoder
1111
import kotlinx.serialization.encoding.Encoder
1212
import kotlinx.serialization.modules.SerializersModule
1313

14+
expect class EncodedObject
15+
16+
expect val emptyEncodedObject: EncodedObject
17+
1418
@Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("encode(strategy, value) { encodeDefaults = shouldEncodeElementDefault }"))
1519
fun <T> encode(strategy: SerializationStrategy<T>, value: T, shouldEncodeElementDefault: Boolean): Any? = encode(strategy, value) {
1620
this.encodeDefaults = shouldEncodeElementDefault
@@ -31,6 +35,23 @@ inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean): An
3135
inline fun <reified T> encode(value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) =
3236
encode(value, EncodeSettings.BuilderImpl().apply(buildSettings).buildEncodeSettings())
3337

38+
inline fun <T : Any> encodeAsObject(strategy: SerializationStrategy<T>, value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}): EncodedObject {
39+
val encoded = encode(strategy, value, buildSettings)
40+
return when (encoded) {
41+
is Map<*, *> -> encoded.asEncodedObject()
42+
null -> throw IllegalArgumentException("$value was encoded as null. Must be of the form Map<Key, Value>")
43+
else -> throw IllegalArgumentException("$value was encoded as ${encoded::class}. Must be of the form Map<Key, Value>")
44+
}
45+
}
46+
inline fun <reified T : Any> encodeAsObject(value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}): EncodedObject {
47+
val encoded = encode(value, buildSettings)
48+
return when (encoded) {
49+
is Map<*, *> -> encoded.asEncodedObject()
50+
null -> throw IllegalArgumentException("$value was encoded as null. Must be of the form Map<Key, Value>")
51+
else -> throw IllegalArgumentException("$value was encoded as ${encoded::class}. Must be of the form Map<Key, Value>")
52+
}
53+
}
54+
3455
@PublishedApi
3556
internal inline fun <reified T> encode(value: T, encodeSettings: EncodeSettings): Any? = value?.let {
3657
FirebaseEncoder(encodeSettings).apply {
@@ -45,6 +66,9 @@ internal inline fun <reified T> encode(value: T, encodeSettings: EncodeSettings)
4566
}.value
4667
}
4768

69+
@PublishedApi
70+
internal expect fun Map<*, *>.asEncodedObject(): EncodedObject
71+
4872
/**
4973
* An extension which which serializer to use for value. Handy in updating fields by name or path
5074
* where using annotation is not possible

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ data class NestedClass(
7474
class EncodersTest {
7575

7676
@Test
77-
fun encodeDecodeNullableString() {
78-
val encoded = encode<String?>(null) { encodeDefaults = true }
79-
80-
nativeAssertEquals(null, encoded)
81-
82-
val decoded = decode(String.serializer().nullable, encoded)
83-
assertNull(decoded)
77+
fun encodeDecodePrimaryTypes() {
78+
assertEncode(true)
79+
assertEncode(42)
80+
assertEncode(8.toShort())
81+
assertEncode(Int.MAX_VALUE.toLong() + 3)
82+
assertEncode(0x03F)
83+
assertEncode(3.33)
84+
assertEncode(6.65f)
85+
assertEncode("Test")
8486
}
8587
@Test
8688
fun encodeDecodeList() {
@@ -413,4 +415,14 @@ class EncodersTest {
413415
reencoded
414416
)
415417
}
418+
419+
private inline fun <reified T> assertEncode(value: T) {
420+
val encoded = encode(value)
421+
assertEquals(value, encoded)
422+
assertEquals(value, decode<T>(encoded))
423+
424+
val nullableEncoded = encode<T?>(null)
425+
assertNull(nullableEncoded)
426+
assertNull(decode<T?>(nullableEncoded))
427+
}
416428
}

firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import kotlinx.serialization.descriptors.SerialDescriptor
99
import kotlinx.serialization.descriptors.StructureKind
1010
import kotlin.collections.set
1111

12+
actual data class EncodedObject(
13+
val ios: Map<Any?, *>
14+
)
15+
16+
actual val emptyEncodedObject: EncodedObject = EncodedObject(emptyMap<Any?, Any?>())
17+
18+
@PublishedApi
19+
internal actual fun Map<*, *>.asEncodedObject() = EncodedObject(
20+
map { (key, value) ->
21+
key to value
22+
}.toMap()
23+
)
1224
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1325
StructureKind.LIST -> encodeAsList()
1426
StructureKind.MAP -> mutableListOf<Any?>()

firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_encoders.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,22 @@ package dev.gitlive.firebase
77
import kotlinx.serialization.descriptors.PolymorphicKind
88
import kotlinx.serialization.descriptors.SerialDescriptor
99
import kotlinx.serialization.descriptors.StructureKind
10+
import kotlin.js.Json
1011
import kotlin.js.json
1112

13+
actual typealias EncodedObject = Json
14+
15+
actual val emptyEncodedObject: EncodedObject = json()
16+
@PublishedApi
17+
internal actual fun Map<*, *>.asEncodedObject() = json(
18+
*map { (key, value) ->
19+
if (key is String) {
20+
key to value
21+
} else {
22+
throw IllegalArgumentException("Expected a String key but received $key")
23+
}
24+
}.toTypedArray()
25+
)
1226
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1327
StructureKind.LIST -> encodeAsList(descriptor)
1428
StructureKind.MAP -> {

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.google.firebase.database.Transaction
1313
import com.google.firebase.database.ValueEventListener
1414
import dev.gitlive.firebase.DecodeSettings
1515
import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
16+
import dev.gitlive.firebase.EncodedObject
1617
import dev.gitlive.firebase.Firebase
1718
import dev.gitlive.firebase.FirebaseApp
1819
import dev.gitlive.firebase.database.ChildEvent.Type
@@ -205,9 +206,8 @@ internal actual class NativeDatabaseReference internal constructor(
205206
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
206207
.run { Unit }
207208

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

@@ -253,7 +253,6 @@ internal actual class NativeDatabaseReference internal constructor(
253253

254254
val DatabaseReference.android get() = nativeReference.android
255255

256-
@Suppress("UNCHECKED_CAST")
257256
actual class DataSnapshot internal constructor(
258257
val android: com.google.firebase.database.DataSnapshot,
259258
private val persistenceEnabled: Boolean
@@ -297,9 +296,8 @@ internal actual class NativeOnDisconnect internal constructor(
297296
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
298297
.run { Unit }
299298

300-
@Suppress("UNCHECKED_CAST")
301-
actual suspend fun updateEncodedChildren(encodedUpdate: Any?) =
302-
android.updateChildren(encodedUpdate as Map<String, Any?>)
299+
actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) =
300+
android.updateChildren(encodedUpdate.android)
303301
.run { if(persistenceEnabled) await() else awaitWhileOnline(database) }
304302
.run { Unit }
305303
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ package dev.gitlive.firebase.database
77
import dev.gitlive.firebase.DecodeSettings
88
import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
99
import dev.gitlive.firebase.EncodeSettings
10+
import dev.gitlive.firebase.EncodedObject
1011
import dev.gitlive.firebase.Firebase
1112
import dev.gitlive.firebase.FirebaseApp
1213
import dev.gitlive.firebase.database.ChildEvent.Type.ADDED
1314
import dev.gitlive.firebase.database.ChildEvent.Type.CHANGED
1415
import dev.gitlive.firebase.database.ChildEvent.Type.MOVED
1516
import dev.gitlive.firebase.database.ChildEvent.Type.REMOVED
1617
import dev.gitlive.firebase.encode
18+
import dev.gitlive.firebase.encodeAsObject
1719
import kotlinx.coroutines.flow.Flow
1820
import kotlinx.serialization.DeserializationStrategy
1921
import kotlinx.serialization.KSerializer
@@ -82,7 +84,7 @@ internal expect class NativeDatabaseReference : NativeQuery {
8284
val key: String?
8385
fun push(): NativeDatabaseReference
8486
suspend fun setValueEncoded(encodedValue: Any?)
85-
suspend fun updateEncodedChildren(encodedUpdate: Any?)
87+
suspend fun updateEncodedChildren(encodedUpdate: EncodedObject)
8688
fun child(path: String): NativeDatabaseReference
8789
fun onDisconnect(): NativeOnDisconnect
8890

@@ -118,7 +120,7 @@ class DatabaseReference internal constructor(@PublishedApi internal val nativeRe
118120
this.encodeDefaults = encodeDefaults
119121
}
120122
suspend inline fun updateChildren(update: Map<String, Any?>, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = nativeReference.updateEncodedChildren(
121-
encode(update, buildSettings))
123+
encodeAsObject(update, buildSettings))
122124

123125
suspend fun removeValue() = nativeReference.removeValue()
124126

@@ -144,7 +146,7 @@ internal expect class NativeOnDisconnect {
144146
suspend fun removeValue()
145147
suspend fun cancel()
146148
suspend fun setValue(encodedValue: Any?)
147-
suspend fun updateEncodedChildren(encodedUpdate: Any?)
149+
suspend fun updateEncodedChildren(encodedUpdate: EncodedObject)
148150
}
149151

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

163-
suspend inline fun updateChildren(update: Map<String, Any?>, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncodedChildren(encode(update, buildSettings))
165+
suspend inline fun updateChildren(update: Map<String, Any?>, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncodedChildren(
166+
encodeAsObject(update, buildSettings)
167+
)
164168
@Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("updateChildren(update) { this.encodeDefaults = encodeDefaults }"))
165169
suspend fun updateChildren(update: Map<String, Any?>, encodeDefaults: Boolean) = updateChildren(update) {
166170
this.encodeDefaults = encodeDefaults

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import cocoapods.FirebaseDatabase.FIRDatabaseReference
1616
import cocoapods.FirebaseDatabase.FIRTransactionResult
1717
import dev.gitlive.firebase.DecodeSettings
1818
import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
19+
import dev.gitlive.firebase.EncodedObject
1920
import dev.gitlive.firebase.Firebase
2021
import dev.gitlive.firebase.FirebaseApp
2122
import dev.gitlive.firebase.database.ChildEvent.Type
@@ -168,9 +169,8 @@ internal actual class NativeDatabaseReference internal constructor(
168169
ios.await(persistenceEnabled) { setValue(encodedValue, it) }
169170
}
170171

171-
@Suppress("UNCHECKED_CAST")
172-
actual suspend fun updateEncodedChildren(encodedUpdate: Any?) {
173-
ios.await(persistenceEnabled) { updateChildValues(encodedUpdate as Map<Any?, *>, it) }
172+
actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) {
173+
ios.await(persistenceEnabled) { updateChildValues(encodedUpdate.ios, it) }
174174
}
175175

176176
actual suspend fun removeValue() {
@@ -199,7 +199,6 @@ internal actual class NativeDatabaseReference internal constructor(
199199

200200
val DatabaseReference.ios: FIRDatabaseReference get() = nativeReference.ios
201201

202-
@Suppress("UNCHECKED_CAST")
203202
actual class DataSnapshot internal constructor(
204203
val ios: FIRDataSnapshot,
205204
private val persistenceEnabled: Boolean
@@ -241,9 +240,8 @@ internal actual class NativeOnDisconnect internal constructor(
241240
ios.await(persistenceEnabled) { onDisconnectSetValue(encodedValue, it) }
242241
}
243242

244-
@Suppress("UNCHECKED_CAST")
245-
actual suspend fun updateEncodedChildren(encodedUpdate: Any?) {
246-
ios.await(persistenceEnabled) { onDisconnectUpdateChildValues(encodedUpdate as Map<Any?, *>, it) }
243+
actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) {
244+
ios.await(persistenceEnabled) { onDisconnectUpdateChildValues(encodedUpdate.ios, it) }
247245
}
248246
}
249247

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package dev.gitlive.firebase.database
66

77
import dev.gitlive.firebase.DecodeSettings
88
import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
9+
import dev.gitlive.firebase.EncodedObject
910
import dev.gitlive.firebase.Firebase
1011
import dev.gitlive.firebase.FirebaseApp
1112
import dev.gitlive.firebase.database.externals.CancelCallback
@@ -182,8 +183,8 @@ internal actual class NativeDatabaseReference internal constructor(
182183
set(js, encodedValue).awaitWhileOnline(database)
183184
}
184185

185-
actual suspend fun updateEncodedChildren(encodedUpdate: Any?) =
186-
rethrow { update(js, encodedUpdate ?: json()).awaitWhileOnline(database) }
186+
actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) =
187+
rethrow { update(js, encodedUpdate).awaitWhileOnline(database) }
187188

188189

189190
actual suspend fun <T> runTransaction(strategy: KSerializer<T>, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit, transactionUpdate: (currentData: T) -> T): DataSnapshot {
@@ -234,8 +235,8 @@ internal actual class NativeOnDisconnect internal constructor(
234235
actual suspend fun setValue(encodedValue: Any?) =
235236
rethrow { js.set(encodedValue).awaitWhileOnline(database) }
236237

237-
actual suspend fun updateEncodedChildren(encodedUpdate: Any?) =
238-
rethrow { js.update(encodedUpdate ?: json()).awaitWhileOnline(database) }
238+
actual suspend fun updateEncodedChildren(encodedUpdate: EncodedObject) =
239+
rethrow { js.update(encodedUpdate).awaitWhileOnline(database) }
239240

240241
}
241242

0 commit comments

Comments
 (0)