diff --git a/README.md b/README.md
index 03628c78c..d486b504e 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,7 @@ db.collection("cities").document("LA").set(City.serializer(), city, encodeDefaul
```
The `encodeDefaults` parameter is optional and defaults to `true`, set this to false to omit writing optional properties if they are equal to theirs default values.
+Using [@EncodeDefault](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-encode-default/) on properties is a recommended way to locally override the behavior set with `encodeDefaults`.
You can also omit the serializer but this is discouraged due to a [current limitation on Kotlin/JS and Kotlin/Native](https://github.com/Kotlin/kotlinx.serialization/issues/1116#issuecomment-704342452)
@@ -110,6 +111,21 @@ data class Post(
)
```
+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 document = PointOfInterest(
+ reference = Firebase.firestore.collection("foo").document("bar"),
+ location = GeoPoint(51.939, 4.506)
+)
+```
+
Polymorphic serialization (sealed classes)
This sdk will handle polymorphic serialization automatically if you have a sealed class and its children marked as `Serializable`. It will include a `type` property that will be used to discriminate which child class is the serialized.
diff --git a/build.gradle.kts b/build.gradle.kts
index cbcc2716d..8e19e4f55 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -65,7 +65,28 @@ subprojects {
onlyIf { !project.gradle.startParameter.taskNames.contains("publishToMavenLocal") }
}
+ val skipPublishing = project.name == "test-utils" // skip publishing for test utils
+
tasks {
+ withType {
+ testLogging {
+ showExceptions = true
+ exceptionFormat = TestExceptionFormat.FULL
+ showStandardStreams = true
+ showCauses = true
+ showStackTraces = true
+ events = setOf(
+ TestLogEvent.STARTED,
+ TestLogEvent.FAILED,
+ TestLogEvent.PASSED,
+ TestLogEvent.SKIPPED,
+ TestLogEvent.STANDARD_OUT,
+ TestLogEvent.STANDARD_ERROR
+ )
+ }
+ }
+
+ if (skipPublishing) return@tasks
val updateVersion by registering(Exec::class) {
commandLine("npm", "--allow-same-version", "--prefix", projectDir, "version", "${project.property("${project.name}.version")}")
@@ -136,24 +157,6 @@ subprojects {
commandLine("npm", "publish")
}
}
-
- withType {
- testLogging {
- showExceptions = true
- exceptionFormat = TestExceptionFormat.FULL
- showStandardStreams = true
- showCauses = true
- showStackTraces = true
- events = setOf(
- TestLogEvent.STARTED,
- TestLogEvent.FAILED,
- TestLogEvent.PASSED,
- TestLogEvent.SKIPPED,
- TestLogEvent.STANDARD_OUT,
- TestLogEvent.STANDARD_ERROR
- )
- }
- }
}
afterEvaluate {
@@ -181,8 +184,10 @@ subprojects {
}
}
- apply(plugin="maven-publish")
- apply(plugin="signing")
+ if (skipPublishing) return@subprojects
+
+ apply(plugin = "maven-publish")
+ apply(plugin = "signing")
configure {
@@ -235,6 +240,7 @@ subprojects {
}
}
}
+
}
}
diff --git a/firebase-common/build.gradle.kts b/firebase-common/build.gradle.kts
index 93df71e17..b0d27b29a 100644
--- a/firebase-common/build.gradle.kts
+++ b/firebase-common/build.gradle.kts
@@ -90,6 +90,12 @@ kotlin {
}
}
+ val commonTest by getting {
+ dependencies {
+ implementation(project(":test-utils"))
+ }
+ }
+
val androidMain by getting {
dependencies {
api("com.google.firebase:firebase-common")
diff --git a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt
index f6a3995ab..e9ab9003f 100644
--- a/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt
+++ b/firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt
@@ -108,7 +108,7 @@ class FirebaseListSerializer : KSerializer> {
* A special case of serializer for values natively supported by Firebase and
* don't require an additional encoding/decoding.
*/
-abstract class SpecialValueSerializer(
+class SpecialValueSerializer(
serialName: String,
private val toNativeValue: (T) -> Any?,
private val fromNativeValue: (Any?) -> T
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 6af1cf0e8..1c33b1437 100644
--- a/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
+++ b/firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
@@ -11,10 +11,6 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
-expect fun nativeMapOf(vararg pairs: Pair): Any
-expect fun nativeListOf(vararg elements: Any): Any
-expect fun nativeAssertEquals(expected: Any?, actual: Any?): Unit
-
@Serializable
data class TestData(val map: Map, val bool: Boolean = false, val nullableBool: Boolean? = null)
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 04c57a497..bd346d925 100644
--- a/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt
+++ b/firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt
@@ -453,6 +453,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 {
@@ -477,6 +479,8 @@ external object firebase {
companion object {
val documentId: FieldPath
}
+
+ fun isEqual(other: FieldPath): Boolean
}
abstract class FieldValue {
@@ -490,6 +494,13 @@ external object firebase {
fun isEqual(other: FieldValue): Boolean
}
+
+ open class GeoPoint(latitude: Double, longitude: Double) {
+ val latitude: Double
+ val longitude: Double
+
+ fun isEqual(other: GeoPoint): Boolean
+ }
}
fun remoteConfig(app: App? = definedExternally): remoteConfig.RemoteConfig
diff --git a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/ServerValue.kt b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/ServerValue.kt
index 7c03178b5..c93ba746a 100644
--- a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/ServerValue.kt
+++ b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/ServerValue.kt
@@ -3,6 +3,7 @@ package dev.gitlive.firebase.database
import dev.gitlive.firebase.FirebaseDecoder
import dev.gitlive.firebase.FirebaseEncoder
import dev.gitlive.firebase.SpecialValueSerializer
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
@@ -18,7 +19,7 @@ expect class ServerValue internal constructor(nativeValue: Any){
}
/** Serializer for [ServerValue]. Must be used with [FirebaseEncoder]/[FirebaseDecoder].*/
-object ServerValueSerializer: SpecialValueSerializer(
+object ServerValueSerializer: KSerializer by SpecialValueSerializer(
serialName = "ServerValue",
toNativeValue = ServerValue::nativeValue,
fromNativeValue = { raw ->
diff --git a/firebase-firestore/build.gradle.kts b/firebase-firestore/build.gradle.kts
index 0fc7a3614..cdc9eab83 100644
--- a/firebase-firestore/build.gradle.kts
+++ b/firebase-firestore/build.gradle.kts
@@ -103,6 +103,12 @@ kotlin {
}
}
+ val commonTest by getting {
+ dependencies {
+ implementation(project(":test-utils"))
+ }
+ }
+
val androidMain by getting {
dependencies {
api("com.google.firebase:firebase-firestore")
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
new file mode 100644
index 000000000..7523619f5
--- /dev/null
+++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
@@ -0,0 +1,19 @@
+package dev.gitlive.firebase.firestore
+
+import kotlinx.serialization.Serializable
+
+/** A class representing a platform specific Firebase GeoPoint. */
+actual typealias NativeGeoPoint = com.google.firebase.firestore.GeoPoint
+
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+actual class GeoPoint internal actual constructor(internal actual val nativeValue: NativeGeoPoint) {
+ actual constructor(latitude: Double, longitude: Double) : this(NativeGeoPoint(latitude, longitude))
+ actual val latitude: Double = nativeValue.latitude
+ actual val longitude: Double = nativeValue.longitude
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is GeoPoint && nativeValue == other.nativeValue
+ override fun hashCode(): Int = nativeValue.hashCode()
+ override fun toString(): String = nativeValue.toString()
+}
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 45ced52d1..32f317fe8 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
@@ -196,8 +196,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 NativeDocumentReference = com.google.firebase.firestore.DocumentReference
+@Serializable(with = DocumentReferenceSerializer::class)
+actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) {
+ val android: NativeDocumentReference by ::nativeValue
actual val id: String
get() = android.id
@@ -270,6 +274,10 @@ actual class DocumentReference(val android: com.google.firebase.firestore.Docume
}
awaitClose { listener.remove() }
}
+ override fun equals(other: Any?): Boolean =
+ this === other || other is DocumentReference && nativeValue == other.nativeValue
+ override fun hashCode(): Int = nativeValue.hashCode()
+ override fun toString(): String = nativeValue.toString()
}
actual open class Query(open val android: com.google.firebase.firestore.Query) {
@@ -432,6 +440,10 @@ actual class SnapshotMetadata(val android: com.google.firebase.firestore.Snapsho
actual class FieldPath private constructor(val android: com.google.firebase.firestore.FieldPath) {
actual constructor(vararg fieldNames: String) : this(com.google.firebase.firestore.FieldPath.of(*fieldNames))
actual val documentId: FieldPath get() = FieldPath(com.google.firebase.firestore.FieldPath.documentId())
+
+ override fun equals(other: Any?): Boolean = other is FieldPath && android == other.android
+ override fun hashCode(): Int = android.hashCode()
+ override fun toString(): String = android.toString()
}
/** Represents a platform specific Firebase FieldValue. */
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
new file mode 100644
index 000000000..3ef3a7ed3
--- /dev/null
+++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
@@ -0,0 +1,30 @@
+package dev.gitlive.firebase.firestore
+
+import dev.gitlive.firebase.SpecialValueSerializer
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+
+/** A class representing a platform specific Firebase GeoPoint. */
+expect class NativeGeoPoint
+
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+expect class GeoPoint internal constructor(nativeValue: NativeGeoPoint) {
+ constructor(latitude: Double, longitude: Double)
+ val latitude: Double
+ val longitude: Double
+ internal val nativeValue: NativeGeoPoint
+}
+
+/** Serializer for [GeoPoint]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
+object GeoPointSerializer : KSerializer by SpecialValueSerializer(
+ serialName = "GeoPoint",
+ toNativeValue = GeoPoint::nativeValue,
+ fromNativeValue = { value ->
+ when (value) {
+ is NativeGeoPoint -> GeoPoint(value)
+ else -> throw SerializationException("Cannot deserialize $value")
+ }
+ }
+)
diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt
index 5e89e7191..8a7b02b5c 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,10 +1,17 @@
package dev.gitlive.firebase.firestore
+import dev.gitlive.firebase.FirebaseDecoder
import dev.gitlive.firebase.FirebaseEncoder
import dev.gitlive.firebase.SpecialValueSerializer
import dev.gitlive.firebase.firestore.DoubleAsTimestampSerializer.serverTimestamp
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.nanoseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
/** A class representing a platform specific Firebase Timestamp. */
expect class NativeTimestamp
@@ -31,12 +38,17 @@ expect class Timestamp internal constructor(nativeValue: NativeTimestamp): BaseT
object ServerTimestamp: BaseTimestamp
}
-fun Timestamp.Companion.fromMilliseconds(milliseconds: Double): Timestamp =
- Timestamp((milliseconds / 1000).toLong(), (milliseconds * 1000).toInt() % 1000000)
-fun Timestamp.toMilliseconds(): Double = seconds * 1000 + (nanoseconds / 1000.0)
+fun Timestamp.Companion.fromDuration(duration: Duration): Timestamp =
+ duration.toComponents { seconds, nanoseconds ->
+ Timestamp(seconds, nanoseconds)
+ }
+fun Timestamp.toDuration(): Duration = seconds.seconds + nanoseconds.nanoseconds
+
+fun Timestamp.Companion.fromMilliseconds(milliseconds: Double): Timestamp = fromDuration(milliseconds.milliseconds)
+fun Timestamp.toMilliseconds(): Double = toDuration().toDouble(DurationUnit.MILLISECONDS)
-/** A serializer for [BaseTimestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
-object BaseTimestampSerializer : SpecialValueSerializer(
+/** A serializer for [BaseTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */
+object BaseTimestampSerializer : KSerializer by SpecialValueSerializer(
serialName = "Timestamp",
toNativeValue = { value ->
when (value) {
@@ -54,8 +66,8 @@ object BaseTimestampSerializer : SpecialValueSerializer(
}
)
-/** A serializer for [Timestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
-object TimestampSerializer : SpecialValueSerializer(
+/** A serializer for [Timestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */
+object TimestampSerializer : KSerializer by SpecialValueSerializer(
serialName = "Timestamp",
toNativeValue = Timestamp::nativeValue,
fromNativeValue = { value ->
@@ -66,8 +78,8 @@ object TimestampSerializer : SpecialValueSerializer(
}
)
-/** A serializer for [Timestamp.ServerTimestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
-object ServerTimestampSerializer : SpecialValueSerializer(
+/** A serializer for [Timestamp.ServerTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */
+object ServerTimestampSerializer : KSerializer by SpecialValueSerializer(
serialName = "Timestamp",
toNativeValue = { FieldValue.serverTimestamp.nativeValue },
fromNativeValue = { value ->
@@ -79,12 +91,12 @@ object ServerTimestampSerializer : SpecialValueSerializer(
+object DoubleAsTimestampSerializer : KSerializer by SpecialValueSerializer(
serialName = "Timestamp",
toNativeValue = { value ->
when(value) {
serverTimestamp -> FieldValue.serverTimestamp.nativeValue
- else -> Timestamp.fromMilliseconds(value)
+ else -> Timestamp.fromMilliseconds(value).nativeValue
}
},
fromNativeValue = { value ->
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 103a9dc38..2dfe5d950 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
@@ -7,6 +7,7 @@ package dev.gitlive.firebase.firestore
import dev.gitlive.firebase.*
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.SerializationStrategy
@@ -79,27 +80,35 @@ expect open class Query {
internal fun _endAt(vararg fieldValues: Any): Query
}
-fun Query.where(field: String, equalTo: Any?) = _where(field, equalTo)
-fun Query.where(path: FieldPath, equalTo: Any?) = _where(path, equalTo)
-fun Query.where(field: String, equalTo: DocumentReference) = _where(field, equalTo)
-fun Query.where(path: FieldPath, equalTo: DocumentReference) = _where(path, equalTo)
-fun Query.where(field: String, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(field, lessThan, greaterThan, arrayContains)
-fun Query.where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(path, lessThan, greaterThan, arrayContains)
-fun Query.where(field: String, inArray: List? = null, arrayContainsAny: List? = null) = _where(field, inArray, arrayContainsAny)
-fun Query.where(path: FieldPath, inArray: List? = null, arrayContainsAny: List? = null) = _where(path, inArray, arrayContainsAny)
+/** @return a native value of a wrapper or self. */
+private val Any.value get() = when (this) {
+ is Timestamp -> nativeValue
+ is GeoPoint -> nativeValue
+ is DocumentReference -> nativeValue
+ else -> this
+}
+
+fun Query.where(field: String, equalTo: Any?) = _where(field, equalTo,value)
+fun Query.where(path: FieldPath, equalTo: Any?) = _where(path, equalTo?.value)
+fun Query.where(field: String, equalTo: DocumentReference) = _where(field, equalTo.value)
+fun Query.where(path: FieldPath, equalTo: DocumentReference) = _where(path, equalTo.value)
+fun Query.where(field: String, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(field, lessThan?.value, greaterThan?.value, arrayContains?.value)
+fun Query.where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(path, lessThan?.value, greaterThan?.value, arrayContains?.value)
+fun Query.where(field: String, inArray: List? = null, arrayContainsAny: List? = null) = _where(field, inArray?.value, arrayContainsAny?.value)
+fun Query.where(path: FieldPath, inArray: List? = null, arrayContainsAny: List? = null) = _where(path, inArray?.value, arrayContainsAny?.value)
fun Query.orderBy(field: String, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction)
fun Query.orderBy(field: FieldPath, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction)
fun Query.startAfter(document: DocumentSnapshot) = _startAfter(document)
-fun Query.startAfter(vararg fieldValues: Any) = _startAfter(*fieldValues)
+fun Query.startAfter(vararg fieldValues: Any) = _startAfter(*(fieldValues.map { it.value }.toTypedArray()))
fun Query.startAt(document: DocumentSnapshot) = _startAt(document)
-fun Query.startAt(vararg fieldValues: Any) = _startAt(*fieldValues)
+fun Query.startAt(vararg fieldValues: Any) = _startAt(*(fieldValues.map { it.value }.toTypedArray()))
fun Query.endBefore(document: DocumentSnapshot) = _endBefore(document)
-fun Query.endBefore(vararg fieldValues: Any) = _endBefore(*fieldValues)
+fun Query.endBefore(vararg fieldValues: Any) = _endBefore(*(fieldValues.map { it.value }.toTypedArray()))
fun Query.endAt(document: DocumentSnapshot) = _endAt(document)
-fun Query.endAt(vararg fieldValues: Any) = _endAt(*fieldValues)
+fun Query.endAt(vararg fieldValues: Any) = _endAt(*(fieldValues.map { it.value }.toTypedArray()))
expect class WriteBatch {
inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, merge: Boolean = false): WriteBatch
@@ -120,7 +129,13 @@ expect class WriteBatch {
suspend fun commit()
}
-expect class DocumentReference {
+/** A class representing a platform specific Firebase DocumentReference. */
+expect class NativeDocumentReference
+
+/** A class representing a Firebase DocumentReference. */
+@Serializable(with = DocumentReferenceSerializer::class)
+expect class DocumentReference internal constructor(nativeValue: NativeDocumentReference) {
+ internal val nativeValue: NativeDocumentReference
val id: String
val path: String
@@ -147,6 +162,20 @@ expect class DocumentReference {
suspend fun delete()
}
+/**
+ * A serializer for [DocumentReference]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms.
+ */
+object DocumentReferenceSerializer : KSerializer by SpecialValueSerializer(
+ serialName = "DocumentReference",
+ toNativeValue = DocumentReference::nativeValue,
+ fromNativeValue = { value ->
+ when (value) {
+ is NativeDocumentReference -> DocumentReference(value)
+ else -> throw SerializationException("Cannot deserialize $value")
+ }
+ }
+)
+
expect class CollectionReference : Query {
val path: String
val document: DocumentReference
@@ -254,7 +283,7 @@ expect class FieldValue internal constructor(nativeValue: Any) {
}
/** A serializer for [FieldValue]. Must be used in conjunction with [FirebaseEncoder]. */
-object FieldValueSerializer : SpecialValueSerializer(
+object FieldValueSerializer : KSerializer by SpecialValueSerializer(
serialName = "FieldValue",
toNativeValue = FieldValue::nativeValue,
fromNativeValue = { raw ->
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
new file mode 100644
index 000000000..6ec7ee545
--- /dev/null
+++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/GeoPointTests.kt
@@ -0,0 +1,43 @@
+package dev.gitlive.firebase.firestore
+
+import dev.gitlive.firebase.decode
+import dev.gitlive.firebase.encode
+import dev.gitlive.firebase.nativeAssertEquals
+import dev.gitlive.firebase.nativeMapOf
+import kotlinx.serialization.Serializable
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+@Serializable
+data class TestDataWithGeoPoint(
+ val uid: String,
+ val location: GeoPoint
+)
+
+@Suppress("UNCHECKED_CAST")
+class GeoPointTests {
+
+ @Test
+ fun encodeGeoPointObject() = runTest {
+ val geoPoint = GeoPoint(12.3, 45.6)
+ val item = TestDataWithGeoPoint("123", geoPoint)
+ // check GeoPoint is encoded to a platform representation
+ nativeAssertEquals(
+ nativeMapOf("uid" to "123", "location" to geoPoint.nativeValue),
+ encode(item, shouldEncodeElementDefault = false)
+ )
+ }
+
+ @Test
+ fun decodeGeoPointObject() = runTest {
+ val geoPoint = GeoPoint(12.3, 45.6)
+ val obj = nativeMapOf(
+ "uid" to "123",
+ "location" to geoPoint.nativeValue
+ )
+ 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
new file mode 100644
index 000000000..d310df37b
--- /dev/null
+++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TimestampTests.kt
@@ -0,0 +1,129 @@
+package dev.gitlive.firebase.firestore
+
+import dev.gitlive.firebase.decode
+import dev.gitlive.firebase.encode
+import dev.gitlive.firebase.firebaseSerializer
+import dev.gitlive.firebase.nativeAssertEquals
+import dev.gitlive.firebase.nativeMapOf
+import kotlinx.serialization.Serializable
+import kotlin.test.*
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.DurationUnit
+
+@Serializable
+data class TestData(
+ val uid: String,
+ val createdAt: Timestamp,
+ var updatedAt: BaseTimestamp,
+ val deletedAt: BaseTimestamp?
+)
+
+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 = Timestamp(123, 456)
+ val item = TestData("uid123", timestamp, timestamp, null)
+ nativeAssertEquals(
+ nativeMapOf(
+ "uid" to "uid123",
+ "createdAt" to timestamp.nativeValue,
+ "updatedAt" to timestamp.nativeValue,
+ "deletedAt" to null
+ ),
+ encode(item, shouldEncodeElementDefault = false)
+ )
+ }
+
+ @Test
+ fun encodeServerTimestampObject() = runTest {
+ val timestamp = Timestamp(123, 456)
+ val item = TestData("uid123", timestamp, Timestamp.ServerTimestamp, Timestamp.ServerTimestamp)
+ nativeAssertEquals(
+ nativeMapOf(
+ "uid" to "uid123",
+ "createdAt" to timestamp.nativeValue,
+ "updatedAt" to FieldValue.serverTimestamp.nativeValue,
+ "deletedAt" to FieldValue.serverTimestamp.nativeValue
+ ),
+ encode(item, shouldEncodeElementDefault = false)
+ )
+ }
+
+ @Test
+ fun decodeTimestampObject() = runTest {
+ val timestamp = Timestamp(123, 345)
+ val obj = nativeMapOf(
+ "uid" to "uid123",
+ "createdAt" to timestamp.nativeValue,
+ "updatedAt" to timestamp.nativeValue,
+ "deletedAt" to timestamp.nativeValue
+ )
+ val decoded: TestData = decode(obj)
+ assertEquals("uid123", decoded.uid)
+ 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 = nativeMapOf(
+ "uid" to "uid123",
+ "createdAt" to Timestamp.now().nativeValue,
+ "updatedAt" to Timestamp.now().nativeValue,
+ "deletedAt" to null
+ )
+ val decoded: TestData = decode(obj)
+ assertEquals("uid123", decoded.uid)
+ assertNotNull(decoded.updatedAt)
+ assertNull(decoded.deletedAt)
+ }
+
+ @Test
+ fun serializers() = runTest {
+ assertEquals(BaseTimestampSerializer, (Timestamp(0, 0) as BaseTimestamp).firebaseSerializer())
+ assertEquals(BaseTimestampSerializer, (Timestamp.ServerTimestamp as BaseTimestamp).firebaseSerializer())
+ assertEquals(TimestampSerializer, Timestamp(0, 0).firebaseSerializer())
+ assertEquals(ServerTimestampSerializer, Timestamp.ServerTimestamp.firebaseSerializer())
+ }
+
+ @Test
+ fun timestampMillisecondsConversion() = runTest {
+ val ms = 1666170858063.0
+
+ val timestamp = Timestamp.fromMilliseconds(ms)
+ assertEquals(ms, timestamp.toMilliseconds())
+ }
+
+ @Test
+ fun timestampDurationConversion() = runTest {
+ val duration = 1666170858063.milliseconds
+ val (seconds, nanoseconds) = duration.toComponents { seconds, nanoseconds -> seconds to nanoseconds }
+ val timestamp = Timestamp.fromDuration(duration)
+ assertEquals(seconds, timestamp.seconds)
+ assertEquals(nanoseconds, timestamp.nanoseconds)
+ assertEquals(duration, timestamp.toDuration())
+ }
+}
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 3142f6e95..b6cc28fd7 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
@@ -459,6 +459,64 @@ class FirebaseFirestoreTest {
assertNotEquals(DoubleAsTimestampSerializer.serverTimestamp, pendingWritesSnapshot.data(DoubleTimestamp.serializer(), ServerTimestampBehavior.ESTIMATE).time)
}
+ @Test
+ fun testLegacyDoubleTimestampWriteNewFormatRead() = runTest {
+ @Serializable
+ data class LegacyDocument(
+ @Serializable(with = DoubleAsTimestampSerializer::class)
+ val time: Double
+ )
+
+ @Serializable
+ data class NewDocument(
+ val time: Timestamp
+ )
+
+ val doc = Firebase.firestore
+ .collection("testLegacyDoubleTimestampEncodeDecode")
+ .document("testLegacy")
+
+ val ms = 12345678.0
+
+ doc.set(LegacyDocument(time = ms))
+
+ val fetched: NewDocument = doc.get().data()
+ assertEquals(ms, fetched.time.toMilliseconds())
+ }
+
+ @Test
+ fun testQueryByTimestamp() = runTest {
+ @Serializable
+ data class DocumentWithTimestamp(
+ val time: Timestamp
+ )
+
+ val collection = Firebase.firestore
+ .collection("testQueryByTimestamp")
+
+ val timestamp = Timestamp.now()
+
+ val pastTimestamp = Timestamp(timestamp.seconds - 60, 12345000) // note: iOS truncates 3 last digits of nanoseconds due to internal conversions
+ val futureTimestamp = Timestamp(timestamp.seconds + 60, 78910000)
+
+ collection.add(DocumentWithTimestamp(pastTimestamp))
+ collection.add(DocumentWithTimestamp(futureTimestamp))
+
+ val equalityQueryResult = collection.where(
+ path = FieldPath(DocumentWithTimestamp::time.name),
+ equalTo = pastTimestamp
+ ).get().documents.map { it.data() }
+
+ assertEquals(listOf(DocumentWithTimestamp(pastTimestamp)), equalityQueryResult)
+
+ val gtQueryResult = collection.where(
+ path = FieldPath(DocumentWithTimestamp::time.name),
+ greaterThan = timestamp
+ ).get().documents.map { it.data() }
+
+ assertEquals(listOf(DocumentWithTimestamp(futureTimestamp)), gtQueryResult)
+ }
+
private suspend fun setupFirestoreData() {
Firebase.firestore.collection("testFirestoreQuerying")
.document("one")
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
new file mode 100644
index 000000000..d4c0aa98f
--- /dev/null
+++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
@@ -0,0 +1,21 @@
+package dev.gitlive.firebase.firestore
+
+import cocoapods.FirebaseFirestore.FIRGeoPoint
+import kotlinx.serialization.Serializable
+
+
+/** A class representing a platform specific Firebase GeoPoint. */
+actual typealias NativeGeoPoint = FIRGeoPoint
+
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+actual class GeoPoint internal actual constructor(internal actual val nativeValue: NativeGeoPoint) {
+ actual constructor(latitude: Double, longitude: Double) : this(NativeGeoPoint(latitude, longitude))
+ actual val latitude: Double = nativeValue.latitude
+ actual val longitude: Double = nativeValue.longitude
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is GeoPoint && nativeValue == other.nativeValue
+ override fun hashCode(): Int = nativeValue.hashCode()
+ override fun toString(): String = nativeValue.toString()
+}
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 2af5e5ed1..2f0f05534 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
@@ -165,8 +165,12 @@ actual class Transaction(val ios: FIRTransaction) {
}
-@Suppress("UNCHECKED_CAST")
-actual class DocumentReference(val ios: FIRDocumentReference) {
+/** A class representing a platform specific Firebase DocumentReference. */
+actual typealias NativeDocumentReference = FIRDocumentReference
+
+@Serializable(with = DocumentReferenceSerializer::class)
+actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) {
+ val ios: NativeDocumentReference by ::nativeValue
actual val id: String
get() = ios.documentID
@@ -232,6 +236,11 @@ actual class DocumentReference(val ios: FIRDocumentReference) {
}
awaitClose { listener.remove() }
}
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is DocumentReference && nativeValue == other.nativeValue
+ override fun hashCode(): Int = nativeValue.hashCode()
+ override fun toString(): String = nativeValue.toString()
}
actual open class Query(open val ios: FIRQuery) {
@@ -451,6 +460,10 @@ actual class SnapshotMetadata(val ios: FIRSnapshotMetadata) {
actual class FieldPath private constructor(val ios: FIRFieldPath) {
actual constructor(vararg fieldNames: String) : this(FIRFieldPath(fieldNames.asList()))
actual val documentId: FieldPath get() = FieldPath(FIRFieldPath.documentID())
+
+ override fun equals(other: Any?): Boolean = other is FieldPath && ios == other.ios
+ override fun hashCode(): Int = ios.hashCode()
+ override fun toString(): String = ios.toString()
}
/** A class representing a platform specific Firebase FieldValue. */
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
new file mode 100644
index 000000000..808704cf5
--- /dev/null
+++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/GeoPoint.kt
@@ -0,0 +1,20 @@
+package dev.gitlive.firebase.firestore
+
+import dev.gitlive.firebase.*
+import kotlinx.serialization.Serializable
+
+/** A class representing a platform specific Firebase GeoPoint. */
+actual typealias NativeGeoPoint = firebase.firestore.GeoPoint
+
+/** A class representing a Firebase GeoPoint. */
+@Serializable(with = GeoPointSerializer::class)
+actual class GeoPoint internal actual constructor(internal actual val nativeValue: NativeGeoPoint) {
+ actual constructor(latitude: Double, longitude: Double) : this(NativeGeoPoint(latitude, longitude))
+ actual val latitude: Double by nativeValue::latitude
+ actual val longitude: Double by nativeValue::longitude
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is GeoPoint && nativeValue.isEqual(other.nativeValue)
+ override fun hashCode(): Int = nativeValue.hashCode()
+ override fun toString(): String = "GeoPoint[lat=$latitude,long=$longitude]"
+}
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 4bf8955cd..78e69dd50 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
@@ -181,7 +181,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 NativeDocumentReference = firebase.firestore.DocumentReference
+
+@Serializable(with = DocumentReferenceSerializer::class)
+actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) {
+ val js: NativeDocumentReference by ::nativeValue
actual val id: String
get() = rethrow { js.id }
@@ -241,6 +246,11 @@ actual class DocumentReference(val js: firebase.firestore.DocumentReference) {
)
awaitClose { unsubscribe() }
}
+
+ override fun equals(other: Any?): Boolean =
+ this === other || other is DocumentReference && nativeValue.isEqual(other.nativeValue)
+ override fun hashCode(): Int = nativeValue.hashCode()
+ override fun toString(): String = "DocumentReference(path=$path)"
}
actual open class Query(open val js: firebase.firestore.Query) {
@@ -412,6 +422,10 @@ actual class FieldPath private constructor(val js: firebase.firestore.FieldPath)
js("Reflect").construct(firebase.firestore.FieldPath, fieldNames).unsafeCast()
})
actual val documentId: FieldPath get() = FieldPath(firebase.firestore.FieldPath.documentId)
+
+ override fun equals(other: Any?): Boolean = other is FieldPath && js.isEqual(other.js)
+ override fun hashCode(): Int = js.hashCode()
+ override fun toString(): String = js.toString()
}
/** Represents a platform specific Firebase FieldValue. */
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 06ccce75c..b81594f9f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -8,7 +8,8 @@ include(
"firebase-functions",
"firebase-installations",
"firebase-perf",
- "firebase-crashlytics"
+ "firebase-crashlytics",
+ "test-utils"
)
//enableFeaturePreview("GRADLE_METADATA")
diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts
new file mode 100644
index 000000000..18579f1e5
--- /dev/null
+++ b/test-utils/build.gradle.kts
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2023 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+// this project is used only in tests to share common code. publishing is disabled in the root build.gradle.kts
+
+version = "0.0.1"
+
+plugins {
+ id("com.android.library")
+ kotlin("multiplatform")
+ kotlin("plugin.serialization") version "1.8.20"
+}
+
+android {
+ compileSdk = property("targetSdkVersion") as Int
+ defaultConfig {
+ minSdk = property("minSdkVersion") as Int
+ targetSdk = property("targetSdkVersion") as Int
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ sourceSets {
+ getByName("main") {
+ manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ }
+ }
+ packagingOptions {
+ resources.pickFirsts.add("META-INF/kotlinx-serialization-core.kotlin_module")
+ resources.pickFirsts.add("META-INF/AL2.0")
+ resources.pickFirsts.add("META-INF/LGPL2.1")
+ }
+ lint {
+ abortOnError = false
+ }
+}
+
+kotlin {
+
+ android {
+ publishAllLibraryVariants()
+ }
+
+ val supportIosTarget = project.property("skipIosTarget") != "true"
+
+ if (supportIosTarget) {
+ ios()
+ iosSimulatorArm64()
+ }
+
+ js {
+ useCommonJs()
+ nodejs()
+ browser()
+ }
+
+ sourceSets {
+ all {
+ languageSettings.apply {
+ apiVersion = "1.8"
+ languageVersion = "1.8"
+ progressiveMode = true
+ }
+ }
+
+ val commonMain by getting {
+ dependencies {
+ implementation(kotlin("test"))
+ }
+ }
+
+ if (supportIosTarget) {
+ val iosMain by getting
+ val iosSimulatorArm64Main by getting
+ iosSimulatorArm64Main.dependsOn(iosMain)
+ val iosTest by sourceSets.getting
+ val iosSimulatorArm64Test by getting
+ iosSimulatorArm64Test.dependsOn(iosTest)
+ }
+
+ val jsMain by getting
+ }
+}
diff --git a/test-utils/src/androidMain/AndroidManifest.xml b/test-utils/src/androidMain/AndroidManifest.xml
new file mode 100644
index 000000000..07001b111
--- /dev/null
+++ b/test-utils/src/androidMain/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/firebase-common/src/androidAndroidTest/kotlin/dev/gitlive/firebase/EncodersTest.kt b/test-utils/src/androidMain/kotlin/dev/gitlive/firebase/TestUtils.kt
similarity index 72%
rename from firebase-common/src/androidAndroidTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
rename to test-utils/src/androidMain/kotlin/dev/gitlive/firebase/TestUtils.kt
index 231ee7b7d..90f236dee 100644
--- a/firebase-common/src/androidAndroidTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
+++ b/test-utils/src/androidMain/kotlin/dev/gitlive/firebase/TestUtils.kt
@@ -6,4 +6,6 @@ package dev.gitlive.firebase
actual fun nativeMapOf(vararg pairs: Pair): Any = mapOf(*pairs)
actual fun nativeListOf(vararg elements: Any): Any = listOf(*elements)
-actual fun nativeAssertEquals(expected: Any?, actual: Any?) = kotlin.test.assertEquals(expected, actual)
+actual fun nativeAssertEquals(expected: Any?, actual: Any?) {
+ kotlin.test.assertEquals(expected, actual)
+}
diff --git a/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt b/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt
new file mode 100644
index 000000000..ee6a2fe7a
--- /dev/null
+++ b/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.gitlive.firebase
+
+expect fun nativeMapOf(vararg pairs: Pair): Any
+expect fun nativeListOf(vararg elements: Any): Any
+expect fun nativeAssertEquals(expected: Any?, actual: Any?)
diff --git a/firebase-common/src/iosTest/kotlin/dev/gitlive/firebase/EncodersTest.kt b/test-utils/src/iosMain/kotlin/dev/gitlive/firebase/TestUtils.kt
similarity index 72%
rename from firebase-common/src/iosTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
rename to test-utils/src/iosMain/kotlin/dev/gitlive/firebase/TestUtils.kt
index 28b9eea5e..90f236dee 100644
--- a/firebase-common/src/iosTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
+++ b/test-utils/src/iosMain/kotlin/dev/gitlive/firebase/TestUtils.kt
@@ -6,4 +6,6 @@ package dev.gitlive.firebase
actual fun nativeMapOf(vararg pairs: Pair): Any = mapOf(*pairs)
actual fun nativeListOf(vararg elements: Any): Any = listOf(*elements)
-actual fun nativeAssertEquals(expected: Any?, actual: Any?) = kotlin.test.assertEquals(expected, actual)
\ No newline at end of file
+actual fun nativeAssertEquals(expected: Any?, actual: Any?) {
+ kotlin.test.assertEquals(expected, actual)
+}
diff --git a/firebase-common/src/jsTest/kotlin/dev/gitlive/firebase/EncodersTest.kt b/test-utils/src/jsMain/kotlin/dev/gitlive/firebase/TestUtils.kt
similarity index 67%
rename from firebase-common/src/jsTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
rename to test-utils/src/jsMain/kotlin/dev/gitlive/firebase/TestUtils.kt
index 2a330f2d9..258541dd5 100644
--- a/firebase-common/src/jsTest/kotlin/dev/gitlive/firebase/EncodersTest.kt
+++ b/test-utils/src/jsMain/kotlin/dev/gitlive/firebase/TestUtils.kt
@@ -8,4 +8,6 @@ import kotlin.js.json
actual fun nativeMapOf(vararg pairs: Pair): Any = json(*pairs)
actual fun nativeListOf(vararg elements: Any): Any = elements
-actual fun nativeAssertEquals(expected: Any?, actual: Any?) = kotlin.test.assertEquals(JSON.stringify(expected), JSON.stringify(actual))
\ No newline at end of file
+actual fun nativeAssertEquals(expected: Any?, actual: Any?) {
+ kotlin.test.assertEquals(JSON.stringify(expected), JSON.stringify(actual))
+}