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() +) +``` +

Default arguments

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)