Skip to content

Commit aa8189f

Browse files
added GeoPoints, serialize DocumentReference natively, fix Timestamp extensions, improve debugging
1 parent 80770ac commit aa8189f

File tree

17 files changed

+379
-19
lines changed

17 files changed

+379
-19
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ db.collection("cities").document("LA").set(City.serializer(), city, encodeDefaul
8888
```
8989

9090
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.
91+
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`.
9192

9293
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)
9394

@@ -110,6 +111,21 @@ data class Post(
110111
)
111112
```
112113

114+
In addition `firebase-firestore` provides [GeoPoint] and [DocumentReference] classes which allow persisting
115+
geo points and document references in a native way:
116+
117+
```kotlin
118+
@Serializable
119+
data class PointOfInterest(
120+
val reference: DocumentReference,
121+
val location: GeoPoint
122+
)
123+
val document = PointOfInterest(
124+
reference = Firebase.firestore.collection("foo").document("bar"),
125+
location = GeoPoint(51.939, 4.506)
126+
)
127+
```
128+
113129
<h4>Polymorphic serialization (sealed classes)</h4>
114130

115131
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.

firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,8 @@ external object firebase {
453453
fun update(field: FieldPath, value: Any?, vararg moreFieldsAndValues: Any?): Promise<Unit>
454454
fun delete(): Promise<Unit>
455455
fun onSnapshot(next: (snapshot: DocumentSnapshot) -> Unit, error: (error: Error) -> Unit): ()->Unit
456+
457+
fun isEqual(other: DocumentReference): Boolean
456458
}
457459

458460
open class WriteBatch {
@@ -477,6 +479,8 @@ external object firebase {
477479
companion object {
478480
val documentId: FieldPath
479481
}
482+
483+
fun isEqual(other: FieldPath): Boolean
480484
}
481485

482486
abstract class FieldValue {
@@ -490,6 +494,13 @@ external object firebase {
490494

491495
fun isEqual(other: FieldValue): Boolean
492496
}
497+
498+
open class GeoPoint(latitude: Double, longitude: Double) {
499+
val latitude: Double
500+
val longitude: Double
501+
502+
fun isEqual(other: GeoPoint): Boolean
503+
}
493504
}
494505

495506
fun remoteConfig(app: App? = definedExternally): remoteConfig.RemoteConfig

firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ actual val emulatorHost: String = "10.0.2.2"
1414
actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext
1515

1616
actual fun runTest(test: suspend CoroutineScope.() -> Unit) = runBlocking { test() }
17+
18+
@Suppress("UNCHECKED_CAST")
19+
actual fun encodedAsMap(encoded: Any?): Map<String, Any?> = encoded as Map<String, Any?>
20+
21+
actual fun Map<String, Any?>.asEncoded(): Any = this
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dev.gitlive.firebase.firestore
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/** A class representing a platform specific Firebase GeoPoint. */
6+
actual typealias NativeGeoPoint = com.google.firebase.firestore.GeoPoint
7+
8+
/** A class representing a Firebase GeoPoint. */
9+
@Serializable(with = GeoPointSerializer::class)
10+
actual class GeoPoint internal actual constructor(internal actual val nativeValue: NativeGeoPoint) {
11+
actual constructor(latitude: Double, longitude: Double) : this(NativeGeoPoint(latitude, longitude))
12+
actual val latitude: Double = nativeValue.latitude
13+
actual val longitude: Double = nativeValue.longitude
14+
15+
override fun equals(other: Any?): Boolean =
16+
this === other || other is GeoPoint && nativeValue == other.nativeValue
17+
override fun hashCode(): Int = nativeValue.hashCode()
18+
override fun toString(): String = nativeValue.toString()
19+
}

firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,12 @@ actual class Transaction(val android: com.google.firebase.firestore.Transaction)
196196
DocumentSnapshot(android.get(documentRef.android))
197197
}
198198

199-
actual class DocumentReference(val android: com.google.firebase.firestore.DocumentReference) {
199+
/** A class representing a platform specific Firebase DocumentReference. */
200+
actual typealias NativeDocumentReference = com.google.firebase.firestore.DocumentReference
200201

202+
@Serializable(with = DocumentReferenceSerializer::class)
203+
actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) {
204+
val android: NativeDocumentReference by ::nativeValue
201205
actual val id: String
202206
get() = android.id
203207

@@ -270,6 +274,10 @@ actual class DocumentReference(val android: com.google.firebase.firestore.Docume
270274
}
271275
awaitClose { listener.remove() }
272276
}
277+
override fun equals(other: Any?): Boolean =
278+
this === other || other is DocumentReference && nativeValue == other.nativeValue
279+
override fun hashCode(): Int = nativeValue.hashCode()
280+
override fun toString(): String = nativeValue.toString()
273281
}
274282

275283
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
432440
actual class FieldPath private constructor(val android: com.google.firebase.firestore.FieldPath) {
433441
actual constructor(vararg fieldNames: String) : this(com.google.firebase.firestore.FieldPath.of(*fieldNames))
434442
actual val documentId: FieldPath get() = FieldPath(com.google.firebase.firestore.FieldPath.documentId())
443+
444+
override fun equals(other: Any?): Boolean = other is FieldPath && android == other.android
445+
override fun hashCode(): Int = android.hashCode()
446+
override fun toString(): String = android.toString()
435447
}
436448

437449
/** Represents a platform specific Firebase FieldValue. */
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.gitlive.firebase.firestore
2+
3+
import dev.gitlive.firebase.SpecialValueSerializer
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.SerializationException
6+
7+
/** A class representing a platform specific Firebase GeoPoint. */
8+
expect class NativeGeoPoint
9+
10+
/** A class representing a Firebase GeoPoint. */
11+
@Serializable(with = GeoPointSerializer::class)
12+
expect class GeoPoint internal constructor(nativeValue: NativeGeoPoint) {
13+
constructor(latitude: Double, longitude: Double)
14+
val latitude: Double
15+
val longitude: Double
16+
internal val nativeValue: NativeGeoPoint
17+
}
18+
19+
/** Serializer for [GeoPoint]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
20+
object GeoPointSerializer : SpecialValueSerializer<GeoPoint>(
21+
serialName = "GeoPoint",
22+
toNativeValue = GeoPoint::nativeValue,
23+
fromNativeValue = { value ->
24+
when (value) {
25+
is NativeGeoPoint -> GeoPoint(value)
26+
else -> throw SerializationException("Cannot deserialize $value")
27+
}
28+
}
29+
)

firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ expect class Timestamp internal constructor(nativeValue: NativeTimestamp): BaseT
3232
}
3333

3434
fun Timestamp.Companion.fromMilliseconds(milliseconds: Double): Timestamp =
35-
Timestamp((milliseconds / 1000).toLong(), (milliseconds * 1000).toInt() % 1000000)
36-
fun Timestamp.toMilliseconds(): Double = seconds * 1000 + (nanoseconds / 1000.0)
35+
Timestamp((milliseconds / 1000).toLong(), ((milliseconds % 1000) * 1000000).toInt())
36+
fun Timestamp.toMilliseconds(): Double = seconds * 1000 + nanoseconds / 1000000.0
3737

3838
/** A serializer for [BaseTimestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
3939
object BaseTimestampSerializer : SpecialValueSerializer<BaseTimestamp>(

firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,27 +79,35 @@ expect open class Query {
7979
internal fun _endAt(vararg fieldValues: Any): Query
8080
}
8181

82-
fun Query.where(field: String, equalTo: Any?) = _where(field, equalTo)
83-
fun Query.where(path: FieldPath, equalTo: Any?) = _where(path, equalTo)
84-
fun Query.where(field: String, equalTo: DocumentReference) = _where(field, equalTo)
85-
fun Query.where(path: FieldPath, equalTo: DocumentReference) = _where(path, equalTo)
86-
fun Query.where(field: String, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(field, lessThan, greaterThan, arrayContains)
87-
fun Query.where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(path, lessThan, greaterThan, arrayContains)
88-
fun Query.where(field: String, inArray: List<Any>? = null, arrayContainsAny: List<Any>? = null) = _where(field, inArray, arrayContainsAny)
89-
fun Query.where(path: FieldPath, inArray: List<Any>? = null, arrayContainsAny: List<Any>? = null) = _where(path, inArray, arrayContainsAny)
82+
/** @return a native value of a wrapper or self. */
83+
private val Any.value get() = when (this) {
84+
is Timestamp -> nativeValue
85+
is GeoPoint -> nativeValue
86+
is DocumentReference -> nativeValue
87+
else -> this
88+
}
89+
90+
fun Query.where(field: String, equalTo: Any?) = _where(field, equalTo,value)
91+
fun Query.where(path: FieldPath, equalTo: Any?) = _where(path, equalTo?.value)
92+
fun Query.where(field: String, equalTo: DocumentReference) = _where(field, equalTo.value)
93+
fun Query.where(path: FieldPath, equalTo: DocumentReference) = _where(path, equalTo.value)
94+
fun Query.where(field: String, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(field, lessThan?.value, greaterThan?.value, arrayContains?.value)
95+
fun Query.where(path: FieldPath, lessThan: Any? = null, greaterThan: Any? = null, arrayContains: Any? = null) = _where(path, lessThan?.value, greaterThan?.value, arrayContains?.value)
96+
fun Query.where(field: String, inArray: List<Any>? = null, arrayContainsAny: List<Any>? = null) = _where(field, inArray?.value, arrayContainsAny?.value)
97+
fun Query.where(path: FieldPath, inArray: List<Any>? = null, arrayContainsAny: List<Any>? = null) = _where(path, inArray?.value, arrayContainsAny?.value)
9098

9199
fun Query.orderBy(field: String, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction)
92100
fun Query.orderBy(field: FieldPath, direction: Direction = Direction.ASCENDING) = _orderBy(field, direction)
93101

94102
fun Query.startAfter(document: DocumentSnapshot) = _startAfter(document)
95-
fun Query.startAfter(vararg fieldValues: Any) = _startAfter(*fieldValues)
103+
fun Query.startAfter(vararg fieldValues: Any) = _startAfter(*(fieldValues.map { it.value }.toTypedArray()))
96104
fun Query.startAt(document: DocumentSnapshot) = _startAt(document)
97-
fun Query.startAt(vararg fieldValues: Any) = _startAt(*fieldValues)
105+
fun Query.startAt(vararg fieldValues: Any) = _startAt(*(fieldValues.map { it.value }.toTypedArray()))
98106

99107
fun Query.endBefore(document: DocumentSnapshot) = _endBefore(document)
100-
fun Query.endBefore(vararg fieldValues: Any) = _endBefore(*fieldValues)
108+
fun Query.endBefore(vararg fieldValues: Any) = _endBefore(*(fieldValues.map { it.value }.toTypedArray()))
101109
fun Query.endAt(document: DocumentSnapshot) = _endAt(document)
102-
fun Query.endAt(vararg fieldValues: Any) = _endAt(*fieldValues)
110+
fun Query.endAt(vararg fieldValues: Any) = _endAt(*(fieldValues.map { it.value }.toTypedArray()))
103111

104112
expect class WriteBatch {
105113
inline fun <reified T> set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean = true, merge: Boolean = false): WriteBatch
@@ -120,7 +128,13 @@ expect class WriteBatch {
120128
suspend fun commit()
121129
}
122130

123-
expect class DocumentReference {
131+
/** A class representing a platform specific Firebase DocumentReference. */
132+
expect class NativeDocumentReference
133+
134+
/** A class representing a Firebase DocumentReference. */
135+
@Serializable(with = DocumentReferenceSerializer::class)
136+
expect class DocumentReference internal constructor(nativeValue: NativeDocumentReference) {
137+
internal val nativeValue: NativeDocumentReference
124138

125139
val id: String
126140
val path: String
@@ -147,6 +161,20 @@ expect class DocumentReference {
147161
suspend fun delete()
148162
}
149163

164+
/**
165+
* A serializer for [DocumentReference]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms.
166+
*/
167+
object DocumentReferenceSerializer : SpecialValueSerializer<DocumentReference>(
168+
serialName = "DocumentReference",
169+
toNativeValue = DocumentReference::nativeValue,
170+
fromNativeValue = { value ->
171+
when (value) {
172+
is NativeDocumentReference -> DocumentReference(value)
173+
else -> throw SerializationException("Cannot deserialize $value")
174+
}
175+
}
176+
)
177+
150178
expect class CollectionReference : Query {
151179
val path: String
152180
val document: DocumentReference
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package dev.gitlive.firebase.firestore
2+
3+
import dev.gitlive.firebase.decode
4+
import dev.gitlive.firebase.encode
5+
import kotlinx.serialization.Serializable
6+
import kotlin.test.Test
7+
import kotlin.test.assertEquals
8+
9+
@Serializable
10+
data class TestDataWithGeoPoint(
11+
val uid: String,
12+
val location: GeoPoint
13+
)
14+
15+
@Suppress("UNCHECKED_CAST")
16+
class GeoPointTests {
17+
18+
@Test
19+
fun encodeGeoPointObject() = runTest {
20+
val geoPoint = GeoPoint(12.3, 45.6)
21+
val item = TestDataWithGeoPoint("123", geoPoint)
22+
val encoded = encodedAsMap(encode(item, shouldEncodeElementDefault = false))
23+
assertEquals("123", encoded["uid"])
24+
// check GeoPoint is encoded to a platform representation
25+
assertEquals(geoPoint.nativeValue, encoded["location"])
26+
}
27+
28+
@Test
29+
fun decodeGeoPointObject() = runTest {
30+
val geoPoint = GeoPoint(12.3, 45.6)
31+
val obj = mapOf(
32+
"uid" to "123",
33+
"location" to geoPoint.nativeValue
34+
).asEncoded()
35+
val decoded: TestDataWithGeoPoint = decode(obj)
36+
assertEquals("123", decoded.uid)
37+
// check a platform GeoPoint is properly wrapped
38+
assertEquals(geoPoint, decoded.location)
39+
}
40+
}

0 commit comments

Comments
 (0)