Skip to content

Commit a3c295f

Browse files
authored
Merge pull request #371 from splendo/native-firebase-classes-serialization
improve native serialization and timestamp fixes
2 parents ce594e4 + 4df30d3 commit a3c295f

File tree

27 files changed

+600
-59
lines changed

27 files changed

+600
-59
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.

build.gradle.kts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,28 @@ subprojects {
6565
onlyIf { !project.gradle.startParameter.taskNames.contains("publishToMavenLocal") }
6666
}
6767

68+
val skipPublishing = project.name == "test-utils" // skip publishing for test utils
69+
6870
tasks {
71+
withType<Test> {
72+
testLogging {
73+
showExceptions = true
74+
exceptionFormat = TestExceptionFormat.FULL
75+
showStandardStreams = true
76+
showCauses = true
77+
showStackTraces = true
78+
events = setOf(
79+
TestLogEvent.STARTED,
80+
TestLogEvent.FAILED,
81+
TestLogEvent.PASSED,
82+
TestLogEvent.SKIPPED,
83+
TestLogEvent.STANDARD_OUT,
84+
TestLogEvent.STANDARD_ERROR
85+
)
86+
}
87+
}
88+
89+
if (skipPublishing) return@tasks
6990

7091
val updateVersion by registering(Exec::class) {
7192
commandLine("npm", "--allow-same-version", "--prefix", projectDir, "version", "${project.property("${project.name}.version")}")
@@ -136,24 +157,6 @@ subprojects {
136157
commandLine("npm", "publish")
137158
}
138159
}
139-
140-
withType<Test> {
141-
testLogging {
142-
showExceptions = true
143-
exceptionFormat = TestExceptionFormat.FULL
144-
showStandardStreams = true
145-
showCauses = true
146-
showStackTraces = true
147-
events = setOf(
148-
TestLogEvent.STARTED,
149-
TestLogEvent.FAILED,
150-
TestLogEvent.PASSED,
151-
TestLogEvent.SKIPPED,
152-
TestLogEvent.STANDARD_OUT,
153-
TestLogEvent.STANDARD_ERROR
154-
)
155-
}
156-
}
157160
}
158161

159162
afterEvaluate {
@@ -181,8 +184,10 @@ subprojects {
181184
}
182185
}
183186

184-
apply(plugin="maven-publish")
185-
apply(plugin="signing")
187+
if (skipPublishing) return@subprojects
188+
189+
apply(plugin = "maven-publish")
190+
apply(plugin = "signing")
186191

187192

188193
configure<PublishingExtension> {
@@ -235,6 +240,7 @@ subprojects {
235240
}
236241
}
237242
}
243+
238244
}
239245
}
240246

firebase-common/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ kotlin {
9090
}
9191
}
9292

93+
val commonTest by getting {
94+
dependencies {
95+
implementation(project(":test-utils"))
96+
}
97+
}
98+
9399
val androidMain by getting {
94100
dependencies {
95101
api("com.google.firebase:firebase-common")

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/serializers.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class FirebaseListSerializer : KSerializer<Iterable<Any?>> {
108108
* A special case of serializer for values natively supported by Firebase and
109109
* don't require an additional encoding/decoding.
110110
*/
111-
abstract class SpecialValueSerializer<T>(
111+
class SpecialValueSerializer<T>(
112112
serialName: String,
113113
private val toNativeValue: (T) -> Any?,
114114
private val fromNativeValue: (Any?) -> T

firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import kotlin.test.Test
1111
import kotlin.test.assertEquals
1212
import kotlin.test.assertNull
1313

14-
expect fun nativeMapOf(vararg pairs: Pair<String, Any?>): Any
15-
expect fun nativeListOf(vararg elements: Any): Any
16-
expect fun nativeAssertEquals(expected: Any?, actual: Any?): Unit
17-
1814
@Serializable
1915
data class TestData(val map: Map<String, String>, val bool: Boolean = false, val nullableBool: Boolean? = null)
2016

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

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

457459
open class WriteBatch {
@@ -476,6 +478,8 @@ external object firebase {
476478
companion object {
477479
val documentId: FieldPath
478480
}
481+
482+
fun isEqual(other: FieldPath): Boolean
479483
}
480484

481485
abstract class FieldValue {
@@ -489,6 +493,13 @@ external object firebase {
489493

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

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

firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/ServerValue.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.gitlive.firebase.database
33
import dev.gitlive.firebase.FirebaseDecoder
44
import dev.gitlive.firebase.FirebaseEncoder
55
import dev.gitlive.firebase.SpecialValueSerializer
6+
import kotlinx.serialization.KSerializer
67
import kotlinx.serialization.Serializable
78
import kotlinx.serialization.SerializationException
89

@@ -18,7 +19,7 @@ expect class ServerValue internal constructor(nativeValue: Any){
1819
}
1920

2021
/** Serializer for [ServerValue]. Must be used with [FirebaseEncoder]/[FirebaseDecoder].*/
21-
object ServerValueSerializer: SpecialValueSerializer<ServerValue>(
22+
object ServerValueSerializer: KSerializer<ServerValue> by SpecialValueSerializer(
2223
serialName = "ServerValue",
2324
toNativeValue = ServerValue::nativeValue,
2425
fromNativeValue = { raw ->

firebase-firestore/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ kotlin {
103103
}
104104
}
105105

106+
val commonTest by getting {
107+
dependencies {
108+
implementation(project(":test-utils"))
109+
}
110+
}
111+
106112
val androidMain by getting {
107113
dependencies {
108114
api("com.google.firebase:firebase-firestore")
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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package dev.gitlive.firebase.firestore
2+
3+
import dev.gitlive.firebase.SpecialValueSerializer
4+
import kotlinx.serialization.KSerializer
5+
import kotlinx.serialization.Serializable
6+
import kotlinx.serialization.SerializationException
7+
8+
/** A class representing a platform specific Firebase GeoPoint. */
9+
expect class NativeGeoPoint
10+
11+
/** A class representing a Firebase GeoPoint. */
12+
@Serializable(with = GeoPointSerializer::class)
13+
expect class GeoPoint internal constructor(nativeValue: NativeGeoPoint) {
14+
constructor(latitude: Double, longitude: Double)
15+
val latitude: Double
16+
val longitude: Double
17+
internal val nativeValue: NativeGeoPoint
18+
}
19+
20+
/** Serializer for [GeoPoint]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
21+
object GeoPointSerializer : KSerializer<GeoPoint> by SpecialValueSerializer(
22+
serialName = "GeoPoint",
23+
toNativeValue = GeoPoint::nativeValue,
24+
fromNativeValue = { value ->
25+
when (value) {
26+
is NativeGeoPoint -> GeoPoint(value)
27+
else -> throw SerializationException("Cannot deserialize $value")
28+
}
29+
}
30+
)

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package dev.gitlive.firebase.firestore
22

3+
import dev.gitlive.firebase.FirebaseDecoder
34
import dev.gitlive.firebase.FirebaseEncoder
45
import dev.gitlive.firebase.SpecialValueSerializer
56
import dev.gitlive.firebase.firestore.DoubleAsTimestampSerializer.serverTimestamp
7+
import kotlinx.serialization.KSerializer
68
import kotlinx.serialization.Serializable
79
import kotlinx.serialization.SerializationException
10+
import kotlin.time.Duration
11+
import kotlin.time.Duration.Companion.milliseconds
12+
import kotlin.time.Duration.Companion.nanoseconds
13+
import kotlin.time.Duration.Companion.seconds
14+
import kotlin.time.DurationUnit
815

916
/** A class representing a platform specific Firebase Timestamp. */
1017
expect class NativeTimestamp
@@ -31,12 +38,17 @@ expect class Timestamp internal constructor(nativeValue: NativeTimestamp): BaseT
3138
object ServerTimestamp: BaseTimestamp
3239
}
3340

34-
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)
41+
fun Timestamp.Companion.fromDuration(duration: Duration): Timestamp =
42+
duration.toComponents { seconds, nanoseconds ->
43+
Timestamp(seconds, nanoseconds)
44+
}
45+
fun Timestamp.toDuration(): Duration = seconds.seconds + nanoseconds.nanoseconds
46+
47+
fun Timestamp.Companion.fromMilliseconds(milliseconds: Double): Timestamp = fromDuration(milliseconds.milliseconds)
48+
fun Timestamp.toMilliseconds(): Double = toDuration().toDouble(DurationUnit.MILLISECONDS)
3749

38-
/** A serializer for [BaseTimestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
39-
object BaseTimestampSerializer : SpecialValueSerializer<BaseTimestamp>(
50+
/** A serializer for [BaseTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */
51+
object BaseTimestampSerializer : KSerializer<BaseTimestamp> by SpecialValueSerializer(
4052
serialName = "Timestamp",
4153
toNativeValue = { value ->
4254
when (value) {
@@ -54,8 +66,8 @@ object BaseTimestampSerializer : SpecialValueSerializer<BaseTimestamp>(
5466
}
5567
)
5668

57-
/** A serializer for [Timestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
58-
object TimestampSerializer : SpecialValueSerializer<Timestamp>(
69+
/** A serializer for [Timestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */
70+
object TimestampSerializer : KSerializer<Timestamp> by SpecialValueSerializer(
5971
serialName = "Timestamp",
6072
toNativeValue = Timestamp::nativeValue,
6173
fromNativeValue = { value ->
@@ -66,8 +78,8 @@ object TimestampSerializer : SpecialValueSerializer<Timestamp>(
6678
}
6779
)
6880

69-
/** A serializer for [Timestamp.ServerTimestamp]. If used with [FirebaseEncoder] performs serialization using native Firebase mechanisms. */
70-
object ServerTimestampSerializer : SpecialValueSerializer<Timestamp.ServerTimestamp>(
81+
/** A serializer for [Timestamp.ServerTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */
82+
object ServerTimestampSerializer : KSerializer<Timestamp.ServerTimestamp> by SpecialValueSerializer(
7183
serialName = "Timestamp",
7284
toNativeValue = { FieldValue.serverTimestamp.nativeValue },
7385
fromNativeValue = { value ->
@@ -79,12 +91,12 @@ object ServerTimestampSerializer : SpecialValueSerializer<Timestamp.ServerTimest
7991
)
8092

8193
/** A serializer for a Double field which is stored as a Timestamp. */
82-
object DoubleAsTimestampSerializer : SpecialValueSerializer<Double>(
94+
object DoubleAsTimestampSerializer : KSerializer<Double> by SpecialValueSerializer(
8395
serialName = "Timestamp",
8496
toNativeValue = { value ->
8597
when(value) {
8698
serverTimestamp -> FieldValue.serverTimestamp.nativeValue
87-
else -> Timestamp.fromMilliseconds(value)
99+
else -> Timestamp.fromMilliseconds(value).nativeValue
88100
}
89101
},
90102
fromNativeValue = { value ->

0 commit comments

Comments
 (0)