Skip to content

Commit

Permalink
Add ServerTimestampBehavior in Firestore module (#246)
Browse files Browse the repository at this point in the history
* Add ServerTimestampBehavior

* Remove redundant parentheses
  • Loading branch information
shepeliev authored Oct 30, 2021
1 parent 8d28273 commit 9ce9222
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
package dev.gitlive.firebase.firestore

import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking

actual val emulatorHost: String = "10.0.2.2"

actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext

actual fun runTest(test: suspend () -> Unit) = runBlocking { test() }
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking { test() }
Original file line number Diff line number Diff line change
Expand Up @@ -409,22 +409,32 @@ actual class DocumentSnapshot(val android: com.google.firebase.firestore.Documen
actual val id get() = android.id
actual val reference get() = DocumentReference(android.reference)

actual inline fun <reified T: Any> data() = decode<T>(value = android.data)
actual inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior): T =
decode(value = android.getData(serverTimestampBehavior.toAndroid()))

actual fun <T> data(strategy: DeserializationStrategy<T>) = decode(strategy, android.data)
actual fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T =
decode(strategy, android.getData(serverTimestampBehavior.toAndroid()))

actual fun dataMap(): Map<String, Any?> = android.data ?: emptyMap()
actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map<String, Any?> =
android.getData(serverTimestampBehavior.toAndroid()) ?: emptyMap()

actual inline fun <reified T> get(field: String) = decode<T>(value = android.get(field))
actual inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T =
decode(value = android.get(field, serverTimestampBehavior.toAndroid()))

actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
decode(strategy, android.get(field))
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T =
decode(strategy, android.get(field, serverTimestampBehavior.toAndroid()))

actual fun contains(field: String) = android.contains(field)

actual val exists get() = android.exists()

actual val metadata: SnapshotMetadata get() = SnapshotMetadata(android.metadata)

fun ServerTimestampBehavior.toAndroid(): com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior = when (this) {
ServerTimestampBehavior.ESTIMATE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.ESTIMATE
ServerTimestampBehavior.NONE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.NONE
ServerTimestampBehavior.PREVIOUS -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.PREVIOUS
}
}

actual class SnapshotMetadata(val android: com.google.firebase.firestore.SnapshotMetadata) {
Expand All @@ -444,4 +454,3 @@ actual object FieldValue {
actual fun arrayRemove(vararg elements: Any): Any = FieldValue.arrayRemove(*elements)
actual fun delete(): Any = delete
}

Original file line number Diff line number Diff line change
Expand Up @@ -188,22 +188,28 @@ expect class DocumentChange {

expect class DocumentSnapshot {

inline fun <reified T> get(field: String): T
fun <T> get(field: String, strategy: DeserializationStrategy<T>): T
inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T
fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T

fun contains(field: String): Boolean

inline fun <reified T: Any> data(): T
fun <T> data(strategy: DeserializationStrategy<T>): T
inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T
fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): T

fun dataMap(): Map<String, Any?>
fun dataMap(serverTimestampBehavior: ServerTimestampBehavior = ServerTimestampBehavior.NONE): Map<String, Any?>

val exists: Boolean
val id: String
val reference: DocumentReference
val metadata: SnapshotMetadata
}

enum class ServerTimestampBehavior {
ESTIMATE,
NONE,
PREVIOUS
}

expect class SnapshotMetadata {
val hasPendingWrites: Boolean
val isFromCache: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,29 @@

package dev.gitlive.firebase.firestore

import dev.gitlive.firebase.*
import kotlinx.serialization.*
import kotlin.test.*
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseOptions
import dev.gitlive.firebase.apps
import dev.gitlive.firebase.initialize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.Serializable
import kotlin.random.Random
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

expect val emulatorHost: String
expect val context: Any
expect fun runTest(test: suspend () -> Unit)
expect fun runTest(test: suspend CoroutineScope.() -> Unit)

class FirebaseFirestoreTest {

Expand Down Expand Up @@ -121,7 +137,73 @@ class FirebaseFirestoreTest {

assertNotEquals(FieldValue.serverTimestamp, doc.get().get("time"))
assertNotEquals(FieldValue.serverTimestamp, doc.get().data(FirestoreTest.serializer()).time)
}

@Test
fun testServerTimestampBehaviorNone() = runTest {
val doc = Firebase.firestore
.collection("testServerTimestampBehaviorNone")
.document("test${Random.nextInt()}")

val deferredPendingWritesSnapshot = async {
withTimeout(5000) {
doc.snapshots.filter { it.exists }.first()
}
}
delay(100) // makes possible to catch pending writes snapshot

doc.set(
FirestoreTest.serializer(),
FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp)
)

val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
assertNull(pendingWritesSnapshot.get<Double?>("time", ServerTimestampBehavior.NONE))
assertNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.NONE)["time"])
}

@Test
fun testServerTimestampBehaviorEstimate() = runTest {
val doc = Firebase.firestore
.collection("testServerTimestampBehaviorEstimate")
.document("test${Random.nextInt()}")

val deferredPendingWritesSnapshot = async {
withTimeout(5000) {
doc.snapshots.filter { it.exists }.first()
}
}
delay(100) // makes possible to catch pending writes snapshot

doc.set(FirestoreTest.serializer(), FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp))

val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
assertNotNull(pendingWritesSnapshot.get<Double?>("time", ServerTimestampBehavior.ESTIMATE))
assertNotNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.ESTIMATE)["time"])
assertNotEquals(0.0, pendingWritesSnapshot.data(FirestoreTest.serializer(), ServerTimestampBehavior.ESTIMATE).time)
}

@Test
fun testServerTimestampBehaviorPrevious() = runTest {
val doc = Firebase.firestore
.collection("testServerTimestampBehaviorPrevious")
.document("test${Random.nextInt()}")

val deferredPendingWritesSnapshot = async {
withTimeout(5000) {
doc.snapshots.filter { it.exists }.first()
}
}
delay(100) // makes possible to catch pending writes snapshot

doc.set(FirestoreTest.serializer(), FirestoreTest("ServerTimestampBehavior", FieldValue.serverTimestamp))

val pendingWritesSnapshot = deferredPendingWritesSnapshot.await()
assertTrue(pendingWritesSnapshot.metadata.hasPendingWrites)
assertNull(pendingWritesSnapshot.get<Double?>("time", ServerTimestampBehavior.PREVIOUS))
assertNull(pendingWritesSnapshot.dataMap(ServerTimestampBehavior.PREVIOUS)["time"])
}

@Test
Expand Down Expand Up @@ -169,4 +251,4 @@ class FirebaseFirestoreTest {
.document("three")
.set(FirestoreTest.serializer(), FirestoreTest("ccc"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import platform.Foundation.NSError
import platform.Foundation.NSNull

@PublishedApi
internal inline fun <reified T> decode(value: Any?): T =
Expand Down Expand Up @@ -377,22 +378,43 @@ actual class DocumentSnapshot(val ios: FIRDocumentSnapshot) {

actual val reference get() = DocumentReference(ios.reference)

actual inline fun <reified T: Any> data() = decode<T>(value = ios.data())
actual inline fun <reified T: Any> data(serverTimestampBehavior: ServerTimestampBehavior): T {
val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
return decode(value = data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } })
}

actual fun <T> data(strategy: DeserializationStrategy<T>) = decode(strategy, ios.data())
actual fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T {
val data = ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
return decode(strategy, data?.mapValues { (_, value) -> value?.takeIf { it !is NSNull } })
}

actual fun dataMap(): Map<String, Any?> = ios.data()?.map { it.key.toString() to it.value }?.toMap() ?: emptyMap()
actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map<String, Any?> =
ios.dataWithServerTimestampBehavior(serverTimestampBehavior.toIos())
?.map { (key, value) -> key.toString() to value?.takeIf { it !is NSNull } }
?.toMap()
?: emptyMap()

actual inline fun <reified T> get(field: String) = decode<T>(value = ios.valueForField(field))
actual inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T {
val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull }
return decode(value)
}

actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
decode(strategy, ios.valueForField(field))
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T {
val value = ios.valueForField(field, serverTimestampBehavior.toIos())?.takeIf { it !is NSNull }
return decode(strategy, value)
}

actual fun contains(field: String) = ios.valueForField(field) != null

actual val exists get() = ios.exists

actual val metadata: SnapshotMetadata get() = SnapshotMetadata(ios.metadata)

fun ServerTimestampBehavior.toIos() : FIRServerTimestampBehavior = when (this) {
ServerTimestampBehavior.ESTIMATE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorEstimate
ServerTimestampBehavior.NONE -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorNone
ServerTimestampBehavior.PREVIOUS -> FIRServerTimestampBehavior.FIRServerTimestampBehaviorPrevious
}
}

actual class SnapshotMetadata(val ios: FIRSnapshotMetadata) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ actual val emulatorHost: String = "localhost"

actual val context: Any = Unit

actual fun runTest(test: suspend () -> Unit) = runBlocking {
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking {
val testRun = MainScope().async { test() }
while (testRun.isActive) {
NSRunLoop.mainRunLoop.runMode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,23 +387,27 @@ actual class DocumentSnapshot(val js: firebase.firestore.DocumentSnapshot) {
actual val id get() = rethrow { js.id }
actual val reference get() = rethrow { DocumentReference(js.ref) }

actual inline fun <reified T : Any> data(): T =
rethrow { decode<T>(value = js.data()) }
actual inline fun <reified T : Any> data(serverTimestampBehavior: ServerTimestampBehavior): T =
rethrow { decode(value = js.data(getTimestampsOptions(serverTimestampBehavior))) }

actual fun <T> data(strategy: DeserializationStrategy<T>): T =
rethrow { decode(strategy, js.data()) }
actual fun <T> data(strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior): T =
rethrow { decode(strategy, js.data(getTimestampsOptions(serverTimestampBehavior))) }

actual fun dataMap(): Map<String, Any?> = rethrow { mapOf(js.data().asDynamic()) }
actual fun dataMap(serverTimestampBehavior: ServerTimestampBehavior): Map<String, Any?> =
rethrow { mapOf(js.data(getTimestampsOptions(serverTimestampBehavior)).asDynamic()) }

actual inline fun <reified T> get(field: String) =
rethrow { decode<T>(value = js.get(field)) }
actual inline fun <reified T> get(field: String, serverTimestampBehavior: ServerTimestampBehavior) =
rethrow { decode<T>(value = js.get(field, getTimestampsOptions(serverTimestampBehavior))) }

actual fun <T> get(field: String, strategy: DeserializationStrategy<T>) =
rethrow { decode(strategy, js.get(field)) }
actual fun <T> get(field: String, strategy: DeserializationStrategy<T>, serverTimestampBehavior: ServerTimestampBehavior) =
rethrow { decode(strategy, js.get(field, getTimestampsOptions(serverTimestampBehavior))) }

actual fun contains(field: String) = rethrow { js.get(field) != undefined }
actual val exists get() = rethrow { js.exists }
actual val metadata: SnapshotMetadata get() = SnapshotMetadata(js.metadata)

fun getTimestampsOptions(serverTimestampBehavior: ServerTimestampBehavior) =
json("serverTimestamps" to serverTimestampBehavior.name.lowercase())
}

actual class SnapshotMetadata(val js: firebase.firestore.SnapshotMetadata) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

package dev.gitlive.firebase.firestore

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise

actual val emulatorHost: String = "localhost"

actual val context: Any = Unit

actual fun runTest(test: suspend () -> Unit) = GlobalScope
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = GlobalScope
.promise {
try {
test()
Expand All @@ -28,4 +29,3 @@ internal fun Throwable.log() {
it.log()
}
}

0 comments on commit 9ce9222

Please sign in to comment.