diff --git a/README.md b/README.md index 7f2d99213..daba85cfa 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,16 @@ The following libraries are available for the various Firebase products. | Service or Product | Gradle Dependency | API Coverage | |---------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Authentication](https://firebase.google.com/docs/auth) | [`dev.gitlive:firebase-auth:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-auth/1.10.4/pom) | [![80%](https://img.shields.io/badge/-80%25-green?style=flat-square)](/firebase-auth/src/commonMain/kotlin/dev/gitlive/firebase/auth/auth.kt) | -| [Realtime Database](https://firebase.google.com/docs/database) | [`dev.gitlive:firebase-database:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-database/1.10.4/pom) | [![70%](https://img.shields.io/badge/-70%25-orange?style=flat-square)](/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt) | -| [Cloud Firestore](https://firebase.google.com/docs/firestore) | [`dev.gitlive:firebase-firestore:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-firestore/1.10.4/pom) | [![60%](https://img.shields.io/badge/-60%25-orange?style=flat-square)](/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt) | -| [Cloud Functions](https://firebase.google.com/docs/functions) | [`dev.gitlive:firebase-functions:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-functions/1.10.4/pom) | [![80%](https://img.shields.io/badge/-80%25-green?style=flat-square)](/firebase-functions/src/commonMain/kotlin/dev/gitlive/firebase/functions/functions.kt) | -| [Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) | [`dev.gitlive:firebase-messaging:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-messaging/1.10.4/pom) | ![0%](https://img.shields.io/badge/-0%25-lightgrey?style=flat-square) | -| [Cloud Storage](https://firebase.google.com/docs/storage) | [`dev.gitlive:firebase-storage:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-storage/1.10.4/pom) | [![40%](https://img.shields.io/badge/-40%25-orange?style=flat-square)](/firebase-storage/src/commonMain/kotlin/dev/gitlive/firebase/storage/storage.kt) | -| [Installations](https://firebase.google.com/docs/projects/manage-installations) | [`dev.gitlive:firebase-installations:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-installations/1.10.4/pom) | [![90%](https://img.shields.io/badge/-90%25-green?style=flat-square)](/firebase-installations/src/commonMain/kotlin/dev/gitlive/firebase/installations/installations.kt) | -| [Remote Config](https://firebase.google.com/docs/remote-config) | [`dev.gitlive:firebase-config:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-config/1.10.4/pom) | [![20%](https://img.shields.io/badge/-20%25-orange?style=flat-square)](/firebase-config/src/commonMain/kotlin/dev/gitlive/firebase/remoteconfig/FirebaseRemoteConfig.kt) | -| [Performance](https://firebase.google.com/docs/perf-mon) | [`dev.gitlive:firebase-perf:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-perf/1.10.4/pom) | [![1%](https://img.shields.io/badge/-1%25-orange?style=flat-square)](/firebase-perf/src/commonMain/kotlin/dev/gitlive/firebase/perf/performance.kt) | -| [Crashlytics](https://firebase.google.com/docs/crashlytics) | [`dev.gitlive:firebase-crashlytics:1.10.4`](https://search.maven.org/artifact/dev.gitlive/firebase-crashlytics/1.10.4/pom) | [![80%](https://img.shields.io/badge/-1%25-orange?style=flat-square)](/firebase-crashlytics/src/commonMain/kotlin/dev/gitlive/firebase/crashlytics/crashlytics.kt) | +| [Authentication](https://firebase.google.com/docs/auth) | [`dev.gitlive:firebase-auth:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-auth/1.12.0/pom) | [![80%](https://img.shields.io/badge/-80%25-green?style=flat-square)](/firebase-auth/src/commonMain/kotlin/dev/gitlive/firebase/auth/auth.kt) | +| [Realtime Database](https://firebase.google.com/docs/database) | [`dev.gitlive:firebase-database:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-database/1.12.0/pom) | [![70%](https://img.shields.io/badge/-70%25-orange?style=flat-square)](/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt) | +| [Cloud Firestore](https://firebase.google.com/docs/firestore) | [`dev.gitlive:firebase-firestore:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-firestore/1.12.0/pom) | [![60%](https://img.shields.io/badge/-60%25-orange?style=flat-square)](/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt) | +| [Cloud Functions](https://firebase.google.com/docs/functions) | [`dev.gitlive:firebase-functions:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-functions/1.12.0/pom) | [![80%](https://img.shields.io/badge/-80%25-green?style=flat-square)](/firebase-functions/src/commonMain/kotlin/dev/gitlive/firebase/functions/functions.kt) | +| [Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) | [`dev.gitlive:firebase-messaging:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-messaging/1.12.0/pom) | ![0%](https://img.shields.io/badge/-0%25-lightgrey?style=flat-square) | +| [Cloud Storage](https://firebase.google.com/docs/storage) | [`dev.gitlive:firebase-storage:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-storage/1.12.0/pom) | [![40%](https://img.shields.io/badge/-40%25-orange?style=flat-square)](/firebase-storage/src/commonMain/kotlin/dev/gitlive/firebase/storage/storage.kt) | +| [Installations](https://firebase.google.com/docs/projects/manage-installations) | [`dev.gitlive:firebase-installations:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-installations/1.12.0/pom) | [![90%](https://img.shields.io/badge/-90%25-green?style=flat-square)](/firebase-installations/src/commonMain/kotlin/dev/gitlive/firebase/installations/installations.kt) | +| [Remote Config](https://firebase.google.com/docs/remote-config) | [`dev.gitlive:firebase-config:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-config/1.12.0/pom) | [![20%](https://img.shields.io/badge/-20%25-orange?style=flat-square)](/firebase-config/src/commonMain/kotlin/dev/gitlive/firebase/remoteconfig/FirebaseRemoteConfig.kt) | +| [Performance](https://firebase.google.com/docs/perf-mon) | [`dev.gitlive:firebase-perf:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-perf/1.12.0/pom) | [![1%](https://img.shields.io/badge/-1%25-orange?style=flat-square)](/firebase-perf/src/commonMain/kotlin/dev/gitlive/firebase/perf/performance.kt) | +| [Crashlytics](https://firebase.google.com/docs/crashlytics) | [`dev.gitlive:firebase-crashlytics:1.12.0`](https://search.maven.org/artifact/dev.gitlive/firebase-crashlytics/1.12.0/pom) | [![80%](https://img.shields.io/badge/-1%25-orange?style=flat-square)](/firebase-crashlytics/src/commonMain/kotlin/dev/gitlive/firebase/crashlytics/crashlytics.kt) | Is the Firebase library or API you need missing? [Create an issue](https://github.com/GitLiveApp/firebase-kotlin-sdk/issues/new?labels=API+coverage&template=increase-api-coverage.md&title=Add+%5Bclass+name%5D.%5Bfunction+name%5D+to+%5Blibrary+name%5D+for+%5Bplatform+names%5D) to request additional API coverage or be awesome and [submit a PR](https://github.com/GitLiveApp/firebase-kotlin-sdk/fork) @@ -70,8 +70,8 @@ The Firebase Kotlin SDK uses Kotlin serialization to read and write custom class ```groovy plugins { - kotlin("multiplatform") version "1.8.21" // or kotlin("jvm") or any other kotlin plugin - kotlin("plugin.serialization") version "1.8.21" + kotlin("multiplatform") version "1.9.20" // or kotlin("jvm") or any other kotlin plugin + kotlin("plugin.serialization") version "1.9.20" } ``` @@ -85,13 +85,43 @@ data class City(val name: String) Instances of these classes can now be passed [along with their serializer](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#introduction-to-serializers) to the SDK: ```kotlin -db.collection("cities").document("LA").set(City.serializer(), city, encodeDefaults = true) +db.collection("cities").document("LA").set(City.serializer(), city) { encodeDefaults = true } ``` -The `encodeDefaults` parameter is optional and defaults to `true`, set this to false to omit writing optional properties if they are equal to theirs default values. +The `buildSettings` closure is optional and allows for configuring serialization behaviour. + +Setting the `encodeDefaults` parameter is optional and defaults to `true`, set this to false to omit writing optional properties if they are equal to theirs default values. Using [@EncodeDefault](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-encode-default/) on properties is a recommended way to locally override the behavior set with `encodeDefaults`. -You can also omit the serializer but this is discouraged due to a [current limitation on Kotlin/JS and Kotlin/Native](https://github.com/Kotlin/kotlinx.serialization/issues/1116#issuecomment-704342452) +You can also omit the serializer if it can be inferred using `serializer()`. +To support [contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization) or [open polymorphism](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism) the `serializersModule` can be overridden in the `buildSettings` closure: + +```kotlin +@Serializable +abstract class AbstractCity { + abstract val name: String +} + +@Serializable +@SerialName("capital") +data class Capital(override val name: String, val isSeatOfGovernment: Boolean) : AbstractCity() + +val module = SerializersModule { + polymorphic(AbstractCity::class, AbstractCity.serializer()) { + subclass(Capital::class, Capital.serializer()) + } +} + +val city = Capital("London", true) +db.collection("cities").document("UK").set(AbstractCity.serializer(), city) { + encodeDefaults = true + serializersModule = module + +} +val storedCity = db.collection("cities").document("UK").get().data(AbstractCity.serializer()) { + serializersModule = module +} +```

Server Timestamp

diff --git a/build.gradle.kts b/build.gradle.kts index 0d36ab044..62966582e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,7 +117,7 @@ subprojects { "commonTestImplementation"(kotlin("test-common")) "commonTestImplementation"(kotlin("test-annotations-common")) if (this@afterEvaluate.name != "firebase-crashlytics") { - "jvmMainApi"("dev.gitlive:firebase-java-sdk:0.3.0") + "jvmMainApi"("dev.gitlive:firebase-java-sdk:0.4.0") "jvmMainApi"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutinesVersion") { exclude("com.google.android.gms") } diff --git a/firebase-app/package.json b/firebase-app/package.json index 1e3d1ef80..81138414d 100644 --- a/firebase-app/package.json +++ b/firebase-app/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-app", - "version": "1.11.0", + "version": "1.11.1", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-app.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-common": "1.11.0", + "@gitlive/firebase-common": "1.11.1", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-auth/package.json b/firebase-auth/package.json index be336c079..7f21dc747 100644 --- a/firebase-auth/package.json +++ b/firebase-auth/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-auth", - "version": "1.11.0", + "version": "1.12.0", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-auth.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.12.0", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-common/package.json b/firebase-common/package.json index 0f5e81321..e5826e53d 100644 --- a/firebase-common/package.json +++ b/firebase-common/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-common", - "version": "1.11.0", + "version": "1.11.1", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-common.js", "scripts": { diff --git a/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_decoders.kt b/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_decoders.kt index 4cd747022..ae930a08a 100644 --- a/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_decoders.kt +++ b/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_decoders.kt @@ -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 \ No newline at end of file + (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 (desc.getElementName(index) == "value") + map + else { + map[desc.getElementName(index)] + } + } else { + map[desc.getElementName(index)] + } + } +} diff --git a/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt b/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt index c34791203..59ac0c431 100644 --- a/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt +++ b/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt @@ -4,6 +4,7 @@ package dev.gitlive.firebase +import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlin.collections.set @@ -11,16 +12,22 @@ import kotlin.collections.set actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) { StructureKind.LIST -> mutableListOf() .also { value = it } - .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } } + .let { FirebaseCompositeEncoder(settings) { _, index, value -> it.add(index, value) } } StructureKind.MAP -> mutableListOf() - .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } } - StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf() - .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() + .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") -} \ No newline at end of file + ) + } diff --git a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/EncodeDecodeSettings.kt b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/EncodeDecodeSettings.kt new file mode 100644 index 000000000..076f208dc --- /dev/null +++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/EncodeDecodeSettings.kt @@ -0,0 +1,70 @@ +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 encodeDefaults 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 internal constructor( + val encodeDefaults: Boolean, + override val serializersModule: SerializersModule, +) : EncodeDecodeSettings() { + + interface Builder { + var encodeDefaults: Boolean + var serializersModule: SerializersModule + + } + + @PublishedApi + internal class BuilderImpl : Builder { + override var encodeDefaults: Boolean = true + override var serializersModule: SerializersModule = EmptySerializersModule() + } +} + +/** + * [EncodeDecodeSettings] used when decoding an object + * @param serializersModule the [SerializersModule] to use for deserialization. This allows for polymorphic serialization on runtime + */ +data class DecodeSettings internal constructor( + override val serializersModule: SerializersModule = EmptySerializersModule(), +) : EncodeDecodeSettings() { + + interface Builder { + var serializersModule: SerializersModule + } + + @PublishedApi + internal class BuilderImpl : Builder { + override var serializersModule: SerializersModule = EmptySerializersModule() + } +} + +interface EncodeDecodeSettingsBuilder : EncodeSettings.Builder, DecodeSettings.Builder + +@PublishedApi +internal class EncodeDecodeSettingsBuilderImpl : EncodeDecodeSettingsBuilder { + + override var encodeDefaults: Boolean = true + override var serializersModule: SerializersModule = EmptySerializersModule() +} + +@PublishedApi +internal fun EncodeSettings.Builder.buildEncodeSettings(): EncodeSettings = EncodeSettings(encodeDefaults, serializersModule) +@PublishedApi +internal fun DecodeSettings.Builder.buildDecodeSettings(): DecodeSettings = DecodeSettings(serializersModule) diff --git a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/Polymorphic.kt b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/Polymorphic.kt index 58f1ed71b..41563d527 100644 --- a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/Polymorphic.kt +++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/Polymorphic.kt @@ -16,10 +16,15 @@ internal fun FirebaseEncoder.encodePolymorphically( value: T, ifPolymorphic: (String) -> Unit ) { + // If serializer is not an AbstractPolymorphicSerializer 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 } + val casted = serializer as AbstractPolymorphicSerializer val baseClassDiscriminator = serializer.descriptor.classDiscriminator() val actualSerializer = casted.findPolymorphicSerializer(this, value as Any) @@ -32,15 +37,15 @@ internal fun FirebaseDecoder.decodeSerializableValuePolymorphic( value: Any?, deserializer: DeserializationStrategy, ): T { + // If deserializer is not an AbstractPolymorphicSerializer we can just use the regular serializer if (deserializer !is AbstractPolymorphicSerializer<*>) { return deserializer.deserialize(this) } - val casted = deserializer as AbstractPolymorphicSerializer val discriminator = deserializer.descriptor.classDiscriminator() val type = getPolymorphicType(value, discriminator) val actualDeserializer = casted.findPolymorphicSerializerOrNull( - structureDecoder(deserializer.descriptor), + structureDecoder(deserializer.descriptor, false), type ) as DeserializationStrategy return actualDeserializer.deserialize(this) @@ -55,4 +60,3 @@ internal fun SerialDescriptor.classDiscriminator(): String { } return "type" } - diff --git a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt index f9501d342..3822399d2 100644 --- a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt +++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt @@ -11,29 +11,37 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer -@Suppress("UNCHECKED_CAST") -inline fun decode(value: Any?): T { +inline fun decode(value: Any?): T = decode(value) {} +inline fun decode(value: Any?, buildSettings: DecodeSettings.Builder.() -> Unit): T = + decode(value, DecodeSettings.BuilderImpl().apply(buildSettings).buildDecodeSettings()) + +@PublishedApi +internal inline fun decode(value: Any?, decodeSettings: DecodeSettings): T { val strategy = serializer() - return decode(strategy as DeserializationStrategy, value) + return decode(strategy as DeserializationStrategy, value, decodeSettings) } +fun decode(strategy: DeserializationStrategy, value: Any?): T = decode(strategy, value) {} +inline fun decode(strategy: DeserializationStrategy, value: Any?, buildSettings: DecodeSettings.Builder.() -> Unit): T = + decode(strategy, value, DecodeSettings.BuilderImpl().apply(buildSettings).buildDecodeSettings()) -fun decode(strategy: DeserializationStrategy, value: Any?): T { +@PublishedApi +internal fun decode(strategy: DeserializationStrategy, value: Any?, decodeSettings: DecodeSettings): 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).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) @@ -59,7 +67,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 decodeSerializableValue(deserializer: DeserializationStrategy): T { return decodeSerializableValuePolymorphic(value, deserializer) @@ -68,26 +76,31 @@ 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 @@ -100,21 +113,30 @@ open class FirebaseCompositeDecoder( index: Int, deserializer: DeserializationStrategy, 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 decodeNullableSerializableElement( descriptor: SerialDescriptor, @@ -123,19 +145,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 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() @@ -201,5 +241,3 @@ internal fun SerialDescriptor.getElementIndexOrThrow(name: String): Int { private fun decodeNotNullMark(value: Any?) = value != null private fun decodeNull(value: Any?) = value as Nothing? - - diff --git a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt index 5e5c409b8..0497ec0d7 100644 --- a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt +++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt @@ -4,25 +4,70 @@ package dev.gitlive.firebase -import kotlinx.serialization.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule + +@Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("encode(strategy, value) { encodeDefaults = shouldEncodeElementDefault }")) +fun encode(strategy: SerializationStrategy, value: T, shouldEncodeElementDefault: Boolean): Any? = encode(strategy, value) { + this.encodeDefaults = shouldEncodeElementDefault +} + +inline fun encode(strategy: SerializationStrategy, value: T, buildSettings: EncodeSettings.Builder.() -> Unit) = + encode(strategy, value, EncodeSettings.BuilderImpl().apply(buildSettings).buildEncodeSettings()) -fun encode(strategy: SerializationStrategy, value: T, shouldEncodeElementDefault: Boolean): Any? = - FirebaseEncoder(shouldEncodeElementDefault).apply { encodeSerializableValue(strategy, value) }.value//.also { println("encoded $it") } +@PublishedApi +internal inline fun encode(strategy: SerializationStrategy, value: T, encodeSettings: EncodeSettings): Any? = + FirebaseEncoder(encodeSettings).apply { encodeSerializableValue(strategy, value) }.value -inline fun encode(value: T, shouldEncodeElementDefault: Boolean): Any? = value?.let { - FirebaseEncoder(shouldEncodeElementDefault).apply { encodeSerializableValue(it.firebaseSerializer(), it) }.value +@Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("encode(value) { this.encodeDefaults = shouldEncodeElementDefault }")) +inline fun encode(value: T, shouldEncodeElementDefault: Boolean): Any? = encode(value) { + this.encodeDefaults = shouldEncodeElementDefault } +inline fun encode(value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + encode(value, EncodeSettings.BuilderImpl().apply(buildSettings).buildEncodeSettings()) + +@PublishedApi +internal inline fun encode(value: T, encodeSettings: EncodeSettings): Any? = value?.let { + FirebaseEncoder(encodeSettings).apply { + if (it is ValueWithSerializer<*> && it.value is T) { + @Suppress("UNCHECKED_CAST") + (it as ValueWithSerializer).let { + encodeSerializableValue(it.serializer, it.value) + } + } else { + encodeSerializableValue(it.firebaseSerializer(), it) + } + }.value +} + +/** + * An extension which which serializer to use for value. Handy in updating fields by name or path + * where using annotation is not possible + * @return a value with a custom serializer. + */ +fun T.withSerializer(serializer: SerializationStrategy): Any = ValueWithSerializer(this, serializer) +data class ValueWithSerializer(val value: T, val serializer: SerializationStrategy) + expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder -class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean) : Encoder { +class FirebaseEncoder( + internal val settings: EncodeSettings +) : Encoder { + + constructor(shouldEncodeElementDefault: Boolean) : this( + EncodeSettings.BuilderImpl().apply { this.encodeDefaults = shouldEncodeElementDefault }.buildEncodeSettings() + ) var value: Any? = null - override val serializersModule = EmptySerializersModule() + internal val shouldEncodeElementDefault = settings.encodeDefaults + override val serializersModule: SerializersModule = settings.serializersModule + private var polymorphicDiscriminator: String? = null override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { @@ -82,8 +127,7 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean) : Encode this.value = value } - override fun encodeInline(descriptor: SerialDescriptor): Encoder = - FirebaseEncoder(shouldEncodeElementDefault) + override fun encodeInline(descriptor: SerialDescriptor): Encoder = this override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { encodePolymorphically(serializer, value) { @@ -93,23 +137,23 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean) : Encode } open class FirebaseCompositeEncoder constructor( - private val shouldEncodeElementDefault: Boolean, + private val settings: EncodeSettings, private val end: () -> Unit = {}, private val setPolymorphicType: (String, String) -> Unit = { _, _ -> }, private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit, ): CompositeEncoder { - override val serializersModule = EmptySerializersModule() - // private fun SerializationStrategy.toFirebase(): SerializationStrategy = when(descriptor.kind) { // StructureKind.MAP -> FirebaseMapSerializer(descriptor.getElementDescriptor(1)) as SerializationStrategy // StructureKind.LIST -> FirebaseListSerializer(descriptor.getElementDescriptor(0)) as SerializationStrategy // else -> this // } + override val serializersModule: SerializersModule = settings.serializersModule + override fun endStructure(descriptor: SerialDescriptor) = end() - override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int) = shouldEncodeElementDefault + override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int) = settings.encodeDefaults override fun encodeNullableSerializableElement( descriptor: SerialDescriptor, @@ -120,7 +164,7 @@ open class FirebaseCompositeEncoder constructor( descriptor, index, value?.let { - FirebaseEncoder(shouldEncodeElementDefault).apply { + FirebaseEncoder(settings).apply { encodeSerializableValue(serializer, value) }.value } @@ -134,11 +178,13 @@ open class FirebaseCompositeEncoder constructor( ) = set( descriptor, index, - FirebaseEncoder(shouldEncodeElementDefault).apply { + FirebaseEncoder(settings).apply { encodeSerializableValue(serializer, value) }.value ) + fun encodeObject(descriptor: SerialDescriptor, index: Int, value: T) = set(descriptor, index, value) + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) = set(descriptor, index, value) override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) = set(descriptor, index, value) @@ -159,10 +205,9 @@ open class FirebaseCompositeEncoder constructor( @ExperimentalSerializationApi override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder = - FirebaseEncoder(shouldEncodeElementDefault) + FirebaseEncoder(settings) fun encodePolymorphicClassDiscriminator(discriminator: String, type: String) { setPolymorphicType(discriminator, type) } } - diff --git a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/reencodeTransformation.kt b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/reencodeTransformation.kt new file mode 100644 index 000000000..7c9704157 --- /dev/null +++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/reencodeTransformation.kt @@ -0,0 +1,22 @@ +package dev.gitlive.firebase + +import kotlinx.serialization.KSerializer + +inline fun reencodeTransformation(value: Any?, builder: EncodeDecodeSettingsBuilder.() -> Unit = {}, transform: (T) -> T): Any? { + val encodeDecodeSettingsBuilder = EncodeDecodeSettingsBuilderImpl().apply(builder) + val oldValue: T = decode(value, encodeDecodeSettingsBuilder.buildDecodeSettings()) + return encode( + transform(oldValue), + encodeDecodeSettingsBuilder.buildEncodeSettings() + ) +} + +inline fun reencodeTransformation(strategy: KSerializer, value: Any?, builder: EncodeDecodeSettingsBuilder.() -> Unit = {}, transform: (T) -> T): Any? { + val encodeDecodeSettingsBuilder = EncodeDecodeSettingsBuilderImpl().apply(builder) + val oldValue: T = decode(strategy, value, encodeDecodeSettingsBuilder.buildDecodeSettings()) + return encode( + strategy, + transform(oldValue), + encodeDecodeSettingsBuilder.buildEncodeSettings() + ) +} \ No newline at end of file diff --git a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt index e9ab9003f..68e9def69 100644 --- a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt +++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt @@ -4,10 +4,16 @@ package dev.gitlive.firebase -import kotlinx.serialization.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.descriptors.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializer @Suppress("UNCHECKED_CAST") inline fun T.firebaseSerializer() = runCatching { serializer() } diff --git a/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt b/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt index 24467783a..5fdeaf72c 100644 --- a/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt +++ b/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt @@ -7,130 +7,377 @@ package dev.gitlive.firebase import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +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.assertNull -import dev.gitlive.firebase.nativeAssertEquals -import dev.gitlive.firebase.nativeMapOf @Serializable -data class TestData(val map: Map, val bool: Boolean = false, val nullableBool: Boolean? = null) +object TestObject { + val map = mapOf("key" to "value", "key2" to 12, "key3" to null) + val bool = false + val nullableBool: Boolean? = null +} + +@Serializable +@JvmInline +value class ValueClass(val int: Int) @Serializable -sealed class TestSealed { +data class TestData( + val map: Map, + val otherMap: Map, + val bool: Boolean = false, + val nullableBool: Boolean? = null, + val valueClass: ValueClass, +) + +@Serializable +sealed class SealedClass { @Serializable - @SerialName("child") - data class ChildClass(val map: Map, val bool: Boolean = false): TestSealed() + @SerialName("test") + data class Test(val value: String) : SealedClass() } @Serializable -data class TestSealedList(val list: List) +data class GenericClass( + val inner: T +) + +@Serializable +abstract class AbstractClass { + abstract val abstractValue: String +} + +@Serializable +@SerialName("implemented") +data class ImplementedClass(override val abstractValue: String, val otherValue: Boolean) : AbstractClass() + +@Serializable +data class NestedClass( + val testData: TestData, + val sealed: SealedClass, + val abstract: AbstractClass, + val testDataList: List, + val sealedList: List, + val abstractList: List, + val testDataMap: Map, + val sealedMap: Map, + val abstractMap: Map +) class EncodersTest { + @Test - fun encodeMap() { - val encoded = encode(mapOf("key" to "value", "key2" to 12, "key3" to null), shouldEncodeElementDefault = true) + fun encodeDecodeList() { + val list = listOf("One", "Two", "Three") + val encoded = encode>(list) { encodeDefaults = true } + + nativeAssertEquals(nativeListOf("One", "Two", "Three"), encoded) - nativeAssertEquals(nativeMapOf("key" to "value", "key2" to 12, "key3" to null), encoded) + val decoded = decode(ListSerializer(String.serializer()), encoded) + assertEquals(listOf("One", "Two", "Three"), decoded) } @Test - fun encodeObject() { - val encoded = encode(TestData.serializer(), TestData(mapOf("key" to "value"), true), shouldEncodeElementDefault = false) - nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "bool" to true), encoded) + fun encodeDecodeMap() { + val map = mapOf("key" to "value", "key2" to "value2", "key3" to "value3") + val encoded = encode>(map) { encodeDefaults = true } + + nativeAssertEquals(nativeMapOf("key" to "value", "key2" to "value2", "key3" to "value3"), encoded) + + val decoded = decode(MapSerializer(String.serializer(), String.serializer()), encoded) + assertEquals(mapOf("key" to "value", "key2" to "value2", "key3" to "value3"), decoded) } @Test - fun encodeObjectNullableValue() { - val encoded = encode(TestData.serializer(), TestData(mapOf("key" to "value"), true, nullableBool = true), shouldEncodeElementDefault = true) - nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "bool" to true, "nullableBool" to true), encoded) + fun encodeDecodeObject() { + val encoded = encode(TestObject.serializer(), TestObject) { encodeDefaults = false } + nativeAssertEquals(nativeMapOf(), encoded) + + val decoded = decode(TestObject.serializer(), encoded) + assertEquals(TestObject, decoded) } @Test - fun encodeSealedClass() { - val encoded = encode(TestSealed.serializer(), TestSealed.ChildClass(mapOf("key" to "value"), true), shouldEncodeElementDefault = true) - nativeAssertEquals(nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true), encoded) + fun encodeDecodeValueClass() { + val testValueClass = ValueClass(42) + val encoded = encode(ValueClass.serializer(), testValueClass) { encodeDefaults = false } + + nativeAssertEquals(42, encoded) + + val decoded = decode(ValueClass.serializer(), encoded) + assertEquals(testValueClass, decoded) } @Test - fun decodeObject() { - val decoded = decode(TestData.serializer(), nativeMapOf("map" to nativeMapOf("key" to "value"))) - assertEquals(TestData(mapOf("key" to "value"), false), decoded) + fun encodeDecodeClass() { + val testDataClass = TestData(mapOf("key" to "value"), mapOf(1 to 1), true, null, ValueClass(42)) + val encoded = encode(TestData.serializer(), testDataClass) { encodeDefaults = false } + + nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "valueClass" to 42), encoded) + + val decoded = decode(TestData.serializer(), encoded) + assertEquals(testDataClass, decoded) } @Test - fun decodeListOfObjects() { - val decoded = decode(ListSerializer(TestData.serializer()), nativeListOf(nativeMapOf("map" to nativeMapOf("key" to "value")))) - assertEquals(listOf(TestData(mapOf("key" to "value"), false)), decoded) + fun encodeDecodeClassNullableValue() { + val testDataClass = TestData(mapOf("key" to "value"), mapOf(1 to 1), true, nullableBool = true, ValueClass(42)) + val encoded = encode(TestData.serializer(), testDataClass) { encodeDefaults = true } + + nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to true, "valueClass" to 42), encoded) + + val decoded = decode(TestData.serializer(), encoded) + assertEquals(testDataClass, decoded) } @Test - fun decodeObjectNullableValue() { - val decoded = decode(TestData.serializer(), nativeMapOf("map" to mapOf("key" to "value"), "nullableBool" to null)) - assertNull(decoded.nullableBool) + fun encodeDecodeGenericClass() { + val innerClass = TestData(mapOf("key" to "value"), mapOf(1 to 1), true, valueClass = ValueClass(42)) + val genericClass = GenericClass(innerClass) + val encoded = encode(GenericClass.serializer(TestData.serializer()), genericClass) { encodeDefaults = true } + + nativeAssertEquals(nativeMapOf("inner" to nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to null, "valueClass" to 42)), encoded) + + val decoded = decode(GenericClass.serializer(TestData.serializer()), encoded) + assertEquals(genericClass, decoded) } @Test - fun decodeSealedClass() { - val decoded = decode(TestSealed.serializer(), nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true)) - assertEquals(TestSealed.ChildClass(mapOf("key" to "value"), true), decoded) + fun encodeDecodeSealedClass() { + val sealedClass = SealedClass.Test("value") + val encoded = encode(SealedClass.serializer(), sealedClass) { encodeDefaults = true } + + nativeAssertEquals(nativeMapOf("type" to "test", "value" to "value"), encoded) + + val decoded = decode(SealedClass.serializer(), encoded) + assertEquals(sealedClass, decoded) } @Test - fun encodeSealedClassList() { - val toEncode = TestSealedList( - list = listOf( - TestSealed.ChildClass( - map = mapOf("key" to "value"), - bool = false - ) - ) - ) - val encoded = encode( - TestSealedList.serializer(), - toEncode, - shouldEncodeElementDefault = true - ) - val expected = nativeMapOf( - "list" to nativeListOf( - nativeMapOf( - "type" to "child", - "map" to nativeMapOf( - "key" to "value" - ), - "bool" to false - ) - ) - ) - nativeAssertEquals(expected, encoded) + fun encodeDecodePolymorphicClass() { + val module = SerializersModule { + polymorphic(AbstractClass::class, AbstractClass.serializer()) { + subclass(ImplementedClass::class, ImplementedClass.serializer()) + } + } + val abstractClass: AbstractClass = ImplementedClass("value", true) + val encoded = + encode(AbstractClass.serializer(), abstractClass) { + encodeDefaults = true + serializersModule = module + } + + nativeAssertEquals(nativeMapOf("type" to "implemented", "abstractValue" to "value", "otherValue" to true), encoded) + + val decoded = decode(AbstractClass.serializer(), encoded) { + serializersModule = module + } + assertEquals(abstractClass, decoded) } @Test - fun decodeSealedClassList() { - val toDecode = nativeMapOf( - "list" to nativeListOf( - nativeMapOf( - "type" to "child", - "map" to nativeMapOf( - "key" to "value" - ), - "bool" to false - ) - ) - ) - val decoded = decode( - TestSealedList.serializer(), - toDecode + fun encodeDecodeNestedClass() { + val module = SerializersModule { + polymorphic(AbstractClass::class, AbstractClass.serializer()) { + subclass(ImplementedClass::class, ImplementedClass.serializer()) + } + } + + val testData = TestData(mapOf("key" to "value"), mapOf(1 to 1), true, null, ValueClass(42)) + val sealedClass: SealedClass = SealedClass.Test("value") + val abstractClass: AbstractClass = ImplementedClass("value", true) + val nestedClass = NestedClass(testData, sealedClass, abstractClass, listOf(testData), listOf(sealedClass), listOf(abstractClass), mapOf(testData to testData), mapOf(sealedClass to sealedClass), mapOf(abstractClass to abstractClass)) + val encoded = encode(NestedClass.serializer(), nestedClass) { + encodeDefaults = true + serializersModule = module + } + + val testDataEncoded = nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to null, "valueClass" to 42) + val sealedEncoded = nativeMapOf("type" to "test", "value" to "value") + val abstractEncoded = nativeMapOf("type" to "implemented", "abstractValue" to "value", "otherValue" to true) + nativeAssertEquals( + nativeMapOf( + "testData" to testDataEncoded, + "sealed" to sealedEncoded, + "abstract" to abstractEncoded, + "testDataList" to nativeListOf(testDataEncoded), + "sealedList" to nativeListOf(sealedEncoded), + "abstractList" to nativeListOf(abstractEncoded), + "testDataMap" to nativeMapOf(testDataEncoded to testDataEncoded), + "sealedMap" to nativeMapOf(sealedEncoded to sealedEncoded), + "abstractMap" to nativeMapOf(abstractEncoded to abstractEncoded) + ), + encoded ) - val expected = TestSealedList( - list = listOf( - TestSealed.ChildClass( - map = mapOf("key" to "value"), - bool = false - ) + + val decoded = decode(NestedClass.serializer(), encoded) { + serializersModule = module + } + assertEquals(nestedClass, decoded) + } + + @Test + fun reencodeTransformationList() { + val reencoded = reencodeTransformation>(nativeListOf("One", "Two", "Three")) { + assertEquals(listOf("One", "Two", "Three"), it) + it.map { value -> "new$value" } + } + nativeAssertEquals(nativeListOf("newOne", "newTwo", "newThree"), reencoded) + } + + @Test + fun reencodeTransformationMap() { + val reencoded = reencodeTransformation>(nativeMapOf("key" to "value", "key2" to "value2", "key3" to "value3")) { + assertEquals(mapOf("key" to "value", "key2" to "value2", "key3" to "value3"), it) + it.mapValues { (_, value) -> "new-$value" } + } + + nativeAssertEquals(nativeMapOf("key" to "new-value", "key2" to "new-value2", "key3" to "new-value3"), reencoded) + } + + @Test + fun reencodeTransformationObject() { + val reencoded = reencodeTransformation(nativeMapOf(), { encodeDefaults = false }) { + assertEquals(TestObject, it) + it + } + nativeAssertEquals(nativeMapOf(), reencoded) + } + + @Test + fun reencodeTransformationValueClass() { + val reencoded = reencodeTransformation( + 42, + { encodeDefaults = false } + ) { + assertEquals(ValueClass(42), it) + ValueClass(23) + } + + nativeAssertEquals(23, reencoded) + } + + @Test + fun reencodeTransformationClass() { + val reencoded = reencodeTransformation( + nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to true, "valueClass" to 42), + { encodeDefaults = false } + ) { + assertEquals(TestData(mapOf("key" to "value"), mapOf(1 to 1), bool = true, nullableBool = true, ValueClass(42)), it) + it.copy(map = mapOf("newKey" to "newValue"), nullableBool = null) + } + + nativeAssertEquals(nativeMapOf("map" to nativeMapOf("newKey" to "newValue"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "valueClass" to 42), reencoded) + } + + @Test + fun reencodeTransformationNullableValue() { + val reencoded = reencodeTransformation( + nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to true, "valueClass" to 42), + { encodeDefaults = false } + ) { + assertEquals(TestData(mapOf("key" to "value"), mapOf(1 to 1), bool = true, nullableBool = true, valueClass = ValueClass(42)), it) + null + } + + nativeAssertEquals(null, reencoded) + } + + @Test + fun reencodeTransformationGenericClass() { + val reencoded = reencodeTransformation( + GenericClass.serializer(TestData.serializer()), + nativeMapOf("inner" to nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to false, "valueClass" to 42)), + { encodeDefaults = false } + ) { + assertEquals( + GenericClass(TestData(mapOf("key" to "value"), mapOf(1 to 1), bool = true, nullableBool = false, valueClass = ValueClass(42))), + it ) - ) + GenericClass(it.inner.copy(map = mapOf("newKey" to "newValue"), nullableBool = null)) + } + + nativeAssertEquals(nativeMapOf("inner" to nativeMapOf("map" to nativeMapOf("newKey" to "newValue"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "valueClass" to 42)), reencoded) + } + + @Test + fun reencodeTransformationSealedClass() { + val reencoded = reencodeTransformation(SealedClass.serializer(), nativeMapOf("type" to "test", "value" to "value")) { + assertEquals(SealedClass.Test("value"), it) + SealedClass.Test("newTest") + } + + nativeAssertEquals(nativeMapOf("type" to "test", "value" to "newTest"), reencoded) + } + + @Test + fun reencodeTransformationPolymorphicClass() { + val module = SerializersModule { + polymorphic(AbstractClass::class, AbstractClass.serializer()) { + subclass(ImplementedClass::class, ImplementedClass.serializer()) + } + } + + val reencoded = reencodeTransformation( + AbstractClass.serializer(), + nativeMapOf("type" to "implemented", "abstractValue" to "value", "otherValue" to true), + builder = { + serializersModule = module + } + ) { + assertEquals(ImplementedClass("value", true), it) + ImplementedClass("new-${it.abstractValue}", false) + } + + nativeAssertEquals(nativeMapOf("type" to "implemented", "abstractValue" to "new-value", "otherValue" to false), reencoded) + } + + @Test + fun reencodeTransformationNestedClass() { + val module = SerializersModule { + polymorphic(AbstractClass::class, AbstractClass.serializer()) { + subclass(ImplementedClass::class, ImplementedClass.serializer()) + } + } + + val testData = TestData(mapOf("key" to "value"), mapOf(1 to 1), true, null, ValueClass(42)) + val sealedClass: SealedClass = SealedClass.Test("value") + val abstractClass: AbstractClass = ImplementedClass("value", true) + val nestedClass = NestedClass(testData, sealedClass, abstractClass, listOf(testData), listOf(sealedClass), listOf(abstractClass), mapOf(testData to testData), mapOf(sealedClass to sealedClass), mapOf(abstractClass to abstractClass)) + val encoded = encode(NestedClass.serializer(), nestedClass) { + encodeDefaults = true + serializersModule = module + } + + val reencoded = reencodeTransformation(NestedClass.serializer(), encoded, builder = { + encodeDefaults = true + serializersModule = module + }) { + assertEquals(nestedClass, it) + it.copy(sealed = SealedClass.Test("newValue")) + } - assertEquals(expected, decoded) + val testDataEncoded = nativeMapOf("map" to nativeMapOf("key" to "value"), "otherMap" to nativeMapOf(1 to 1), "bool" to true, "nullableBool" to null, "valueClass" to 42) + val sealedEncoded = nativeMapOf("type" to "test", "value" to "value") + val abstractEncoded = nativeMapOf("type" to "implemented", "abstractValue" to "value", "otherValue" to true) + nativeAssertEquals( + nativeMapOf( + "testData" to testDataEncoded, + "sealed" to nativeMapOf("type" to "test", "value" to "newValue"), + "abstract" to abstractEncoded, + "testDataList" to nativeListOf(testDataEncoded), + "sealedList" to nativeListOf(sealedEncoded), + "abstractList" to nativeListOf(abstractEncoded), + "testDataMap" to nativeMapOf(testDataEncoded to testDataEncoded), + "sealedMap" to nativeMapOf(sealedEncoded to sealedEncoded), + "abstractMap" to nativeMapOf(abstractEncoded to abstractEncoded) + ), + reencoded + ) } -} \ No newline at end of file +} diff --git a/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_decoders.kt b/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_decoders.kt index ddc5843c1..43589a7a9 100644 --- a/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_decoders.kt +++ b/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_decoders.kt @@ -9,32 +9,32 @@ import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind -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") { +actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, polymorphicIsNested: Boolean): CompositeDecoder = when(descriptor.kind) { + StructureKind.CLASS, StructureKind.OBJECT -> decodeAsMap(false) + StructureKind.LIST -> decodeAsList() + 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<*,*>).orEmpty()[discriminator] as String + +private fun FirebaseDecoder.decodeAsList(): CompositeDecoder = (value as? List<*>).orEmpty().let { + FirebaseCompositeDecoder(it.size, settings) { _, index -> it[index] } +} +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 (desc.getElementName(index) == "value") map - } else { + else { map[desc.getElementName(index)] } + } 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 getPolymorphicType(value: Any?, discriminator: String): String = - (value as Map<*,*>)[discriminator] as String \ No newline at end of file diff --git a/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt b/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt index d4584218a..144b0e60f 100644 --- a/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt +++ b/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt @@ -5,24 +5,30 @@ package dev.gitlive.firebase import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.encoding.CompositeEncoder 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() - .also { value = it } - .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } } + StructureKind.LIST -> encodeAsList() StructureKind.MAP -> mutableListOf() - .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } } - StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> mutableMapOf() - .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.encodeAsList(): FirebaseCompositeEncoder = mutableListOf() + .also { value = it } + .let { FirebaseCompositeEncoder(settings) { _, index, value -> it.add(index, value) } } +private fun FirebaseEncoder.encodeAsMap(descriptor: SerialDescriptor): FirebaseCompositeEncoder = mutableMapOf() + .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") -} \ No newline at end of file + ) + } diff --git a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_decoders.kt b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_decoders.kt index b64f97129..a849dd190 100644 --- a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_decoders.kt +++ b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_decoders.kt @@ -5,39 +5,51 @@ package dev.gitlive.firebase import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.CompositeDecoder import kotlin.js.Json -@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") -actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor): CompositeDecoder = when(descriptor.kind) { - StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> (value as Json).let { json -> - FirebaseClassDecoder(js("Object").keys(value).length as Int, { json[it] != undefined }) { desc, index -> - val elementName = desc.getElementName(index) - if (desc.kind is PolymorphicKind && elementName == "value") { - json +actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, polymorphicIsNested: Boolean): CompositeDecoder = when (descriptor.kind) { + StructureKind.CLASS, StructureKind.OBJECT -> decodeAsMap(false) + StructureKind.LIST -> decodeAsList() + StructureKind.MAP -> (js("Object").entries(value) as Array>).let { + FirebaseCompositeDecoder( + it.size, + settings + ) { desc, index -> it[index / 2].run { if (index % 2 == 0) { + val key = get(0) as String + if (desc.getElementDescriptor(index).kind == PrimitiveKind.STRING) { + key } else { - json[desc.getElementName(index)] + JSON.parse(key) } - } - } - StructureKind.LIST -> - when(value) { - is Array<*> -> value.toList() - else -> (js("Object").entries(value) as Array>) - .asSequence() - .sortedBy { (it) -> it.toString().toIntOrNull() } - .map { (_, it) -> it } - .toList() - } - .let { FirebaseCompositeDecoder(it.size) { _, index -> it[index] } } - StructureKind.MAP -> (js("Object").entries(value) as Array>).let { - FirebaseCompositeDecoder(it.size) { _, index -> it[index/2].run { if(index % 2 == 0) get(0) else get(1) } } + } else get(1) } } } + + is PolymorphicKind -> decodeAsMap(polymorphicIsNested) else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet") } @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") actual fun getPolymorphicType(value: Any?, discriminator: String): String = (value as Json)[discriminator] as String + +private fun FirebaseDecoder.decodeAsList(): CompositeDecoder = (value as Array<*>).let { + FirebaseCompositeDecoder(it.size, settings) { _, index -> it[index] } +} +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +private fun FirebaseDecoder.decodeAsMap(isNestedPolymorphic: Boolean): CompositeDecoder = (value as Json).let { json -> + FirebaseClassDecoder(js("Object").keys(value).length as Int, settings, { json[it] != undefined }) { desc, index -> + if (isNestedPolymorphic) { + if (desc.getElementName(index) == "value") { + json + } else { + json[desc.getElementName(index)] + } + } else { + json[desc.getElementName(index)] + } + } +} \ No newline at end of file diff --git a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_encoders.kt b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_encoders.kt index 69959f4ed..dbb3d1f15 100644 --- a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_encoders.kt +++ b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_encoders.kt @@ -5,30 +5,40 @@ package dev.gitlive.firebase import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.encoding.CompositeEncoder import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlin.js.json actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) { - StructureKind.LIST -> Array(descriptor.elementsCount) { null } - .also { value = it } - .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[index] = value } } + StructureKind.LIST -> encodeAsList(descriptor) StructureKind.MAP -> { val map = json() - var lastKey: String = "" + var lastKey = "" value = map - FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> if(index % 2 == 0) lastKey = value as String else map[lastKey] = value } + FirebaseCompositeEncoder(settings) { _, index, value -> + if (index % 2 == 0) { + lastKey = (value as? String) ?: JSON.stringify(value) + } else { + map[lastKey] = value + } + } } - StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> json() - .also { value = it } - .let { FirebaseCompositeEncoder( - shouldEncodeElementDefault, + 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.encodeAsList(descriptor: SerialDescriptor): FirebaseCompositeEncoder = Array(descriptor.elementsCount) { null } + .also { value = it } + .let { FirebaseCompositeEncoder(settings) { _, index, value -> it[index] = value } } +private fun FirebaseEncoder.encodeAsMap(descriptor: SerialDescriptor): FirebaseCompositeEncoder = json() + .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") -} - + ) + } diff --git a/firebase-config/package.json b/firebase-config/package.json index c005ac238..454740a67 100644 --- a/firebase-config/package.json +++ b/firebase-config/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-config", - "version": "1.11.0", + "version": "1.11.1", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-config.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-crashlytics/package.json b/firebase-crashlytics/package.json index 2e03d9beb..8b0981a1a 100644 --- a/firebase-crashlytics/package.json +++ b/firebase-crashlytics/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-crashlytics", - "version": "1.11.0", + "version": "1.12.0", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-crashlytics.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.6.10", "kotlinx-coroutines-core": "1.6.1-native-mt" diff --git a/firebase-crashlytics/src/iosMain/kotlin/dev/gitlive/firebase/crashlytics/crashlytics.kt b/firebase-crashlytics/src/iosMain/kotlin/dev/gitlive/firebase/crashlytics/crashlytics.kt index 209e82f9f..65cb6cbd8 100644 --- a/firebase-crashlytics/src/iosMain/kotlin/dev/gitlive/firebase/crashlytics/crashlytics.kt +++ b/firebase-crashlytics/src/iosMain/kotlin/dev/gitlive/firebase/crashlytics/crashlytics.kt @@ -43,5 +43,5 @@ private fun Throwable.asNSError(): NSError { if (message != null) { userInfo[NSLocalizedDescriptionKey] = message } - return NSError.errorWithDomain("KotlinException", 0, userInfo) + return NSError.errorWithDomain(this::class.qualifiedName, 0, userInfo) } \ No newline at end of file diff --git a/firebase-database/package.json b/firebase-database/package.json index ed9924756..090086c4a 100644 --- a/firebase-database/package.json +++ b/firebase-database/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-database", - "version": "1.11.0", + "version": "1.11.1", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-database.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt index 0ce0e0d12..ffdcc32a9 100644 --- a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -5,36 +5,49 @@ package dev.gitlive.firebase.database import com.google.android.gms.tasks.Task -import com.google.firebase.database.* +import com.google.firebase.database.ChildEventListener +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.Logger +import com.google.firebase.database.MutableData +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.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.database.ChildEvent.Type import dev.gitlive.firebase.database.FirebaseDatabase.Companion.FirebaseDatabase import dev.gitlive.firebase.decode -import dev.gitlive.firebase.encode +import dev.gitlive.firebase.reencodeTransformation import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.tasks.await import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationStrategy -import java.util.* +import java.util.WeakHashMap import kotlin.time.Duration.Companion.seconds -suspend fun Task.awaitWhileOnline(): T = +suspend fun Task.awaitWhileOnline(database: FirebaseDatabase): T = merge( flow { emit(await()) }, - Firebase.database + database .reference(".info/connected") .valueEvents .debounce(2.seconds) .filterNot { it.value() } .map { throw DatabaseException("Database not connected", null) } ) - .first() - + .first() actual val Firebase.database by lazy { FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance()) } @@ -61,10 +74,10 @@ actual class FirebaseDatabase private constructor(val android: com.google.fireba private var persistenceEnabled = true actual fun reference(path: String) = - DatabaseReference(android.getReference(path), persistenceEnabled) + DatabaseReference(NativeDatabaseReference(android.getReference(path), persistenceEnabled)) actual fun reference() = - DatabaseReference(android.reference, persistenceEnabled) + DatabaseReference(NativeDatabaseReference(android.reference, persistenceEnabled)) actual fun setPersistenceEnabled(enabled: Boolean) = android.setPersistenceEnabled(enabled).also { persistenceEnabled = enabled } @@ -76,10 +89,23 @@ actual class FirebaseDatabase private constructor(val android: com.google.fireba android.useEmulator(host, port) } -actual open class Query internal constructor( +internal actual open class NativeQuery( open val android: com.google.firebase.database.Query, - val persistenceEnabled: Boolean + val persistenceEnabled: Boolean, +) + +actual open class Query internal actual constructor( + nativeQuery: NativeQuery ) { + + internal constructor( + android: com.google.firebase.database.Query, + persistenceEnabled: Boolean + ) : this(NativeQuery(android, persistenceEnabled)) + + open val android: com.google.firebase.database.Query = nativeQuery.android + val persistenceEnabled: Boolean = nativeQuery.persistenceEnabled + actual fun orderByKey() = Query(android.orderByKey(), persistenceEnabled) actual fun orderByValue() = Query(android.orderByValue(), persistenceEnabled) @@ -110,18 +136,18 @@ actual open class Query internal constructor( actual val valueEvents: Flow get() = callbackFlow { - val listener = object : ValueEventListener { - override fun onDataChange(snapshot: com.google.firebase.database.DataSnapshot) { - trySendBlocking(DataSnapshot(snapshot, persistenceEnabled)) - } + val listener = object : ValueEventListener { + override fun onDataChange(snapshot: com.google.firebase.database.DataSnapshot) { + trySendBlocking(DataSnapshot(snapshot, persistenceEnabled)) + } - override fun onCancelled(error: com.google.firebase.database.DatabaseError) { - close(error.toException()) + override fun onCancelled(error: com.google.firebase.database.DatabaseError) { + close(error.toException()) + } } + android.addValueEventListener(listener) + awaitClose { android.removeEventListener(listener) } } - android.addValueEventListener(listener) - awaitClose { android.removeEventListener(listener) } - } actual fun childEvents(vararg types: Type): Flow = callbackFlow { val listener = object : ChildEventListener { @@ -157,44 +183,49 @@ actual open class Query internal constructor( override fun toString() = android.toString() } -actual class DatabaseReference internal constructor( +@PublishedApi +internal actual class NativeDatabaseReference internal constructor( override val android: com.google.firebase.database.DatabaseReference, persistenceEnabled: Boolean -): Query(android, persistenceEnabled) { +): NativeQuery(android, persistenceEnabled) { actual val key get() = android.key + val database = FirebaseDatabase(android.database) - actual fun child(path: String) = DatabaseReference(android.child(path), persistenceEnabled) + actual fun child(path: String) = NativeDatabaseReference(android.child(path), persistenceEnabled) - actual fun push() = DatabaseReference(android.push(), persistenceEnabled) - actual fun onDisconnect() = OnDisconnect(android.onDisconnect(), persistenceEnabled) + actual fun push() = NativeDatabaseReference(android.push(), persistenceEnabled) + actual fun onDisconnect() = NativeOnDisconnect(android.onDisconnect(), persistenceEnabled, database) - actual suspend inline fun setValue(value: T?, encodeDefaults: Boolean) = android.setValue(encode(value, encodeDefaults)) - .run { if(persistenceEnabled) await() else awaitWhileOnline() } + actual suspend fun setValueEncoded(encodedValue: Any?) = android.setValue(encodedValue) + .run { if(persistenceEnabled) await() else awaitWhileOnline(database) } .run { Unit } - actual suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) = - android.setValue(encode(strategy, value, encodeDefaults)) - .run { if(persistenceEnabled) await() else awaitWhileOnline() } - .run { Unit } - @Suppress("UNCHECKED_CAST") - actual suspend fun updateChildren(update: Map, encodeDefaults: Boolean) = - android.updateChildren(encode(update, encodeDefaults) as Map) - .run { if(persistenceEnabled) await() else awaitWhileOnline() } + actual suspend fun updateEncodedChildren(encodedUpdate: Any?) = + android.updateChildren(encodedUpdate as Map) + .run { if(persistenceEnabled) await() else awaitWhileOnline(database) } .run { Unit } actual suspend fun removeValue() = android.removeValue() - .run { if(persistenceEnabled) await() else awaitWhileOnline() } + .run { if(persistenceEnabled) await() else awaitWhileOnline(database) } .run { Unit } - actual suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot { + @OptIn(ExperimentalSerializationApi::class) + actual suspend fun runTransaction(strategy: KSerializer, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit, transactionUpdate: (currentData: T) -> T): DataSnapshot { val deferred = CompletableDeferred() android.runTransaction(object : Transaction.Handler { override fun doTransaction(currentData: MutableData): Transaction.Result { - currentData.value = currentData.value?.let { - transactionUpdate(decode(strategy, it)) + val valueToReencode = currentData.value + // Value may be null initially, so only reencode if this is allowed + if (strategy.descriptor.isNullable || valueToReencode != null) { + currentData.value = reencodeTransformation( + strategy, + valueToReencode, + buildSettings, + transactionUpdate + ) } return Transaction.success(currentData) } @@ -215,6 +246,9 @@ actual class DatabaseReference internal constructor( return deferred.await() } } + +val DatabaseReference.android get() = nativeReference.android + @Suppress("UNCHECKED_CAST") actual class DataSnapshot internal constructor( val android: com.google.firebase.database.DataSnapshot, @@ -225,48 +259,48 @@ actual class DataSnapshot internal constructor( actual val key get() = android.key - actual val ref: DatabaseReference get() = DatabaseReference(android.ref, persistenceEnabled) + actual val ref: DatabaseReference get() = DatabaseReference(NativeDatabaseReference(android.ref, persistenceEnabled)) actual val value get() = android.value actual inline fun value() = decode(value = android.value) - actual fun value(strategy: DeserializationStrategy) = - decode(strategy, android.value) + actual inline fun value(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + decode(strategy, android.value, buildSettings) actual fun child(path: String) = DataSnapshot(android.child(path), persistenceEnabled) actual val hasChildren get() = android.hasChildren() actual val children: Iterable get() = android.children.map { DataSnapshot(it, persistenceEnabled) } } -actual class OnDisconnect internal constructor( +@PublishedApi +internal actual class NativeOnDisconnect internal constructor( val android: com.google.firebase.database.OnDisconnect, - val persistenceEnabled: Boolean + val persistenceEnabled: Boolean, + val database: FirebaseDatabase, ) { actual suspend fun removeValue() = android.removeValue() - .run { if(persistenceEnabled) await() else awaitWhileOnline() } + .run { if(persistenceEnabled) await() else awaitWhileOnline(database) } .run { Unit } actual suspend fun cancel() = android.cancel() - .run { if(persistenceEnabled) await() else awaitWhileOnline() } + .run { if(persistenceEnabled) await() else awaitWhileOnline(database) } .run { Unit } - actual suspend inline fun setValue(value: T, encodeDefaults: Boolean) = - android.setValue(encode(value, encodeDefaults)) - .run { if(persistenceEnabled) await() else awaitWhileOnline() } - .run { Unit } - - actual suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) = - android.setValue(encode(strategy, value, encodeDefaults)) - .run { if(persistenceEnabled) await() else awaitWhileOnline() } - .run { Unit} + actual suspend fun setValue(encodedValue: Any?) = android.setValue(encodedValue) + .run { if(persistenceEnabled) await() else awaitWhileOnline(database) } + .run { Unit } - actual suspend fun updateChildren(update: Map, encodeDefaults: Boolean) = - android.updateChildren(update.mapValues { (_, it) -> encode(it, encodeDefaults) }) - .run { if(persistenceEnabled) await() else awaitWhileOnline() } + actual suspend fun updateEncodedChildren(encodedUpdate: Map) = + android.updateChildren(encodedUpdate) + .run { if(persistenceEnabled) await() else awaitWhileOnline(database) } .run { Unit } } +val OnDisconnect.android get() = native.android +val OnDisconnect.persistenceEnabled get() = native.persistenceEnabled +val OnDisconnect.database get() = native.database + actual typealias DatabaseException = com.google.firebase.database.DatabaseException diff --git a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt index d5bbf6aee..d1e272a38 100644 --- a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -4,9 +4,16 @@ package dev.gitlive.firebase.database +import dev.gitlive.firebase.DecodeSettings +import dev.gitlive.firebase.EncodeDecodeSettingsBuilder +import dev.gitlive.firebase.EncodeSettings import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp -import dev.gitlive.firebase.database.ChildEvent.Type.* +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 kotlinx.coroutines.flow.Flow import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.KSerializer @@ -45,7 +52,9 @@ data class ChildEvent internal constructor( } } -expect open class Query { +internal expect open class NativeQuery + +expect open class Query internal constructor(nativeQuery: NativeQuery) { val valueEvents: Flow fun childEvents(vararg types: ChildEvent.Type = arrayOf(ADDED, CHANGED, MOVED, REMOVED)): Flow fun orderByKey(): Query @@ -64,17 +73,52 @@ expect open class Query { fun equalTo(value: Boolean, key: String? = null): Query } -expect class DatabaseReference : Query { +@PublishedApi +internal expect class NativeDatabaseReference : NativeQuery { val key: String? - fun push(): DatabaseReference - fun child(path: String): DatabaseReference - fun onDisconnect(): OnDisconnect - suspend inline fun setValue(value: T?, encodeDefaults: Boolean = true) - suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean = true) - suspend fun updateChildren(update: Map, encodeDefaults: Boolean = true) + fun push(): NativeDatabaseReference + suspend fun setValueEncoded(encodedValue: Any?) + suspend fun updateEncodedChildren(encodedUpdate: Any?) + fun child(path: String): NativeDatabaseReference + fun onDisconnect(): NativeOnDisconnect + suspend fun removeValue() - suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot + suspend fun runTransaction(strategy: KSerializer, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit = {}, transactionUpdate: (currentData: T) -> T): DataSnapshot +} + +class DatabaseReference internal constructor(@PublishedApi internal val nativeReference: NativeDatabaseReference) : Query(nativeReference) { + + val key: String? = nativeReference.key + fun push(): DatabaseReference = DatabaseReference(nativeReference.push()) + fun child(path: String): DatabaseReference = DatabaseReference(nativeReference.child(path)) + fun onDisconnect(): OnDisconnect = OnDisconnect(nativeReference.onDisconnect()) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("setValue(value) { this.encodeDefaults = encodeDefaults }")) + suspend inline fun setValue(value: T?, encodeDefaults: Boolean) = + setValue(value) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun setValue(value: T?, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + nativeReference.setValueEncoded(encode(value, buildSettings)) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("setValue(strategy, value) { this.encodeDefaults = encodeDefaults }")) + suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) = + setValue(strategy, value) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun setValue(strategy: SerializationStrategy, value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = nativeReference.setValueEncoded(encode(strategy, value, buildSettings)) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("updateChildren(update) { this.encodeDefaults = encodeDefaults }")) + suspend fun updateChildren(update: Map, encodeDefaults: Boolean) = updateChildren(update) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun updateChildren(update: Map, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = nativeReference.updateEncodedChildren( + encode(update, buildSettings)) + + suspend fun removeValue() = nativeReference.removeValue() + + suspend fun runTransaction(strategy: KSerializer, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit = {}, transactionUpdate: (currentData: T) -> T): DataSnapshot = nativeReference.runTransaction(strategy, buildSettings, transactionUpdate) } expect class DataSnapshot { @@ -83,7 +127,7 @@ expect class DataSnapshot { val ref: DatabaseReference val value: Any? inline fun value(): T - fun value(strategy: DeserializationStrategy): T + inline fun value(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit = {}): T fun child(path: String): DataSnapshot val hasChildren: Boolean val children: Iterable @@ -91,10 +135,30 @@ expect class DataSnapshot { expect class DatabaseException(message: String?, cause: Throwable?) : RuntimeException -expect class OnDisconnect { +@PublishedApi +internal expect class NativeOnDisconnect { suspend fun removeValue() suspend fun cancel() - suspend inline fun setValue(value: T, encodeDefaults: Boolean = true) - suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean = true) - suspend fun updateChildren(update: Map, encodeDefaults: Boolean = true) + suspend fun setValue(encodedValue: Any?) + suspend fun updateEncodedChildren(encodedUpdate: Map) +} + +class OnDisconnect internal constructor(@PublishedApi internal val native: NativeOnDisconnect) { + suspend fun removeValue() = native.removeValue() + suspend fun cancel() = native.cancel() + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("setValue(value) { this.encodeDefaults = encodeDefaults }")) + suspend inline fun setValue(value: T?, encodeDefaults: Boolean) = + setValue(value) { this.encodeDefaults = encodeDefaults } + suspend inline fun setValue(value: T?, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + native.setValue(encode(value, buildSettings)) + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("setValue(strategy, value) { this.encodeDefaults = encodeDefaults }")) + suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) = + setValue(strategy, value) { this.encodeDefaults = encodeDefaults } + suspend inline fun setValue(strategy: SerializationStrategy, value: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = setValue(encode(strategy, value, buildSettings)) + + suspend inline fun updateChildren(update: Map, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncodedChildren(update.mapValues { (_, it) -> encode(it, buildSettings) }) + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("updateChildren(update) { this.encodeDefaults = encodeDefaults }")) + suspend fun updateChildren(update: Map, encodeDefaults: Boolean) = updateChildren(update) { + this.encodeDefaults = encodeDefaults + } } diff --git a/firebase-database/src/commonTest/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/commonTest/kotlin/dev/gitlive/firebase/database/database.kt index 00a8f85dc..ba3f7be59 100644 --- a/firebase-database/src/commonTest/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/commonTest/kotlin/dev/gitlive/firebase/database/database.kt @@ -1,13 +1,22 @@ package dev.gitlive.firebase.database -import dev.gitlive.firebase.* +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseOptions +import dev.gitlive.firebase.apps +import dev.gitlive.firebase.initialize +import dev.gitlive.firebase.runBlockingTest +import dev.gitlive.firebase.runTest import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy -import kotlin.test.* +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue import kotlin.time.Duration.Companion.minutes expect val emulatorHost: String @@ -79,51 +88,47 @@ class FirebaseDatabaseTest { assertEquals(3, firebaseDatabaseChildCount) } -// @Test -// fun testBasicIncrementTransaction() = runTest { -// val data = DatabaseTest("PostOne", 2) -// val userRef = database.reference("users/user_1/post_id_1") -// setupDatabase(userRef, data, DatabaseTest.serializer()) -// -// // Check database before transaction -// val userDocBefore = userRef.valueEvents.first().value(DatabaseTest.serializer()) -// assertEquals(data.title, userDocBefore.title) -// assertEquals(data.likes, userDocBefore.likes) -// -// // Run transaction -// val transactionSnapshot = userRef.runTransaction(DatabaseTest.serializer()) { DatabaseTest(data.title, it.likes + 1) } -// val userDocAfter = transactionSnapshot.value(DatabaseTest.serializer()) -// -// // Check the database after transaction -// assertEquals(data.title, userDocAfter.title) -// assertEquals(data.likes + 1, userDocAfter.likes) -// -// // cleanUp Firebase -// cleanUp() -// } -// -// @Test -// fun testBasicDecrementTransaction() = runTest { -// val data = DatabaseTest("PostTwo", 2) -// val userRef = database.reference("users/user_1/post_id_2") -// setupDatabase(userRef, data, DatabaseTest.serializer()) -// -// // Check database before transaction -// val userDocBefore = userRef.valueEvents.first().value(DatabaseTest.serializer()) -// assertEquals(data.title, userDocBefore.title) -// assertEquals(data.likes, userDocBefore.likes) -// -// // Run transaction -// val transactionSnapshot = userRef.runTransaction(DatabaseTest.serializer()) { DatabaseTest(data.title, it.likes - 1) } -// val userDocAfter = transactionSnapshot.value(DatabaseTest.serializer()) -// -// // Check the database after transaction -// assertEquals(data.title, userDocAfter.title) -// assertEquals(data.likes - 1, userDocAfter.likes) -// -// // cleanUp Firebase -// cleanUp() -// } + @Test + fun testBasicIncrementTransaction() = runTest { + ensureDatabaseConnected() + val data = DatabaseTest("PostOne", 2) + val userRef = database.reference("users/user_1/post_id_1") + setupDatabase(userRef, data, DatabaseTest.serializer()) + + // Check database before transaction + val userDocBefore = userRef.valueEvents.first().value(DatabaseTest.serializer()) + assertEquals(data.title, userDocBefore.title) + assertEquals(data.likes, userDocBefore.likes) + + // Run transaction + val transactionSnapshot = userRef.runTransaction(DatabaseTest.serializer()) { it.copy(likes = it.likes + 1) } + val userDocAfter = transactionSnapshot.value(DatabaseTest.serializer()) + + // Check the database after transaction + assertEquals(data.title, userDocAfter.title) + assertEquals(data.likes + 1, userDocAfter.likes) + } + + @Test + fun testBasicDecrementTransaction() = runTest { + ensureDatabaseConnected() + val data = DatabaseTest("PostTwo", 2) + val userRef = database.reference("users/user_1/post_id_2") + setupDatabase(userRef, data, DatabaseTest.serializer()) + + // Check database before transaction + val userDocBefore = userRef.valueEvents.first().value(DatabaseTest.serializer()) + assertEquals(data.title, userDocBefore.title) + assertEquals(data.likes, userDocBefore.likes) + + // Run transaction + val transactionSnapshot = userRef.runTransaction(DatabaseTest.serializer()) { it.copy(likes = it.likes - 1) } + val userDocAfter = transactionSnapshot.value(DatabaseTest.serializer()) + + // Check the database after transaction + assertEquals(data.title, userDocAfter.title) + assertEquals(data.likes - 1, userDocAfter.likes) + } @Test fun testSetServerTimestamp() = runTest { diff --git a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt index 128d7b6c2..a78a26fe1 100644 --- a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -4,14 +4,27 @@ package dev.gitlive.firebase.database -import cocoapods.FirebaseDatabase.* -import cocoapods.FirebaseDatabase.FIRDataEventType.* +import cocoapods.FirebaseDatabase.FIRDataEventType.FIRDataEventTypeChildAdded +import cocoapods.FirebaseDatabase.FIRDataEventType.FIRDataEventTypeChildChanged +import cocoapods.FirebaseDatabase.FIRDataEventType.FIRDataEventTypeChildMoved +import cocoapods.FirebaseDatabase.FIRDataEventType.FIRDataEventTypeChildRemoved +import cocoapods.FirebaseDatabase.FIRDataEventType.FIRDataEventTypeValue +import cocoapods.FirebaseDatabase.FIRDataSnapshot +import cocoapods.FirebaseDatabase.FIRDatabase +import cocoapods.FirebaseDatabase.FIRDatabaseQuery +import cocoapods.FirebaseDatabase.FIRDatabaseReference +import cocoapods.FirebaseDatabase.FIRTransactionResult +import dev.gitlive.firebase.DecodeSettings +import dev.gitlive.firebase.EncodeDecodeSettingsBuilder import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.database.ChildEvent.Type -import dev.gitlive.firebase.database.ChildEvent.Type.* +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.decode -import dev.gitlive.firebase.encode +import dev.gitlive.firebase.reencodeTransformation import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose @@ -22,11 +35,8 @@ import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.selects.select import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationStrategy import platform.Foundation.NSError import platform.Foundation.allObjects -import kotlin.collections.component1 -import kotlin.collections.component2 actual val Firebase.database by lazy { FirebaseDatabase(FIRDatabase.database()) } @@ -45,10 +55,10 @@ actual fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase = actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) { actual fun reference(path: String) = - DatabaseReference(ios.referenceWithPath(path), ios.persistenceEnabled) + DatabaseReference(NativeDatabaseReference(ios.referenceWithPath(path), ios.persistenceEnabled)) actual fun reference() = - DatabaseReference(ios.reference(), ios.persistenceEnabled) + DatabaseReference(NativeDatabaseReference(ios.reference(), ios.persistenceEnabled)) actual fun setPersistenceEnabled(enabled: Boolean) { ios.persistenceEnabled = enabled @@ -68,10 +78,20 @@ fun Type.toEventType() = when(this) { REMOVED -> FIRDataEventTypeChildRemoved } -actual open class Query internal constructor( +internal actual open class NativeQuery( open val ios: FIRDatabaseQuery, val persistenceEnabled: Boolean +) + +actual open class Query internal actual constructor( + nativeQuery: NativeQuery ) { + + internal constructor(ios: FIRDatabaseQuery, persistenceEnabled: Boolean) : this(NativeQuery(ios, persistenceEnabled)) + + open val ios: FIRDatabaseQuery = nativeQuery.ios + val persistenceEnabled: Boolean = nativeQuery.persistenceEnabled + actual fun orderByKey() = Query(ios.queryOrderedByKey(), persistenceEnabled) actual fun orderByValue() = Query(ios.queryOrderedByValue(), persistenceEnabled) @@ -127,42 +147,37 @@ actual open class Query internal constructor( override fun toString() = ios.toString() } -actual class DatabaseReference internal constructor( +@PublishedApi +internal actual class NativeDatabaseReference internal constructor( override val ios: FIRDatabaseReference, persistenceEnabled: Boolean -): Query(ios, persistenceEnabled) { +): NativeQuery(ios, persistenceEnabled) { actual val key get() = ios.key - actual fun child(path: String) = DatabaseReference(ios.child(path), persistenceEnabled) - - actual fun push() = DatabaseReference(ios.childByAutoId(), persistenceEnabled) - actual fun onDisconnect() = OnDisconnect(ios, persistenceEnabled) + actual fun child(path: String) = NativeDatabaseReference(ios.child(path), persistenceEnabled) - actual suspend inline fun setValue(value: T?, encodeDefaults: Boolean) { - ios.await(persistenceEnabled) { setValue(encode(value, encodeDefaults), it) } - } + actual fun push() = NativeDatabaseReference(ios.childByAutoId(), persistenceEnabled) + actual fun onDisconnect() = NativeOnDisconnect(ios, persistenceEnabled) - actual suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) { - ios.await(persistenceEnabled) { setValue(encode(strategy, value, encodeDefaults), it) } + actual suspend fun setValueEncoded(encodedValue: Any?) { + ios.await(persistenceEnabled) { setValue(encodedValue, it) } } @Suppress("UNCHECKED_CAST") - actual suspend fun updateChildren(update: Map, encodeDefaults: Boolean) { - ios.await(persistenceEnabled) { updateChildValues(encode(update, encodeDefaults) as Map, it) } + actual suspend fun updateEncodedChildren(encodedUpdate: Any?) { + ios.await(persistenceEnabled) { updateChildValues(encodedUpdate as Map, it) } } actual suspend fun removeValue() { ios.await(persistenceEnabled) { removeValueWithCompletionBlock(it) } } - actual suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot { + actual suspend fun runTransaction(strategy: KSerializer, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit, transactionUpdate: (currentData: T) -> T): DataSnapshot { val deferred = CompletableDeferred() ios.runTransactionBlock( block = { firMutableData -> - firMutableData?.value = firMutableData?.value?.let { - transactionUpdate(decode(strategy, it)) - } + firMutableData?.value = reencodeTransformation(strategy, firMutableData?.value, buildSettings, transactionUpdate) FIRTransactionResult.successWithValue(firMutableData!!) }, andCompletionBlock = { error, _, snapshot -> @@ -178,6 +193,8 @@ actual class DatabaseReference internal constructor( } } +val DatabaseReference.ios: FIRDatabaseReference get() = nativeReference.ios + @Suppress("UNCHECKED_CAST") actual class DataSnapshot internal constructor( val ios: FIRDataSnapshot, @@ -188,22 +205,23 @@ actual class DataSnapshot internal constructor( actual val key: String? get() = ios.key - actual val ref: DatabaseReference get() = DatabaseReference(ios.ref, persistenceEnabled) + actual val ref: DatabaseReference get() = DatabaseReference(NativeDatabaseReference(ios.ref, persistenceEnabled)) actual val value get() = ios.value actual inline fun value() = decode(value = ios.value) - actual fun value(strategy: DeserializationStrategy) = - decode(strategy, ios.value) + actual inline fun value(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + decode(strategy, ios.value, buildSettings) actual fun child(path: String) = DataSnapshot(ios.childSnapshotForPath(path), persistenceEnabled) actual val hasChildren get() = ios.hasChildren() actual val children: Iterable get() = ios.children.allObjects.map { DataSnapshot(it as FIRDataSnapshot, persistenceEnabled) } } -actual class OnDisconnect internal constructor( +@PublishedApi +internal actual class NativeOnDisconnect internal constructor( val ios: FIRDatabaseReference, val persistenceEnabled: Boolean ) { @@ -215,20 +233,19 @@ actual class OnDisconnect internal constructor( ios.await(persistenceEnabled) { cancelDisconnectOperationsWithCompletionBlock(it) } } - actual suspend inline fun setValue(value: T, encodeDefaults: Boolean) { - ios.await(persistenceEnabled) { onDisconnectSetValue(encode(value, encodeDefaults), it) } - } - - actual suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) { - ios.await(persistenceEnabled) { onDisconnectSetValue(encode(strategy, value, encodeDefaults), it) } + actual suspend fun setValue(encodedValue: Any?) { + ios.await(persistenceEnabled) { onDisconnectSetValue(encodedValue, it) } } @Suppress("UNCHECKED_CAST") - actual suspend fun updateChildren(update: Map, encodeDefaults: Boolean) { - ios.await(persistenceEnabled) { onDisconnectUpdateChildValues(update.mapValues { (_, it) -> encode(it, encodeDefaults) } as Map, it) } + actual suspend fun updateEncodedChildren(encodedUpdate: Map) { + ios.await(persistenceEnabled) { onDisconnectUpdateChildValues(encodedUpdate as Map, it) } } } +val OnDisconnect.ios: FIRDatabaseReference get() = native.ios +val OnDisconnect.persistenceEnabled get() = native.persistenceEnabled + actual class DatabaseException actual constructor(message: String?, cause: Throwable?) : RuntimeException(message, cause) private suspend inline fun T.awaitResult(whileOnline: Boolean, function: T.(callback: (NSError?, R?) -> Unit) -> Unit): R { diff --git a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt index d4d47b6be..cf0023d0e 100644 --- a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -4,18 +4,40 @@ package dev.gitlive.firebase.database -import dev.gitlive.firebase.* +import dev.gitlive.firebase.DecodeSettings +import dev.gitlive.firebase.EncodeDecodeSettingsBuilder +import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp -import dev.gitlive.firebase.database.externals.* -import kotlinx.coroutines.* +import dev.gitlive.firebase.database.externals.CancelCallback +import dev.gitlive.firebase.database.externals.ChangeSnapshotCallback +import dev.gitlive.firebase.database.externals.Database +import dev.gitlive.firebase.database.externals.child +import dev.gitlive.firebase.database.externals.connectDatabaseEmulator +import dev.gitlive.firebase.database.externals.enableLogging +import dev.gitlive.firebase.database.externals.getDatabase +import dev.gitlive.firebase.database.externals.onChildAdded +import dev.gitlive.firebase.database.externals.onChildChanged +import dev.gitlive.firebase.database.externals.onChildMoved +import dev.gitlive.firebase.database.externals.onChildRemoved +import dev.gitlive.firebase.database.externals.onDisconnect +import dev.gitlive.firebase.database.externals.onValue +import dev.gitlive.firebase.database.externals.push +import dev.gitlive.firebase.database.externals.query +import dev.gitlive.firebase.database.externals.ref +import dev.gitlive.firebase.database.externals.remove +import dev.gitlive.firebase.database.externals.set +import dev.gitlive.firebase.database.externals.update +import dev.gitlive.firebase.decode +import dev.gitlive.firebase.reencodeTransformation +import kotlinx.coroutines.asDeferred import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.selects.select import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationStrategy import kotlin.js.Promise import kotlin.js.json import dev.gitlive.firebase.database.externals.DataSnapshot as JsDataSnapshot @@ -29,6 +51,7 @@ import dev.gitlive.firebase.database.externals.limitToLast as jsLimitToLast import dev.gitlive.firebase.database.externals.orderByChild as jsOrderByChild import dev.gitlive.firebase.database.externals.orderByKey as jsOrderByKey import dev.gitlive.firebase.database.externals.orderByValue as jsOrderByValue +import dev.gitlive.firebase.database.externals.runTransaction as jsRunTransaction import dev.gitlive.firebase.database.externals.startAt as jsStartAt actual val Firebase.database @@ -44,18 +67,27 @@ actual fun Firebase.database(app: FirebaseApp, url: String) = rethrow { FirebaseDatabase(getDatabase(app = app.js, url = url)) } actual class FirebaseDatabase internal constructor(val js: Database) { - actual fun reference(path: String) = rethrow { DatabaseReference(ref(js, path), js) } - actual fun reference() = rethrow { DatabaseReference(ref(js), js) } + actual fun reference(path: String) = rethrow { DatabaseReference(NativeDatabaseReference(ref(js, path), js)) } + actual fun reference() = rethrow { DatabaseReference(NativeDatabaseReference(ref(js), js)) } actual fun setPersistenceEnabled(enabled: Boolean) {} actual fun setLoggingEnabled(enabled: Boolean) = rethrow { enableLogging(enabled) } actual fun useEmulator(host: String, port: Int) = rethrow { connectDatabaseEmulator(js, host, port) } } -actual open class Query internal constructor( +internal actual open class NativeQuery( open val js: JsQuery, val database: Database +) + +actual open class Query internal actual constructor( + nativeQuery: NativeQuery ) { + internal constructor(js: JsQuery, database: Database) : this(NativeQuery(js, database)) + + open val js: JsQuery = nativeQuery.js + val database: Database = nativeQuery.database + actual fun orderByKey() = Query(query(js, jsOrderByKey()), database) actual fun orderByValue() = Query(query(js, jsOrderByValue()), database) actual fun orderByChild(path: String) = Query(query(js, jsOrderByChild(path)), database) @@ -78,7 +110,7 @@ actual open class Query internal constructor( val callback: ChangeSnapshotCallback = { snapshot, previousChildName -> trySend( ChildEvent( - DataSnapshot(snapshot, database), + DataSnapshot(snapshot, database), type, previousChildName ) @@ -126,38 +158,33 @@ actual open class Query internal constructor( } -actual class DatabaseReference internal constructor( +@PublishedApi +internal actual class NativeDatabaseReference internal constructor( override val js: JsDatabaseReference, database: Database -) : Query(js, database) { +) : NativeQuery(js, database) { actual val key get() = rethrow { js.key } - actual fun push() = rethrow { DatabaseReference(push(js), database) } - actual fun child(path: String) = rethrow { DatabaseReference(child(js, path), database) } - - actual fun onDisconnect() = rethrow { OnDisconnect(onDisconnect(js), database) } + actual fun push() = rethrow { NativeDatabaseReference(push(js), database) } + actual fun child(path: String) = rethrow { NativeDatabaseReference(child(js, path), database) } - actual suspend fun updateChildren(update: Map, encodeDefaults: Boolean) = - rethrow { update(js, encode(update, encodeDefaults) ?: json()).awaitWhileOnline(database) } + actual fun onDisconnect() = rethrow { NativeOnDisconnect(onDisconnect(js), database) } actual suspend fun removeValue() = rethrow { remove(js).awaitWhileOnline(database) } - actual suspend inline fun setValue(value: T?, encodeDefaults: Boolean) = rethrow { - set(js, encode(value, encodeDefaults)).awaitWhileOnline(database) + actual suspend fun setValueEncoded(encodedValue: Any?) = rethrow { + set(js, encodedValue).awaitWhileOnline(database) } - actual suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) = - rethrow { set(js, encode(strategy, value, encodeDefaults)).awaitWhileOnline(database) } + actual suspend fun updateEncodedChildren(encodedUpdate: Any?) = + rethrow { update(js, encodedUpdate ?: json()).awaitWhileOnline(database) } - actual suspend fun runTransaction(strategy: KSerializer, transactionUpdate: (currentData: T) -> T): DataSnapshot = - rethrow { - val result = runTransaction( - js, - transactionUpdate, - ).awaitWhileOnline(database) - DataSnapshot(result.snapshot, database) - } + actual suspend fun runTransaction(strategy: KSerializer, buildSettings: EncodeDecodeSettingsBuilder.() -> Unit, transactionUpdate: (currentData: T) -> T): DataSnapshot { + return DataSnapshot(jsRunTransaction(js, transactionUpdate = { currentData -> + reencodeTransformation(strategy, currentData ?: json(), buildSettings, transactionUpdate) + }).awaitWhileOnline(database).snapshot, database) + } } actual class DataSnapshot internal constructor( @@ -172,8 +199,8 @@ actual class DataSnapshot internal constructor( actual inline fun value() = rethrow { decode(value = js.`val`()) } - actual fun value(strategy: DeserializationStrategy) = - rethrow { decode(strategy, js.`val`()) } + actual inline fun value(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + rethrow { decode(strategy, js.`val`(), buildSettings) } actual val exists get() = rethrow { js.exists() } actual val key get() = rethrow { js.key } @@ -185,11 +212,12 @@ actual class DataSnapshot internal constructor( } } actual val ref: DatabaseReference - get() = DatabaseReference(js.ref, database) + get() = DatabaseReference(NativeDatabaseReference(js.ref, database)) } -actual class OnDisconnect internal constructor( +@PublishedApi +internal actual class NativeOnDisconnect internal constructor( val js: JsOnDisconnect, val database: Database ) { @@ -197,16 +225,17 @@ actual class OnDisconnect internal constructor( actual suspend fun removeValue() = rethrow { js.remove().awaitWhileOnline(database) } actual suspend fun cancel() = rethrow { js.cancel().awaitWhileOnline(database) } - actual suspend fun updateChildren(update: Map, encodeDefaults: Boolean) = - rethrow { js.update(encode(update, encodeDefaults) ?: json()).awaitWhileOnline(database) } + actual suspend fun setValue(encodedValue: Any?) = + rethrow { js.set(encodedValue).awaitWhileOnline(database) } - actual suspend inline fun setValue(value: T, encodeDefaults: Boolean) = - rethrow { js.set(encode(value, encodeDefaults)).awaitWhileOnline(database) } + actual suspend fun updateEncodedChildren(encodedUpdate: Map) = + rethrow { js.update(encodedUpdate).awaitWhileOnline(database) } - actual suspend fun setValue(strategy: SerializationStrategy, value: T, encodeDefaults: Boolean) = - rethrow { js.set(encode(strategy, value, encodeDefaults)).awaitWhileOnline(database) } } +val OnDisconnect.js get() = native.js +val OnDisconnect.database get() = native.database + actual class DatabaseException actual constructor(message: String?, cause: Throwable?) : RuntimeException(message, cause) { constructor(error: dynamic) : this("${error.code ?: "UNKNOWN"}: ${error.message}", error.unsafeCast()) } diff --git a/firebase-firestore/package.json b/firebase-firestore/package.json index fa2b48a23..eed05cf10 100644 --- a/firebase-firestore/package.json +++ b/firebase-firestore/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-firestore", - "version": "1.11.0", + "version": "1.11.1", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-firestore.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-firestore/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt b/firebase-firestore/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt new file mode 100644 index 000000000..1f70d3731 --- /dev/null +++ b/firebase-firestore/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt @@ -0,0 +1,6 @@ +package dev.gitlive.firebase.firestore + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreJs +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreForAndroidUnitTest diff --git a/firebase-firestore/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index cf185f630..8c6035f28 100644 --- a/firebase-firestore/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -11,5 +11,5 @@ actual val emulatorHost: String = "10.0.2.2" actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) -actual annotation class IgnoreForAndroidUnitTest +actual fun encodedAsMap(encoded: Any?): Map = encoded as Map +actual fun Map.asEncoded(): Any = this diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt new file mode 100644 index 000000000..f5f2cee34 --- /dev/null +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt @@ -0,0 +1,26 @@ +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.Serializable + +/** Represents a platform specific Firebase FieldValue. */ +typealias NativeFieldValue = com.google.firebase.firestore.FieldValue + +/** Represents a Firebase FieldValue. */ +@Serializable(with = FieldValueSerializer::class) +actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { + init { + require(nativeValue is NativeFieldValue) + } + override fun equals(other: Any?): Boolean = + this === other || other is FieldValue && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual val serverTimestamp: FieldValue get() = FieldValue(NativeFieldValue.serverTimestamp()) + actual val delete: FieldValue get() = FieldValue(NativeFieldValue.delete()) + actual fun increment(value: Int): FieldValue = FieldValue(NativeFieldValue.increment(value.toDouble())) + actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayUnion(*elements)) + actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayRemove(*elements)) + } +} diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt new file mode 100644 index 000000000..84445fb4d --- /dev/null +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt @@ -0,0 +1,10 @@ +package dev.gitlive.firebase.firestore + +@PublishedApi +internal actual fun isSpecialValue(value: Any) = when(value) { + is NativeFieldValue, + is NativeGeoPoint, + is NativeTimestamp, + is NativeDocumentReferenceType -> true + else -> false +} diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 99d5ca298..242618671 100644 --- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -5,20 +5,18 @@ @file:JvmName("android") package dev.gitlive.firebase.firestore -import com.google.firebase.firestore.* -import dev.gitlive.firebase.* +import com.google.firebase.firestore.MetadataChanges +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await -import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationStrategy - -import com.google.firebase.firestore.Query as AndroidQuery import com.google.firebase.firestore.FieldPath as AndroidFieldPath import com.google.firebase.firestore.Filter as AndroidFilter +import com.google.firebase.firestore.Query as AndroidQuery actual val Firebase.firestore get() = FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance()) @@ -26,35 +24,21 @@ actual val Firebase.firestore get() = actual fun Firebase.firestore(app: FirebaseApp) = FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance(app.android)) -/** Helper method to perform an update operation. */ -@JvmName("performUpdateFields") -private fun performUpdate( - fieldsAndValues: Array>, - update: (String, Any?, Array) -> R -) = performUpdate(fieldsAndValues, { it }, { encode(it, true) }, update) - -/** Helper method to perform an update operation. */ -@JvmName("performUpdateFieldPaths") -private fun performUpdate( - fieldsAndValues: Array>, - update: (com.google.firebase.firestore.FieldPath, Any?, Array) -> R -) = performUpdate(fieldsAndValues, { it.android }, { encode(it, true) }, update) - actual class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) { - actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) + actual fun collection(collectionPath: String) = CollectionReference(NativeCollectionReference(android.collection(collectionPath))) - actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId)) + actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId).native) - actual fun document(documentPath: String) = DocumentReference(android.document(documentPath)) + actual fun document(documentPath: String) = DocumentReference(NativeDocumentReference(android.document(documentPath))) - actual fun batch() = WriteBatch(android.batch()) + actual fun batch() = WriteBatch(NativeWriteBatch(android.batch())) actual fun setLoggingEnabled(loggingEnabled: Boolean) = com.google.firebase.firestore.FirebaseFirestore.setLoggingEnabled(loggingEnabled) - actual suspend fun runTransaction(func: suspend Transaction.() -> T) = - android.runTransaction { runBlocking { Transaction(it).func() } }.await() + actual suspend fun runTransaction(func: suspend Transaction.() -> T): T = + android.runTransaction { runBlocking { Transaction(NativeTransaction(it)).func() } }.await() actual suspend fun clearPersistence() = android.clearPersistence().await().run { } @@ -68,12 +52,12 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) { android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder().also { builder -> - persistenceEnabled?.let { builder.setPersistenceEnabled(it) } - sslEnabled?.let { builder.isSslEnabled = it } - host?.let { builder.host = it } - cacheSizeBytes?.let { builder.cacheSizeBytes = it } - }.build() - } + persistenceEnabled?.let { builder.setPersistenceEnabled(it) } + sslEnabled?.let { builder.isSslEnabled = it } + host?.let { builder.host = it } + cacheSizeBytes?.let { builder.cacheSizeBytes = it } + }.build() + } actual suspend fun disableNetwork() = android.disableNetwork().await().run { } @@ -83,214 +67,172 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba } -actual class WriteBatch(val android: com.google.firebase.firestore.WriteBatch) { - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { - true -> android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.merge()) - false -> android.set(documentRef.android, encode(data, encodeDefaults)!!) - }.let { this } - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) - .let { this } - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) - .let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { - true -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) - false -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!) - }.let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) - .let { this } +internal val SetOptions.android: com.google.firebase.firestore.SetOptions? get() = when (this) { + is SetOptions.Merge -> com.google.firebase.firestore.SetOptions.merge() + is SetOptions.Overwrite -> null + is SetOptions.MergeFields -> com.google.firebase.firestore.SetOptions.mergeFields(fields) + is SetOptions.MergeFieldPaths -> com.google.firebase.firestore.SetOptions.mergeFieldPaths(encodedFieldPaths) +} - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) - .let { this } +@PublishedApi +internal actual class NativeWriteBatch(val android: com.google.firebase.firestore.WriteBatch) { - @Suppress("UNCHECKED_CAST") - actual inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) = - android.update(documentRef.android, encode(data, encodeDefaults) as Map).let { this } + actual fun setEncoded( + documentRef: DocumentReference, + encodedData: Any, + setOptions: SetOptions + ): NativeWriteBatch = (setOptions.android?.let { + android.set(documentRef.android, encodedData, it) + } ?: android.set(documentRef.android, encodedData)).let { + this + } @Suppress("UNCHECKED_CAST") - actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - android.update(documentRef.android, encode(strategy, data, encodeDefaults) as Map).let { this } + actual fun updateEncoded(documentRef: DocumentReference, encodedData: Any) = android.update(documentRef.android, encodedData as Map).let { this } - @JvmName("updateFields") - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - android.update(documentRef.android, field, value, *moreFieldsAndValues) - }.let { this } + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ) = encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } - @JvmName("updateFieldPaths") - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - android.update(documentRef.android, field, value, *moreFieldsAndValues) - }.let { this } + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ) = encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } actual fun delete(documentRef: DocumentReference) = android.delete(documentRef.android).let { this } - actual suspend fun commit() = android.commit().await().run { Unit } - + actual suspend fun commit() { + android.commit().await() + } } -actual class Transaction(val android: com.google.firebase.firestore.Transaction) { +val WriteBatch.android get() = native.android - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, merge: Boolean) = when(merge) { - true -> android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.merge()) - false -> android.set(documentRef.android, encode(data, encodeDefaults)!!) - }.let { this } - - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFields: String) = - android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) - .let { this } +@PublishedApi +internal actual class NativeTransaction(val android: com.google.firebase.firestore.Transaction) { - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) - .let { this } - - actual fun set( + actual fun setEncoded( documentRef: DocumentReference, - strategy: SerializationStrategy, - data: T, - encodeDefaults: Boolean, - merge: Boolean - ) = when(merge) { - true -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) - false -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!) - }.let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) - .let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) - .let { this } - - @Suppress("UNCHECKED_CAST") - actual fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean) = - android.update(documentRef.android, encode(data, encodeDefaults) as Map).let { this } + encodedData: Any, + setOptions: SetOptions + ): NativeTransaction { + setOptions.android?.let { + android.set(documentRef.android, encodedData, it) + } ?: android.set(documentRef.android, encodedData) + return this + } @Suppress("UNCHECKED_CAST") - actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - android.update(documentRef.android, encode(strategy, data, encodeDefaults) as Map).let { this } + actual fun updateEncoded(documentRef: DocumentReference, encodedData: Any) = android.update(documentRef.android, encodedData as Map).let { this } - @JvmName("updateFields") - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - android.update(documentRef.android, field, value, *moreFieldsAndValues) - }.let { this } + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ) = encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } - @JvmName("updateFieldPaths") - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - android.update(documentRef.android, field, value, *moreFieldsAndValues) - }.let { this } + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ) = encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } actual fun delete(documentRef: DocumentReference) = android.delete(documentRef.android).let { this } actual suspend fun get(documentRef: DocumentReference) = - DocumentSnapshot(android.get(documentRef.android)) + NativeDocumentSnapshot(android.get(documentRef.android)) } +val Transaction.android get() = native.android + /** A class representing a platform specific Firebase DocumentReference. */ -actual typealias NativeDocumentReference = com.google.firebase.firestore.DocumentReference +actual typealias NativeDocumentReferenceType = com.google.firebase.firestore.DocumentReference -@Serializable(with = DocumentReferenceSerializer::class) -actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) { - val android: NativeDocumentReference by ::nativeValue +@PublishedApi +internal actual class NativeDocumentReference actual constructor(actual val nativeValue: NativeDocumentReferenceType) { + val android: NativeDocumentReferenceType by ::nativeValue actual val id: String get() = android.id actual val path: String get() = android.path - actual val parent: CollectionReference - get() = CollectionReference(android.parent) - - actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { - true -> android.set(encode(data, encodeDefaults)!!, SetOptions.merge()) - false -> android.set(encode(data, encodeDefaults)!!) - }.await().run { Unit } - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - android.set(encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) - .await().run { Unit } - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - android.set(encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) - .await().run { Unit } + actual val parent: NativeCollectionReference + get() = NativeCollectionReference(android.parent) - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { - true -> android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) - false -> android.set(encode(strategy, data, encodeDefaults)!!) - }.await().run { Unit } + actual fun collection(collectionPath: String) = NativeCollectionReference(android.collection(collectionPath)) - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) - .await().run { Unit } - - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) - .await().run { Unit } + actual suspend fun get() = + NativeDocumentSnapshot(android.get().await()) - @Suppress("UNCHECKED_CAST") - actual suspend inline fun update(data: T, encodeDefaults: Boolean) = - android.update(encode(data, encodeDefaults) as Map).await().run { Unit } + actual suspend fun setEncoded(encodedData: Any, setOptions: SetOptions) { + val task = (setOptions.android?.let { + android.set(encodedData, it) + } ?: android.set(encodedData)) + task.await() + } @Suppress("UNCHECKED_CAST") - actual suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - android.update(encode(strategy, data, encodeDefaults) as Map).await().run { Unit } - - @JvmName("updateFields") - actual suspend fun update(vararg fieldsAndValues: Pair) = - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - android.update(field, value, *moreFieldsAndValues) - }?.await() - .run { Unit } + actual suspend fun updateEncoded(encodedData: Any) { + android.update(encodedData as Map).await() + } - @JvmName("updateFieldPaths") - actual suspend fun update(vararg fieldsAndValues: Pair) = - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - android.update(field, value, *moreFieldsAndValues) + actual suspend fun updateEncodedFieldsAndValues(encodedFieldsAndValues: List>) { + encodedFieldsAndValues.takeUnless { encodedFieldsAndValues.isEmpty() }?.let { + android.update(encodedFieldsAndValues.toMap()) }?.await() - .run { Unit } + } - actual suspend fun delete() = - android.delete().await().run { Unit } + actual suspend fun updateEncodedFieldPathsAndValues(encodedFieldsAndValues: List>) { + encodedFieldsAndValues.takeUnless { encodedFieldsAndValues.isEmpty() } + ?.performUpdate { field, value, moreFieldsAndValues -> + android.update(field, value, *moreFieldsAndValues) + }?.await() + } - actual suspend fun get() = - DocumentSnapshot(android.get().await()) + actual suspend fun delete() { + android.delete().await() + } - actual val snapshots: Flow get() = snapshots() + actual val snapshots: Flow get() = snapshots() actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE val listener = android.addSnapshotListener(metadataChanges) { snapshot, exception -> - snapshot?.let { trySend(DocumentSnapshot(snapshot)) } + snapshot?.let { trySend(NativeDocumentSnapshot(snapshot)) } exception?.let { close(exception) } } awaitClose { listener.remove() } } + override fun equals(other: Any?): Boolean = - this === other || other is DocumentReference && nativeValue == other.nativeValue + this === other || other is NativeDocumentReference && nativeValue == other.nativeValue override fun hashCode(): Int = nativeValue.hashCode() override fun toString(): String = nativeValue.toString() } -actual open class Query(open val android: AndroidQuery) { +val DocumentReference.android get() = native.android + +@PublishedApi +internal actual open class NativeQuery(open val android: AndroidQuery) +internal val AndroidQuery.native get() = NativeQuery(this) + +actual open class Query internal actual constructor(nativeQuery: NativeQuery) { + + open val android = nativeQuery.android actual suspend fun get() = QuerySnapshot(android.get().await()) - actual fun limit(limit: Number) = Query(android.limit(limit.toLong())) + actual fun limit(limit: Number) = Query(NativeQuery(android.limit(limit.toLong()))) actual val snapshots get() = callbackFlow { val listener = android.addSnapshotListener { snapshot, exception -> @@ -310,7 +252,7 @@ actual open class Query(open val android: AndroidQuery) { } internal actual fun where(filter: Filter) = Query( - android.where(filter.toAndroidFilter()) + android.where(filter.toAndroidFilter()).native ) private fun Filter.toAndroidFilter(): AndroidFilter = when (this) { @@ -376,45 +318,42 @@ actual open class Query(open val android: AndroidQuery) { } } - internal actual fun _orderBy(field: String, direction: Direction) = Query(android.orderBy(field, direction)) - internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(android.orderBy(field.android, direction)) + internal actual fun _orderBy(field: String, direction: Direction) = Query(android.orderBy(field, direction).native) + internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(android.orderBy(field.android, direction).native) - internal actual fun _startAfter(document: DocumentSnapshot) = Query(android.startAfter(document.android)) - internal actual fun _startAfter(vararg fieldValues: Any) = Query(android.startAfter(*fieldValues)) - internal actual fun _startAt(document: DocumentSnapshot) = Query(android.startAt(document.android)) - internal actual fun _startAt(vararg fieldValues: Any) = Query(android.startAt(*fieldValues)) + internal actual fun _startAfter(document: DocumentSnapshot) = Query(android.startAfter(document.android).native) + internal actual fun _startAfter(vararg fieldValues: Any) = Query(android.startAfter(*fieldValues).native) + internal actual fun _startAt(document: DocumentSnapshot) = Query(android.startAt(document.android).native) + internal actual fun _startAt(vararg fieldValues: Any) = Query(android.startAt(*fieldValues).native) - internal actual fun _endBefore(document: DocumentSnapshot) = Query(android.endBefore(document.android)) - internal actual fun _endBefore(vararg fieldValues: Any) = Query(android.endBefore(*fieldValues)) - internal actual fun _endAt(document: DocumentSnapshot) = Query(android.endAt(document.android)) - internal actual fun _endAt(vararg fieldValues: Any) = Query(android.endAt(*fieldValues)) + internal actual fun _endBefore(document: DocumentSnapshot) = Query(android.endBefore(document.android).native) + internal actual fun _endBefore(vararg fieldValues: Any) = Query(android.endBefore(*fieldValues).native) + internal actual fun _endAt(document: DocumentSnapshot) = Query(android.endAt(document.android).native) + internal actual fun _endAt(vararg fieldValues: Any) = Query(android.endAt(*fieldValues).native) } actual typealias Direction = com.google.firebase.firestore.Query.Direction actual typealias ChangeType = com.google.firebase.firestore.DocumentChange.Type -actual class CollectionReference(override val android: com.google.firebase.firestore.CollectionReference) : Query(android) { +@PublishedApi +internal actual class NativeCollectionReference(override val android: com.google.firebase.firestore.CollectionReference) : NativeQuery(android) { actual val path: String get() = android.path - actual val document: DocumentReference - get() = DocumentReference(android.document()) - - actual val parent: DocumentReference? - get() = android.parent?.let{DocumentReference(it)} + actual val document: NativeDocumentReference + get() = NativeDocumentReference(android.document()) - actual fun document(documentPath: String) = DocumentReference(android.document(documentPath)) + actual val parent: NativeDocumentReference? + get() = android.parent?.let{ NativeDocumentReference(it) } - actual suspend inline fun add(data: T, encodeDefaults: Boolean) = - DocumentReference(android.add(encode(data, encodeDefaults)!!).await()) + actual fun document(documentPath: String) = NativeDocumentReference(android.document(documentPath)) - actual suspend fun add(data: T, strategy: SerializationStrategy, encodeDefaults: Boolean) = - DocumentReference(android.add(encode(strategy, data, encodeDefaults)!!).await()) - actual suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - DocumentReference(android.add(encode(strategy, data, encodeDefaults)!!).await()) + actual suspend fun addEncoded(data: Any) = NativeDocumentReference(android.add(data).await()) } +val CollectionReference.android get() = native.android + actual typealias FirebaseFirestoreException = com.google.firebase.firestore.FirebaseFirestoreException actual val FirebaseFirestoreException.code: FirestoreExceptionCode get() = code @@ -423,7 +362,7 @@ actual typealias FirestoreExceptionCode = com.google.firebase.firestore.Firebase actual class QuerySnapshot(val android: com.google.firebase.firestore.QuerySnapshot) { actual val documents - get() = android.documents.map { DocumentSnapshot(it) } + get() = android.documents.map { DocumentSnapshot(NativeDocumentSnapshot(it)) } actual val documentChanges get() = android.documentChanges.map { DocumentChange(it) } actual val metadata: SnapshotMetadata get() = SnapshotMetadata(android.metadata) @@ -431,7 +370,7 @@ actual class QuerySnapshot(val android: com.google.firebase.firestore.QuerySnaps actual class DocumentChange(val android: com.google.firebase.firestore.DocumentChange) { actual val document: DocumentSnapshot - get() = DocumentSnapshot(android.document) + get() = DocumentSnapshot(NativeDocumentSnapshot(android.document)) actual val newIndex: Int get() = android.newIndex actual val oldIndex: Int @@ -440,23 +379,14 @@ actual class DocumentChange(val android: com.google.firebase.firestore.DocumentC get() = android.type } -@Suppress("UNCHECKED_CAST") -actual class DocumentSnapshot(val android: com.google.firebase.firestore.DocumentSnapshot) { +@PublishedApi +internal actual class NativeDocumentSnapshot(val android: com.google.firebase.firestore.DocumentSnapshot) { actual val id get() = android.id - actual val reference get() = DocumentReference(android.reference) - - actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T = - decode(value = android.getData(serverTimestampBehavior.toAndroid())) - - actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = - decode(strategy, android.getData(serverTimestampBehavior.toAndroid())) + actual val reference get() = NativeDocumentReference(android.reference) - actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T = - decode(value = android.get(field, serverTimestampBehavior.toAndroid())) - - actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = - decode(strategy, android.get(field, serverTimestampBehavior.toAndroid())) + actual fun getEncoded(field: String, serverTimestampBehavior: ServerTimestampBehavior): Any? = android.get(field, serverTimestampBehavior.toAndroid()) + actual fun encodedData(serverTimestampBehavior: ServerTimestampBehavior): Any? = android.getData(serverTimestampBehavior.toAndroid()) actual fun contains(field: String) = android.contains(field) @@ -471,39 +401,25 @@ actual class DocumentSnapshot(val android: com.google.firebase.firestore.Documen } } +val DocumentSnapshot.android get() = native.android + actual class SnapshotMetadata(val android: com.google.firebase.firestore.SnapshotMetadata) { actual val hasPendingWrites: Boolean get() = android.hasPendingWrites() - actual val isFromCache: Boolean get() = android.isFromCache() + actual val isFromCache: Boolean get() = android.isFromCache } actual class FieldPath private constructor(val android: com.google.firebase.firestore.FieldPath) { - actual constructor(vararg fieldNames: String) : this(com.google.firebase.firestore.FieldPath.of(*fieldNames)) - actual val documentId: FieldPath get() = FieldPath(com.google.firebase.firestore.FieldPath.documentId()) + actual constructor(vararg fieldNames: String) : this( + com.google.firebase.firestore.FieldPath.of( + *fieldNames + ) + ) + actual val documentId: FieldPath get() = FieldPath(com.google.firebase.firestore.FieldPath.documentId()) + actual val encoded: EncodedFieldPath = android override fun equals(other: Any?): Boolean = other is FieldPath && android == other.android override fun hashCode(): Int = android.hashCode() override fun toString(): String = android.toString() } -/** Represents a platform specific Firebase FieldValue. */ -private typealias NativeFieldValue = com.google.firebase.firestore.FieldValue - -/** Represents a Firebase FieldValue. */ -@Serializable(with = FieldValueSerializer::class) -actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { - init { - require(nativeValue is NativeFieldValue) - } - override fun equals(other: Any?): Boolean = - this === other || other is FieldValue && nativeValue == other.nativeValue - override fun hashCode(): Int = nativeValue.hashCode() - override fun toString(): String = nativeValue.toString() - - actual companion object { - actual val serverTimestamp: FieldValue get() = FieldValue(NativeFieldValue.serverTimestamp()) - actual val delete: FieldValue get() = FieldValue(NativeFieldValue.delete()) - actual fun increment(value: Int): FieldValue = FieldValue(NativeFieldValue.increment(value.toDouble())) - actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayUnion(*elements)) - actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayRemove(*elements)) - } -} +actual typealias EncodedFieldPath = com.google.firebase.firestore.FieldPath diff --git a/firebase-firestore/src/androidUnitTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt b/firebase-firestore/src/androidUnitTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt new file mode 100644 index 000000000..6deceefb3 --- /dev/null +++ b/firebase-firestore/src/androidUnitTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt @@ -0,0 +1,7 @@ +package dev.gitlive.firebase.firestore + +import org.junit.Ignore + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreJs +actual typealias IgnoreForAndroidUnitTest = Ignore diff --git a/firebase-firestore/src/androidUnitTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidUnitTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index da0c6a584..65d1a0bb7 100644 --- a/firebase-firestore/src/androidUnitTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidUnitTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -5,15 +5,9 @@ @file:JvmName("tests") package dev.gitlive.firebase.firestore -import kotlinx.coroutines.CoroutineScope -import org.junit.Ignore - actual val emulatorHost: String = "10.0.2.2" actual val context: Any = "" -// Tests are to be run on AndroidInstrumentedTests. -// Kotlin 1.8 does not allow us to remove the commonTest dependency from AndroidUnitTest -// Therefore we just wont run them -// Kotlin 1.9 will introduce methods for disabling tests properly -actual typealias IgnoreForAndroidUnitTest = Ignore +actual fun encodedAsMap(encoded: Any?): Map = encoded as Map +actual fun Map.asEncoded(): Any = this diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceSerializer.kt new file mode 100644 index 000000000..0f2f8fe30 --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceSerializer.kt @@ -0,0 +1,20 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.FirebaseEncoder +import dev.gitlive.firebase.SpecialValueSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException + +/** + * A serializer for [DocumentReference]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. + */ +object DocumentReferenceSerializer : KSerializer by SpecialValueSerializer( + serialName = "DocumentReference", + toNativeValue = { it.native.nativeValue }, + fromNativeValue = { value -> + when (value) { + is NativeDocumentReferenceType -> DocumentReference(NativeDocumentReference(value)) + else -> throw SerializationException("Cannot deserialize $value") + } + } +) diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt new file mode 100644 index 000000000..a091c4c05 --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt @@ -0,0 +1,17 @@ +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.Serializable + +/** Represents a Firebase FieldValue. */ +@Serializable(with = FieldValueSerializer::class) +expect class FieldValue internal constructor(nativeValue: Any) { + internal val nativeValue: Any + + companion object { + val serverTimestamp: FieldValue + val delete: FieldValue + fun increment(value: Int): FieldValue + fun arrayUnion(vararg elements: Any): FieldValue + fun arrayRemove(vararg elements: Any): FieldValue + } +} diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FieldValueSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FieldValueSerializer.kt new file mode 100644 index 000000000..2dc95492f --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FieldValueSerializer.kt @@ -0,0 +1,15 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.FirebaseEncoder +import dev.gitlive.firebase.SpecialValueSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException + +/** A serializer for [FieldValue]. Must be used in conjunction with [FirebaseEncoder]. */ +object FieldValueSerializer : KSerializer by SpecialValueSerializer( + serialName = "FieldValue", + toNativeValue = FieldValue::nativeValue, + fromNativeValue = { raw -> + raw?.let(::FieldValue) ?: throw SerializationException("Cannot deserialize $raw") + } +) diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt index 3ef3a7ed3..77093b9cb 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt @@ -1,9 +1,6 @@ package dev.gitlive.firebase.firestore -import dev.gitlive.firebase.SpecialValueSerializer -import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException /** A class representing a platform specific Firebase GeoPoint. */ expect class NativeGeoPoint @@ -16,15 +13,3 @@ expect class GeoPoint internal constructor(nativeValue: NativeGeoPoint) { val longitude: Double internal val nativeValue: NativeGeoPoint } - -/** Serializer for [GeoPoint]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */ -object GeoPointSerializer : KSerializer by SpecialValueSerializer( - serialName = "GeoPoint", - toNativeValue = GeoPoint::nativeValue, - fromNativeValue = { value -> - when (value) { - is NativeGeoPoint -> GeoPoint(value) - else -> throw SerializationException("Cannot deserialize $value") - } - } -) diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPointSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPointSerializer.kt new file mode 100644 index 000000000..221456628 --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPointSerializer.kt @@ -0,0 +1,17 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.SpecialValueSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException + +/** Serializer for [GeoPoint]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */ +object GeoPointSerializer : KSerializer by SpecialValueSerializer( + serialName = "GeoPoint", + toNativeValue = GeoPoint::nativeValue, + fromNativeValue = { value -> + when (value) { + is NativeGeoPoint -> GeoPoint(value) + else -> throw SerializationException("Cannot deserialize $value") + } + } +) \ No newline at end of file diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt index 8a7b02b5c..c6df088af 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt @@ -1,12 +1,6 @@ package dev.gitlive.firebase.firestore -import dev.gitlive.firebase.FirebaseDecoder -import dev.gitlive.firebase.FirebaseEncoder -import dev.gitlive.firebase.SpecialValueSerializer -import dev.gitlive.firebase.firestore.DoubleAsTimestampSerializer.serverTimestamp -import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds @@ -46,67 +40,3 @@ fun Timestamp.toDuration(): Duration = seconds.seconds + nanoseconds.nanoseconds fun Timestamp.Companion.fromMilliseconds(milliseconds: Double): Timestamp = fromDuration(milliseconds.milliseconds) fun Timestamp.toMilliseconds(): Double = toDuration().toDouble(DurationUnit.MILLISECONDS) - -/** A serializer for [BaseTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */ -object BaseTimestampSerializer : KSerializer by SpecialValueSerializer( - serialName = "Timestamp", - toNativeValue = { value -> - when (value) { - Timestamp.ServerTimestamp -> FieldValue.serverTimestamp.nativeValue - is Timestamp -> value.nativeValue - else -> throw SerializationException("Cannot serialize $value") - } - }, - fromNativeValue = { value -> - when (value) { - is NativeTimestamp -> Timestamp(value) - FieldValue.serverTimestamp.nativeValue -> Timestamp.ServerTimestamp - else -> throw SerializationException("Cannot deserialize $value") - } - } -) - -/** A serializer for [Timestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */ -object TimestampSerializer : KSerializer by SpecialValueSerializer( - serialName = "Timestamp", - toNativeValue = Timestamp::nativeValue, - fromNativeValue = { value -> - when (value) { - is NativeTimestamp -> Timestamp(value) - else -> throw SerializationException("Cannot deserialize $value") - } - } -) - -/** A serializer for [Timestamp.ServerTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */ -object ServerTimestampSerializer : KSerializer by SpecialValueSerializer( - serialName = "Timestamp", - toNativeValue = { FieldValue.serverTimestamp.nativeValue }, - fromNativeValue = { value -> - when (value) { - FieldValue.serverTimestamp.nativeValue -> Timestamp.ServerTimestamp - else -> throw SerializationException("Cannot deserialize $value") - } - } -) - -/** A serializer for a Double field which is stored as a Timestamp. */ -object DoubleAsTimestampSerializer : KSerializer by SpecialValueSerializer( - serialName = "Timestamp", - toNativeValue = { value -> - when(value) { - serverTimestamp -> FieldValue.serverTimestamp.nativeValue - else -> Timestamp.fromMilliseconds(value).nativeValue - } - }, - fromNativeValue = { value -> - when(value) { - FieldValue.serverTimestamp.nativeValue -> serverTimestamp - is NativeTimestamp -> Timestamp(value).toMilliseconds() - is Double -> value - else -> throw SerializationException("Cannot deserialize $value") - } - } -) { - const val serverTimestamp = Double.POSITIVE_INFINITY -} diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/TimestampSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/TimestampSerializer.kt new file mode 100644 index 000000000..92fe32f17 --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/TimestampSerializer.kt @@ -0,0 +1,71 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.SpecialValueSerializer +import dev.gitlive.firebase.firestore.* +import dev.gitlive.firebase.firestore.DoubleAsTimestampSerializer.serverTimestamp +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException + +/** A serializer for [BaseTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */ +object BaseTimestampSerializer : KSerializer by SpecialValueSerializer( + serialName = "Timestamp", + toNativeValue = { value -> + when (value) { + Timestamp.ServerTimestamp -> FieldValue.serverTimestamp.nativeValue + is Timestamp -> value.nativeValue + else -> throw SerializationException("Cannot serialize $value") + } + }, + fromNativeValue = { value -> + when (value) { + is NativeTimestamp -> Timestamp(value) + FieldValue.serverTimestamp.nativeValue -> Timestamp.ServerTimestamp + else -> throw SerializationException("Cannot deserialize $value") + } + } +) + +/** A serializer for [Timestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */ +object TimestampSerializer : KSerializer by SpecialValueSerializer( + serialName = "Timestamp", + toNativeValue = Timestamp::nativeValue, + fromNativeValue = { value -> + when (value) { + is NativeTimestamp -> Timestamp(value) + else -> throw SerializationException("Cannot deserialize $value") + } + } +) + +/** A serializer for [Timestamp.ServerTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */ +object ServerTimestampSerializer : KSerializer by SpecialValueSerializer( + serialName = "Timestamp", + toNativeValue = { FieldValue.serverTimestamp.nativeValue }, + fromNativeValue = { value -> + when (value) { + FieldValue.serverTimestamp.nativeValue -> Timestamp.ServerTimestamp + else -> throw SerializationException("Cannot deserialize $value") + } + } +) + +/** A serializer for a Double field which is stored as a Timestamp. */ +object DoubleAsTimestampSerializer : KSerializer by SpecialValueSerializer( + serialName = "Timestamp", + toNativeValue = { value -> + when(value) { + serverTimestamp -> FieldValue.serverTimestamp.nativeValue + else -> Timestamp.fromMilliseconds(value).nativeValue + } + }, + fromNativeValue = { value -> + when(value) { + FieldValue.serverTimestamp.nativeValue -> serverTimestamp + is NativeTimestamp -> Timestamp(value).toMilliseconds() + is Double -> value + else -> throw SerializationException("Cannot deserialize $value") + } + } +) { + const val serverTimestamp = Double.POSITIVE_INFINITY +} \ No newline at end of file diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/encoders.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/encoders.kt new file mode 100644 index 000000000..04a3f32cf --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/encoders.kt @@ -0,0 +1,15 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.EncodeSettings + +/** @return whether value is special and shouldn't be encoded/decoded. */ +@PublishedApi +internal expect fun isSpecialValue(value: Any): Boolean + +@PublishedApi +internal inline fun encode(value: T, buildSettings: EncodeSettings.Builder.() -> Unit) = + if (value?.let(::isSpecialValue) == true) { + value + } else { + dev.gitlive.firebase.encode(value, buildSettings) + } diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 97b4e50e9..f92436805 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -6,11 +6,11 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationStrategy +import kotlin.jvm.JvmName /** Returns the [FirebaseFirestore] instance of the default [FirebaseApp]. */ expect val Firebase.firestore: FirebaseFirestore @@ -32,27 +32,102 @@ expect class FirebaseFirestore { suspend fun enableNetwork() } -expect class Transaction { +@PublishedApi +internal sealed class SetOptions { + data object Merge : SetOptions() + data object Overwrite : SetOptions() + data class MergeFields(val fields: List) : SetOptions() + data class MergeFieldPaths(val fieldPaths: List) : SetOptions() { + val encodedFieldPaths = fieldPaths.map { it.encoded } + } +} - fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean = true, merge: Boolean = false): Transaction - fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean = true, vararg mergeFields: String): Transaction - fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean = true, vararg mergeFieldPaths: FieldPath): Transaction +@PublishedApi +internal expect class NativeTransaction { + fun setEncoded(documentRef: DocumentReference, encodedData: Any, setOptions: SetOptions): NativeTransaction + fun updateEncoded(documentRef: DocumentReference, encodedData: Any): NativeTransaction + fun updateEncodedFieldsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>): NativeTransaction + fun updateEncodedFieldPathsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>): NativeTransaction + fun delete(documentRef: DocumentReference): NativeTransaction + suspend fun get(documentRef: DocumentReference): NativeDocumentSnapshot +} - fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, merge: Boolean = false): Transaction - fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, vararg mergeFields: String): Transaction - fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, vararg mergeFieldPaths: FieldPath): Transaction +data class Transaction internal constructor(@PublishedApi internal val native: NativeTransaction) { - fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean = true): Transaction - fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true): Transaction + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, data, merge) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, merge: Boolean = false): Transaction = set(documentRef, data, merge) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, data: Any, merge: Boolean = false, buildSettings: EncodeSettings.Builder.() -> Unit = {}): Transaction = setEncoded(documentRef, encode(data, buildSettings)!!, if (merge) SetOptions.Merge else SetOptions.Overwrite) - fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair): Transaction - fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair): Transaction - fun delete(documentRef: DocumentReference): Transaction - suspend fun get(documentRef: DocumentReference): DocumentSnapshot + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, data, mergeFields) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFields: String) = set(documentRef, data, *mergeFields) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, data: Any, vararg mergeFields: String, buildSettings: EncodeSettings.Builder.() -> Unit = {}): Transaction = setEncoded(documentRef, encode(data, buildSettings)!!, SetOptions.MergeFields(mergeFields.asList())) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, data, mergeFieldPaths) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = set(documentRef, data, *mergeFieldPaths) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, data: Any, vararg mergeFieldPaths: FieldPath, buildSettings: EncodeSettings.Builder.() -> Unit = {}): Transaction = setEncoded(documentRef, encode(data, buildSettings)!!, SetOptions.MergeFieldPaths(mergeFieldPaths.asList())) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, strategy, data, merge) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean = false) = set(documentRef, strategy, data, merge) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, merge: Boolean = false, buildSettings: EncodeSettings.Builder.() -> Unit = {}): Transaction = setEncoded(documentRef, encode(strategy, data, buildSettings)!!, if (merge) SetOptions.Merge else SetOptions.Overwrite) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, strategy, data, mergeFields) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = set(documentRef, strategy, data, *mergeFields) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, vararg mergeFields: String, buildSettings: EncodeSettings.Builder.() -> Unit = {}): Transaction = setEncoded(documentRef, encode(strategy, data, buildSettings)!!, SetOptions.MergeFields(mergeFields.asList())) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, strategy, data, mergeFieldPaths) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = set(documentRef, strategy, data, *mergeFieldPaths) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, vararg mergeFieldPaths: FieldPath, buildSettings: EncodeSettings.Builder.() -> Unit = {}): Transaction = setEncoded(documentRef, encode(strategy, data, buildSettings)!!, SetOptions.MergeFieldPaths(mergeFieldPaths.asList())) + + @PublishedApi + internal fun setEncoded(documentRef: DocumentReference, encodedData: Any, setOptions: SetOptions): Transaction = Transaction(native.setEncoded(documentRef, encodedData, setOptions)) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("update(documentRef, data) { this.encodeDefaults = encodeDefaults }")) + fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean) = update(documentRef, data) { + this.encodeDefaults = encodeDefaults + } + inline fun update(documentRef: DocumentReference, data: Any, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = updateEncoded(documentRef, encode(data, buildSettings)!!) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("update(documentRef, strategy, data) { this.encodeDefaults = encodeDefaults }")) + fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = update(documentRef, strategy, data) { + this.encodeDefaults = encodeDefaults + } + inline fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = updateEncoded(documentRef, encode(strategy, data, buildSettings)!!) + + @JvmName("updateFields") + inline fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = updateEncodedFieldsAndValues(documentRef, encodeFieldAndValue(fieldsAndValues, buildSettings).orEmpty()) + @JvmName("updateFieldPaths") + inline fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = updateEncodedFieldPathsAndValues(documentRef, encodeFieldAndValue(fieldsAndValues, buildSettings).orEmpty()) + + @PublishedApi + internal fun updateEncoded(documentRef: DocumentReference, encodedData: Any): Transaction = Transaction(native.updateEncoded(documentRef, encodedData)) + + @PublishedApi + internal fun updateEncodedFieldsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>): Transaction = Transaction(native.updateEncodedFieldsAndValues(documentRef, encodedFieldsAndValues)) + + @PublishedApi + internal fun updateEncodedFieldPathsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>): Transaction = Transaction(native.updateEncodedFieldPathsAndValues(documentRef, encodedFieldsAndValues)) + + fun delete(documentRef: DocumentReference): Transaction = Transaction(native.delete(documentRef)) + suspend fun get(documentRef: DocumentReference): DocumentSnapshot = DocumentSnapshot(native.get(documentRef)) } -expect open class Query { +@PublishedApi +internal expect open class NativeQuery + +expect open class Query internal constructor(nativeQuery: NativeQuery) { fun limit(limit: Number): Query val snapshots: Flow fun snapshots(includeMetadataChanges: Boolean = false): Flow @@ -144,89 +219,233 @@ fun Query.endAt(vararg fieldValues: Any) = _endAt(*(fieldValues.mapNotNull { it. internal val Any.safeValue: Any get() = when (this) { is Timestamp -> nativeValue is GeoPoint -> nativeValue - is DocumentReference -> nativeValue + is DocumentReference -> native.nativeValue is Map<*, *> -> this.mapNotNull { (key, value) -> key?.let { it.safeValue to value?.safeValue } } is Collection<*> -> this.mapNotNull { it?.safeValue } else -> this } -expect class WriteBatch { - inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, merge: Boolean = false): WriteBatch - inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, vararg mergeFields: String): WriteBatch - inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, vararg mergeFieldPaths: FieldPath): WriteBatch +@PublishedApi +internal expect class NativeWriteBatch { + fun setEncoded(documentRef: DocumentReference, encodedData: Any, setOptions: SetOptions): NativeWriteBatch + fun updateEncoded(documentRef: DocumentReference, encodedData: Any): NativeWriteBatch + fun updateEncodedFieldsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>): NativeWriteBatch + fun updateEncodedFieldPathsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>): NativeWriteBatch + fun delete(documentRef: DocumentReference): NativeWriteBatch + suspend fun commit() +} + +data class WriteBatch internal constructor(@PublishedApi internal val native: NativeWriteBatch) { - fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, merge: Boolean = false): WriteBatch - fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, vararg mergeFields: String): WriteBatch - fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, vararg mergeFieldPaths: FieldPath): WriteBatch + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, data, merge) { this.encodeDefaults = encodeDefaults }")) + inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, merge: Boolean = false) = set(documentRef, data, merge) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, data: T, merge: Boolean = false, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + setEncoded(documentRef, encode(data, buildSettings)!!, if (merge) SetOptions.Merge else SetOptions.Overwrite) - inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true): WriteBatch - fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true): WriteBatch + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, data, mergeFields) { this.encodeDefaults = encodeDefaults }")) + inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = set(documentRef, data, *mergeFields) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, data: T, vararg mergeFields: String, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + setEncoded(documentRef, encode(data, buildSettings)!!, SetOptions.MergeFields(mergeFields.asList())) - fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair): WriteBatch - fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair): WriteBatch + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, data, mergeFieldPaths) { this.encodeDefaults = encodeDefaults }")) + inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = set(documentRef, data, *mergeFieldPaths) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, data: T, vararg mergeFieldPaths: FieldPath, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + setEncoded(documentRef, encode(data, buildSettings)!!, SetOptions.MergeFieldPaths(mergeFieldPaths.asList())) - fun delete(documentRef: DocumentReference): WriteBatch - suspend fun commit() + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, strategy, data, merge) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean = false) = set(documentRef, strategy, data, merge) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, merge: Boolean = false, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + setEncoded(documentRef, encode(strategy, data, buildSettings)!!, if (merge) SetOptions.Merge else SetOptions.Overwrite) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, strategy, data, mergeFields) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = set(documentRef, strategy, data, *mergeFields){ + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, vararg mergeFields: String, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + setEncoded(documentRef, encode(strategy, data, buildSettings)!!, SetOptions.MergeFields(mergeFields.asList())) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(documentRef, strategy, data, mergeFieldPaths) { this.encodeDefaults = encodeDefaults }")) + fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = set(documentRef, strategy, data, *mergeFieldPaths) { + this.encodeDefaults = encodeDefaults + } + inline fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, vararg mergeFieldPaths: FieldPath, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + setEncoded(documentRef, encode(strategy, data, buildSettings)!!, SetOptions.MergeFieldPaths(mergeFieldPaths.asList())) + + @PublishedApi + internal fun setEncoded(documentRef: DocumentReference, encodedData: Any, setOptions: SetOptions) = WriteBatch(native.setEncoded(documentRef, encodedData, setOptions)) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("update(documentRef, data) { this.encodeDefaults = encodeDefaults }")) + inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) = update(documentRef, data) { + this.encodeDefaults = encodeDefaults + } + inline fun update(documentRef: DocumentReference, data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + updateEncoded(documentRef, encode(data, buildSettings)!!) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("update(documentRef, strategy, data) { this.encodeDefaults = encodeDefaults }")) + fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = update(documentRef, strategy, data) { + this.encodeDefaults = encodeDefaults + } + inline fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = + updateEncoded(documentRef, encode(strategy, data, buildSettings)!!) + + @JvmName("updateField") + inline fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = updateEncodedFieldsAndValues(documentRef, encodeFieldAndValue(fieldsAndValues, buildSettings).orEmpty()) + @JvmName("updateFieldPath") + inline fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = updateEncodedFieldPathsAndValues(documentRef, encodeFieldAndValue(fieldsAndValues, buildSettings).orEmpty()) + + @PublishedApi + internal fun updateEncoded(documentRef: DocumentReference, encodedData: Any) = WriteBatch(native.updateEncoded(documentRef, encodedData)) + + @PublishedApi + internal fun updateEncodedFieldsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>) = WriteBatch(native.updateEncodedFieldsAndValues(documentRef, encodedFieldsAndValues)) + + @PublishedApi + internal fun updateEncodedFieldPathsAndValues(documentRef: DocumentReference, encodedFieldsAndValues: List>) = WriteBatch(native.updateEncodedFieldPathsAndValues(documentRef, encodedFieldsAndValues)) + + fun delete(documentRef: DocumentReference): WriteBatch = WriteBatch(native.delete(documentRef)) + suspend fun commit() = native.commit() } /** A class representing a platform specific Firebase DocumentReference. */ -expect class NativeDocumentReference +expect class NativeDocumentReferenceType + +@PublishedApi +internal expect class NativeDocumentReference(nativeValue: NativeDocumentReferenceType) { + val nativeValue: NativeDocumentReferenceType + val id: String + val path: String + val snapshots: Flow + val parent: NativeCollectionReference + fun snapshots(includeMetadataChanges: Boolean = false): Flow + + fun collection(collectionPath: String): NativeCollectionReference + suspend fun get(): NativeDocumentSnapshot + suspend fun setEncoded(encodedData: Any, setOptions: SetOptions) + suspend fun updateEncoded(encodedData: Any) + suspend fun updateEncodedFieldsAndValues(encodedFieldsAndValues: List>) + suspend fun updateEncodedFieldPathsAndValues(encodedFieldsAndValues: List>) + suspend fun delete() +} /** A class representing a Firebase DocumentReference. */ @Serializable(with = DocumentReferenceSerializer::class) -expect class DocumentReference internal constructor(nativeValue: NativeDocumentReference) { - internal val nativeValue: NativeDocumentReference +data class DocumentReference internal constructor(@PublishedApi internal val native: NativeDocumentReference) { - val id: String - val path: String - val snapshots: Flow - val parent: CollectionReference - fun snapshots(includeMetadataChanges: Boolean = false): Flow + internal val nativeValue get() = native.nativeValue - fun collection(collectionPath: String): CollectionReference - suspend fun get(): DocumentSnapshot + val id: String get() = native.id + val path: String get() = native.path + val snapshots: Flow get() = native.snapshots.map(::DocumentSnapshot) + val parent: CollectionReference get() = CollectionReference(native.parent) + fun snapshots(includeMetadataChanges: Boolean = false): Flow = native.snapshots(includeMetadataChanges).map(::DocumentSnapshot) - suspend inline fun set(data: T, encodeDefaults: Boolean = true, merge: Boolean = false) - suspend inline fun set(data: T, encodeDefaults: Boolean = true, vararg mergeFields: String) - suspend inline fun set(data: T, encodeDefaults: Boolean = true, vararg mergeFieldPaths: FieldPath) + fun collection(collectionPath: String): CollectionReference = CollectionReference(native.collection(collectionPath)) + suspend fun get(): DocumentSnapshot = DocumentSnapshot(native.get()) - suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, merge: Boolean = false) - suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, vararg mergeFields: String) - suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true, vararg mergeFieldPaths: FieldPath) + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(data, merge) { this.encodeDefaults = encodeDefaults }")) + suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean = false) = set(data, merge) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun set(data: T, merge: Boolean = false, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.setEncoded(encode(data, buildSettings)!!, if (merge) SetOptions.Merge else SetOptions.Overwrite) - suspend inline fun update(data: T, encodeDefaults: Boolean = true) - suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true) + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(data, mergeFields) { this.encodeDefaults = encodeDefaults }")) + suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFields: String) = set(data, *mergeFields) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun set(data: T, vararg mergeFields: String, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.setEncoded(encode(data, buildSettings)!!, SetOptions.MergeFields(mergeFields.asList())) - suspend fun update(vararg fieldsAndValues: Pair) - suspend fun update(vararg fieldsAndValues: Pair) + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(data, mergeFieldPaths) { this.encodeDefaults = encodeDefaults }")) + suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = set(data, *mergeFieldPaths) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun set(data: T, vararg mergeFieldPaths: FieldPath, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.setEncoded(encode(data, buildSettings)!!, SetOptions.MergeFieldPaths(mergeFieldPaths.asList())) - suspend fun delete() -} + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(strategy, data, merge) { this.encodeDefaults = encodeDefaults }")) + suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean = false) = set(strategy, data, merge) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun set(strategy: SerializationStrategy, data: T, merge: Boolean = false, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.setEncoded( + encode(strategy, data, buildSettings)!!, if (merge) SetOptions.Merge else SetOptions.Overwrite) -/** - * A serializer for [DocumentReference]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. - */ -object DocumentReferenceSerializer : KSerializer by SpecialValueSerializer( - serialName = "DocumentReference", - toNativeValue = DocumentReference::nativeValue, - fromNativeValue = { value -> - when (value) { - is NativeDocumentReference -> DocumentReference(value) - else -> throw SerializationException("Cannot deserialize $value") - } + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(strategy, data, mergeFields) { this.encodeDefaults = encodeDefaults }")) + suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = set(strategy, data, *mergeFields) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun set(strategy: SerializationStrategy, data: T, vararg mergeFields: String, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.setEncoded( + encode(strategy, data, buildSettings)!!, SetOptions.MergeFields(mergeFields.asList())) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("set(strategy, data, mergeFieldPaths) { this.encodeDefaults = encodeDefaults }")) + suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = set(strategy, data, *mergeFieldPaths) { + this.encodeDefaults = encodeDefaults } -) + suspend inline fun set(strategy: SerializationStrategy, data: T, vararg mergeFieldPaths: FieldPath, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.setEncoded( + encode(strategy, data, buildSettings)!!, SetOptions.MergeFieldPaths(mergeFieldPaths.asList())) -expect class CollectionReference : Query { + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("update(data) { this.encodeDefaults = encodeDefaults }")) + suspend inline fun update(data: T, encodeDefaults: Boolean) = update(data) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun update(data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncoded(encode(data, buildSettings)!!) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("update(strategy, data) { this.encodeDefaults = encodeDefaults }")) + suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = update(strategy, data) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun update(strategy: SerializationStrategy, data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncoded(encode(strategy, data, buildSettings)!!) + + @JvmName("updateFields") + suspend inline fun update(vararg fieldsAndValues: Pair, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncodedFieldsAndValues(encodeFieldAndValue(fieldsAndValues, buildSettings).orEmpty()) + + @JvmName("updateFieldPaths") + suspend inline fun update(vararg fieldsAndValues: Pair, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = native.updateEncodedFieldPathsAndValues(encodeFieldAndValue(fieldsAndValues, buildSettings).orEmpty()) + + suspend fun delete() = native.delete() +} + +@PublishedApi +internal expect class NativeCollectionReference : NativeQuery { val path: String - val document: DocumentReference - val parent: DocumentReference? + val document: NativeDocumentReference + val parent: NativeDocumentReference? - fun document(documentPath: String): DocumentReference - suspend inline fun add(data: T, encodeDefaults: Boolean = true): DocumentReference - @Deprecated("This will be replaced with add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true)") - suspend fun add(data: T, strategy: SerializationStrategy, encodeDefaults: Boolean = true): DocumentReference - suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true): DocumentReference + fun document(documentPath: String): NativeDocumentReference + suspend fun addEncoded(data: Any): NativeDocumentReference +} + +data class CollectionReference internal constructor(@PublishedApi internal val native: NativeCollectionReference) : Query(native) { + + val path: String get() = native.path + val document: DocumentReference get() = DocumentReference(native.document) + val parent: DocumentReference? get() = native.parent?.let(::DocumentReference) + + fun document(documentPath: String): DocumentReference = DocumentReference(native.document(documentPath)) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("add(data) { this.encodeDefaults = encodeDefaults }")) + suspend inline fun add(data: T, encodeDefaults: Boolean) = add(data) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun add(data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = addEncoded( + encode(data, buildSettings)!! + ) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("add(strategy, data) { this.encodeDefaults = encodeDefaults }")) + suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = add(strategy, data) { + this.encodeDefaults = encodeDefaults + } + suspend inline fun add(strategy: SerializationStrategy, data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}) = addEncoded( + encode(strategy, data, buildSettings)!! + ) + + @PublishedApi + internal suspend fun addEncoded(data: Any): DocumentReference = DocumentReference(native.addEncoded(data)) } expect class FirebaseFirestoreException : FirebaseException @@ -278,20 +497,39 @@ expect class DocumentChange { val type: ChangeType } -expect class DocumentSnapshot { +@PublishedApi +internal expect class NativeDocumentSnapshot { - inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T - fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T + val exists: Boolean + val id: String + val reference: NativeDocumentReference + val metadata: SnapshotMetadata fun contains(field: String): Boolean - inline fun data(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T - fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T + fun getEncoded(field: String, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): Any? + fun encodedData(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): Any? +} - val exists: Boolean - val id: String - val reference: DocumentReference - val metadata: SnapshotMetadata +data class DocumentSnapshot internal constructor(@PublishedApi internal val native: NativeDocumentSnapshot) { + + val exists: Boolean get() = native.exists + val id: String get() = native.id + val reference: DocumentReference get() = DocumentReference(native.reference) + val metadata: SnapshotMetadata get() = native.metadata + + + inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE, buildSettings: DecodeSettings.Builder.() -> Unit = {}): T = decode(value = getEncoded(field, serverTimestampBehavior), buildSettings) + inline fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE, buildSettings: DecodeSettings.Builder.() -> Unit = {}): T = decode(strategy, getEncoded(field, serverTimestampBehavior), buildSettings) + + @PublishedApi + internal fun getEncoded(field: String, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): Any? = native.getEncoded(field, serverTimestampBehavior) + + inline fun data(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE, buildSettings: DecodeSettings.Builder.() -> Unit = {}): T = decode(encodedData(serverTimestampBehavior), buildSettings) + inline fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE, buildSettings: DecodeSettings.Builder.() -> Unit = {}): T = decode(strategy, encodedData(serverTimestampBehavior), buildSettings) + + @PublishedApi + internal fun encodedData(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): Any? = native.encodedData(serverTimestampBehavior) } enum class ServerTimestampBehavior { @@ -307,27 +545,7 @@ expect class SnapshotMetadata { expect class FieldPath(vararg fieldNames: String) { val documentId: FieldPath + val encoded: EncodedFieldPath } -/** Represents a Firebase FieldValue. */ -@Serializable(with = FieldValueSerializer::class) -expect class FieldValue internal constructor(nativeValue: Any) { - internal val nativeValue: Any - - companion object { - val serverTimestamp: FieldValue - val delete: FieldValue - fun increment(value: Int): FieldValue - fun arrayUnion(vararg elements: Any): FieldValue - fun arrayRemove(vararg elements: Any): FieldValue - } -} - -/** A serializer for [FieldValue]. Must be used in conjunction with [FirebaseEncoder]. */ -object FieldValueSerializer : KSerializer by SpecialValueSerializer( - serialName = "FieldValue", - toNativeValue = FieldValue::nativeValue, - fromNativeValue = { raw -> - raw?.let(::FieldValue) ?: throw SerializationException("Cannot deserialize $raw") - } -) +expect class EncodedFieldPath diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/helpers.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/helpers.kt index 807ee34bb..bde2fd6bf 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/helpers.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/helpers.kt @@ -1,18 +1,38 @@ package dev.gitlive.firebase.firestore +import dev.gitlive.firebase.EncodeSettings +import kotlin.jvm.JvmName + +//** Helper method to perform an update operation. */ +@JvmName("performUpdateFields") +@PublishedApi +internal inline fun encodeFieldAndValue( + fieldsAndValues: Array>, + buildSettings: EncodeSettings.Builder.() -> Unit, +) = encodeFieldAndValue(fieldsAndValues, encodeField = { it }, encodeValue = { encode(it, buildSettings) }) + +/** Helper method to perform an update operation. */ +@JvmName("performUpdateFieldPaths") +@PublishedApi +internal inline fun encodeFieldAndValue( + fieldsAndValues: Array>, + buildSettings: EncodeSettings.Builder.() -> Unit, +) = encodeFieldAndValue(fieldsAndValues, { it.encoded }, { encode(it, buildSettings) }) + /** Helper method to perform an update operation in Android and JS. */ -internal fun performUpdate( +@PublishedApi +internal inline fun encodeFieldAndValue( fieldsAndValues: Array>, encodeField: (T) -> K, - encodeValue: (Any?) -> Any?, - update: (K, Any?, Array) -> R -) : R? = + encodeValue: (Any?) -> Any? +) : List>? = fieldsAndValues.takeUnless { fieldsAndValues.isEmpty() } ?.map { (field, value) -> encodeField(field) to value?.let { encodeValue(it) } } - ?.let { encoded -> - update( - encoded[0].first, - encoded[0].second, - encoded.drop(1).flatMap { (field, value) -> listOf(field, value) }.toTypedArray() - ) - } + +internal fun List>.performUpdate( + update: (K, Any?, Array) -> R +) = update( + this[0].first, + this[0].second, + this.drop(1).flatMap { (field, value) -> listOf(field, value) }.toTypedArray() +) diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/serializers.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/serializers.kt new file mode 100644 index 000000000..5916bce4c --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/serializers.kt @@ -0,0 +1,28 @@ +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.descriptors.ClassSerialDescriptorBuilder +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nullable + +/** + * Builder for a [SerialDescriptor] which fixes an nullability issue in [kotlinx.serialization.descriptors.buildClassSerialDescriptor] + * @return a class [SerialDescriptor]. */ +fun buildClassSerialDescriptor( + serialName: String, + vararg typeParameters: SerialDescriptor, + isNullable: Boolean, + builderAction: ClassSerialDescriptorBuilder.() -> Unit = {} +): SerialDescriptor { + val descriptor = kotlinx.serialization.descriptors.buildClassSerialDescriptor( + serialName = serialName, + typeParameters = typeParameters, + builderAction = builderAction + ) + + return if (isNullable && !descriptor.isNullable) { + // bug https://github.com/Kotlin/kotlinx.serialization/issues/1929 + descriptor.nullable + } else { + descriptor + } +} diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt index 6dc5fa45f..ad4b5374b 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt @@ -1,5 +1,6 @@ package dev.gitlive.firebase.firestore +import dev.gitlive.firebase.firebaseSerializer import dev.gitlive.firebase.runTest import kotlin.test.Test import kotlin.test.assertEquals @@ -13,4 +14,10 @@ class FieldValueTests { assertNotEquals(FieldValue.delete, FieldValue.serverTimestamp) // Note: arrayUnion and arrayRemove can't be checked due to vararg to array conversion } + + @Test + @IgnoreJs + fun serializers() = runTest { + assertEquals(FieldValueSerializer, FieldValue.delete.firebaseSerializer()) + } } diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/GeoPointTests.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/GeoPointTests.kt index 45457040c..216621064 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/GeoPointTests.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/GeoPointTests.kt @@ -1,10 +1,6 @@ package dev.gitlive.firebase.firestore -import dev.gitlive.firebase.decode -import dev.gitlive.firebase.encode -import dev.gitlive.firebase.nativeAssertEquals -import dev.gitlive.firebase.nativeMapOf -import dev.gitlive.firebase.runTest +import dev.gitlive.firebase.* import kotlinx.serialization.Serializable import kotlin.test.Test import kotlin.test.assertEquals @@ -22,23 +18,32 @@ class GeoPointTests { fun encodeGeoPointObject() = runTest { val geoPoint = GeoPoint(12.3, 45.6) val item = TestDataWithGeoPoint("123", geoPoint) - // check GeoPoint is encoded to a platform representation - nativeAssertEquals( - nativeMapOf("uid" to "123", "location" to geoPoint.nativeValue), - encode(item, shouldEncodeElementDefault = false) + val encoded = encodedAsMap( + encode(item) { + encodeDefaults = false + } ) + assertEquals("123", encoded["uid"]) + // check GeoPoint is encoded to a platform representation + assertEquals(geoPoint.nativeValue, encoded["location"]) } @Test fun decodeGeoPointObject() = runTest { val geoPoint = GeoPoint(12.3, 45.6) - val obj = nativeMapOf( + val obj = mapOf( "uid" to "123", "location" to geoPoint.nativeValue - ) + ).asEncoded() val decoded: TestDataWithGeoPoint = decode(obj) assertEquals("123", decoded.uid) // check a platform GeoPoint is properly wrapped assertEquals(geoPoint, decoded.location) } + + @Test + @IgnoreJs + fun serializers() = runTest { + assertEquals(GeoPointSerializer, GeoPoint(0.0,0.0).firebaseSerializer()) + } } diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt new file mode 100644 index 000000000..c98152a85 --- /dev/null +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt @@ -0,0 +1,5 @@ +package dev.gitlive.firebase.firestore + +/** Marker annotation to ignore JS test. */ +expect annotation class IgnoreJs() +expect annotation class IgnoreForAndroidUnitTest() diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TimestampTests.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TimestampTests.kt index 98292048a..8c2541ba9 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TimestampTests.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TimestampTests.kt @@ -7,9 +7,12 @@ import dev.gitlive.firebase.nativeAssertEquals import dev.gitlive.firebase.nativeMapOf import dev.gitlive.firebase.runTest import kotlinx.serialization.Serializable -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.DurationUnit @Serializable data class TestData( @@ -41,7 +44,7 @@ class TimestampTests { "updatedAt" to timestamp.nativeValue, "deletedAt" to null ), - encode(item, shouldEncodeElementDefault = false) + encode(item) { encodeDefaults = false } ) } @@ -56,7 +59,7 @@ class TimestampTests { "updatedAt" to FieldValue.serverTimestamp.nativeValue, "deletedAt" to FieldValue.serverTimestamp.nativeValue ), - encode(item, shouldEncodeElementDefault = false) + encode(item) { encodeDefaults = false } ) } @@ -104,11 +107,10 @@ class TimestampTests { @Test fun serializers() = runTest { - //todo dont work in js due to use of reified type in firebaseSerializer - uncomment once switched to IR -// assertEquals(BaseTimestampSerializer, (Timestamp(0, 0) as BaseTimestamp).firebaseSerializer()) -// assertEquals(BaseTimestampSerializer, (Timestamp.ServerTimestamp as BaseTimestamp).firebaseSerializer()) -// assertEquals(TimestampSerializer, Timestamp(0, 0).firebaseSerializer()) -// assertEquals(ServerTimestampSerializer, Timestamp.ServerTimestamp.firebaseSerializer()) + assertEquals(BaseTimestampSerializer, (Timestamp(0, 0) as BaseTimestamp).firebaseSerializer()) + assertEquals(BaseTimestampSerializer, (Timestamp.ServerTimestamp as BaseTimestamp).firebaseSerializer()) + assertEquals(TimestampSerializer, Timestamp(0, 0).firebaseSerializer()) + assertEquals(ServerTimestampSerializer, Timestamp.ServerTimestamp.firebaseSerializer()) } @Test diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 1f5f57693..f59c0b2c2 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -7,9 +7,11 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseOptions import dev.gitlive.firebase.apps +import dev.gitlive.firebase.decode import dev.gitlive.firebase.initialize import dev.gitlive.firebase.runBlockingTest import dev.gitlive.firebase.runTest +import dev.gitlive.firebase.withSerializer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -18,7 +20,9 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.builtins.serializer import kotlin.random.Random import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -31,7 +35,11 @@ import kotlin.test.assertTrue expect val emulatorHost: String expect val context: Any -expect annotation class IgnoreForAndroidUnitTest() + +/** @return a map extracted from the encoded data. */ +expect fun encodedAsMap(encoded: Any?): Map +/** @return pairs as raw encoded data. */ +expect fun Map.asEncoded(): Any @IgnoreForAndroidUnitTest class FirebaseFirestoreTest { @@ -182,6 +190,11 @@ class FirebaseFirestoreTest { val doc = firestore .collection("testServerTimestampFieldValue") .document("test") + doc.set( + FirestoreTimeTest.serializer(), + FirestoreTimeTest("ServerTimestamp", Timestamp(123, 0)), + ) + assertEquals(Timestamp(123, 0), doc.get().get("time", TimestampSerializer)) doc.set(FirestoreTimeTest.serializer(), FirestoreTimeTest("ServerTimestamp", Timestamp.ServerTimestamp)) @@ -207,7 +220,27 @@ class FirebaseFirestoreTest { val pendingWritesSnapshot = deferredPendingWritesSnapshot.await() assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites) - assertNull(pendingWritesSnapshot.get("time", BaseTimestamp.serializer().nullable, ServerTimestampBehavior.NONE)) + assertNull(pendingWritesSnapshot.get("time", BaseTimestamp.serializer().nullable, serverTimestampBehavior = ServerTimestampBehavior.NONE)) + } + + @Test + fun testSetBatch() = runTest { + val doc = firestore + .collection("testServerTestSetBatch") + .document("test") + val batch = firestore.batch() + batch.set( + documentRef = doc, + strategy = FirestoreTest.serializer(), + data = FirestoreTest( + prop1 = "prop1", + time = 123.0 + ), + ) + batch.commit() + + assertEquals("prop1", doc.get().data(FirestoreTest.serializer()).prop1) + } @Test @@ -225,8 +258,8 @@ class FirebaseFirestoreTest { val pendingWritesSnapshot = deferredPendingWritesSnapshot.await() assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites) - assertNotNull(pendingWritesSnapshot.get("time", BaseTimestamp.serializer().nullable, ServerTimestampBehavior.ESTIMATE)) - assertNotEquals(Timestamp.ServerTimestamp, pendingWritesSnapshot.data(FirestoreTimeTest.serializer(), ServerTimestampBehavior.ESTIMATE).time) + assertNotNull(pendingWritesSnapshot.get("time", ServerTimestampBehavior.ESTIMATE)) + assertNotEquals(Timestamp.ServerTimestamp, pendingWritesSnapshot.data(FirestoreTimeTest.serializer(), serverTimestampBehavior = ServerTimestampBehavior.ESTIMATE).time) } @Test @@ -244,7 +277,7 @@ class FirebaseFirestoreTest { val pendingWritesSnapshot = deferredPendingWritesSnapshot.await() assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites) - assertNull(pendingWritesSnapshot.get("time", BaseTimestamp.serializer().nullable, ServerTimestampBehavior.PREVIOUS)) + assertNull(pendingWritesSnapshot.get("time", BaseTimestamp.serializer().nullable, serverTimestampBehavior = ServerTimestampBehavior.PREVIOUS)) } @Test @@ -465,6 +498,85 @@ class FirebaseFirestoreTest { assertEquals(listOf("first"), dataAfter.list) } + @Test + fun testSetBatchDoesNotEncodeEmptyValues() = runTest { + val doc = firestore + .collection("testServerTestSetBatch") + .document("test") + val batch = firestore.batch() + batch.set( + documentRef = doc, + strategy = FirestoreTest.serializer(), + data = FirestoreTest( + prop1 = "prop1-set", + time = 125.0 + ) + ) + batch.commit() + + assertEquals(125.0, doc.get().get("time") as Double?) + assertEquals("prop1-set", doc.get().data(FirestoreTest.serializer()).prop1) + } + + @Test + fun testUpdateBatch() = runTest { + val doc = firestore + .collection("testServerTestSetBatch") + .document("test").apply { + set( + FirestoreTest( + prop1 = "prop1", + time = 123.0 + ) + ) + } + + + val batch = firestore.batch() + batch.update( + documentRef = doc, + strategy = FirestoreTest.serializer(), + data = FirestoreTest( + prop1 = "prop1-updated", + time = 123.0 + ), + ) { + encodeDefaults = false + } + batch.commit() + + assertEquals("prop1-updated", doc.get().data(FirestoreTest.serializer()).prop1) + } + + @Test + fun testUpdateBatchDoesNotEncodeEmptyValues() = runTest { + val doc = firestore + .collection("testServerTestSetBatch") + .document("test").apply { + set( + FirestoreTest( + prop1 = "prop1", + time = 123.0 + ) + ) + } + val batch = firestore.batch() + batch.update( + documentRef = doc, + strategy = FirestoreTest.serializer(), + data = FirestoreTest( + prop1 = "prop1-set", + time = 126.0 + ), + ) { + encodeDefaults = false + } + batch.commit() + + assertEquals(126.0, doc.get().get("time") as Double?) + assertEquals("prop1-set", doc.get().data(FirestoreTest.serializer()).prop1) + } + @Test fun testLegacyDoubleTimestamp() = runTest { @Serializable @@ -486,8 +598,8 @@ class FirebaseFirestoreTest { val pendingWritesSnapshot = deferredPendingWritesSnapshot.await() assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites) - assertNotNull(pendingWritesSnapshot.get("time", DoubleAsTimestampSerializer, ServerTimestampBehavior.ESTIMATE )) - assertNotEquals(DoubleAsTimestampSerializer.serverTimestamp, pendingWritesSnapshot.data(DoubleTimestamp.serializer(), ServerTimestampBehavior.ESTIMATE).time) + assertNotNull(pendingWritesSnapshot.get("time", DoubleAsTimestampSerializer, serverTimestampBehavior = ServerTimestampBehavior.ESTIMATE )) + assertNotEquals(DoubleAsTimestampSerializer.serverTimestamp, pendingWritesSnapshot.data(DoubleTimestamp.serializer(), serverTimestampBehavior = ServerTimestampBehavior.ESTIMATE).time) } @Test @@ -546,6 +658,151 @@ class FirebaseFirestoreTest { assertEquals(setOf(DocumentWithTimestamp(futureTimestamp)), gtQueryResult) } + + @Test + fun testGeoPointSerialization() = runTest { + @Serializable + data class DataWithGeoPoint(val geoPoint: GeoPoint) + + fun getDocument() = firestore.collection("geoPointSerialization") + .document("geoPointSerialization") + + val data = DataWithGeoPoint(GeoPoint(12.34, 56.78)) + // store geo point + getDocument().set(DataWithGeoPoint.serializer(), data) + // restore data + val savedData = getDocument().get().data(DataWithGeoPoint.serializer()) + assertEquals(data.geoPoint, savedData.geoPoint) + + // update data + val updatedData = DataWithGeoPoint(GeoPoint(87.65, 43.21)) + getDocument().update(FieldPath(DataWithGeoPoint::geoPoint.name) to updatedData.geoPoint) + // verify update + val updatedSavedData = getDocument().get().data(DataWithGeoPoint.serializer()) + assertEquals(updatedData.geoPoint, updatedSavedData.geoPoint) + } + + @Test + fun testDocumentReferenceSerialization() = runTest { + @Serializable + data class DataWithDocumentReference( + val documentReference: DocumentReference + ) + + fun getCollection() = firestore.collection("documentReferenceSerialization") + fun getDocument() = getCollection() + .document("documentReferenceSerialization") + val documentRef1 = getCollection().document("refDoc1").apply { + set(mapOf("value" to 1)) + } + val documentRef2 = getCollection().document("refDoc2").apply { + set(mapOf("value" to 2)) + } + + val data = DataWithDocumentReference(documentRef1) + // store reference + getDocument().set(DataWithDocumentReference.serializer(), data) + // restore data + val savedData = getDocument().get().data(DataWithDocumentReference.serializer()) + assertEquals(data.documentReference.path, savedData.documentReference.path) + + // update data + val updatedData = DataWithDocumentReference(documentRef2) + getDocument().update( + FieldPath(DataWithDocumentReference::documentReference.name) to updatedData.documentReference.withSerializer(DocumentReferenceSerializer) + ) + // verify update + val updatedSavedData = getDocument().get().data(DataWithDocumentReference.serializer()) + assertEquals(updatedData.documentReference.path, updatedSavedData.documentReference.path) + } + + @Serializable + data class TestDataWithDocumentReference( + val uid: String, + val reference: DocumentReference, + val optionalReference: DocumentReference? + ) + + @Serializable + data class TestDataWithOptionalDocumentReference( + val optionalReference: DocumentReference? + ) + + @Test + fun encodeDocumentReference() = runTest { + val doc = firestore.document("a/b") + val item = TestDataWithDocumentReference("123", doc, doc) + val encoded = encodedAsMap( + encode(item) { + encodeDefaults = false + } + ) + assertEquals("123", encoded["uid"]) + assertEquals(doc.nativeValue, encoded["reference"]) + assertEquals(doc.nativeValue, encoded["optionalReference"]) + } + + @Test + fun encodeNullDocumentReference() = runTest { + val item = TestDataWithOptionalDocumentReference(null) + val encoded = encodedAsMap( + encode(item) { + encodeDefaults = false + } + ) + assertNull(encoded["optionalReference"]) + } + + @Test + fun decodeDocumentReference() = runTest { + val doc = firestore.document("a/b") + val obj = mapOf( + "uid" to "123", + "reference" to doc.nativeValue, + "optionalReference" to doc.nativeValue + ).asEncoded() + val decoded: TestDataWithDocumentReference = decode(obj) + assertEquals("123", decoded.uid) + assertEquals(doc.path, decoded.reference.path) + assertEquals(doc.path, decoded.optionalReference?.path) + } + + @Test + fun decodeNullDocumentReference() = runTest { + val obj = mapOf("optionalReference" to null).asEncoded() + val decoded: TestDataWithOptionalDocumentReference = decode(obj) + assertNull(decoded.optionalReference?.path) + } + + @Test + fun testFieldValuesOps() = runTest { + @Serializable + data class TestData(val values: List) + fun getDocument() = firestore.collection("fieldValuesOps") + .document("fieldValuesOps") + + val data = TestData(listOf(1)) + // store + getDocument().set(TestData.serializer(), data) + // append & verify + getDocument().update(FieldPath(TestData::values.name) to FieldValue.arrayUnion(2)) + + var savedData = getDocument().get().data(TestData.serializer()) + assertEquals(listOf(1, 2), savedData.values) + + // remove & verify + getDocument().update(FieldPath(TestData::values.name) to FieldValue.arrayRemove(1)) + savedData = getDocument().get().data(TestData.serializer()) + assertEquals(listOf(2), savedData.values) + + val list = getDocument().get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) + assertEquals(listOf(2), list) + // delete & verify + getDocument().update(FieldPath(TestData::values.name) to FieldValue.delete) + val deletedList = getDocument().get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) + assertNull(deletedList) + } + @Test fun testQueryEqualTo() = runTest { setupFirestoreData() diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt new file mode 100644 index 000000000..f3c0efd0a --- /dev/null +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt @@ -0,0 +1,27 @@ +package dev.gitlive.firebase.firestore + +import cocoapods.FirebaseFirestoreInternal.FIRFieldValue +import kotlinx.serialization.Serializable + +/** A class representing a platform specific Firebase FieldValue. */ +private typealias NativeFieldValue = FIRFieldValue + +/** Represents a Firebase FieldValue. */ +@Serializable(with = FieldValueSerializer::class) +actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { + init { + require(nativeValue is NativeFieldValue) + } + override fun equals(other: Any?): Boolean = + this === other || other is FieldValue && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual val serverTimestamp: FieldValue get() = FieldValue(NativeFieldValue.fieldValueForServerTimestamp()) + actual val delete: FieldValue get() = FieldValue(NativeFieldValue.fieldValueForDelete()) + actual fun increment(value: Int): FieldValue = FieldValue(NativeFieldValue.fieldValueForIntegerIncrement(value.toLong())) + actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.fieldValueForArrayUnion(elements.asList())) + actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.fieldValueForArrayRemove(elements.asList())) + } +} diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt new file mode 100644 index 000000000..5bde38bac --- /dev/null +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt @@ -0,0 +1,12 @@ +package dev.gitlive.firebase.firestore + +import cocoapods.FirebaseFirestoreInternal.FIRFieldValue + +@PublishedApi +internal actual fun isSpecialValue(value: Any) = when(value) { + is FIRFieldValue, + is NativeGeoPoint, + is NativeTimestamp, + is NativeDocumentReferenceType -> true + else -> false +} diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 6beefc629..06c46f1e6 100644 --- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -9,6 +9,7 @@ import cocoapods.FirebaseFirestoreInternal.FIRDocumentChangeType.* import dev.gitlive.firebase.* import kotlinx.cinterop.* import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.runBlocking @@ -28,19 +29,19 @@ actual fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFir @Suppress("UNCHECKED_CAST") actual class FirebaseFirestore(val ios: FIRFirestore) { - actual fun collection(collectionPath: String) = CollectionReference(ios.collectionWithPath(collectionPath)) + actual fun collection(collectionPath: String) = CollectionReference(NativeCollectionReference(ios.collectionWithPath(collectionPath))) - actual fun collectionGroup(collectionId: String) = Query(ios.collectionGroupWithID(collectionId)) + actual fun collectionGroup(collectionId: String) = Query(ios.collectionGroupWithID(collectionId).native) - actual fun document(documentPath: String) = DocumentReference(ios.documentWithPath(documentPath)) + actual fun document(documentPath: String) = DocumentReference(NativeDocumentReference(ios.documentWithPath(documentPath))) - actual fun batch() = WriteBatch(ios.batch()) + actual fun batch() = WriteBatch(NativeWriteBatch(ios.batch())) actual fun setLoggingEnabled(loggingEnabled: Boolean): Unit = FIRFirestore.enableLogging(loggingEnabled) actual suspend fun runTransaction(func: suspend Transaction.() -> T) = - awaitResult { ios.runTransactionWithBlock({ transaction, _ -> runBlocking { Transaction(transaction!!).func() } }, it) } as T + awaitResult { ios.runTransactionWithBlock({ transaction, _ -> runBlocking { Transaction(NativeTransaction(transaction!!)).func() } }, it) } as T actual suspend fun clearPersistence() = await { ios.clearPersistenceWithCompletion(it) } @@ -72,104 +73,105 @@ actual class FirebaseFirestore(val ios: FIRFirestore) { } @Suppress("UNCHECKED_CAST") -actual class WriteBatch(val ios: FIRWriteBatch) { - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, merge: Boolean) = - ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this } - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this } - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = - ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this } - - actual inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) = - ios.updateData(encode(data, encodeDefaults) as Map, documentRef.ios).let { this } - - actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - ios.updateData(encode(strategy, data, encodeDefaults) as Map, documentRef.ios).let { this } - - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - ios.updateData( - fieldsAndValues.associate { (field, value) -> field to encode(value, true) }, - documentRef.ios - ).let { this } - - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - ios.updateData( - fieldsAndValues.associate { (path, value) -> path.ios to encode(value, true) }, - documentRef.ios - ).let { this } +@PublishedApi +internal actual class NativeWriteBatch(val ios: FIRWriteBatch) { + + actual fun setEncoded( + documentRef: DocumentReference, + encodedData: Any, + setOptions: SetOptions + ): NativeWriteBatch = when (setOptions) { + is SetOptions.Merge -> ios.setData(encodedData as Map, documentRef.ios, true) + is SetOptions.Overwrite -> ios.setData(encodedData as Map, documentRef.ios, false) + is SetOptions.MergeFields -> ios.setData(encodedData as Map, documentRef.ios, setOptions.fields) + is SetOptions.MergeFieldPaths -> ios.setData(encodedData as Map, documentRef.ios, setOptions.encodedFieldPaths) + }.let { this } + + actual fun updateEncoded(documentRef: DocumentReference, encodedData: Any): NativeWriteBatch = ios.updateData(encodedData as Map, documentRef.ios).let { this } + + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeWriteBatch = ios.updateData( + encodedFieldsAndValues.toMap(), + documentRef.ios + ).let { this } + + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeWriteBatch = ios.updateData( + encodedFieldsAndValues.toMap(), + documentRef.ios + ).let { this } actual fun delete(documentRef: DocumentReference) = ios.deleteDocument(documentRef.ios).let { this } actual suspend fun commit() = await { ios.commitWithCompletion(it) } - } -@Suppress("UNCHECKED_CAST") -actual class Transaction(val ios: FIRTransaction) { - - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, merge: Boolean) = - ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this } - - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFields: String) = - ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this } - - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - ios.setData(encode(data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = - ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, merge).let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFields.asList()).let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - ios.setData(encode(strategy, data, encodeDefaults)!! as Map, documentRef.ios, mergeFieldPaths.map { it.ios }).let { this } - - actual fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean) = - ios.updateData(encode(data, encodeDefaults) as Map, documentRef.ios).let { this } - - actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - ios.updateData(encode(strategy, data, encodeDefaults) as Map, documentRef.ios).let { this } +val WriteBatch.ios get() = native.ios - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - ios.updateData( - fieldsAndValues.associate { (field, value) -> field to encode(value, true) }, - documentRef.ios - ).let { this } - - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = - ios.updateData( - fieldsAndValues.associate { (path, value) -> path.ios to encode(value, true) }, - documentRef.ios - ).let { this } +@Suppress("UNCHECKED_CAST") +@PublishedApi +internal actual class NativeTransaction(val ios: FIRTransaction) { + + actual fun setEncoded( + documentRef: DocumentReference, + encodedData: Any, + setOptions: SetOptions + ): NativeTransaction = when (setOptions) { + is SetOptions.Merge -> ios.setData(encodedData as Map, documentRef.ios, true) + is SetOptions.Overwrite -> ios.setData(encodedData as Map, documentRef.ios, false) + is SetOptions.MergeFields -> ios.setData(encodedData as Map, documentRef.ios, setOptions.fields) + is SetOptions.MergeFieldPaths -> ios.setData(encodedData as Map, documentRef.ios, setOptions.encodedFieldPaths) + }.let { this } + + actual fun updateEncoded(documentRef: DocumentReference, encodedData: Any): NativeTransaction = ios.updateData(encodedData as Map, documentRef.ios).let { this } + + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeTransaction = ios.updateData( + encodedFieldsAndValues.toMap(), + documentRef.ios + ).let { this } + + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeTransaction = ios.updateData( + encodedFieldsAndValues.toMap(), + documentRef.ios + ).let { this } actual fun delete(documentRef: DocumentReference) = ios.deleteDocument(documentRef.ios).let { this } actual suspend fun get(documentRef: DocumentReference) = - throwError { DocumentSnapshot(ios.getDocument(documentRef.ios, it)!!) } + throwError { NativeDocumentSnapshot(ios.getDocument(documentRef.ios, it)!!) } } +val Transaction.ios get() = native.ios + /** A class representing a platform specific Firebase DocumentReference. */ -actual typealias NativeDocumentReference = FIRDocumentReference +actual typealias NativeDocumentReferenceType = FIRDocumentReference -@Serializable(with = DocumentReferenceSerializer::class) -actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) { - val ios: NativeDocumentReference by ::nativeValue +@Suppress("UNCHECKED_CAST") +@PublishedApi +internal actual class NativeDocumentReference actual constructor(actual val nativeValue: NativeDocumentReferenceType) { + + actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { + val listener = ios.addSnapshotListenerWithIncludeMetadataChanges(includeMetadataChanges) { snapshot, error -> + snapshot?.let { trySend(NativeDocumentSnapshot(snapshot)) } + error?.let { close(error.toException()) } + } + awaitClose { listener.remove() } + } + + val ios: NativeDocumentReferenceType by ::nativeValue actual val id: String get() = ios.documentID @@ -177,84 +179,65 @@ actual class DocumentReference actual constructor(internal actual val nativeValu actual val path: String get() = ios.path - actual val parent: CollectionReference - get() = CollectionReference(ios.parent) - - actual fun collection(collectionPath: String) = CollectionReference(ios.collectionWithPath(collectionPath)) - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean) = - await { ios.setData(encode(data, encodeDefaults)!! as Map, merge, it) } - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - await { ios.setData(encode(data, encodeDefaults)!! as Map, mergeFields.asList(), it) } - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - await { ios.setData(encode(data, encodeDefaults)!! as Map, mergeFieldPaths.map { it.ios }, it) } - - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = - await { ios.setData(encode(strategy, data, encodeDefaults)!! as Map, merge, it) } - - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - await { ios.setData(encode(strategy, data, encodeDefaults)!! as Map, mergeFields.asList(), it) } - - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - await { ios.setData(encode(strategy, data, encodeDefaults)!! as Map, mergeFieldPaths.map { it.ios }, it) } + actual val parent: NativeCollectionReference + get() = NativeCollectionReference(ios.parent) - actual suspend inline fun update(data: T, encodeDefaults: Boolean) = - await { ios.updateData(encode(data, encodeDefaults) as Map, it) } - actual suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - await { ios.updateData(encode(strategy, data, encodeDefaults) as Map, it) } + actual fun collection(collectionPath: String) = NativeCollectionReference(ios.collectionWithPath(collectionPath)) - actual suspend fun update(vararg fieldsAndValues: Pair) = - await { block -> - ios.updateData( - fieldsAndValues.associate { (field, value) -> field to encode(value, true) }, - block - ) - } - - actual suspend fun update(vararg fieldsAndValues: Pair) = - await { block -> - ios.updateData( - fieldsAndValues.associate { (path, value) -> path.ios to encode(value, true) }, - block - ) + actual suspend fun get() = + NativeDocumentSnapshot(awaitResult { ios.getDocumentWithCompletion(it) }) + + actual suspend fun setEncoded(encodedData: Any, setOptions: SetOptions) = await { + when (setOptions) { + is SetOptions.Merge -> ios.setData(encodedData as Map, true, it) + is SetOptions.Overwrite -> ios.setData(encodedData as Map, false, it) + is SetOptions.MergeFields -> ios.setData(encodedData as Map, setOptions.fields, it) + is SetOptions.MergeFieldPaths -> ios.setData(encodedData as Map, setOptions.encodedFieldPaths, it) } + } - actual suspend fun delete() = - await { ios.deleteDocumentWithCompletion(it) } + actual suspend fun updateEncoded(encodedData: Any) = await { + ios.updateData(encodedData as Map, it) + } - actual suspend fun get() = - DocumentSnapshot(awaitResult { ios.getDocumentWithCompletion(it) }) + actual suspend fun updateEncodedFieldsAndValues(encodedFieldsAndValues: List>) = await { + ios.updateData(encodedFieldsAndValues.toMap(), it) + } - actual val snapshots get() = callbackFlow { - val listener = ios.addSnapshotListener { snapshot, error -> - snapshot?.let { trySend(DocumentSnapshot(snapshot)) } - error?.let { close(error.toException()) } - } - awaitClose { listener.remove() } + actual suspend fun updateEncodedFieldPathsAndValues(encodedFieldsAndValues: List>) = await { + ios.updateData(encodedFieldsAndValues.toMap(), it) } - actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { - val listener = ios.addSnapshotListenerWithIncludeMetadataChanges(includeMetadataChanges) { snapshot, error -> - snapshot?.let { trySend(DocumentSnapshot(snapshot)) } + actual suspend fun delete() = await { ios.deleteDocumentWithCompletion(it) } + + actual val snapshots get() = callbackFlow { + val listener = ios.addSnapshotListener { snapshot, error -> + snapshot?.let { trySend(NativeDocumentSnapshot(snapshot)) } error?.let { close(error.toException()) } } awaitClose { listener.remove() } } override fun equals(other: Any?): Boolean = - this === other || other is DocumentReference && nativeValue == other.nativeValue + this === other || other is NativeDocumentReference && nativeValue == other.nativeValue override fun hashCode(): Int = nativeValue.hashCode() override fun toString(): String = nativeValue.toString() } -actual open class Query(open val ios: FIRQuery) { +val DocumentReference.ios get() = native.ios + +@PublishedApi +internal actual open class NativeQuery(open val ios: FIRQuery) +internal val FIRQuery.native get() = NativeQuery(this) + +actual open class Query internal actual constructor(nativeQuery: NativeQuery) { + + open val ios: FIRQuery = nativeQuery.ios actual suspend fun get() = QuerySnapshot(awaitResult { ios.getDocumentsWithCompletion(it) }) - actual fun limit(limit: Number) = Query(ios.queryLimitedTo(limit.toLong())) + actual fun limit(limit: Number) = Query(ios.queryLimitedTo(limit.toLong()).native) actual val snapshots get() = callbackFlow { val listener = ios.addSnapshotListener { snapshot, error -> @@ -273,7 +256,7 @@ actual open class Query(open val ios: FIRQuery) { } internal actual fun where(filter: Filter): Query = Query( - ios.queryWhereFilter(filter.toFIRFilter()) + ios.queryWhereFilter(filter.toFIRFilter()).native ) private fun Filter.toFIRFilter(): FIRFilter = when (this) { @@ -305,41 +288,39 @@ actual open class Query(open val ios: FIRQuery) { } } - internal actual fun _orderBy(field: String, direction: Direction) = Query(ios.queryOrderedByField(field, direction == Direction.DESCENDING)) - internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(ios.queryOrderedByFieldPath(field.ios, direction == Direction.DESCENDING)) + internal actual fun _orderBy(field: String, direction: Direction) = Query(ios.queryOrderedByField(field, direction == Direction.DESCENDING).native) + internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(ios.queryOrderedByFieldPath(field.ios, direction == Direction.DESCENDING).native) - internal actual fun _startAfter(document: DocumentSnapshot) = Query(ios.queryStartingAfterDocument(document.ios)) - internal actual fun _startAfter(vararg fieldValues: Any) = Query(ios.queryStartingAfterValues(fieldValues.asList())) - internal actual fun _startAt(document: DocumentSnapshot) = Query(ios.queryStartingAtDocument(document.ios)) - internal actual fun _startAt(vararg fieldValues: Any) = Query(ios.queryStartingAtValues(fieldValues.asList())) + internal actual fun _startAfter(document: DocumentSnapshot) = Query(ios.queryStartingAfterDocument(document.ios).native) + internal actual fun _startAfter(vararg fieldValues: Any) = Query(ios.queryStartingAfterValues(fieldValues.asList()).native) + internal actual fun _startAt(document: DocumentSnapshot) = Query(ios.queryStartingAtDocument(document.ios).native) + internal actual fun _startAt(vararg fieldValues: Any) = Query(ios.queryStartingAtValues(fieldValues.asList()).native) - internal actual fun _endBefore(document: DocumentSnapshot) = Query(ios.queryEndingBeforeDocument(document.ios)) - internal actual fun _endBefore(vararg fieldValues: Any) = Query(ios.queryEndingBeforeValues(fieldValues.asList())) - internal actual fun _endAt(document: DocumentSnapshot) = Query(ios.queryEndingAtDocument(document.ios)) - internal actual fun _endAt(vararg fieldValues: Any) = Query(ios.queryEndingAtValues(fieldValues.asList())) + internal actual fun _endBefore(document: DocumentSnapshot) = Query(ios.queryEndingBeforeDocument(document.ios).native) + internal actual fun _endBefore(vararg fieldValues: Any) = Query(ios.queryEndingBeforeValues(fieldValues.asList()).native) + internal actual fun _endAt(document: DocumentSnapshot) = Query(ios.queryEndingAtDocument(document.ios).native) + internal actual fun _endAt(vararg fieldValues: Any) = Query(ios.queryEndingAtValues(fieldValues.asList()).native) } + @Suppress("UNCHECKED_CAST") -actual class CollectionReference(override val ios: FIRCollectionReference) : Query(ios) { +@PublishedApi +internal actual class NativeCollectionReference(override val ios: FIRCollectionReference) : NativeQuery(ios) { actual val path: String get() = ios.path - actual val document get() = DocumentReference(ios.documentWithAutoID()) - - actual val parent get() = ios.parent?.let{DocumentReference(it)} + actual val document get() = NativeDocumentReference(ios.documentWithAutoID()) - actual fun document(documentPath: String) = DocumentReference(ios.documentWithPath(documentPath)) + actual val parent get() = ios.parent?.let{ NativeDocumentReference(it) } - actual suspend inline fun add(data: T, encodeDefaults: Boolean) = - DocumentReference(await { ios.addDocumentWithData(encode(data, encodeDefaults) as Map, it) }) + actual fun document(documentPath: String) = NativeDocumentReference(ios.documentWithPath(documentPath)) - actual suspend fun add(data: T, strategy: SerializationStrategy, encodeDefaults: Boolean) = - DocumentReference(await { ios.addDocumentWithData(encode(strategy, data, encodeDefaults) as Map, it) }) - actual suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - DocumentReference(await { ios.addDocumentWithData(encode(strategy, data, encodeDefaults) as Map, it) }) + actual suspend fun addEncoded(data: Any) = NativeDocumentReference(await { ios.addDocumentWithData(data as Map, it) }) } +val CollectionReference.ios get() = native.ios + actual class FirebaseFirestoreException(message: String, val code: FirestoreExceptionCode) : FirebaseException(message) actual val FirebaseFirestoreException.code: FirestoreExceptionCode get() = code @@ -401,7 +382,7 @@ fun NSError.toException() = when(domain) { actual class QuerySnapshot(val ios: FIRQuerySnapshot) { actual val documents - get() = ios.documents.map { DocumentSnapshot(it as FIRDocumentSnapshot) } + get() = ios.documents.map { DocumentSnapshot(NativeDocumentSnapshot(it as FIRDocumentSnapshot)) } actual val documentChanges get() = ios.documentChanges.map { DocumentChange(it as FIRDocumentChange) } actual val metadata: SnapshotMetadata get() = SnapshotMetadata(ios.metadata) @@ -409,7 +390,7 @@ actual class QuerySnapshot(val ios: FIRQuerySnapshot) { actual class DocumentChange(val ios: FIRDocumentChange) { actual val document: DocumentSnapshot - get() = DocumentSnapshot(ios.document) + get() = DocumentSnapshot(NativeDocumentSnapshot(ios.document)) actual val newIndex: Int get() = ios.newIndex.toInt() actual val oldIndex: Int @@ -418,32 +399,21 @@ actual class DocumentChange(val ios: FIRDocumentChange) { get() = ChangeType.values().first { it.ios == ios.type } } -@Suppress("UNCHECKED_CAST") -actual class DocumentSnapshot(val ios: FIRDocumentSnapshot) { +@PublishedApi +internal actual class NativeDocumentSnapshot(val ios: FIRDocumentSnapshot) { actual val id get() = ios.documentID - actual val reference get() = DocumentReference(ios.reference) - - actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T { - val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos()) - return decode(value = data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } }) - } - - actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T { - val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos()) - return decode(strategy, data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } }) - } + actual val reference get() = NativeDocumentReference(ios.reference) - actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T { - val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull } - return decode(value) - } + actual fun getEncoded(field: String, serverTimestampBehavior: ServerTimestampBehavior): Any? = + ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull } - actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T { - val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull } - return decode(strategy, value) - } + actual fun encodedData(serverTimestampBehavior: ServerTimestampBehavior): Any? = + ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos()) + ?.mapValues { (_, value) -> + value?.takeIf { it !is NSNull } + } actual fun contains(field: String) = ios.valueForField(field) != null @@ -458,6 +428,8 @@ actual class DocumentSnapshot(val ios: FIRDocumentSnapshot) { } } +val DocumentSnapshot.ios get() = native.ios + actual class SnapshotMetadata(val ios: FIRSnapshotMetadata) { actual val hasPendingWrites: Boolean get() = ios.pendingWrites actual val isFromCache: Boolean get() = ios.fromCache @@ -466,34 +438,13 @@ actual class SnapshotMetadata(val ios: FIRSnapshotMetadata) { actual class FieldPath private constructor(val ios: FIRFieldPath) { actual constructor(vararg fieldNames: String) : this(FIRFieldPath(fieldNames.asList())) actual val documentId: FieldPath get() = FieldPath(FIRFieldPath.documentID()) - + actual val encoded: EncodedFieldPath = ios override fun equals(other: Any?): Boolean = other is FieldPath && ios == other.ios override fun hashCode(): Int = ios.hashCode() override fun toString(): String = ios.toString() } -/** A class representing a platform specific Firebase FieldValue. */ -private typealias NativeFieldValue = FIRFieldValue - -/** Represents a Firebase FieldValue. */ -@Serializable(with = FieldValueSerializer::class) -actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { - init { - require(nativeValue is NativeFieldValue) - } - override fun equals(other: Any?): Boolean = - this === other || other is FieldValue && nativeValue == other.nativeValue - override fun hashCode(): Int = nativeValue.hashCode() - override fun toString(): String = nativeValue.toString() - - actual companion object { - actual val serverTimestamp: FieldValue get() = FieldValue(NativeFieldValue.fieldValueForServerTimestamp()) - actual val delete: FieldValue get() = FieldValue(NativeFieldValue.fieldValueForDelete()) - actual fun increment(value: Int): FieldValue = FieldValue(NativeFieldValue.fieldValueForIntegerIncrement(value.toLong())) - actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.fieldValueForArrayUnion(elements.asList())) - actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.fieldValueForArrayRemove(elements.asList())) - } -} +actual typealias EncodedFieldPath = FIRFieldPath private fun T.throwError(block: T.(errorPointer: CPointer>) -> R): R { memScoped { @@ -510,7 +461,7 @@ private fun T.throwError(block: T.(errorPointer: CPointer awaitResult(function: (callback: (T?, NSError?) -> Unit) -> Unit): T { val job = CompletableDeferred() function { result, error -> - if(error == null) { + if(error == null) { job.complete(result) } else { job.completeExceptionally(error.toException()) diff --git a/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt new file mode 100644 index 000000000..1f70d3731 --- /dev/null +++ b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt @@ -0,0 +1,6 @@ +package dev.gitlive.firebase.firestore + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreJs +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreForAndroidUnitTest diff --git a/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index f28d93276..3f54909b5 100644 --- a/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -8,5 +8,6 @@ actual val emulatorHost: String = "localhost" actual val context: Any = Unit -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) -actual annotation class IgnoreForAndroidUnitTest +@Suppress("UNCHECKED_CAST") +actual fun encodedAsMap(encoded: Any?): Map = encoded as Map +actual fun Map.asEncoded(): Any = this diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt new file mode 100644 index 000000000..39aba6c99 --- /dev/null +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/FieldValue.kt @@ -0,0 +1,32 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.firestore.externals.deleteField +import kotlinx.serialization.Serializable +import dev.gitlive.firebase.firestore.externals.serverTimestamp as jsServerTimestamp +import dev.gitlive.firebase.firestore.externals.arrayRemove as jsArrayRemove +import dev.gitlive.firebase.firestore.externals.arrayUnion as jsArrayUnion +import dev.gitlive.firebase.firestore.externals.increment as jsIncrement + +/** Represents a platform specific Firebase FieldValue. */ +typealias NativeFieldValue = dev.gitlive.firebase.firestore.externals.FieldValue + +/** Represents a Firebase FieldValue. */ +@Serializable(with = FieldValueSerializer::class) +actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { + init { + require(nativeValue is NativeFieldValue) + } + override fun equals(other: Any?): Boolean = + this === other || other is FieldValue && + (nativeValue as NativeFieldValue).isEqual(other.nativeValue as NativeFieldValue) + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual val serverTimestamp: FieldValue get() = rethrow { FieldValue(jsServerTimestamp()) } + actual val delete: FieldValue get() = rethrow { FieldValue(deleteField()) } + actual fun increment(value: Int): FieldValue = rethrow { FieldValue(jsIncrement(value)) } + actual fun arrayUnion(vararg elements: Any): FieldValue = rethrow { FieldValue(jsArrayUnion(*elements)) } + actual fun arrayRemove(vararg elements: Any): FieldValue = rethrow { FieldValue(jsArrayRemove(*elements)) } + } +} diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt index 9b14b7af3..2271e446e 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt @@ -1,6 +1,5 @@ package dev.gitlive.firebase.firestore -import dev.gitlive.firebase.* import kotlinx.serialization.Serializable /** A class representing a platform specific Firebase GeoPoint. */ diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt new file mode 100644 index 000000000..84445fb4d --- /dev/null +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt @@ -0,0 +1,10 @@ +package dev.gitlive.firebase.firestore + +@PublishedApi +internal actual fun isSpecialValue(value: Any) = when(value) { + is NativeFieldValue, + is NativeGeoPoint, + is NativeTimestamp, + is NativeDocumentReferenceType -> true + else -> false +} diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt index 7f5065a40..d4f02686e 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt @@ -75,7 +75,7 @@ external fun getFirestore(app: FirebaseApp? = definedExternally): Firestore external fun increment(n: Int): FieldValue -external fun initializeFirestore(app: FirebaseApp, settings: Any): Firestore +external fun initializeFirestore(app: FirebaseApp, settings: dynamic = definedExternally, databaseId: String? = definedExternally): Firestore external fun limit(limit: Number): QueryConstraint @@ -287,3 +287,16 @@ external class Timestamp(seconds: Double, nanoseconds: Double) { fun isEqual(other: Timestamp): Boolean } + +external interface FirestoreLocalCache { + val kind: String +} + +external interface PersistentTabManager { + val kind: String +} + +external fun memoryLocalCache(): FirestoreLocalCache +external fun persistentLocalCache(settings: dynamic = definedExternally): FirestoreLocalCache +external fun persistentSingleTabManager(settings: dynamic = definedExternally): PersistentTabManager +external fun persistentMultipleTabManager(): PersistentTabManager diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 998cdfe31..5c8e47a66 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -7,18 +7,35 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.FirebaseException -import dev.gitlive.firebase.decode -import dev.gitlive.firebase.encode -import dev.gitlive.firebase.firestore.externals.* +import dev.gitlive.firebase.firestore.externals.Firestore +import dev.gitlive.firebase.firestore.externals.QueryConstraint +import dev.gitlive.firebase.firestore.externals.addDoc +import dev.gitlive.firebase.firestore.externals.and +import dev.gitlive.firebase.firestore.externals.clearIndexedDbPersistence +import dev.gitlive.firebase.firestore.externals.connectFirestoreEmulator +import dev.gitlive.firebase.firestore.externals.deleteDoc +import dev.gitlive.firebase.firestore.externals.doc +import dev.gitlive.firebase.firestore.externals.enableIndexedDbPersistence +import dev.gitlive.firebase.firestore.externals.getDoc +import dev.gitlive.firebase.firestore.externals.getDocs +import dev.gitlive.firebase.firestore.externals.getFirestore +import dev.gitlive.firebase.firestore.externals.initializeFirestore +import dev.gitlive.firebase.firestore.externals.onSnapshot +import dev.gitlive.firebase.firestore.externals.or +import dev.gitlive.firebase.firestore.externals.orderBy +import dev.gitlive.firebase.firestore.externals.query +import dev.gitlive.firebase.firestore.externals.refEqual +import dev.gitlive.firebase.firestore.externals.setDoc +import dev.gitlive.firebase.firestore.externals.setLogLevel +import dev.gitlive.firebase.firestore.externals.writeBatch import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.await import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.promise -import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationStrategy +import kotlin.js.Json import kotlin.js.json import dev.gitlive.firebase.firestore.externals.CollectionReference as JsCollectionReference import dev.gitlive.firebase.firestore.externals.DocumentChange as JsDocumentChange @@ -30,12 +47,14 @@ import dev.gitlive.firebase.firestore.externals.QuerySnapshot as JsQuerySnapshot import dev.gitlive.firebase.firestore.externals.SnapshotMetadata as JsSnapshotMetadata import dev.gitlive.firebase.firestore.externals.Transaction as JsTransaction import dev.gitlive.firebase.firestore.externals.WriteBatch as JsWriteBatch -import dev.gitlive.firebase.firestore.externals.arrayRemove as jsArrayRemove -import dev.gitlive.firebase.firestore.externals.arrayUnion as jsArrayUnion +import dev.gitlive.firebase.firestore.externals.collection as jsCollection +import dev.gitlive.firebase.firestore.externals.collectionGroup as jsCollectionGroup +import dev.gitlive.firebase.firestore.externals.disableNetwork as jsDisableNetwork +import dev.gitlive.firebase.firestore.externals.enableNetwork as jsEnableNetwork import dev.gitlive.firebase.firestore.externals.endAt as jsEndAt import dev.gitlive.firebase.firestore.externals.endBefore as jsEndBefore -import dev.gitlive.firebase.firestore.externals.increment as jsIncrement import dev.gitlive.firebase.firestore.externals.limit as jsLimit +import dev.gitlive.firebase.firestore.externals.runTransaction as jsRunTransaction import dev.gitlive.firebase.firestore.externals.startAfter as jsStartAfter import dev.gitlive.firebase.firestore.externals.startAt as jsStartAt import dev.gitlive.firebase.firestore.externals.updateDoc as jsUpdate @@ -47,36 +66,24 @@ actual val Firebase.firestore get() = actual fun Firebase.firestore(app: FirebaseApp) = rethrow { FirebaseFirestore(getFirestore(app.js)) } -/** Helper method to perform an update operation. */ -private fun performUpdate( - fieldsAndValues: Array>, - update: (String, Any?, Array) -> R -) = performUpdate(fieldsAndValues, { it }, { encode(it, true) }, update) - -/** Helper method to perform an update operation. */ -private fun performUpdate( - fieldsAndValues: Array>, - update: (dev.gitlive.firebase.firestore.externals.FieldPath, Any?, Array) -> R -) = performUpdate(fieldsAndValues, { it.js }, { encode(it, true) }, update) - actual class FirebaseFirestore(jsFirestore: Firestore) { var js: Firestore = jsFirestore private set - actual fun collection(collectionPath: String) = rethrow { CollectionReference(collection(js, collectionPath)) } + actual fun collection(collectionPath: String) = rethrow { CollectionReference(NativeCollectionReference(jsCollection(js, collectionPath))) } - actual fun collectionGroup(collectionId: String) = rethrow { Query(collectionGroup(js, collectionId)) } + actual fun collectionGroup(collectionId: String) = rethrow { Query(jsCollectionGroup(js, collectionId)) } - actual fun document(documentPath: String) = rethrow { DocumentReference(doc(js, documentPath)) } + actual fun document(documentPath: String) = rethrow { DocumentReference(NativeDocumentReference(doc(js, documentPath))) } - actual fun batch() = rethrow { WriteBatch(writeBatch(js)) } + actual fun batch() = rethrow { WriteBatch(NativeWriteBatch(writeBatch(js))) } actual fun setLoggingEnabled(loggingEnabled: Boolean) = rethrow { setLogLevel( if(loggingEnabled) "error" else "silent") } actual suspend fun runTransaction(func: suspend Transaction.() -> T) = - rethrow { runTransaction(js, { GlobalScope.promise { Transaction(it).func() } } ).await() } + rethrow { jsRunTransaction(js, { GlobalScope.promise { Transaction(NativeTransaction(it)).func() } } ).await() } actual suspend fun clearPersistence() = rethrow { clearIndexedDbPersistence(js).await() } @@ -95,56 +102,47 @@ actual class FirebaseFirestore(jsFirestore: Firestore) { } actual suspend fun disableNetwork() { - rethrow { disableNetwork(js).await() } + rethrow { jsDisableNetwork(js).await() } } actual suspend fun enableNetwork() { - rethrow { enableNetwork(js).await() } + rethrow { jsEnableNetwork(js).await() } } } -actual class WriteBatch(val js: JsWriteBatch) { - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, merge: Boolean) = - rethrow { js.set(documentRef.js, encode(data, encodeDefaults)!!, json("merge" to merge)) } - .let { this } - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - rethrow { js.set(documentRef.js, encode(data, encodeDefaults)!!, json("mergeFields" to mergeFields)) } - .let { this } - - actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - rethrow { js.set(documentRef.js, encode(data, encodeDefaults)!!, json("mergeFields" to mergeFieldPaths.map { it.js }.toTypedArray())) } - .let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = - rethrow { js.set(documentRef.js, encode(strategy, data, encodeDefaults)!!, json("merge" to merge)) } - .let { this } +internal val SetOptions.js: Json get() = when (this) { + is SetOptions.Merge -> json("merge" to true) + is SetOptions.Overwrite -> json("merge" to false) + is SetOptions.MergeFields -> json("mergeFields" to fields.toTypedArray()) + is SetOptions.MergeFieldPaths -> json("mergeFields" to encodedFieldPaths.toTypedArray()) +} - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - rethrow { js.set(documentRef.js, encode(strategy, data, encodeDefaults)!!, json("mergeFields" to mergeFields)) } - .let { this } +@PublishedApi +internal actual class NativeWriteBatch(val js: JsWriteBatch) { - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - rethrow { js.set(documentRef.js, encode(strategy, data, encodeDefaults)!!, json("mergeFields" to mergeFieldPaths.map { it.js }.toTypedArray())) } - .let { this } + actual fun setEncoded( + documentRef: DocumentReference, + encodedData: Any, + setOptions: SetOptions + ): NativeWriteBatch = rethrow { js.set(documentRef.js, encodedData, setOptions.js) }.let { this } - actual inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) = - rethrow { js.update(documentRef.js, encode(data, encodeDefaults)!!) } - .let { this } + actual fun updateEncoded(documentRef: DocumentReference, encodedData: Any): NativeWriteBatch = rethrow { js.update(documentRef.js, encodedData) } + .let { this } - actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - rethrow { js.update(documentRef.js, encode(strategy, data, encodeDefaults)!!) } - .let { this } - - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = rethrow { - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeWriteBatch = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> js.update(documentRef.js, field, value, *moreFieldsAndValues) } }.let { this } - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = rethrow { - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeWriteBatch = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> js.update(documentRef.js, field, value, *moreFieldsAndValues) } }.let { this } @@ -154,51 +152,39 @@ actual class WriteBatch(val js: JsWriteBatch) { .let { this } actual suspend fun commit() = rethrow { js.commit().await() } - } -actual class Transaction(val js: JsTransaction) { - - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, merge: Boolean) = - rethrow { js.set(documentRef.js, encode(data, encodeDefaults)!!, json("merge" to merge)) } - .let { this } - - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFields: String) = - rethrow { js.set(documentRef.js, encode(data, encodeDefaults)!!, json("mergeFields" to mergeFields)) } - .let { this } - - actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - rethrow { js.set(documentRef.js, encode(data, encodeDefaults)!!, json("mergeFields" to mergeFieldPaths.map { it.js }.toTypedArray())) } - .let { this } - - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = - rethrow { js.set(documentRef.js, encode(strategy, data, encodeDefaults)!!, json("merge" to merge)) } - .let { this } +val WriteBatch.js get() = native.js - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - rethrow { js.set(documentRef.js, encode(strategy, data, encodeDefaults)!!, json("mergeFields" to mergeFields)) } - .let { this } +@PublishedApi +internal actual class NativeTransaction(val js: JsTransaction) { - actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - rethrow { js.set(documentRef.js, encode(strategy, data, encodeDefaults)!!, json("mergeFields" to mergeFieldPaths.map { it.js }.toTypedArray())) } - .let { this } - - actual fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean) = - rethrow { js.update(documentRef.js, encode(data, encodeDefaults)!!) } - .let { this } + actual fun setEncoded( + documentRef: DocumentReference, + encodedData: Any, + setOptions: SetOptions + ): NativeTransaction = rethrow { + js.set(documentRef.js, encodedData, setOptions.js) + } + .let { this } - actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - rethrow { js.update(documentRef.js, encode(strategy, data, encodeDefaults)!!) } - .let { this } + actual fun updateEncoded(documentRef: DocumentReference, encodedData: Any): NativeTransaction = rethrow { js.update(documentRef.js, encodedData) } + .let { this } - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = rethrow { - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + actual fun updateEncodedFieldsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeTransaction = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> js.update(documentRef.js, field, value, *moreFieldsAndValues) } }.let { this } - actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = rethrow { - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + actual fun updateEncodedFieldPathsAndValues( + documentRef: DocumentReference, + encodedFieldsAndValues: List> + ): NativeTransaction = rethrow { + encodedFieldsAndValues.performUpdate { field, value, moreFieldsAndValues -> js.update(documentRef.js, field, value, *moreFieldsAndValues) } }.let { this } @@ -208,15 +194,17 @@ actual class Transaction(val js: JsTransaction) { .let { this } actual suspend fun get(documentRef: DocumentReference) = - rethrow { DocumentSnapshot(js.get(documentRef.js).await()) } + rethrow { NativeDocumentSnapshot(js.get(documentRef.js).await()) } } +val Transaction.js get() = native.js + /** A class representing a platform specific Firebase DocumentReference. */ -actual typealias NativeDocumentReference = JsDocumentReference +actual typealias NativeDocumentReferenceType = JsDocumentReference -@Serializable(with = DocumentReferenceSerializer::class) -actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) { - val js: NativeDocumentReference by ::nativeValue +@PublishedApi +internal actual class NativeDocumentReference actual constructor(actual val nativeValue: NativeDocumentReferenceType) { + val js: NativeDocumentReferenceType = nativeValue actual val id: String get() = rethrow { js.id } @@ -224,70 +212,68 @@ actual class DocumentReference actual constructor(internal actual val nativeValu actual val path: String get() = rethrow { js.path } - actual val parent: CollectionReference - get() = rethrow { CollectionReference(js.parent) } - - actual fun collection(collectionPath: String) = rethrow { CollectionReference(collection(js, collectionPath)) } - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean) = - rethrow { setDoc(js, encode(data, encodeDefaults)!!, json("merge" to merge)).await() } - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - rethrow { setDoc(js, encode(data, encodeDefaults)!!, json("mergeFields" to mergeFields)).await() } - - actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - rethrow { setDoc(js, encode(data, encodeDefaults)!!, json("mergeFields" to mergeFieldPaths.map { it.js }.toTypedArray())).await() } - - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = - rethrow { setDoc(js, encode(strategy, data, encodeDefaults)!!, json("merge" to merge)).await() } - - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = - rethrow { setDoc(js, encode(strategy, data, encodeDefaults)!!, json("mergeFields" to mergeFields)).await() } + actual val parent: NativeCollectionReference + get() = rethrow { NativeCollectionReference(js.parent) } - actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = - rethrow { setDoc(js, encode(strategy, data, encodeDefaults)!!, json("mergeFields" to mergeFieldPaths.map { it.js }.toTypedArray())).await() } + actual fun collection(collectionPath: String) = rethrow { NativeCollectionReference(jsCollection(js, collectionPath)) } - actual suspend inline fun update(data: T, encodeDefaults: Boolean) = - rethrow { jsUpdate(js, encode(data, encodeDefaults)!!).await() } + actual suspend fun get() = rethrow { NativeDocumentSnapshot( getDoc(js).await()) } - actual suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - rethrow { jsUpdate(js, encode(strategy, data, encodeDefaults)!!).await() } + actual val snapshots: Flow get() = snapshots() - actual suspend fun update(vararg fieldsAndValues: Pair) = rethrow { - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - jsUpdate(js, field, value, *moreFieldsAndValues) - }?.await() - }.run { Unit } - - actual suspend fun update(vararg fieldsAndValues: Pair) = rethrow { - performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> - jsUpdate(js, field, value, *moreFieldsAndValues) - }?.await() - }.run { Unit } - - actual suspend fun delete() = rethrow { deleteDoc(js).await() } - - actual suspend fun get() = rethrow { DocumentSnapshot(getDoc(js).await()) } - - actual val snapshots: Flow get() = snapshots() - - actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { + actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow { val unsubscribe = onSnapshot( js, json("includeMetadataChanges" to includeMetadataChanges), - { trySend(DocumentSnapshot(it)) }, + { trySend(NativeDocumentSnapshot(it)) }, { close(errorToException(it)) } ) awaitClose { unsubscribe() } } + actual suspend fun setEncoded(encodedData: Any, setOptions: SetOptions) = rethrow { + setDoc(js, encodedData, setOptions.js).await() + } + + actual suspend fun updateEncoded(encodedData: Any) = rethrow { jsUpdate(js, encodedData).await() } + + actual suspend fun updateEncodedFieldsAndValues(encodedFieldsAndValues: List>) { + rethrow { + encodedFieldsAndValues.takeUnless { encodedFieldsAndValues.isEmpty() } + ?.performUpdate { field, value, moreFieldsAndValues -> + jsUpdate(js, field, value, *moreFieldsAndValues) + } + ?.await() + } + } + + actual suspend fun updateEncodedFieldPathsAndValues(encodedFieldsAndValues: List>) { + rethrow { + encodedFieldsAndValues.takeUnless { encodedFieldsAndValues.isEmpty() } + ?.performUpdate { field, value, moreFieldsAndValues -> + jsUpdate(js, field, value, *moreFieldsAndValues) + }?.await() + } + } + + actual suspend fun delete() = rethrow { deleteDoc(js).await() } + override fun equals(other: Any?): Boolean = - this === other || other is DocumentReference && refEqual(nativeValue, other.nativeValue) + this === other || other is NativeDocumentReference && refEqual(nativeValue, other.nativeValue) override fun hashCode(): Int = nativeValue.hashCode() override fun toString(): String = "DocumentReference(path=$path)" } -actual open class Query(open val js: JsQuery) { +val DocumentReference.js get() = native.js + +@PublishedApi +internal actual open class NativeQuery(open val js: JsQuery) + +actual open class Query internal actual constructor(nativeQuery: NativeQuery) { + + constructor(js: JsQuery) : this(NativeQuery(js)) + + open val js: JsQuery = nativeQuery.js actual suspend fun get() = rethrow { QuerySnapshot(getDocs(js).await()) } @@ -379,26 +365,25 @@ actual open class Query(open val js: JsQuery) { } } -actual class CollectionReference(override val js: JsCollectionReference) : Query(js) { +@PublishedApi +internal actual class NativeCollectionReference(override val js: JsCollectionReference) : NativeQuery(js) { actual val path: String get() = rethrow { js.path } - actual val document get() = rethrow { DocumentReference(doc(js)) } - - actual val parent get() = rethrow { js.parent?.let{DocumentReference(it)} } + actual val document get() = rethrow { NativeDocumentReference(doc(js)) } - actual fun document(documentPath: String) = rethrow { DocumentReference(doc(js, documentPath)) } + actual val parent get() = rethrow { js.parent?.let{ NativeDocumentReference(it) } } - actual suspend inline fun add(data: T, encodeDefaults: Boolean) = - rethrow { DocumentReference(addDoc(js, encode(data, encodeDefaults)!!).await()) } + actual fun document(documentPath: String) = rethrow { NativeDocumentReference(doc(js, documentPath)) } - actual suspend fun add(data: T, strategy: SerializationStrategy, encodeDefaults: Boolean) = - rethrow { DocumentReference(addDoc(js, encode(strategy, data, encodeDefaults)!!).await()) } - actual suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - rethrow { DocumentReference(addDoc(js, encode(strategy, data, encodeDefaults)!!).await()) } + actual suspend fun addEncoded(data: Any) = rethrow { + NativeDocumentReference(addDoc(js, data).await()) + } } +val CollectionReference.js get() = native.js + actual class FirebaseFirestoreException(cause: Throwable, val code: FirestoreExceptionCode) : FirebaseException(code.toString(), cause) @Suppress("EXTENSION_SHADOWED_BY_MEMBER") @@ -406,7 +391,7 @@ actual val FirebaseFirestoreException.code: FirestoreExceptionCode get() = code actual class QuerySnapshot(val js: JsQuerySnapshot) { actual val documents - get() = js.docs.map { DocumentSnapshot(it) } + get() = js.docs.map { DocumentSnapshot(NativeDocumentSnapshot(it)) } actual val documentChanges get() = js.docChanges().map { DocumentChange(it) } actual val metadata: SnapshotMetadata get() = SnapshotMetadata(js.metadata) @@ -414,7 +399,7 @@ actual class QuerySnapshot(val js: JsQuerySnapshot) { actual class DocumentChange(val js: JsDocumentChange) { actual val document: DocumentSnapshot - get() = DocumentSnapshot(js.doc) + get() = DocumentSnapshot(NativeDocumentSnapshot(js.doc)) actual val newIndex: Int get() = js.newIndex actual val oldIndex: Int @@ -423,22 +408,19 @@ actual class DocumentChange(val js: JsDocumentChange) { get() = ChangeType.values().first { it.jsString == js.type } } -actual class DocumentSnapshot(val js: JsDocumentSnapshot) { +@PublishedApi +internal actual class NativeDocumentSnapshot(val js: JsDocumentSnapshot) { actual val id get() = rethrow { js.id } - actual val reference get() = rethrow { DocumentReference(js.ref) } + actual val reference get() = rethrow { NativeDocumentReference(js.ref) } - actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T = - rethrow { decode(value = js.data(getTimestampsOptions(serverTimestampBehavior))) } - - actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = - rethrow { decode(strategy, js.data(getTimestampsOptions(serverTimestampBehavior))) } - - actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior) = - rethrow { decode(value = js.get(field, getTimestampsOptions(serverTimestampBehavior))) } + actual fun getEncoded(field: String, serverTimestampBehavior: ServerTimestampBehavior): Any? = rethrow { + js.get(field, getTimestampsOptions(serverTimestampBehavior)) + } - actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior) = - rethrow { decode(strategy, js.get(field, getTimestampsOptions(serverTimestampBehavior))) } + actual fun encodedData(serverTimestampBehavior: ServerTimestampBehavior): Any? = rethrow { + js.data(getTimestampsOptions(serverTimestampBehavior)) + } actual fun contains(field: String) = rethrow { js.get(field) != undefined } actual val exists get() = rethrow { js.exists() } @@ -448,6 +430,8 @@ actual class DocumentSnapshot(val js: JsDocumentSnapshot) { json("serverTimestamps" to serverTimestampBehavior.name.lowercase()) } +val DocumentSnapshot.js get() = native.js + actual class SnapshotMetadata(val js: JsSnapshotMetadata) { actual val hasPendingWrites: Boolean get() = js.hasPendingWrites actual val isFromCache: Boolean get() = js.fromCache @@ -458,35 +442,13 @@ actual class FieldPath private constructor(val js: JsFieldPath) { js("Reflect").construct(JsFieldPath, fieldNames).unsafeCast() }) actual val documentId: FieldPath get() = FieldPath(JsFieldPath.documentId) - + actual val encoded: EncodedFieldPath = js override fun equals(other: Any?): Boolean = other is FieldPath && js.isEqual(other.js) override fun hashCode(): Int = js.hashCode() override fun toString(): String = js.toString() } -/** Represents a platform specific Firebase FieldValue. */ -private typealias NativeFieldValue = dev.gitlive.firebase.firestore.externals.FieldValue - -/** Represents a Firebase FieldValue. */ -@Serializable(with = FieldValueSerializer::class) -actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { - init { - require(nativeValue is NativeFieldValue) - } - override fun equals(other: Any?): Boolean = - this === other || other is FieldValue && - (nativeValue as NativeFieldValue).isEqual(other.nativeValue as NativeFieldValue) - override fun hashCode(): Int = nativeValue.hashCode() - override fun toString(): String = nativeValue.toString() - - actual companion object { - actual val serverTimestamp: FieldValue get() = rethrow { FieldValue(serverTimestamp()) } - actual val delete: FieldValue get() = rethrow { FieldValue(deleteField()) } - actual fun increment(value: Int): FieldValue = rethrow { FieldValue(jsIncrement(value)) } - actual fun arrayUnion(vararg elements: Any): FieldValue = rethrow { FieldValue(jsArrayUnion(*elements)) } - actual fun arrayRemove(vararg elements: Any): FieldValue = rethrow { FieldValue(jsArrayRemove(*elements)) } - } -} +actual typealias EncodedFieldPath = JsFieldPath //actual data class FirebaseFirestoreSettings internal constructor( // val cacheSizeBytes: Number? = undefined, @@ -565,7 +527,7 @@ fun errorToException(e: dynamic) = (e?.code ?: e?.message ?: "") FirebaseFirestoreException(e, FirestoreExceptionCode.UNKNOWN) } } -} + } // from: https://discuss.kotlinlang.org/t/how-to-access-native-js-object-as-a-map-string-any/509/8 fun entriesOf(jsObject: dynamic): List> = diff --git a/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt b/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt new file mode 100644 index 000000000..65c18a3c9 --- /dev/null +++ b/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt @@ -0,0 +1,7 @@ +package dev.gitlive.firebase.firestore + +import kotlin.test.Ignore + +actual typealias IgnoreJs = Ignore +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreForAndroidUnitTest diff --git a/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 52e53d749..7dc9a19a8 100644 --- a/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -4,10 +4,16 @@ package dev.gitlive.firebase.firestore +import kotlin.js.json + actual val emulatorHost: String = "localhost" actual val context: Any = Unit -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) -actual annotation class IgnoreForAndroidUnitTest - +actual fun encodedAsMap(encoded: Any?): Map { + return (js("Object").entries(encoded) as Array>).associate { + it[0] as String to it[1] + } +} +actual fun Map.asEncoded(): Any = + json(*entries.map { (key, value) -> key to value }.toTypedArray()) diff --git a/firebase-firestore/src/jvmTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt b/firebase-firestore/src/jvmTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt new file mode 100644 index 000000000..1f70d3731 --- /dev/null +++ b/firebase-firestore/src/jvmTest/kotlin/dev/gitlive/firebase/firestore/Ignore.kt @@ -0,0 +1,6 @@ +package dev.gitlive.firebase.firestore + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreJs +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IgnoreForAndroidUnitTest diff --git a/firebase-functions/package.json b/firebase-functions/package.json index f60f381b7..0e5277af7 100644 --- a/firebase-functions/package.json +++ b/firebase-functions/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-functions", - "version": "1.11.0", + "version": "1.11.1", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-functions.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-functions/src/androidMain/kotlin/dev/gitlive/firebase/functions/functions.kt b/firebase-functions/src/androidMain/kotlin/dev/gitlive/firebase/functions/functions.kt index 10ddc4821..d93f743ec 100644 --- a/firebase-functions/src/androidMain/kotlin/dev/gitlive/firebase/functions/functions.kt +++ b/firebase-functions/src/androidMain/kotlin/dev/gitlive/firebase/functions/functions.kt @@ -4,13 +4,12 @@ package dev.gitlive.firebase.functions +import dev.gitlive.firebase.DecodeSettings import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.decode -import dev.gitlive.firebase.encode import kotlinx.coroutines.tasks.await import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.SerializationStrategy import java.util.concurrent.TimeUnit actual val Firebase.functions @@ -25,30 +24,30 @@ actual fun Firebase.functions(app: FirebaseApp) = actual fun Firebase.functions(app: FirebaseApp, region: String) = FirebaseFunctions(com.google.firebase.functions.FirebaseFunctions.getInstance(app.android, region)) -actual class FirebaseFunctions internal constructor(val android: com.google.firebase.functions.FirebaseFunctions) { +actual data class FirebaseFunctions internal constructor(val android: com.google.firebase.functions.FirebaseFunctions) { actual fun httpsCallable(name: String, timeout: Long?) = - HttpsCallableReference(android.getHttpsCallable(name).apply { timeout?.let { setTimeout(it, TimeUnit.MILLISECONDS) } }) + HttpsCallableReference(android.getHttpsCallable(name).apply { timeout?.let { setTimeout(it, TimeUnit.MILLISECONDS) } }.native) actual fun useEmulator(host: String, port: Int) = android.useEmulator(host, port) } -actual class HttpsCallableReference internal constructor(val android: com.google.firebase.functions.HttpsCallableReference) { - actual suspend operator fun invoke() = HttpsCallableResult(android.call().await()) +@PublishedApi +internal actual data class NativeHttpsCallableReference(val android: com.google.firebase.functions.HttpsCallableReference){ + actual suspend fun invoke(encodedData: Any): HttpsCallableResult = HttpsCallableResult(android.call(encodedData).await()) + actual suspend fun invoke(): HttpsCallableResult = HttpsCallableResult(android.call().await()) +} - actual suspend operator inline fun invoke(data: T, encodeDefaults: Boolean) = - HttpsCallableResult(android.call(encode(data, encodeDefaults)).await()) +internal val com.google.firebase.functions.HttpsCallableReference.native get() = NativeHttpsCallableReference(this) - actual suspend operator fun invoke(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - HttpsCallableResult(android.call(encode(strategy, data, encodeDefaults)).await()) -} +val HttpsCallableReference.android: com.google.firebase.functions.HttpsCallableReference get() = native.android actual class HttpsCallableResult constructor(val android: com.google.firebase.functions.HttpsCallableResult) { actual inline fun data() = decode(value = android.data) - actual fun data(strategy: DeserializationStrategy) = - decode(strategy, android.data) + actual inline fun data(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + decode(strategy, android.data, buildSettings) } actual typealias FirebaseFunctionsException = com.google.firebase.functions.FirebaseFunctionsException @@ -58,4 +57,3 @@ actual val FirebaseFunctionsException.code: FunctionsExceptionCode get() = code actual val FirebaseFunctionsException.details: Any? get() = details actual typealias FunctionsExceptionCode = com.google.firebase.functions.FirebaseFunctionsException.Code - diff --git a/firebase-functions/src/commonMain/kotlin/dev/gitlive/firebase/functions/functions.kt b/firebase-functions/src/commonMain/kotlin/dev/gitlive/firebase/functions/functions.kt index 300c66726..9a152a430 100644 --- a/firebase-functions/src/commonMain/kotlin/dev/gitlive/firebase/functions/functions.kt +++ b/firebase-functions/src/commonMain/kotlin/dev/gitlive/firebase/functions/functions.kt @@ -4,9 +4,7 @@ package dev.gitlive.firebase.functions -import dev.gitlive.firebase.Firebase -import dev.gitlive.firebase.FirebaseApp -import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.* import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy @@ -15,15 +13,33 @@ expect class FirebaseFunctions { fun useEmulator(host: String, port: Int) } -expect class HttpsCallableReference { - suspend operator inline fun invoke(data: T, encodeDefaults: Boolean = true): HttpsCallableResult - suspend operator fun invoke(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean = true): HttpsCallableResult - suspend operator fun invoke(): HttpsCallableResult +@PublishedApi +internal expect class NativeHttpsCallableReference { + suspend fun invoke(encodedData: Any): HttpsCallableResult + suspend fun invoke(): HttpsCallableResult +} + +class HttpsCallableReference internal constructor( + @PublishedApi + internal val native: NativeHttpsCallableReference +) { + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("invoke(data) { this.encodeDefaults = encodeDefaults }")) + suspend inline operator fun invoke(data: T, encodeDefaults: Boolean) = invoke(data) { + this.encodeDefaults = encodeDefaults + } + suspend inline operator fun invoke(data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}): HttpsCallableResult = native.invoke(encodedData = encode(data, buildSettings)!!) + + @Deprecated("Deprecated. Use builder instead", replaceWith = ReplaceWith("invoke(strategy, data) { this.encodeDefaults = encodeDefaults }")) + suspend operator fun invoke(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean): HttpsCallableResult = invoke(strategy, data) { + this.encodeDefaults = encodeDefaults + } + suspend inline operator fun invoke(strategy: SerializationStrategy, data: T, buildSettings: EncodeSettings.Builder.() -> Unit = {}): HttpsCallableResult = invoke(encode(strategy, data, buildSettings)!!) + suspend operator fun invoke(): HttpsCallableResult = native.invoke() } expect class HttpsCallableResult { inline fun data(): T - fun data(strategy: DeserializationStrategy): T + inline fun data(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit = {}): T } /** Returns the [FirebaseFunctions] instance of the default [FirebaseApp]. */ diff --git a/firebase-functions/src/iosMain/kotlin/dev/gitlive/firebase/functions/functions.kt b/firebase-functions/src/iosMain/kotlin/dev/gitlive/firebase/functions/functions.kt index 8ff0a5e11..907508278 100644 --- a/firebase-functions/src/iosMain/kotlin/dev/gitlive/firebase/functions/functions.kt +++ b/firebase-functions/src/iosMain/kotlin/dev/gitlive/firebase/functions/functions.kt @@ -19,10 +19,12 @@ actual val Firebase.functions actual fun Firebase.functions(region: String) = FirebaseFunctions(FIRFunctions.functionsForRegion(region)) +@Suppress("CAST_NEVER_SUCCEEDS") actual fun Firebase.functions(app: FirebaseApp): FirebaseFunctions = FirebaseFunctions( FIRFunctions.functionsForApp(app.ios as objcnames.classes.FIRApp) ) +@Suppress("CAST_NEVER_SUCCEEDS") actual fun Firebase.functions( app: FirebaseApp, region: String, @@ -30,30 +32,30 @@ actual fun Firebase.functions( FIRFunctions.functionsForApp(app.ios as objcnames.classes.FIRApp, region = region) ) -actual class FirebaseFunctions internal constructor(val ios: FIRFunctions) { +actual data class FirebaseFunctions internal constructor(val ios: FIRFunctions) { actual fun httpsCallable(name: String, timeout: Long?) = - HttpsCallableReference(ios.HTTPSCallableWithName(name).apply { timeout?.let { setTimeoutInterval(it/1000.0) } }) + HttpsCallableReference(ios.HTTPSCallableWithName(name).apply { timeout?.let { setTimeoutInterval(it/1000.0) } }.native) actual fun useEmulator(host: String, port: Int) = ios.useEmulatorWithHost(host, port.toLong()) } -actual class HttpsCallableReference internal constructor(val ios: FIRHTTPSCallable) { - actual suspend operator fun invoke() = HttpsCallableResult(ios.awaitResult { callWithCompletion(it) }) +@PublishedApi +internal actual data class NativeHttpsCallableReference(val ios: FIRHTTPSCallable) { + actual suspend fun invoke(encodedData: Any): HttpsCallableResult = HttpsCallableResult(ios.awaitResult { callWithObject(encodedData, it) }) + actual suspend fun invoke(): HttpsCallableResult = HttpsCallableResult(ios.awaitResult { callWithCompletion(it) }) +} - actual suspend inline operator fun invoke(data: T, encodeDefaults: Boolean) = - HttpsCallableResult(ios.awaitResult { callWithObject(encode(data, encodeDefaults), it) }) +internal val FIRHTTPSCallable.native get() = NativeHttpsCallableReference(this) - actual suspend operator fun invoke(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - HttpsCallableResult(ios.awaitResult { callWithObject(encode(strategy, data, encodeDefaults), it) }) -} +val HttpsCallableReference.ios: FIRHTTPSCallable get() = native.ios actual class HttpsCallableResult constructor(val ios: FIRHTTPSCallableResult) { actual inline fun data() = decode(value = ios.data()) - actual fun data(strategy: DeserializationStrategy) = - decode(strategy, ios.data()) + actual inline fun data(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + decode(strategy, ios.data(), buildSettings) } actual class FirebaseFunctionsException(message: String, val code: FunctionsExceptionCode, val details: Any?) : FirebaseException(message) diff --git a/firebase-functions/src/jsMain/kotlin/dev/gitlive/firebase/functions/functions.kt b/firebase-functions/src/jsMain/kotlin/dev/gitlive/firebase/functions/functions.kt index 1e06a16c5..b8b9b7bee 100644 --- a/firebase-functions/src/jsMain/kotlin/dev/gitlive/firebase/functions/functions.kt +++ b/firebase-functions/src/jsMain/kotlin/dev/gitlive/firebase/functions/functions.kt @@ -26,31 +26,30 @@ actual fun Firebase.functions(app: FirebaseApp, region: String) = actual class FirebaseFunctions internal constructor(val js: Functions) { actual fun httpsCallable(name: String, timeout: Long?) = - rethrow { HttpsCallableReference(httpsCallable(js, name, timeout?.let { json("timeout" to timeout.toDouble()) })) } + rethrow { HttpsCallableReference( httpsCallable(js, name, timeout?.let { json("timeout" to timeout.toDouble()) }).native) } actual fun useEmulator(host: String, port: Int) = connectFunctionsEmulator(js, host, port) } -@Suppress("UNCHECKED_CAST") -actual class HttpsCallableReference internal constructor(val js: HttpsCallable) { - - actual suspend operator fun invoke() = - rethrow { HttpsCallableResult(js().await()) } +@PublishedApi +internal actual data class NativeHttpsCallableReference(val js: HttpsCallable) { + actual suspend fun invoke(encodedData: Any): HttpsCallableResult = rethrow { + HttpsCallableResult(js(encodedData).await()) + } + actual suspend fun invoke(): HttpsCallableResult = rethrow { HttpsCallableResult(js().await()) } +} - actual suspend inline operator fun invoke(data: T, encodeDefaults: Boolean) = - rethrow { HttpsCallableResult(js(encode(data, encodeDefaults)).await()) } +internal val HttpsCallable.native get() = NativeHttpsCallableReference(this) - actual suspend operator fun invoke(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = - rethrow { HttpsCallableResult(js(encode(strategy, data, encodeDefaults)).await()) } -} +val HttpsCallableReference.js: HttpsCallable get() = native.js -actual class HttpsCallableResult constructor(val js: JsHttpsCallableResult) { +actual class HttpsCallableResult(val js: JsHttpsCallableResult) { actual inline fun data() = rethrow { decode(value = js.data) } - actual fun data(strategy: DeserializationStrategy) = - rethrow { decode(strategy, js.data) } + actual inline fun data(strategy: DeserializationStrategy, buildSettings: DecodeSettings.Builder.() -> Unit) = + rethrow { decode(strategy, js.data, buildSettings) } } diff --git a/firebase-installations/package.json b/firebase-installations/package.json index 3b8e2c9fc..ab86a6f71 100644 --- a/firebase-installations/package.json +++ b/firebase-installations/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-installations", - "version": "1.11.0", + "version": "1.12.0", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-installations.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-perf/package.json b/firebase-perf/package.json index dcfc99cd5..f454d775c 100644 --- a/firebase-perf/package.json +++ b/firebase-perf/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-perf", - "version": "1.11.0", + "version": "1.12.0", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-perf.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.6.10", "kotlinx-coroutines-core": "1.6.1-native-mt" diff --git a/firebase-storage/package.json b/firebase-storage/package.json index fbc5c0571..67f721da8 100644 --- a/firebase-storage/package.json +++ b/firebase-storage/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-storage", - "version": "1.11.0", + "version": "1.12.0", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-storage.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.11.0", + "@gitlive/firebase-app": "1.11.1", "firebase": "9.19.1", "kotlin": "1.6.10", "kotlinx-coroutines-core": "1.6.1-native-mt" diff --git a/gradle.properties b/gradle.properties index 53a73a811..3fb471e2b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,17 +47,17 @@ firebase-perf.skipJsTests=false firebase-storage.skipJsTests=false # Versions: -firebase-app.version=1.11.0 -firebase-auth.version=1.11.0 -firebase-common.version=1.11.0 -firebase-config.version=1.11.0 -firebase-database.version=1.11.0 -firebase-firestore.version=1.11.0 -firebase-functions.version=1.11.0 -firebase-installations.version=1.11.0 -firebase-perf.version=1.11.0 -firebase-crashlytics.version=1.11.0 -firebase-storage.version=1.11.0 +firebase-app.version=1.12.0 +firebase-auth.version=1.12.0 +firebase-common.version=1.12.0 +firebase-config.version=1.12.0 +firebase-database.version=1.12.0 +firebase-firestore.version=1.12.0 +firebase-functions.version=1.12.0 +firebase-installations.version=1.12.0 +firebase-perf.version=1.12.0 +firebase-crashlytics.version=1.12.0 +firebase-storage.version=1.12.0 # Dependencies Versions: gradlePluginVersion=8.1.3