Skip to content

improve native serialization and timestamp fixes #371

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it helped me a lot so why not to mention it for other devs


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)

Expand All @@ -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)
)
```

<h4>Polymorphic serialization (sealed classes)</h4>

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.
Expand Down
46 changes: 26 additions & 20 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Test> {
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")}")
Expand Down Expand Up @@ -136,24 +157,6 @@ subprojects {
commandLine("npm", "publish")
}
}

withType<Test> {
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 {
Expand Down Expand Up @@ -181,8 +184,10 @@ subprojects {
}
}

apply(plugin="maven-publish")
apply(plugin="signing")
if (skipPublishing) return@subprojects

apply(plugin = "maven-publish")
apply(plugin = "signing")


configure<PublishingExtension> {
Expand Down Expand Up @@ -235,6 +240,7 @@ subprojects {
}
}
}

}
}

Expand Down
6 changes: 6 additions & 0 deletions firebase-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class FirebaseListSerializer : KSerializer<Iterable<Any?>> {
* A special case of serializer for values natively supported by Firebase and
* don't require an additional encoding/decoding.
*/
abstract class SpecialValueSerializer<T>(
class SpecialValueSerializer<T>(
serialName: String,
private val toNativeValue: (T) -> Any?,
private val fromNativeValue: (Any?) -> T
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

expect fun nativeMapOf(vararg pairs: Pair<String, Any?>): Any
expect fun nativeListOf(vararg elements: Any): Any
expect fun nativeAssertEquals(expected: Any?, actual: Any?): Unit

@Serializable
data class TestData(val map: Map<String, String>, val bool: Boolean = false, val nullableBool: Boolean? = null)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ external object firebase {
fun update(field: FieldPath, value: Any?, vararg moreFieldsAndValues: Any?): Promise<Unit>
fun delete(): Promise<Unit>
fun onSnapshot(next: (snapshot: DocumentSnapshot) -> Unit, error: (error: Error) -> Unit): ()->Unit

fun isEqual(other: DocumentReference): Boolean
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proper equality checks help a lot when writing tests in the applications using this SDK

}

open class WriteBatch {
Expand All @@ -477,6 +479,8 @@ external object firebase {
companion object {
val documentId: FieldPath
}

fun isEqual(other: FieldPath): Boolean
}

abstract class FieldValue {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

/** Serializer for [ServerValue]. Must be used with [FirebaseEncoder]/[FirebaseDecoder].*/
object ServerValueSerializer: SpecialValueSerializer<ServerValue>(
object ServerValueSerializer: KSerializer<ServerValue> by SpecialValueSerializer(
serialName = "ServerValue",
toNativeValue = ServerValue::nativeValue,
fromNativeValue = { raw ->
Expand Down
6 changes: 6 additions & 0 deletions firebase-firestore/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this approach is similar to a PR #312

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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GeoPoint> by SpecialValueSerializer(
serialName = "GeoPoint",
toNativeValue = GeoPoint::nativeValue,
fromNativeValue = { value ->
when (value) {
is NativeGeoPoint -> GeoPoint(value)
else -> throw SerializationException("Cannot deserialize $value")
}
}
)
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<BaseTimestamp>(
/** A serializer for [BaseTimestamp]. Must be used with [FirebaseEncoder]/[FirebaseDecoder]. */
object BaseTimestampSerializer : KSerializer<BaseTimestamp> by SpecialValueSerializer(
serialName = "Timestamp",
toNativeValue = { value ->
when (value) {
Expand All @@ -54,8 +66,8 @@ object BaseTimestampSerializer : SpecialValueSerializer<BaseTimestamp>(
}
)

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

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

/** A serializer for a Double field which is stored as a Timestamp. */
object DoubleAsTimestampSerializer : SpecialValueSerializer<Double>(
object DoubleAsTimestampSerializer : KSerializer<Double> by SpecialValueSerializer(
serialName = "Timestamp",
toNativeValue = { value ->
when(value) {
serverTimestamp -> FieldValue.serverTimestamp.nativeValue
else -> Timestamp.fromMilliseconds(value)
else -> Timestamp.fromMilliseconds(value).nativeValue
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bug fix - the native value shall be extracted as in the line above

}
},
fromNativeValue = { value ->
Expand Down
Loading