diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index f5b5d1ab5..9ef8c16c5 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -30,8 +30,7 @@ jobs:
- name: Assemble
run: ./gradlew assemble
- name: Run JS Tests
- if: ${{ false }}
- run: ./gradlew cleanTest jsTest
+ run: ./gradlew cleanTest jsLegacyTest
- name: Upload JS test artifact
uses: actions/upload-artifact@v2
if: failure()
diff --git a/README.md b/README.md
index d0d1bbeb0..06e4bbcde 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ The Firebase Kotlin SDK uses Kotlin serialization to read and write custom class
```groovy
plugins {
kotlin("multiplatform") // or kotlin("jvm") or any other kotlin plugin
- kotlin("plugin.serialization") version "1.5.30"
+ kotlin("plugin.serialization") version "1.6.10"
}
```
@@ -96,6 +96,30 @@ You can also omit the serializer but this is discouraged due to a [current limit
data class Post(val timestamp: Double = ServerValue.TIMESTAMP)
```
+Alternatively, `firebase-firestore` also provides a [Timestamp] class which could be used:
+```kotlin
+@Serializable
+data class Post(val timestamp: Timestamp = Timestamp.serverValue())
+```
+
+In addition `firebase-firestore` provides [GeoPoint] and [DocumentReference] classes which allow persisting
+geo points and document references in a native way:
+
+```kotlin
+@Serializable
+data class PointOfInterest(
+ val reference: DocumentReference,
+ val location: GeoPoint,
+ val timestamp: Timestamp
+)
+
+val document = PointOfInterest(
+ reference = Firebase.firestore.collection("foo").document("bar"),
+ location = GeoPoint(51.939, 4.506),
+ timestamp = Timestamp.now()
+)
+```
+
To reduce boilerplate, default arguments are used in the places where the Firebase Android SDK employs the builder pattern:
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 e40e3a4ac..ac0bed0c1 100644
--- a/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt
+++ b/firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt
@@ -13,12 +13,11 @@ import kotlin.collections.set
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
StructureKind.LIST, is PolymorphicKind -> mutableListOf()
.also { value = it }
- .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
+ .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf()
- .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
- StructureKind.CLASS -> mutableMapOf()
+ .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, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
- StructureKind.OBJECT -> FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, _, obj -> value = obj }
+ .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[descriptor.getElementName(index)] = value } }
else -> TODO("Not implemented ${descriptor.kind}")
}
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 2147518ef..a893a5fc1 100644
--- a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt
+++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt
@@ -16,12 +16,12 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer
@Suppress("UNCHECKED_CAST")
-inline fun decode(value: Any?, noinline decodeDouble: (value: Any?) -> Double? = { null }): T {
+inline fun decode(value: Any?): T {
val strategy = serializer()
- return decode(strategy as DeserializationStrategy, value, decodeDouble)
+ return decode(strategy as DeserializationStrategy, value)
}
-fun decode(strategy: DeserializationStrategy, value: Any?, decodeDouble: (value: Any?) -> Double? = { null }): T {
+fun decode(strategy: DeserializationStrategy, value: Any?): T {
require(value != null || strategy.descriptor.isNullable) { "Value was null for non-nullable type ${strategy.descriptor.serialName}" }
return FirebaseDecoder(value).decodeSerializableValue(strategy)
}
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 ebc67fcd9..16d723070 100644
--- a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt
+++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt
@@ -9,11 +9,11 @@ import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.modules.EmptySerializersModule
-fun encode(strategy: SerializationStrategy, value: T, shouldEncodeElementDefault: Boolean, positiveInfinity: Any = Double.POSITIVE_INFINITY): Any? =
- FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply { encodeSerializableValue(strategy, value) }.value
+fun encode(strategy: SerializationStrategy, value: T, shouldEncodeElementDefault: Boolean): Any? =
+ FirebaseEncoder(shouldEncodeElementDefault).apply { encodeSerializableValue(strategy, value) }.value
-inline fun encode(value: T, shouldEncodeElementDefault: Boolean, positiveInfinity: Any = Double.POSITIVE_INFINITY): Any? = value?.let {
- FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
+inline fun encode(value: T, shouldEncodeElementDefault: Boolean): Any? = value?.let {
+ FirebaseEncoder(shouldEncodeElementDefault).apply {
if (it is ValueWithSerializer<*> && it.value is T) {
@Suppress("UNCHECKED_CAST")
(it as ValueWithSerializer).let {
@@ -35,7 +35,7 @@ data class ValueWithSerializer(val value: T, val serializer: SerializationStr
expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder
-class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positiveInfinity: Any) : TimestampEncoder(positiveInfinity), Encoder {
+class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean) : Encoder {
var value: Any? = null
@@ -55,7 +55,7 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
}
override fun encodeDouble(value: Double) {
- this.value = encodeTimestamp(value)
+ this.value = value
}
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
@@ -92,22 +92,14 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
@ExperimentalSerializationApi
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
- FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
-}
-
-abstract class TimestampEncoder(internal val positiveInfinity: Any) {
- fun encodeTimestamp(value: Double) = when(value) {
- Double.POSITIVE_INFINITY -> positiveInfinity
- else -> value
- }
+ FirebaseEncoder(shouldEncodeElementDefault)
}
open class FirebaseCompositeEncoder constructor(
private val shouldEncodeElementDefault: Boolean,
- positiveInfinity: Any,
private val end: () -> Unit = {},
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit
-): TimestampEncoder(positiveInfinity), CompositeEncoder {
+): CompositeEncoder {
override val serializersModule = EmptySerializersModule
@@ -130,7 +122,7 @@ open class FirebaseCompositeEncoder constructor(
descriptor,
index,
value?.let {
- FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
+ FirebaseEncoder(shouldEncodeElementDefault).apply {
encodeSerializableValue(serializer, value)
}.value
}
@@ -144,7 +136,7 @@ open class FirebaseCompositeEncoder constructor(
) = set(
descriptor,
index,
- FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
+ FirebaseEncoder(shouldEncodeElementDefault).apply {
encodeSerializableValue(serializer, value)
}.value
)
@@ -157,7 +149,7 @@ open class FirebaseCompositeEncoder constructor(
override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = set(descriptor, index, value)
- override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, encodeTimestamp(value))
+ override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, value)
override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = set(descriptor, index, value)
@@ -171,7 +163,7 @@ open class FirebaseCompositeEncoder constructor(
@ExperimentalSerializationApi
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder =
- FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
+ FirebaseEncoder(shouldEncodeElementDefault)
}
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 8f9e6674c..25cf06992 100644
--- a/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
+++ b/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
@@ -71,8 +71,9 @@ class EncodersTest {
@Test
fun testEncodeDecodedSealedClass() {
val test = SealedClass.Test("Foo")
- val encoded = encode(test, false)
- val decoded = decode(encoded) as? SealedClass.Test
+ val serializer = SealedClass.Test.serializer() // has to be used because of JS issue
+ val encoded = encode(serializer, test, false)
+ val decoded = decode(serializer, encoded)
assertEquals(test, decoded)
}
@@ -80,8 +81,9 @@ class EncodersTest {
fun testEncodeDecodeGenericClass() {
val test = SealedClass.Test("Foo")
val generic = GenericClass(test)
- val encoded = encode(generic, false)
- val decoded = decode(encoded) as? GenericClass
+ val serializer = GenericClass.serializer(SealedClass.Test.serializer())
+ val encoded = encode(serializer, generic, false)
+ val decoded = decode(serializer, encoded)
assertEquals(generic, decoded)
}
}
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 e40e3a4ac..ac0bed0c1 100644
--- a/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt
+++ b/firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt
@@ -13,12 +13,11 @@ import kotlin.collections.set
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
StructureKind.LIST, is PolymorphicKind -> mutableListOf()
.also { value = it }
- .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
+ .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf()
- .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
- StructureKind.CLASS -> mutableMapOf()
+ .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, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
- StructureKind.OBJECT -> FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, _, obj -> value = obj }
+ .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[descriptor.getElementName(index)] = value } }
else -> TODO("Not implemented ${descriptor.kind}")
}
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 12ec6387e..5256ed97e 100644
--- a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_encoders.kt
+++ b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/_encoders.kt
@@ -13,15 +13,15 @@ import kotlin.js.json
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
StructureKind.LIST, is PolymorphicKind -> Array(descriptor.elementsCount) { null }
.also { value = it }
- .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[index] = value } }
+ .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[index] = value } }
StructureKind.MAP -> {
val map = json()
var lastKey: String = ""
value = map
- FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> if(index % 2 == 0) lastKey = value as String else map[lastKey] = value }
+ FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> if(index % 2 == 0) lastKey = value as String else map[lastKey] = value }
}
StructureKind.CLASS, StructureKind.OBJECT -> json()
.also { value = it }
- .let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
+ .let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[descriptor.getElementName(index)] = value } }
else -> TODO("Not implemented ${descriptor.kind}")
}
diff --git a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt
index 15248dd19..9fd37bea7 100644
--- a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt
+++ b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt
@@ -422,6 +422,8 @@ external object firebase {
fun update(field: FieldPath, value: Any?, vararg moreFieldsAndValues: Any?): Promise
fun delete(): Promise
fun onSnapshot(next: (snapshot: DocumentSnapshot) -> Unit, error: (error: Error) -> Unit): ()->Unit
+
+ fun isEqual(other: DocumentReference): Boolean
}
open class WriteBatch {
@@ -442,13 +444,15 @@ external object firebase {
fun delete(documentReference: DocumentReference): Transaction
}
- open class Timestamp(seconds: Long, nanoseconds: Int) {
+ open class Timestamp(seconds: Double, nanoseconds: Double) {
companion object {
fun now(): Timestamp
}
- val seconds: Long
- val nanoseconds: Int
+ val seconds: Double
+ val nanoseconds: Double
+
+ fun isEqual(other: Timestamp): Boolean
}
open class FieldPath(vararg fieldNames: String) {
companion object {
@@ -459,6 +463,8 @@ external object firebase {
open class GeoPoint(latitude: Double, longitude: Double) {
val latitude: Double
val longitude: Double
+
+ fun isEqual(other: GeoPoint): Boolean
}
abstract class FieldValue {
@@ -468,6 +474,7 @@ external object firebase {
fun arrayUnion(vararg elements: Any): FieldValue
fun serverTimestamp(): FieldValue
}
+ fun isEqual(other: FieldValue): Boolean
}
}
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 e92f95dd3..4e68eca76 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
@@ -7,14 +7,13 @@ package dev.gitlive.firebase.database
import com.google.android.gms.tasks.Task
import com.google.firebase.database.ChildEventListener
import com.google.firebase.database.Logger
-import com.google.firebase.database.ServerValue
import com.google.firebase.database.ValueEventListener
+import dev.gitlive.firebase.encode
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.database.ChildEvent.Type
import dev.gitlive.firebase.decode
import dev.gitlive.firebase.safeOffer
-import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
@@ -27,13 +26,6 @@ import kotlinx.coroutines.tasks.await
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
-@PublishedApi
-internal inline fun encode(value: T, shouldEncodeElementDefault: Boolean) =
- dev.gitlive.firebase.encode(value, shouldEncodeElementDefault, ServerValue.TIMESTAMP)
-
-internal fun encode(strategy: SerializationStrategy , value: T, shouldEncodeElementDefault: Boolean): Any? =
- dev.gitlive.firebase.encode(strategy, value, shouldEncodeElementDefault, ServerValue.TIMESTAMP)
-
suspend fun Task.awaitWhileOnline(): T = coroutineScope {
val notConnected = Firebase.database
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 c3b532c4d..64d3f9522 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
@@ -82,10 +82,6 @@ expect class DataSnapshot {
val children: Iterable
}
-object ServerValue {
- val TIMESTAMP = Double.POSITIVE_INFINITY
-}
-
expect class DatabaseException(message: String?, cause: Throwable?) : RuntimeException
expect class OnDisconnect {
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 9ba833dc6..fdf97be48 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
@@ -6,6 +6,7 @@ package dev.gitlive.firebase.database
import cocoapods.FirebaseDatabase.*
import cocoapods.FirebaseDatabase.FIRDataEventType.*
+import dev.gitlive.firebase.encode
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.database.ChildEvent.Type
@@ -14,7 +15,6 @@ import dev.gitlive.firebase.decode
import dev.gitlive.firebase.safeOffer
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.callbackFlow
@@ -27,13 +27,6 @@ import platform.Foundation.*
import kotlin.collections.component1
import kotlin.collections.component2
-@PublishedApi
-internal inline fun encode(value: T, shouldEncodeElementDefault: Boolean) =
- dev.gitlive.firebase.encode(value, shouldEncodeElementDefault, FIRServerValue.timestamp())
-
-internal fun encode(strategy: SerializationStrategy , value: T, shouldEncodeElementDefault: Boolean): Any? =
- dev.gitlive.firebase.encode(strategy, value, shouldEncodeElementDefault, FIRServerValue.timestamp())
-
actual val Firebase.database
by lazy { FirebaseDatabase(FIRDatabase.database()) }
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 9fbc04bba..92b3dd301 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
@@ -15,14 +15,6 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlin.js.Promise
-@PublishedApi
-internal inline fun encode(value: T, shouldEncodeElementDefault: Boolean) =
- encode(value, shouldEncodeElementDefault, firebase.database.ServerValue.TIMESTAMP)
-
-internal fun encode(strategy: SerializationStrategy, value: T, shouldEncodeElementDefault: Boolean): Any? =
- encode(strategy, value, shouldEncodeElementDefault, firebase.database.ServerValue.TIMESTAMP)
-
-
actual val Firebase.database
get() = rethrow { dev.gitlive.firebase.database; FirebaseDatabase(firebase.database()) }
diff --git a/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt
index dd8e774a8..4563ac81f 100644
--- a/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt
+++ b/firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt
@@ -5,12 +5,9 @@
@file:JvmName("tests")
package dev.gitlive.firebase.firestore
-import dev.gitlive.firebase.*
-import kotlinx.serialization.Serializable
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
-import kotlin.test.*
actual val emulatorHost: String = "10.0.2.2"
@@ -19,64 +16,4 @@ actual val context: Any = InstrumentationRegistry.getInstrumentation().targetCon
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking { test() }
actual fun encodedAsMap(encoded: Any?): Map = encoded as Map
-
-class FirebaseFirestoreAndroidTest {
-
- @BeforeTest
- fun initializeFirebase() {
- Firebase
- .takeIf { Firebase.apps(context).isEmpty() }
- ?.apply {
- initialize(
- context,
- FirebaseOptions(
- applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
- apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
- databaseUrl = "https://fir-kotlin-sdk.firebaseio.com",
- storageBucket = "fir-kotlin-sdk.appspot.com",
- projectId = "fir-kotlin-sdk"
- )
- )
- Firebase.firestore.useEmulator(emulatorHost, 8080)
- }
- }
-
- @Serializable
- data class TestDataWithDocumentReference(
- val uid: String,
- @Serializable(with = FirebaseDocumentReferenceSerializer::class)
- val reference: DocumentReference,
- @Serializable(with = FirebaseReferenceNullableSerializer::class)
- val ref: FirebaseReference?
- )
-
- @Test
- fun encodeDocumentReferenceObject() = runTest {
- val doc = Firebase.firestore.document("a/b")
- val item = TestDataWithDocumentReference("123", doc, FirebaseReference.Value(doc))
- val encoded = encode(item, shouldEncodeElementDefault = false) as Map
- assertEquals("123", encoded["uid"])
- assertEquals(doc.android, encoded["reference"])
- assertEquals(doc.android, encoded["ref"])
- }
-
- @Test
- fun encodeDeleteDocumentReferenceObject() = runTest {
- val doc = Firebase.firestore.document("a/b")
- val item = TestDataWithDocumentReference("123", doc, FirebaseReference.ServerDelete)
- val encoded = encode(item, shouldEncodeElementDefault = false) as Map
- assertEquals("123", encoded["uid"])
- assertEquals(doc.android, encoded["reference"])
- assertEquals(FieldValue.delete, encoded["ref"])
- }
-
- @Test
- fun decodeDocumentReferenceObject() = runTest {
- val doc = Firebase.firestore.document("a/b")
- val obj = mapOf("uid" to "123", "reference" to doc.android, "ref" to doc.android)
- val decoded: TestDataWithDocumentReference = decode(obj)
- assertEquals("123", decoded.uid)
- assertEquals(doc.path, decoded.reference.path)
- assertEquals(doc.path, decoded.ref?.reference?.path)
- }
-}
+actual fun Map.asEncoded(): Any = this
diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
deleted file mode 100644
index 948d218d4..000000000
--- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package dev.gitlive.firebase.firestore
-
-actual val DocumentReference.platformValue: Any get() = android
-actual fun DocumentReference.Companion.fromPlatformValue(platformValue: Any): DocumentReference =
- DocumentReference(platformValue as com.google.firebase.firestore.DocumentReference)
diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
index 6a6cc43c3..447d3da34 100644
--- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
+++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
@@ -1,7 +1,18 @@
package dev.gitlive.firebase.firestore
-actual typealias GeoPoint = com.google.firebase.firestore.GeoPoint
+import kotlinx.serialization.Serializable
-actual fun geoPointWith(latitude: Double, longitude: Double) = GeoPoint(latitude, longitude)
-actual val GeoPoint.latitude: Double get() = latitude
-actual val GeoPoint.longitude: Double get() = longitude
+/** A class representing a platform specific Firebase GeoPoint. */
+actual typealias PlatformGeoPoint = com.google.firebase.firestore.GeoPoint
+
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+actual class GeoPoint internal actual constructor(internal actual val platformValue: PlatformGeoPoint) {
+ actual constructor(latitude: Double, longitude: Double) : this(PlatformGeoPoint(latitude, longitude))
+ actual val latitude: Double = platformValue.latitude
+ actual val longitude: Double = platformValue.longitude
+ override fun equals(other: Any?): Boolean =
+ this === other || other is GeoPoint && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+}
diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
index 1e668e1e1..35978bd5e 100644
--- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
+++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
@@ -1,8 +1,33 @@
package dev.gitlive.firebase.firestore
-actual typealias Timestamp = com.google.firebase.Timestamp
+import kotlinx.serialization.Serializable
-actual fun timestampNow(): Timestamp = Timestamp.now()
-actual fun timestampWith(seconds: Long, nanoseconds: Int) = Timestamp(seconds, nanoseconds)
-actual val Timestamp.seconds: Long get() = seconds
-actual val Timestamp.nanoseconds: Int get() = nanoseconds
+/** A class representing a platform specific Firebase Timestamp. */
+actual typealias PlatformTimestamp = com.google.firebase.Timestamp
+
+/** A base class that could be used to combine [Timestamp] and [Timestamp.ServerTimestamp] in the same field. */
+@Serializable(with = BaseTimestampSerializer::class)
+actual sealed class BaseTimestamp
+
+/** A class representing a Firebase Timestamp. */
+@Serializable(with = TimestampSerializer::class)
+actual class Timestamp internal actual constructor(
+ internal actual val platformValue: PlatformTimestamp
+): BaseTimestamp() {
+ actual constructor(seconds: Long, nanoseconds: Int) : this(PlatformTimestamp(seconds, nanoseconds))
+
+ actual val seconds: Long = platformValue.seconds
+ actual val nanoseconds: Int = platformValue.nanoseconds
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is Timestamp && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+
+ actual companion object {
+ actual fun now(): Timestamp = Timestamp(PlatformTimestamp.now())
+ }
+
+ /** A server time timestamp. */
+ actual object ServerTimestamp: BaseTimestamp()
+}
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
index 5829aa14e..2b50b16e0 100644
--- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt
+++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt
@@ -1,13 +1,11 @@
package dev.gitlive.firebase.firestore
-import com.google.firebase.Timestamp
import com.google.firebase.firestore.FieldValue
-import com.google.firebase.firestore.GeoPoint
actual fun isSpecialValue(value: Any) = when(value) {
is FieldValue,
- is GeoPoint,
- is Timestamp,
- is DocumentReference -> true
+ is PlatformGeoPoint,
+ is PlatformTimestamp,
+ is PlatformDocumentReference -> 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 8424f5a35..ac6c80a40 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,7 +5,6 @@
@file:JvmName("android")
package dev.gitlive.firebase.firestore
-import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.SetOptions
import dev.gitlive.firebase.*
import kotlinx.coroutines.channels.awaitClose
@@ -13,15 +12,9 @@ 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
-@PublishedApi
-internal inline fun decode(value: Any?): T =
- decode(value) { (it as? Timestamp)?.run { seconds * 1000 + (nanoseconds / 1000000.0) } }
-
-internal fun decode(strategy: DeserializationStrategy, value: Any?): T =
- decode(strategy, value) { (it as? Timestamp)?.run { seconds * 1000 + (nanoseconds / 1000000.0) } }
-
actual val Firebase.firestore get() =
FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance())
@@ -107,10 +100,12 @@ actual class WriteBatch(val android: com.google.firebase.firestore.WriteBatch) {
merge: Boolean,
vararg fieldsAndValues: Pair
): WriteBatch {
- val serializedItem = encodeAsMap(strategy, data, encodeDefaults)
- val serializedFieldAndValues = encodeAsMap(fieldsAndValues = fieldsAndValues)
+ val serializedItem = encode(strategy, data, encodeDefaults) as Map
+ val serializedFieldAndValues = fieldsAndValues.associate { (field, value) ->
+ field to encode(value, encodeDefaults)
+ }
- val result = serializedItem + (serializedFieldAndValues ?: emptyMap())
+ val result = serializedItem + serializedFieldAndValues
if (merge) {
android.set(documentRef.android, result, SetOptions.merge())
} else {
@@ -142,10 +137,12 @@ actual class WriteBatch(val android: com.google.firebase.firestore.WriteBatch) {
encodeDefaults: Boolean,
vararg fieldsAndValues: Pair
): WriteBatch {
- val serializedItem = encodeAsMap(strategy, data, encodeDefaults)
- val serializedFieldAndValues = encodeAsMap(fieldsAndValues = fieldsAndValues)
+ val serializedItem = encode(strategy, data, encodeDefaults) as Map
+ val serializedFieldAndValues = fieldsAndValues.associate { (field, value) ->
+ field to encode(value, encodeDefaults)
+ }
- val result = serializedItem + (serializedFieldAndValues ?: emptyMap())
+ val result = serializedItem + serializedFieldAndValues
return android.update(documentRef.android, result).let { this }
}
@@ -244,7 +241,12 @@ actual class Transaction(val android: com.google.firebase.firestore.Transaction)
DocumentSnapshot(android.get(documentRef.android))
}
-actual class DocumentReference(val android: com.google.firebase.firestore.DocumentReference) {
+/** A class representing a platform specific Firebase DocumentReference. */
+actual typealias PlatformDocumentReference = com.google.firebase.firestore.DocumentReference
+
+@Serializable(with = DocumentReferenceSerializer::class)
+actual class DocumentReference actual constructor(internal actual val platformValue: PlatformDocumentReference) {
+ val android: PlatformDocumentReference = platformValue
actual val id: String
get() = android.id
@@ -326,7 +328,10 @@ actual class DocumentReference(val android: com.google.firebase.firestore.Docume
awaitClose { listener.remove() }
}
- actual companion object
+ override fun equals(other: Any?): Boolean =
+ this === other || other is DocumentReference && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
}
actual open class Query(open val android: com.google.firebase.firestore.Query) {
@@ -492,10 +497,26 @@ actual class FieldPath private constructor(val android: com.google.firebase.fire
actual val documentId: FieldPath get() = FieldPath(com.google.firebase.firestore.FieldPath.documentId())
}
-actual object FieldValue {
- actual fun serverTimestamp(): Any = FieldValue.serverTimestamp()
- actual val delete: Any get() = FieldValue.delete()
- actual fun arrayUnion(vararg elements: Any): Any = FieldValue.arrayUnion(*elements)
- actual fun arrayRemove(vararg elements: Any): Any = FieldValue.arrayRemove(*elements)
- actual fun delete(): Any = delete
+/** A class representing a platform specific Firebase FieldValue. */
+internal typealias PlatformFieldValue = com.google.firebase.firestore.FieldValue
+
+/** A class representing a Firebase FieldValue. */
+@Serializable(with = FieldValueSerializer::class)
+actual class FieldValue internal actual constructor(internal actual val platformValue: Any) {
+ init {
+ require(platformValue is PlatformFieldValue)
+ }
+ override fun equals(other: Any?): Boolean =
+ this === other || other is FieldValue && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+
+ actual companion object {
+ actual val delete: FieldValue get() = FieldValue(PlatformFieldValue.delete())
+ actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(PlatformFieldValue.arrayUnion(*elements))
+ actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(PlatformFieldValue.arrayRemove(*elements))
+ actual fun serverTimestamp(): FieldValue = FieldValue(PlatformFieldValue.serverTimestamp())
+ @Deprecated("Replaced with FieldValue.delete", replaceWith = ReplaceWith("delete"))
+ actual fun delete(): FieldValue = delete
+ }
}
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceSerializer.kt
similarity index 77%
rename from firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
rename to firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceSerializer.kt
index 571ee10db..5e8d51b3a 100644
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceSerializer.kt
@@ -2,6 +2,7 @@ package dev.gitlive.firebase.firestore
import dev.gitlive.firebase.*
import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.Decoder
@@ -9,16 +10,10 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
-/** Platform specific value of the document reference. */
-internal expect val DocumentReference.platformValue: Any
-/** Constructs [DocumentReference] from a platform specific value. */
-internal expect fun DocumentReference.Companion.fromPlatformValue(platformValue: Any): DocumentReference
-
/**
* A serializer for [DocumentReference]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms.
- *
*/
-object FirebaseDocumentReferenceSerializer : KSerializer {
+object DocumentReferenceSerializer : KSerializer {
override val descriptor = buildClassSerialDescriptor("DocumentReference") {
element("path")
@@ -38,7 +33,10 @@ object FirebaseDocumentReferenceSerializer : KSerializer {
override fun deserialize(decoder: Decoder): DocumentReference {
return if (decoder is FirebaseDecoder) {
// special case if decoding. Firestore encodes and decodes DocumentReferences without use of serializers
- DocumentReference.fromPlatformValue(requireNotNull(decoder.value))
+ when (val value = decoder.value) {
+ is PlatformDocumentReference -> DocumentReference(value)
+ else -> throw SerializationException("Cannot deserialize $value")
+ }
} else {
decoder.decodeStructure(descriptor) {
val path = decodeStringElement(descriptor, 0)
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..e3f58203a
--- /dev/null
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FieldValueSerializer.kt
@@ -0,0 +1,33 @@
+package dev.gitlive.firebase.firestore
+
+import dev.gitlive.firebase.FirebaseDecoder
+import dev.gitlive.firebase.FirebaseEncoder
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+/** A serializer for [FieldValue]. Must be used in conjunction with [FirebaseEncoder]. */
+object FieldValueSerializer : KSerializer {
+ override val descriptor = buildClassSerialDescriptor("FieldValue") { }
+
+ override fun serialize(encoder: Encoder, value: FieldValue) {
+ if (encoder is FirebaseEncoder) {
+ encoder.value = value.platformValue
+ } else {
+ throw IllegalArgumentException("This serializer must be used with FirebaseEncoder")
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): FieldValue {
+ return if (decoder is FirebaseDecoder) {
+ when (val value = decoder.value) {
+ null -> throw SerializationException("Cannot deserialize $value")
+ else -> FieldValue(value)
+ }
+ } else {
+ throw IllegalArgumentException("This serializer must be used with FirebaseDecoder")
+ }
+ }
+}
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseReference.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseReference.kt
deleted file mode 100644
index 6a2a76f1c..000000000
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseReference.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package dev.gitlive.firebase.firestore
-
-/**
- * A wrapper object for [DocumentReference] which allows to store a reference value and
- * perform a field deletion using the same field.
- */
-@Deprecated("Consider using DocumentReference instead")
-sealed class FirebaseReference {
- data class Value(val value: DocumentReference) : FirebaseReference()
- object ServerDelete : FirebaseReference()
-}
-
-val FirebaseReference.reference: DocumentReference? get() = when (this) {
- is FirebaseReference.Value -> value
- is FirebaseReference.ServerDelete -> null
-}
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseTimestamp.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseTimestamp.kt
deleted file mode 100644
index 9e72b80db..000000000
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseTimestamp.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package dev.gitlive.firebase.firestore
-
-/**
- * A wrapper object for [Timestamp] which allows to store a timestamp value, set a server timestamp and
- * perform a field deletion using the same field.
- */
-sealed class FirebaseTimestamp {
- data class Value(val value: Timestamp) : FirebaseTimestamp()
- object ServerValue : FirebaseTimestamp()
- @Deprecated("Consider using DocumentReference.update with FieldValue.delete")
- object ServerDelete : FirebaseTimestamp()
-}
-
-val FirebaseTimestamp.timestamp: Timestamp? get() = when (this) {
- is FirebaseTimestamp.Value -> value
- is FirebaseTimestamp.ServerValue,
- is FirebaseTimestamp.ServerDelete -> null
-}
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseTimestampSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseTimestampSerializer.kt
deleted file mode 100644
index 1350198f9..000000000
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseTimestampSerializer.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-package dev.gitlive.firebase.firestore
-
-import dev.gitlive.firebase.FirebaseDecoder
-import dev.gitlive.firebase.FirebaseEncoder
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerializationException
-import kotlinx.serialization.descriptors.element
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-import kotlinx.serialization.encoding.decodeStructure
-import kotlinx.serialization.encoding.encodeStructure
-
-sealed class AbstractFirebaseTimestampSerializer(
- private val isNullable: Boolean
-) : KSerializer {
- override val descriptor = buildClassSerialDescriptor("Timestamp", isNullable = isNullable) {
- isNullable = this@AbstractFirebaseTimestampSerializer.isNullable
- element("seconds")
- element("nanoseconds")
- }
-
- protected fun encode(encoder: Encoder, value: Any?) {
- when(value) {
- is Timestamp -> encodeTimestamp(encoder, value)
- null -> encodeTimestamp(encoder, null)
- else -> {
- if (isSpecialValue(value) && encoder is FirebaseEncoder) {
- encoder.value = value
- } else {
- throw SerializationException("Cannot serialize $value")
- }
- }
- }
- }
-
- private fun encodeTimestamp(encoder: Encoder, value: Timestamp?) {
- require(value != null || isNullable)
-
- if (encoder is FirebaseEncoder) {
- // special case if encoding. Firestore encodes and decodes Timestamp without use of serializers
- encoder.value = value
- } else {
- if (value != null) {
- encoder.encodeStructure(descriptor) {
- encodeLongElement(descriptor, 0, value.seconds)
- encodeIntElement(descriptor, 1, value.nanoseconds)
- }
- } else {
- encoder.encodeNull()
- }
- }
- }
-
- protected fun decode(decoder: Decoder): Timestamp? {
- return if (decoder is FirebaseDecoder) {
- // special case if decoding. Firestore encodes and decodes Timestamp without use of serializers
- when (val value = decoder.value) {
- null -> null
- is Timestamp -> value
- else -> {
- if (isSpecialValue(value)) {
- null
- } else {
- throw SerializationException("Cannot deserialize $value")
- }
- }
- }
- } else {
- decoder.decodeStructure(descriptor) {
- timestampWith(
- seconds = decodeLongElement(descriptor, 0),
- nanoseconds = decodeIntElement(descriptor, 1)
- )
- }
- }
- }
-}
-
-/** A serializer for [Timestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
-object FirebaseTimestampSerializer : AbstractFirebaseTimestampSerializer(
- isNullable = false
-) {
- override fun serialize(encoder: Encoder, value: Any) = encode(encoder, value)
- override fun deserialize(decoder: Decoder): Any = requireNotNull(decode(decoder))
-}
-
-/**
- * A serializer for [Timestamp] which decodes `null` in case of deserialization errors.
- * If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms.
- */
-object FirebaseNullableTimestampSerializer : AbstractFirebaseTimestampSerializer(
- isNullable = true
-) {
- override fun serialize(encoder: Encoder, value: Any?) = encode(encoder, value)
-
- override fun deserialize(decoder: Decoder): Any? {
- return try {
- decode(decoder)
- } catch (exception: SerializationException) {
- null
- }
- }
-}
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 037264ad7..f95657651 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,8 +1,15 @@
package dev.gitlive.firebase.firestore
-/** A class representing a Firebase GeoPoint. */
-expect class GeoPoint
+import kotlinx.serialization.Serializable
+
+/** A class representing a platform specific Firebase GeoPoint. */
+expect class PlatformGeoPoint
-expect fun geoPointWith(latitude: Double, longitude: Double): GeoPoint
-expect val GeoPoint.latitude: Double
-expect val GeoPoint.longitude: Double
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+expect class GeoPoint internal constructor(platformValue: PlatformGeoPoint) {
+ constructor(latitude: Double, longitude: Double)
+ val latitude: Double
+ val longitude: Double
+ internal val platformValue: PlatformGeoPoint
+}
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseGeoPointSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPointSerializer.kt
similarity index 81%
rename from firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseGeoPointSerializer.kt
rename to firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPointSerializer.kt
index 702a5517c..2aaa36ee8 100644
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/FirebaseGeoPointSerializer.kt
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPointSerializer.kt
@@ -3,6 +3,7 @@ package dev.gitlive.firebase.firestore
import dev.gitlive.firebase.FirebaseDecoder
import dev.gitlive.firebase.FirebaseEncoder
import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.Decoder
@@ -11,7 +12,7 @@ import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
/** Serializer for [GeoPoint].If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
-object FirebaseGeoPointSerializer : KSerializer {
+object GeoPointSerializer : KSerializer {
override val descriptor = buildClassSerialDescriptor("GeoPoint") {
element("latitude")
element("longitude")
@@ -20,7 +21,7 @@ object FirebaseGeoPointSerializer : KSerializer {
override fun serialize(encoder: Encoder, value: GeoPoint) {
if (encoder is FirebaseEncoder) {
// special case if encoding. Firestore encodes and decodes GeoPoints without use of serializers
- encoder.value = value
+ encoder.value = value.platformValue
} else {
encoder.encodeStructure(descriptor) {
encodeDoubleElement(descriptor, 0, value.latitude)
@@ -32,10 +33,13 @@ object FirebaseGeoPointSerializer : KSerializer {
override fun deserialize(decoder: Decoder): GeoPoint {
return if (decoder is FirebaseDecoder) {
// special case if decoding. Firestore encodes and decodes GeoPoints without use of serializers
- decoder.value as GeoPoint
+ when (val value = decoder.value) {
+ is PlatformGeoPoint -> GeoPoint(value)
+ else -> throw SerializationException("Cannot deserialize $value")
+ }
} else {
decoder.decodeStructure(descriptor) {
- geoPointWith(
+ GeoPoint(
latitude = decodeDoubleElement(descriptor, 0),
longitude = decodeDoubleElement(descriptor, 1)
)
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/ReferenceSerializer.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/ReferenceSerializer.kt
deleted file mode 100644
index ecc0b9982..000000000
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/ReferenceSerializer.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package dev.gitlive.firebase.firestore
-
-import dev.gitlive.firebase.*
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.descriptors.element
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-
-sealed class AbstractFirebaseReferenceSerializer(
- private val isNullable: Boolean
-) : KSerializer {
-
- override val descriptor = buildClassSerialDescriptor("DocumentReference", isNullable = isNullable) {
- isNullable = this@AbstractFirebaseReferenceSerializer.isNullable
- element("path")
- }
-
- protected fun encode(encoder: Encoder, value: FirebaseReference?) {
- if (encoder is FirebaseEncoder) {
- encoder.value = value?.let {
- when(value) {
- is FirebaseReference.Value -> value.value.platformValue
- is FirebaseReference.ServerDelete -> FieldValue.delete
- }
- }
- } else {
- throw IllegalArgumentException("This serializer must be used with FirebaseEncoder")
- }
- }
-
- protected fun decode(decoder: Decoder): FirebaseReference? {
- return if (decoder is FirebaseDecoder) {
- decoder.value
- ?.let(DocumentReference::fromPlatformValue)
- ?.let(FirebaseReference::Value)
- } else {
- throw IllegalArgumentException("This serializer must be used with FirebaseDecoder")
- }
- }
-}
-
-/** A nullable serializer for [FirebaseReference]. */
-@Deprecated("Consider using DocumentReference and FirebaseDocumentReferenceSerializer instead")
-object FirebaseReferenceNullableSerializer : AbstractFirebaseReferenceSerializer(
- isNullable = true
-) {
- override fun serialize(encoder: Encoder, value: FirebaseReference?) = encode(encoder, value)
- override fun deserialize(decoder: Decoder): FirebaseReference? = decode(decoder)
-}
-
-/** A serializer for [FirebaseReference]. */
-@Deprecated("Consider using DocumentReference and FirebaseDocumentReferenceSerializer instead")
-object FirebaseReferenceSerializer : AbstractFirebaseReferenceSerializer(
- isNullable = false
-) {
- override fun serialize(encoder: Encoder, value: FirebaseReference) = encode(encoder, value)
- override fun deserialize(decoder: Decoder): FirebaseReference = requireNotNull(decode(decoder))
-}
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 37d81a8aa..ede143571 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,9 +1,27 @@
package dev.gitlive.firebase.firestore
+import kotlinx.serialization.Serializable
+
+/** A class representing a platform specific Firebase Timestamp. */
+expect class PlatformTimestamp
+
+/** A base class that could be used to combine [Timestamp] and [Timestamp.ServerTimestamp] in the same field. */
+@Serializable(with = BaseTimestampSerializer::class)
+expect sealed class BaseTimestamp
+
/** A class representing a Firebase Timestamp. */
-expect class Timestamp
+@Serializable(with = TimestampSerializer::class)
+expect class Timestamp internal constructor(platformValue: PlatformTimestamp): BaseTimestamp {
+ constructor(seconds: Long, nanoseconds: Int)
+ val seconds: Long
+ val nanoseconds: Int
+
+ internal val platformValue: PlatformTimestamp
-expect fun timestampNow(): Timestamp
-expect fun timestampWith(seconds: Long, nanoseconds: Int): Timestamp
-expect val Timestamp.seconds: Long
-expect val Timestamp.nanoseconds: Int
+ companion object {
+ /** @return a local time timestamp. */
+ fun now(): Timestamp
+ }
+ /** A server time timestamp. */
+ object ServerTimestamp: BaseTimestamp
+}
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
index 5dbfb463b..28646ab53 100644
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/TimestampSerializer.kt
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/TimestampSerializer.kt
@@ -4,63 +4,105 @@ import dev.gitlive.firebase.FirebaseDecoder
import dev.gitlive.firebase.FirebaseEncoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.decodeStructure
+import kotlinx.serialization.encoding.encodeStructure
-sealed class AbstractTimestampSerializer(
- private val isNullable: Boolean
-) : KSerializer {
- override val descriptor = buildClassSerialDescriptor("Timestamp", isNullable = isNullable) {
- isNullable = this@AbstractTimestampSerializer.isNullable
+/** A serializer for [BaseTimestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
+object BaseTimestampSerializer : KSerializer {
+ override val descriptor = buildClassSerialDescriptor("Timestamp") {
element("seconds")
element("nanoseconds")
+ element("isServerTimestamp")
}
- protected fun encode(encoder: Encoder, value: FirebaseTimestamp?) {
+ override fun serialize(encoder: Encoder, value: BaseTimestamp) {
if (encoder is FirebaseEncoder) {
- encoder.value = value?.let {
- when(value) {
- is FirebaseTimestamp.Value -> value.value
- is FirebaseTimestamp.ServerValue -> FieldValue.serverTimestamp()
- is FirebaseTimestamp.ServerDelete -> FieldValue.delete
- }
+ // special case if encoding. Firestore encodes and decodes Timestamp without use of serializers
+ encoder.value = when (value) {
+ Timestamp.ServerTimestamp -> FieldValue.serverTimestamp().platformValue
+ is Timestamp -> value.platformValue
+ else -> throw SerializationException("Cannot serialize $value")
}
} else {
- throw IllegalArgumentException("This serializer must be used with FirebaseEncoder")
+ encoder.encodeStructure(descriptor) {
+ when (value) {
+ Timestamp.ServerTimestamp -> {
+ encodeLongElement(descriptor, 0, 0)
+ encodeIntElement(descriptor, 1, 0)
+ encodeBooleanElement(descriptor, 2, true)
+ }
+ is Timestamp -> {
+ encodeLongElement(descriptor, 0, value.seconds)
+ encodeIntElement(descriptor, 1, value.nanoseconds)
+ encodeBooleanElement(descriptor, 2, false)
+ }
+ else -> throw SerializationException("Cannot serialize $value")
+ }
+ }
}
}
- protected fun decode(decoder: Decoder): FirebaseTimestamp? {
+ override fun deserialize(decoder: Decoder): BaseTimestamp {
return if (decoder is FirebaseDecoder) {
- (decoder.value as? Timestamp)?.let(FirebaseTimestamp::Value)
+ // special case if decoding. Firestore encodes and decodes Timestamp without use of serializers
+ when (val value = decoder.value) {
+ is PlatformTimestamp -> Timestamp(value)
+ FieldValue.serverTimestamp().platformValue -> Timestamp.ServerTimestamp
+ else -> throw SerializationException("Cannot deserialize $value")
+ }
} else {
- throw IllegalArgumentException("This serializer must be used with FirebaseDecoder")
+ decoder.decodeStructure(descriptor) {
+ if (decodeBooleanElement(descriptor, 2)) {
+ Timestamp.ServerTimestamp
+ } else {
+ Timestamp(
+ seconds = decodeLongElement(descriptor, 0),
+ nanoseconds = decodeIntElement(descriptor, 1)
+ )
+ }
+ }
}
}
}
-/** A nullable serializer for [FirebaseTimestamp]. */
-object TimestampNullableSerializer : AbstractTimestampSerializer(
- isNullable = true
-) {
-
- override fun serialize(encoder: Encoder, value: FirebaseTimestamp?) = encode(encoder, value)
+/** A serializer for [Timestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
+object TimestampSerializer : KSerializer {
+ override val descriptor = buildClassSerialDescriptor("Timestamp") {
+ element("seconds")
+ element("nanoseconds")
+ }
- override fun deserialize(decoder: Decoder): FirebaseTimestamp? {
- return try {
- decode(decoder)
- } catch (exception: SerializationException) {
- null
+ override fun serialize(encoder: Encoder, value: Timestamp) {
+ if (encoder is FirebaseEncoder) {
+ // special case if encoding. Firestore encodes and decodes Timestamp without use of serializers
+ encoder.value = value.platformValue
+ } else {
+ encoder.encodeStructure(descriptor) {
+ encodeLongElement(descriptor, 0, value.seconds)
+ encodeIntElement(descriptor, 1, value.nanoseconds)
+ }
}
}
-}
-/** A nullable serializer for [FirebaseTimestamp]. */
-object TimestampSerializer : AbstractTimestampSerializer(
- isNullable = false
-) {
- override fun serialize(encoder: Encoder, value: FirebaseTimestamp) = encode(encoder, value)
- override fun deserialize(decoder: Decoder): FirebaseTimestamp = requireNotNull(decode(decoder))
+ override fun deserialize(decoder: Decoder): Timestamp {
+ return if (decoder is FirebaseDecoder) {
+ // special case if decoding. Firestore encodes and decodes Timestamp without use of serializers
+ when (val value = decoder.value) {
+ is PlatformTimestamp -> Timestamp(value)
+ else -> throw SerializationException("Cannot deserialize $value")
+ }
+ } else {
+ decoder.decodeStructure(descriptor) {
+ Timestamp(
+ seconds = decodeLongElement(descriptor, 0),
+ nanoseconds = decodeIntElement(descriptor, 1)
+ )
+ }
+ }
+ }
}
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
index f204f07b0..e9625f75d 100644
--- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/encoders.kt
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/encoders.kt
@@ -1,7 +1,5 @@
package dev.gitlive.firebase.firestore
-import kotlinx.serialization.SerializationStrategy
-
/** @return whether value is special and shouldn't be encoded/decoded. */
@PublishedApi
internal expect fun isSpecialValue(value: Any): Boolean
@@ -11,28 +9,5 @@ internal inline fun encode(value: T, shouldEncodeElementDefault: Boo
if (value?.let(::isSpecialValue) == true) {
value
} else {
- dev.gitlive.firebase.encode(value, shouldEncodeElementDefault, FieldValue.serverTimestamp())
+ dev.gitlive.firebase.encode(value, shouldEncodeElementDefault)
}
-
-@PublishedApi
-internal fun encode(strategy: SerializationStrategy, value: T, shouldEncodeElementDefault: Boolean): Any? =
- dev.gitlive.firebase.encode(
- strategy,
- value,
- shouldEncodeElementDefault,
- FieldValue.serverTimestamp()
- )
-
-@PublishedApi
-internal fun encodeAsMap(
- strategy: SerializationStrategy,
- data: T,
- encodeDefaults: Boolean = false
-): Map = encode(strategy, data, encodeDefaults) as Map
-
-@PublishedApi
-internal fun encodeAsMap(
- encodeDefaults: Boolean = false,
- vararg fieldsAndValues: Pair
-): Map? = fieldsAndValues.takeUnless { fieldsAndValues.isEmpty() }
- ?.associate { (field, value) -> field to encode(value, encodeDefaults) }
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 eb7b49e05..4015d5865 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
@@ -9,6 +9,7 @@ import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.FirebaseException
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationStrategy
import kotlin.js.JsName
@@ -116,7 +117,13 @@ expect class WriteBatch {
suspend fun commit()
}
-expect class DocumentReference {
+/** A class representing a platform specific Firebase DocumentReference. */
+expect class PlatformDocumentReference
+
+/** A class representing a Firebase DocumentReference. */
+@Serializable(with = DocumentReferenceSerializer::class)
+expect class DocumentReference internal constructor(platformValue: PlatformDocumentReference) {
+ internal val platformValue: PlatformDocumentReference
val id: String
val path: String
@@ -140,8 +147,6 @@ expect class DocumentReference {
suspend fun update(vararg fieldsAndValues: Pair)
suspend fun delete()
-
- companion object
}
expect class CollectionReference : Query {
@@ -235,12 +240,33 @@ expect class FieldPath(vararg fieldNames: String) {
val documentId: FieldPath
}
-expect object FieldValue {
- val delete: Any
- fun arrayUnion(vararg elements: Any): Any
- fun arrayRemove(vararg elements: Any): Any
- fun serverTimestamp(): Any
- @Deprecated("Replaced with FieldValue.delete")
- @JsName("deprecatedDelete")
- fun delete(): Any
+/** A class representing a Firebase FieldValue. */
+@Serializable(with = FieldValueSerializer::class)
+expect class FieldValue internal constructor(platformValue: Any) {
+ // implementation note. unfortunately declaring a common `expect PlatformFieldValue`
+ // is not possible due to different platform class signatures
+ internal val platformValue: Any
+
+ companion object {
+ val delete: FieldValue
+ fun arrayUnion(vararg elements: Any): FieldValue
+ fun arrayRemove(vararg elements: Any): FieldValue
+ fun serverTimestamp(): FieldValue
+
+ @Deprecated("Replaced with FieldValue.delete")
+ @JsName("deprecatedDelete")
+ fun delete(): FieldValue
+ }
+}
+
+@Serializable
+internal sealed class FieldValueRepresentation(val isSerializable: Boolean) {
+ @Serializable
+ object Delete : FieldValueRepresentation(true)
+ @Serializable
+ object Union : FieldValueRepresentation(false) // TODO use json to serialize?
+ @Serializable
+ object Remove : FieldValueRepresentation(false)
+ @Serializable
+ object ServerTimestamp : FieldValueRepresentation(true)
}
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
new file mode 100644
index 000000000..34cc5f83d
--- /dev/null
+++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FieldValueTests.kt
@@ -0,0 +1,24 @@
+package dev.gitlive.firebase.firestore
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+class FieldValueTests {
+ @Test
+ fun equalityChecks() = runTest {
+ assertEquals(FieldValue.delete, FieldValue.delete)
+ assertEquals(FieldValue.delete(), FieldValue.delete())
+ assertEquals(FieldValue.serverTimestamp(), FieldValue.serverTimestamp())
+ assertNotEquals(FieldValue.delete, FieldValue.serverTimestamp())
+
+ // Note: arrayUnion and arrayRemove can't be checked due to vararg to array conversion
+// assertEquals(FieldValue.arrayUnion(1, 2, 3), FieldValue.arrayUnion(1, 2, 3))
+// assertNotEquals(FieldValue.arrayUnion(1, 2, 3), FieldValue.arrayUnion(1, 2, 3, 4))
+//
+// assertEquals(FieldValue.arrayRemove(1, 2, 3), FieldValue.arrayRemove(1, 2, 3, 4))
+// assertNotEquals(FieldValue.arrayRemove(1, 2, 3), FieldValue.arrayRemove(1, 2, 3, 4))
+//
+// assertNotEquals(FieldValue.arrayUnion(1, 2, 3), FieldValue.arrayRemove(1, 2, 3))
+ }
+}
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 b1b71ecdf..a11e3982a 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
@@ -9,7 +9,6 @@ import kotlin.test.assertEquals
@Serializable
data class TestDataWithGeoPoint(
val uid: String,
- @Serializable(with = FirebaseGeoPointSerializer::class)
val location: GeoPoint
)
@@ -18,20 +17,24 @@ class GeoPointTests {
@Test
fun encodeGeoPointObject() = runTest {
- val geoPoint = geoPointWith(12.3, 45.6)
+ val geoPoint = GeoPoint(12.3, 45.6)
val item = TestDataWithGeoPoint("123", geoPoint)
- val encoded = encode(item, shouldEncodeElementDefault = false)
- val encodedMap = encodedAsMap(encoded)
- assertEquals("123", encodedMap["uid"])
- assertEquals(geoPoint, encodedMap["location"])
+ val encoded = encodedAsMap(encode(item, shouldEncodeElementDefault = false))
+ assertEquals("123", encoded["uid"])
+ // check GeoPoint is encoded to a platform representation
+ assertEquals(geoPoint.platformValue, encoded["location"])
}
@Test
fun decodeGeoPointObject() = runTest {
- val geoPoint = geoPointWith(12.3, 45.6)
- val obj = mapOf("uid" to "123", "location" to geoPoint)
+ val geoPoint = GeoPoint(12.3, 45.6)
+ val obj = mapOf(
+ "uid" to "123",
+ "location" to geoPoint.platformValue
+ ).asEncoded()
val decoded: TestDataWithGeoPoint = decode(obj)
assertEquals("123", decoded.uid)
+ // check a platform GeoPoint is properly wrapped
assertEquals(geoPoint, decoded.location)
}
}
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 fb6708edc..6aa131c51 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
@@ -2,90 +2,91 @@ package dev.gitlive.firebase.firestore
import dev.gitlive.firebase.decode
import dev.gitlive.firebase.encode
-import kotlin.test.Test
-import kotlin.test.assertEquals
import kotlinx.serialization.Serializable
+import kotlin.test.*
@Serializable
data class TestData(
val uid: String,
- @Serializable(with = FirebaseTimestampSerializer::class)
- val createdAt: Any,
- @Serializable(with = FirebaseNullableTimestampSerializer::class)
- var updatedAt: Any?,
- @Serializable(with = TimestampNullableSerializer::class)
- var deletedAt: FirebaseTimestamp?
+ val createdAt: Timestamp,
+ var updatedAt: BaseTimestamp,
+ val deletedAt: BaseTimestamp?
)
@Suppress("UNCHECKED_CAST")
class TimestampTests {
+ @Test
+ fun testEquality() = runTest {
+ val timestamp = Timestamp(123, 456)
+ assertEquals(timestamp, Timestamp(123, 456))
+ assertNotEquals(timestamp, Timestamp(123, 457))
+ assertNotEquals(timestamp, Timestamp(124, 456))
+ assertNotEquals(timestamp, Timestamp.now())
+ assertEquals(Timestamp.ServerTimestamp, Timestamp.ServerTimestamp)
+ }
@Test
fun encodeTimestampObject() = runTest {
- val timestamp = timestampWith(123, 456)
- val item = TestData("uid123", timestamp, null, FirebaseTimestamp.Value(timestamp))
- val encoded = encode(item, shouldEncodeElementDefault = false) as Map
+ val timestamp = Timestamp(123, 456)
+ val item = TestData("uid123", timestamp, timestamp, null)
+ val encoded = encodedAsMap(encode(item, shouldEncodeElementDefault = false))
assertEquals("uid123", encoded["uid"])
- assertEquals(timestamp, encoded["createdAt"])
- assertEquals(timestamp, encoded["deletedAt"])
+ // NOTE: wrapping is required because JS does not override equals
+ assertEquals(timestamp, Timestamp(encoded["createdAt"] as PlatformTimestamp))
+ assertEquals(timestamp, Timestamp(encoded["updatedAt"] as PlatformTimestamp))
+ assertNull(encoded["deletedAt"])
}
@Test
fun encodeServerTimestampObject() = runTest {
- val timestamp = FieldValue.serverTimestamp()
- val item = TestData("uid123", timestamp, null, FirebaseTimestamp.ServerValue)
- val encoded = encode(item, shouldEncodeElementDefault = false) as Map
+ val timestamp = Timestamp(123, 456)
+ val item = TestData("uid123", timestamp, Timestamp.ServerTimestamp, Timestamp.ServerTimestamp)
+ val encoded = encodedAsMap(encode(item, shouldEncodeElementDefault = false))
assertEquals("uid123", encoded["uid"])
- assertEquals(timestamp, encoded["createdAt"])
- assertEquals(FieldValue.serverTimestamp(), encoded["deletedAt"])
+ assertEquals(timestamp, Timestamp(encoded["createdAt"] as PlatformTimestamp))
+ assertEquals(FieldValue.serverTimestamp(), FieldValue(encoded["updatedAt"]!!))
+ assertEquals(FieldValue.serverTimestamp(), FieldValue(encoded["deletedAt"]!!))
}
@Test
fun decodeTimestampObject() = runTest {
- val timestamp = timestampWith(123, 345)
- val obj = mapOf("uid" to "uid123", "createdAt" to timestamp, "deletedAt" to timestamp)
+ val timestamp = Timestamp(123, 345)
+ val obj = mapOf(
+ "uid" to "uid123",
+ "createdAt" to timestamp.platformValue,
+ "updatedAt" to timestamp.platformValue,
+ "deletedAt" to timestamp.platformValue
+ ).asEncoded()
val decoded: TestData = decode(obj)
assertEquals("uid123", decoded.uid)
- assertEquals(timestamp, decoded.createdAt)
- val createdAt: Timestamp = timestamp
- assertEquals(123, createdAt.seconds)
- assertEquals(345, createdAt.nanoseconds)
- val deletedAt: Timestamp? = decoded.deletedAt?.timestamp
- assertEquals(123, deletedAt?.seconds)
- assertEquals(345, deletedAt?.nanoseconds)
+ with(decoded.createdAt) {
+ assertEquals(timestamp, this)
+ assertEquals(123, seconds)
+ assertEquals(345, nanoseconds)
+ }
+ with(decoded.updatedAt as Timestamp) {
+ assertEquals(timestamp, this)
+ assertEquals(123, seconds)
+ assertEquals(345, nanoseconds)
+ }
+ with(decoded.deletedAt as Timestamp) {
+ assertEquals(timestamp, this)
+ assertEquals(123, seconds)
+ assertEquals(345, nanoseconds)
+ }
}
@Test
fun decodeEmptyTimestampObject() = runTest {
- val obj = mapOf("uid" to "uid123", "createdAt" to timestampNow(), "updatedAt" to null)
- val decoded: TestData = decode(obj)
- assertEquals("uid123", decoded.uid)
- assertEquals(null, decoded.updatedAt)
- }
-
- @Test
- fun decodeDeletedTimestampObject() = runTest {
- val timestamp = timestampWith(123, 345)
val obj = mapOf(
"uid" to "uid123",
- "createdAt" to timestampNow(),
- "updatedAt" to FieldValue.delete,
- "deletedAt" to FirebaseTimestamp.ServerDelete
- )
+ "createdAt" to Timestamp.now().platformValue,
+ "updatedAt" to Timestamp.now().platformValue,
+ "deletedAt" to null
+ ).asEncoded()
val decoded: TestData = decode(obj)
-
assertEquals("uid123", decoded.uid)
- assertEquals(null, decoded.updatedAt)
- assertEquals(null, decoded.deletedAt)
- }
-
- @Test
- fun encodeDeletedTimestampObject() = runTest {
- val timestamp = FieldValue.delete
- val item = TestData("uid123", timestamp, null, FirebaseTimestamp.ServerDelete)
- val encoded = encode(item, shouldEncodeElementDefault = false) as Map
- assertEquals("uid123", encoded["uid"])
- assertEquals(timestamp, encoded["createdAt"])
- assertEquals(FieldValue.delete, encoded["deletedAt"])
+ assertNotNull(decoded.updatedAt)
+ assertNull(decoded.deletedAt)
}
}
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 581220f18..49e391e87 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
@@ -12,6 +12,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
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.*
@@ -19,8 +22,12 @@ expect val emulatorHost: String
expect val context: Any
expect fun runTest(test: suspend CoroutineScope.() -> Unit)
+/** @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
+// NOTE: serializer() does not work in a legacy JS so serializers have to be provided explicitly
class FirebaseFirestoreTest {
@Serializable
@@ -32,8 +39,7 @@ class FirebaseFirestoreTest {
@Serializable
data class FirestoreTimeTest(
val prop1: String,
- @Serializable(with = FirebaseNullableTimestampSerializer::class)
- val time: Any?
+ val time: BaseTimestamp?
)
@BeforeTest
@@ -135,20 +141,19 @@ class FirebaseFirestoreTest {
.document("test")
doc.set(
FirestoreTimeTest.serializer(),
- FirestoreTimeTest("ServerTimestamp", timestampWith(0, 0)),
+ FirestoreTimeTest("ServerTimestamp", Timestamp(123, 0)),
)
- assertEquals(timestampWith(0, 0), doc.get().get("time", FirebaseTimestampSerializer))
+ assertEquals(Timestamp(123, 0), doc.get().get("time", TimestampSerializer))
doc.update(
fieldsAndValues = arrayOf(
- "time" to timestampWith(123, 0)
- .withSerializer(FirebaseTimestampSerializer)
+ "time" to Timestamp(321, 0)
)
)
- assertEquals(timestampWith(123, 0), doc.get().data(FirestoreTimeTest.serializer()).time)
+ assertEquals(Timestamp(321, 0), doc.get().data(FirestoreTimeTest.serializer()).time)
- assertNotEquals(FieldValue.serverTimestamp(), doc.get().get("time", FirebaseTimestampSerializer))
- assertNotEquals(FieldValue.serverTimestamp(), doc.get().data(FirestoreTimeTest.serializer()).time)
+ assertNotEquals(Timestamp.ServerTimestamp, doc.get().get("time", TimestampSerializer))
+ assertNotEquals(Timestamp.ServerTimestamp, doc.get().data(FirestoreTimeTest.serializer()).time)
}
@Test
@@ -166,12 +171,12 @@ class FirebaseFirestoreTest {
doc.set(
FirestoreTimeTest.serializer(),
- FirestoreTimeTest("ServerTimestampBehavior", FieldValue.serverTimestamp())
+ FirestoreTimeTest("ServerTimestampBehavior", Timestamp.ServerTimestamp)
)
val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
- assertNull(pendingWritesSnapshot.get("time", FirebaseNullableTimestampSerializer, ServerTimestampBehavior.NONE))
+ assertNull(pendingWritesSnapshot.get("time", TimestampSerializer.nullable, ServerTimestampBehavior.NONE))
}
@Test
@@ -211,12 +216,12 @@ class FirebaseFirestoreTest {
}
delay(100) // makes possible to catch pending writes snapshot
- doc.set(FirestoreTimeTest.serializer(), FirestoreTimeTest("ServerTimestampBehavior", FieldValue.serverTimestamp()))
+ doc.set(FirestoreTimeTest.serializer(), FirestoreTimeTest("ServerTimestampBehavior", Timestamp.ServerTimestamp))
val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
- assertNotNull(pendingWritesSnapshot.get("time", FirebaseTimestampSerializer, ServerTimestampBehavior.ESTIMATE))
- assertNotEquals(timestampWith(0, 0), pendingWritesSnapshot.data(FirestoreTimeTest.serializer(), ServerTimestampBehavior.ESTIMATE).time)
+ assertNotNull(pendingWritesSnapshot.get("time", TimestampSerializer, ServerTimestampBehavior.ESTIMATE))
+ assertNotEquals(Timestamp(0, 0), pendingWritesSnapshot.data(FirestoreTimeTest.serializer(), ServerTimestampBehavior.ESTIMATE).time)
}
@Test
@@ -232,11 +237,11 @@ class FirebaseFirestoreTest {
}
delay(100) // makes possible to catch pending writes snapshot
- doc.set(FirestoreTimeTest.serializer(), FirestoreTimeTest("ServerTimestampBehavior", FieldValue.serverTimestamp()))
+ doc.set(FirestoreTimeTest.serializer(), FirestoreTimeTest("ServerTimestampBehavior", Timestamp.ServerTimestamp))
val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
- assertNull(pendingWritesSnapshot.get("time", FirebaseNullableTimestampSerializer, ServerTimestampBehavior.PREVIOUS))
+ assertNull(pendingWritesSnapshot.get("time", TimestampSerializer.nullable, ServerTimestampBehavior.PREVIOUS))
}
@Test
@@ -350,39 +355,35 @@ class FirebaseFirestoreTest {
@Test
fun testGeoPointSerialization() = runTest {
@Serializable
- data class DataWithGeoPoint(
- @Serializable(with = FirebaseGeoPointSerializer::class)
- val geoPoint: GeoPoint
- )
+ data class DataWithGeoPoint(val geoPoint: GeoPoint)
fun getDocument() = Firebase.firestore.collection("geoPointSerialization")
.document("geoPointSerialization")
- val data = DataWithGeoPoint(geoPointWith(12.34, 56.78))
+ val data = DataWithGeoPoint(GeoPoint(12.34, 56.78))
// store geo point
- getDocument().set(data)
+ getDocument().set(DataWithGeoPoint.serializer(), data)
// restore data
- val savedData = getDocument().get().data()
- assertEquals(data, savedData)
+ val savedData = getDocument().get().data(DataWithGeoPoint.serializer())
+ assertEquals(data.geoPoint, savedData.geoPoint)
// update data
- val updatedData = DataWithGeoPoint(geoPointWith(87.65, 43.21))
+ val updatedData = DataWithGeoPoint(GeoPoint(87.65, 43.21))
getDocument().update(FieldPath(DataWithGeoPoint::geoPoint.name) to updatedData.geoPoint)
// verify update
- val updatedSavedData = getDocument().get().data()
- assertEquals(updatedData, updatedSavedData)
+ val updatedSavedData = getDocument().get().data(DataWithGeoPoint.serializer())
+ assertEquals(updatedData.geoPoint, updatedSavedData.geoPoint)
}
@Test
fun testDocumentReferenceSerialization() = runTest {
@Serializable
data class DataWithDocumentReference(
- @Serializable(with = FirebaseDocumentReferenceSerializer::class)
val documentReference: DocumentReference
)
fun getCollection() = Firebase.firestore.collection("documentReferenceSerialization")
- fun getDocument() = Firebase.firestore.collection("documentReferenceSerialization")
+ fun getDocument() = getCollection()
.document("documentReferenceSerialization")
val documentRef1 = getCollection().document("refDoc1").apply {
set(mapOf("value" to 1))
@@ -392,19 +393,99 @@ class FirebaseFirestoreTest {
}
val data = DataWithDocumentReference(documentRef1)
- // store geo point
- getDocument().set(data)
+ // store reference
+ getDocument().set(DataWithDocumentReference.serializer(), data)
// restore data
- val savedData = getDocument().get().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(FirebaseDocumentReferenceSerializer)
+ FieldPath(DataWithDocumentReference::documentReference.name) to updatedData.documentReference.withSerializer(DocumentReferenceSerializer)
)
// verify update
- val updatedSavedData = getDocument().get().data()
+ 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 = Firebase.firestore.document("a/b")
+ val item = TestDataWithDocumentReference("123", doc, doc)
+ val encoded = encodedAsMap(encode(item, shouldEncodeElementDefault = false))
+ assertEquals("123", encoded["uid"])
+ assertEquals(doc.platformValue, encoded["reference"])
+ assertEquals(doc.platformValue, encoded["optionalReference"])
+ }
+
+ @Test
+ fun encodeNullDocumentReference() = runTest {
+ val item = TestDataWithOptionalDocumentReference(null)
+ val encoded = encodedAsMap(encode(item, shouldEncodeElementDefault = false))
+ assertNull(encoded["optionalReference"])
+ }
+
+ @Test
+ fun decodeDocumentReference() = runTest {
+ val doc = Firebase.firestore.document("a/b")
+ val obj = mapOf(
+ "uid" to "123",
+ "reference" to doc.platformValue,
+ "optionalReference" to doc.platformValue
+ ).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() = Firebase.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)
+ }
+
}
diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
deleted file mode 100644
index f8c4878cd..000000000
--- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package dev.gitlive.firebase.firestore
-
-import cocoapods.FirebaseFirestore.FIRDocumentReference
-
-actual val DocumentReference.platformValue: Any get() = ios
-actual fun DocumentReference.Companion.fromPlatformValue(platformValue: Any): DocumentReference =
- DocumentReference(platformValue as FIRDocumentReference)
diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
index 71d37d68b..89509480b 100644
--- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
+++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
@@ -1,9 +1,20 @@
package dev.gitlive.firebase.firestore
import cocoapods.FirebaseFirestore.FIRGeoPoint
+import kotlinx.serialization.Serializable
-actual typealias GeoPoint = FIRGeoPoint
-actual fun geoPointWith(latitude: Double, longitude: Double) = FIRGeoPoint(latitude, longitude)
-actual val GeoPoint.latitude: Double get() = latitude
-actual val GeoPoint.longitude: Double get() = longitude
+/** A class representing a platform specific Firebase GeoPoint. */
+actual typealias PlatformGeoPoint = FIRGeoPoint
+
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+actual class GeoPoint internal actual constructor(internal actual val platformValue: PlatformGeoPoint) {
+ actual constructor(latitude: Double, longitude: Double) : this(PlatformGeoPoint(latitude, longitude))
+ actual val latitude: Double = platformValue.latitude
+ actual val longitude: Double = platformValue.longitude
+ override fun equals(other: Any?): Boolean =
+ this === other || other is GeoPoint && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+}
diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
index c2dcda110..43f271b2b 100644
--- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
+++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
@@ -1,10 +1,34 @@
package dev.gitlive.firebase.firestore
import cocoapods.FirebaseFirestore.FIRTimestamp
+import kotlinx.serialization.Serializable
-actual typealias Timestamp = FIRTimestamp
+/** A class representing a platform specific Firebase Timestamp. */
+actual typealias PlatformTimestamp = FIRTimestamp
-actual fun timestampNow(): Timestamp = FIRTimestamp.timestamp()
-actual fun timestampWith(seconds: Long, nanoseconds: Int) = FIRTimestamp(seconds, nanoseconds)
-actual val Timestamp.seconds: Long get() = seconds
-actual val Timestamp.nanoseconds: Int get() = nanoseconds
+/** A base class that could be used to combine [Timestamp] and [Timestamp.ServerTimestamp] in the same field. */
+@Serializable(with = BaseTimestampSerializer::class)
+actual sealed class BaseTimestamp
+
+/** A class representing a Firebase Timestamp. */
+@Serializable(with = TimestampSerializer::class)
+actual class Timestamp internal actual constructor(
+ internal actual val platformValue: PlatformTimestamp
+): BaseTimestamp() {
+ actual constructor(seconds: Long, nanoseconds: Int) : this(PlatformTimestamp(seconds, nanoseconds))
+
+ actual val seconds: Long = platformValue.seconds
+ actual val nanoseconds: Int = platformValue.nanoseconds
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is Timestamp && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+
+ actual companion object {
+ actual fun now(): Timestamp = Timestamp(PlatformTimestamp.timestamp())
+ }
+
+ /** A server time timestamp. */
+ actual object ServerTimestamp: BaseTimestamp()
+}
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
index 43f42a509..d13eec8a8 100644
--- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt
+++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt
@@ -1,14 +1,11 @@
package dev.gitlive.firebase.firestore
-import cocoapods.FirebaseFirestore.FIRDocumentReference
import cocoapods.FirebaseFirestore.FIRFieldValue
-import cocoapods.FirebaseFirestore.FIRGeoPoint
-import cocoapods.FirebaseFirestore.FIRTimestamp
actual fun isSpecialValue(value: Any) = when(value) {
is FIRFieldValue,
- is FIRGeoPoint,
- is FIRTimestamp,
- is FIRDocumentReference -> true
+ is PlatformGeoPoint,
+ is PlatformTimestamp,
+ is PlatformDocumentReference -> 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 6399b1671..019510de0 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
@@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationStrategy
import platform.Foundation.NSError
import platform.Foundation.NSNull
@@ -99,10 +100,12 @@ actual class WriteBatch(val ios: FIRWriteBatch) {
merge: Boolean,
vararg fieldsAndValues: Pair
): WriteBatch {
- val serializedItem = encodeAsMap(strategy, data, encodeDefaults)
- val serializedFieldAndValues = encodeAsMap(fieldsAndValues = fieldsAndValues)
+ val serializedItem = encode(strategy, data, encodeDefaults) as Map
+ val serializedFieldAndValues = fieldsAndValues.associate { (field, value) ->
+ field to encode(value, encodeDefaults)
+ }
- val result = serializedItem + (serializedFieldAndValues ?: emptyMap())
+ val result = serializedItem + serializedFieldAndValues
ios.setData(result as Map, documentRef.ios, merge)
return this
}
@@ -126,10 +129,12 @@ actual class WriteBatch(val ios: FIRWriteBatch) {
encodeDefaults: Boolean,
vararg fieldsAndValues: Pair
): WriteBatch {
- val serializedItem = encodeAsMap(strategy, data, encodeDefaults)
- val serializedFieldAndValues = encodeAsMap(fieldsAndValues = fieldsAndValues)
+ val serializedItem = encode(strategy, data, encodeDefaults) as Map
+ val serializedFieldAndValues = fieldsAndValues.associate { (field, value) ->
+ field to encode(value, encodeDefaults)
+ }
- val result = serializedItem + (serializedFieldAndValues ?: emptyMap())
+ val result = serializedItem + serializedFieldAndValues
return ios.updateData(result as Map, documentRef.ios).let { this }
}
@@ -193,8 +198,13 @@ actual class Transaction(val ios: FIRTransaction) {
}
+/** A class representing a platform specific Firebase DocumentReference. */
+actual typealias PlatformDocumentReference = FIRDocumentReference
+
@Suppress("UNCHECKED_CAST")
-actual class DocumentReference(val ios: FIRDocumentReference) {
+@Serializable(with = DocumentReferenceSerializer::class)
+actual class DocumentReference actual constructor(internal actual val platformValue: PlatformDocumentReference) {
+ val ios: PlatformDocumentReference = platformValue
actual val id: String
get() = ios.documentID
@@ -258,7 +268,10 @@ actual class DocumentReference(val ios: FIRDocumentReference) {
awaitClose { listener.remove() }
}
- actual companion object
+ override fun equals(other: Any?): Boolean =
+ this === other || other is DocumentReference && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
}
actual open class Query(open val ios: FIRQuery) {
@@ -484,12 +497,28 @@ actual class FieldPath private constructor(val ios: FIRFieldPath) {
actual val documentId: FieldPath get() = FieldPath(FIRFieldPath.documentID())
}
-actual object FieldValue {
- actual val delete: Any get() = FIRFieldValue.fieldValueForDelete()
- actual fun arrayUnion(vararg elements: Any): Any = FIRFieldValue.fieldValueForArrayUnion(elements.asList())
- actual fun arrayRemove(vararg elements: Any): Any = FIRFieldValue.fieldValueForArrayUnion(elements.asList())
- actual fun serverTimestamp(): Any = FIRFieldValue.fieldValueForServerTimestamp()
- actual fun delete(): Any = delete
+/** A class representing a platform specific Firebase FieldValue. */
+private typealias PlatformFieldValue = FIRFieldValue
+
+/** A class representing a Firebase FieldValue. */
+@Serializable(with = FieldValueSerializer::class)
+actual class FieldValue internal actual constructor(internal actual val platformValue: Any) {
+ init {
+ require(platformValue is PlatformFieldValue)
+ }
+ override fun equals(other: Any?): Boolean =
+ this === other || other is FieldValue && platformValue == other.platformValue
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+
+ actual companion object {
+ actual val delete: FieldValue get() = FieldValue(PlatformFieldValue.fieldValueForDelete())
+ actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(PlatformFieldValue.fieldValueForArrayUnion(elements.asList()))
+ actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(PlatformFieldValue.fieldValueForArrayRemove(elements.asList()))
+ actual fun serverTimestamp(): FieldValue = FieldValue(PlatformFieldValue.fieldValueForServerTimestamp())
+ @Deprecated("Replaced with FieldValue.delete", replaceWith = ReplaceWith("delete"))
+ actual fun delete(): FieldValue = delete
+ }
}
private fun T.throwError(block: T.(errorPointer: CPointer>) -> R): R {
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 f023832dc..d1f28674b 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
@@ -29,64 +29,4 @@ actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking {
}
actual fun encodedAsMap(encoded: Any?): Map = encoded as Map
-
-class FirebaseFirestoreIOSTest {
-
- @BeforeTest
- fun initializeFirebase() {
- Firebase
- .takeIf { Firebase.apps(context).isEmpty() }
- ?.apply {
- initialize(
- context,
- FirebaseOptions(
- applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
- apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
- databaseUrl = "https://fir-kotlin-sdk.firebaseio.com",
- storageBucket = "fir-kotlin-sdk.appspot.com",
- projectId = "fir-kotlin-sdk"
- )
- )
- Firebase.firestore.useEmulator(emulatorHost, 8080)
- }
- }
-
- @Serializable
- data class TestDataWithDocumentReference(
- val uid: String,
- @Serializable(with = FirebaseDocumentReferenceSerializer::class)
- val reference: DocumentReference,
- @Serializable(with = FirebaseReferenceNullableSerializer::class)
- val ref: FirebaseReference?
- )
-
- @Test
- fun encodeDocumentReferenceObject() = runTest {
- val doc = Firebase.firestore.document("a/b")
- val item = TestDataWithDocumentReference("123", doc, FirebaseReference.Value(doc))
- val encoded = encode(item, shouldEncodeElementDefault = false) as Map
- assertEquals("123", encoded["uid"])
- assertEquals(doc.ios, encoded["reference"])
- assertEquals(doc.ios, encoded["ref"])
- }
-
- @Test
- fun encodeDeleteDocumentReferenceObject() = runTest {
- val doc = Firebase.firestore.document("a/b")
- val item = TestDataWithDocumentReference("123", doc, FirebaseReference.ServerDelete)
- val encoded = encode(item, shouldEncodeElementDefault = false) as Map
- assertEquals("123", encoded["uid"])
- assertEquals(doc.ios, encoded["reference"])
- assertEquals(FieldValue.delete, encoded["ref"])
- }
-
- @Test
- fun decodeDocumentReferenceObject() = runTest {
- val doc = Firebase.firestore.document("a/b")
- val obj = mapOf("uid" to "123", "reference" to doc.ios, "ref" to doc.ios)
- val decoded: TestDataWithDocumentReference = decode(obj)
- assertEquals("123", decoded.uid)
- assertEquals(doc.path, decoded.reference.path)
- assertEquals(doc.path, decoded.ref?.reference?.path)
- }
-}
+actual fun Map.asEncoded(): Any = this
diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
deleted file mode 100644
index 33d1051e4..000000000
--- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/FirebaseDocumentReferenceSerializer.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package dev.gitlive.firebase.firestore
-
-import dev.gitlive.firebase.firebase
-
-actual val DocumentReference.platformValue: Any get() = js
-actual fun DocumentReference.Companion.fromPlatformValue(platformValue: Any): DocumentReference =
- DocumentReference(platformValue as firebase.firestore.DocumentReference)
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 b4caeffd3..f5a62c6c1 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,9 +1,20 @@
package dev.gitlive.firebase.firestore
import dev.gitlive.firebase.*
+import kotlinx.serialization.Serializable
-actual typealias GeoPoint = firebase.firestore.GeoPoint
+/** A class representing a platform specific Firebase GeoPoint. */
+actual typealias PlatformGeoPoint = firebase.firestore.GeoPoint
-actual fun geoPointWith(latitude: Double, longitude: Double) = GeoPoint(latitude, longitude)
-actual val GeoPoint.latitude: Double get() = latitude
-actual val GeoPoint.longitude: Double get() = longitude
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+actual class GeoPoint internal actual constructor(internal actual val platformValue: PlatformGeoPoint) {
+ actual constructor(latitude: Double, longitude: Double) : this(PlatformGeoPoint(latitude, longitude))
+ actual val latitude: Double = platformValue.latitude
+ actual val longitude: Double = platformValue.longitude
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is GeoPoint && platformValue.isEqual(other.platformValue)
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = "GeoPoint[lat=$latitude,long=$longitude]"
+}
diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
index 2311660c3..5018b9f23 100644
--- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
+++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
@@ -1,10 +1,35 @@
package dev.gitlive.firebase.firestore
import dev.gitlive.firebase.*
+import kotlinx.serialization.Serializable
-actual typealias Timestamp = firebase.firestore.Timestamp
+/** A base class that could be used to combine [Timestamp] and [Timestamp.ServerTimestamp] in the same field. */
+@Serializable(with = BaseTimestampSerializer::class)
+actual sealed class BaseTimestamp
+
+/** A class representing a platform specific Firebase Timestamp. */
+actual typealias PlatformTimestamp = firebase.firestore.Timestamp
+
+/** A class representing a Firebase Timestamp. */
+@Serializable(with = TimestampSerializer::class)
+actual class Timestamp internal actual constructor(
+ internal actual val platformValue: PlatformTimestamp
+): BaseTimestamp() {
+ actual constructor(seconds: Long, nanoseconds: Int) : this(PlatformTimestamp(seconds.toDouble(), nanoseconds.toDouble()))
+
+ actual val seconds: Long = platformValue.seconds.toLong()
+ actual val nanoseconds: Int = platformValue.nanoseconds.toInt()
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is Timestamp && platformValue.isEqual(other.platformValue)
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+
+ actual companion object {
+ actual fun now(): Timestamp = Timestamp(PlatformTimestamp.now())
+ }
+
+ /** A server time timestamp. */
+ actual object ServerTimestamp: BaseTimestamp()
+}
-actual fun timestampNow(): Timestamp = Timestamp.now()
-actual fun timestampWith(seconds: Long, nanoseconds: Int) = Timestamp(seconds, nanoseconds)
-actual val Timestamp.seconds: Long get() = seconds
-actual val Timestamp.nanoseconds: Int get() = nanoseconds
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
index 00bdbd890..d66b48e8e 100644
--- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt
+++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/_encoders.kt
@@ -4,8 +4,8 @@ import dev.gitlive.firebase.firebase
actual fun isSpecialValue(value: Any) = when(value) {
is firebase.firestore.FieldValue,
- is firebase.firestore.GeoPoint,
- is firebase.firestore.Timestamp,
- is firebase.firestore.DocumentReference -> true
+ is PlatformGeoPoint,
+ is PlatformTimestamp,
+ is PlatformDocumentReference -> true
else -> false
}
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 47077d97e..10f581dbd 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
@@ -11,7 +11,9 @@ import kotlinx.coroutines.channels.awaitClose
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
actual val Firebase.firestore get() =
@@ -86,25 +88,20 @@ actual class WriteBatch(val js: firebase.firestore.WriteBatch) {
rethrow { js.set(documentRef.js, encode(strategy, 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,
- vararg fieldsAndValues: Pair
- ): WriteBatch {
- val serializedItem = encodeAsMap(strategy, data, encodeDefaults)
- val serializedFieldAndValues = encodeAsMap(fieldsAndValues = fieldsAndValues)
-
- val result = serializedItem + (serializedFieldAndValues ?: emptyMap())
- if (merge) {
- js.set(documentRef.js, result, json("merge" to merge))
- } else {
- js.set(documentRef.js, result)
- }
- return this
- }
+ actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean, vararg fieldsAndValues: Pair) =
+ rethrow {
+ val serializedItem = encode(strategy, data, encodeDefaults) as Json
+ val serializedFieldAndValues = fieldsAndValues.map { (field, value) ->
+ field to encode(value, encodeDefaults)
+ }.let { json(*it.toTypedArray()) }
+
+ val result = serializedItem.add(serializedFieldAndValues)
+ if (merge) {
+ js.set(documentRef.js, result, json("merge" to merge))
+ } else {
+ js.set(documentRef.js, result)
+ }
+ }.let { this }
actual inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) =
rethrow { js.update(documentRef.js, encode(data, encodeDefaults)!!) }
@@ -126,13 +123,15 @@ actual class WriteBatch(val js: firebase.firestore.WriteBatch) {
data: T,
encodeDefaults: Boolean,
vararg fieldsAndValues: Pair
- ): WriteBatch {
- val serializedItem = encodeAsMap(strategy, data, encodeDefaults)
- val serializedFieldAndValues = encodeAsMap(fieldsAndValues = fieldsAndValues)
+ ) = rethrow {
+ val serializedItem = encode(strategy, data, encodeDefaults) as Json
+ val serializedFieldAndValues = fieldsAndValues.map { (field, value) ->
+ field to encode(value, encodeDefaults)
+ }.let { json(*it.toTypedArray()) }
- val result = serializedItem + (serializedFieldAndValues ?: emptyMap())
- return js.update(documentRef.js, result).let { this }
- }
+ val result = serializedItem.add(serializedFieldAndValues)
+ js.update(documentRef.js, result)
+ }.let { this }
actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = rethrow {
fieldsAndValues.takeUnless { fieldsAndValues.isEmpty() }
@@ -220,7 +219,12 @@ actual class Transaction(val js: firebase.firestore.Transaction) {
rethrow { DocumentSnapshot(js.get(documentRef.js).await()) }
}
-actual class DocumentReference(val js: firebase.firestore.DocumentReference) {
+/** A class representing a platform specific Firebase DocumentReference. */
+actual typealias PlatformDocumentReference = firebase.firestore.DocumentReference
+
+@Serializable(with = DocumentReferenceSerializer::class)
+actual class DocumentReference actual constructor(internal actual val platformValue: PlatformDocumentReference) {
+ val js: PlatformDocumentReference = platformValue
actual val id: String
get() = rethrow { js.id }
@@ -257,7 +261,15 @@ actual class DocumentReference(val js: firebase.firestore.DocumentReference) {
actual suspend fun update(vararg fieldsAndValues: Pair) = rethrow {
fieldsAndValues.takeUnless { fieldsAndValues.isEmpty() }
?.map { (field, value) -> field to encode(value, true) }
- ?.let { encoded -> js.update(encoded.toMap()) }
+ ?.let { encoded ->
+ js.update(
+ encoded.first().first,
+ encoded.first().second,
+ *encoded.drop(1)
+ .flatMap { (field, value) -> listOf(field, value) }
+ .toTypedArray()
+ )
+ }
?.await()
}.run { Unit }
@@ -287,7 +299,11 @@ actual class DocumentReference(val js: firebase.firestore.DocumentReference) {
)
awaitClose { unsubscribe() }
}
- actual companion object
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is DocumentReference && platformValue.isEqual(other.platformValue)
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = "DocumentReference(path=$path)"
}
actual open class Query(open val js: firebase.firestore.Query) {
@@ -460,16 +476,6 @@ actual class FieldPath private constructor(val js: firebase.firestore.FieldPath)
actual val documentId: FieldPath get() = FieldPath(firebase.firestore.FieldPath.documentId)
}
-actual object FieldValue {
- @JsName("_serverTimestamp")
- actual val delete: Any get() = rethrow { firebase.firestore.FieldValue.delete() }
- actual fun arrayUnion(vararg elements: Any): Any = rethrow { firebase.firestore.FieldValue.arrayUnion(*elements) }
- actual fun arrayRemove(vararg elements: Any): Any = rethrow { firebase.firestore.FieldValue.arrayRemove(*elements) }
- actual fun serverTimestamp(): Any = rethrow { firebase.firestore.FieldValue.serverTimestamp() }
- @JsName("deprecatedDelete")
- actual fun delete(): Any = delete
-}
-
//actual data class FirebaseFirestoreSettings internal constructor(
// val cacheSizeBytes: Number? = undefined,
// val host: String? = undefined,
@@ -478,6 +484,32 @@ actual object FieldValue {
// var enablePersistence: Boolean = false
//)
+/** A class representing a platform specific Firebase FieldValue. */
+private typealias PlatformFieldValue = firebase.firestore.FieldValue
+
+/** A class representing a Firebase FieldValue. */
+@Serializable(with = FieldValueSerializer::class)
+actual class FieldValue internal actual constructor(internal actual val platformValue: Any) {
+ init {
+ require(platformValue is PlatformFieldValue)
+ }
+ override fun equals(other: Any?): Boolean =
+ this === other || other is FieldValue &&
+ (platformValue as PlatformFieldValue).isEqual(other.platformValue as PlatformFieldValue)
+ override fun hashCode(): Int = platformValue.hashCode()
+ override fun toString(): String = platformValue.toString()
+
+ actual companion object {
+ actual val delete: FieldValue get() = FieldValue(PlatformFieldValue.delete())
+ actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(PlatformFieldValue.arrayUnion(*elements))
+ actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(PlatformFieldValue.arrayRemove(*elements))
+ actual fun serverTimestamp(): FieldValue = FieldValue(PlatformFieldValue.serverTimestamp())
+ @Deprecated("Replaced with FieldValue.delete", replaceWith = ReplaceWith("delete"))
+ @JsName("deprecatedDelete")
+ actual fun delete(): FieldValue = delete
+ }
+}
+
actual enum class FirestoreExceptionCode {
OK,
CANCELLED,
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 8691ea7ab..69f9d5fb3 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
@@ -7,7 +7,7 @@ package dev.gitlive.firebase.firestore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
-import kotlin.js.Json
+import kotlin.js.json
actual val emulatorHost: String = "localhost"
@@ -28,6 +28,8 @@ actual fun encodedAsMap(encoded: Any?): Map {
it[0] as String to it[1]
}
}
+actual fun Map.asEncoded(): Any =
+ json(*entries.map { (key, value) -> key to value }.toTypedArray())
internal fun Throwable.log() {
console.error(this)