Skip to content

Commit

Permalink
Merge pull request #45 from splendo/fix-js-tests
Browse files Browse the repository at this point in the history
Fix js + enable js tests + code clean
  • Loading branch information
vpodlesnyak authored Jun 20, 2022
2 parents 6d3a44c + cd7cbff commit 86797ab
Show file tree
Hide file tree
Showing 47 changed files with 774 additions and 718 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ jobs:
- name: Assemble
run: ./gradlew assemble
- name: Run JS Tests
if: ${{ false }}
run: ./gradlew cleanTest jsTest
run: ./gradlew cleanTest jsLegacyTest
- name: Upload JS test artifact
uses: actions/upload-artifact@v2
if: failure()
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The Firebase Kotlin SDK uses Kotlin serialization to read and write custom class
```groovy
plugins {
kotlin("multiplatform") // or kotlin("jvm") or any other kotlin plugin
kotlin("plugin.serialization") version "1.5.30"
kotlin("plugin.serialization") version "1.6.10"
}
```

Expand Down Expand Up @@ -96,6 +96,30 @@ You can also omit the serializer but this is discouraged due to a [current limit
data class Post(val timestamp: Double = ServerValue.TIMESTAMP)
```

Alternatively, `firebase-firestore` also provides a [Timestamp] class which could be used:
```kotlin
@Serializable
data class Post(val timestamp: Timestamp = Timestamp.serverValue())
```

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 timestamp: Timestamp
)

val document = PointOfInterest(
reference = Firebase.firestore.collection("foo").document("bar"),
location = GeoPoint(51.939, 4.506),
timestamp = Timestamp.now()
)
```

<h3><a href="https://kotlinlang.org/docs/reference/functions.html#default-arguments">Default arguments</a></h3>

To reduce boilerplate, default arguments are used in the places where the Firebase Android SDK employs the builder pattern:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import kotlin.collections.set
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
StructureKind.LIST, is PolymorphicKind -> mutableListOf<Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf<Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS -> mutableMapOf<Any?, Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
StructureKind.OBJECT -> FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, _, obj -> value = obj }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[descriptor.getElementName(index)] = value } }
else -> TODO("Not implemented ${descriptor.kind}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer

@Suppress("UNCHECKED_CAST")
inline fun <reified T> decode(value: Any?, noinline decodeDouble: (value: Any?) -> Double? = { null }): T {
inline fun <reified T> decode(value: Any?): T {
val strategy = serializer<T>()
return decode(strategy as DeserializationStrategy<T>, value, decodeDouble)
return decode(strategy as DeserializationStrategy<T>, value)
}

fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?, decodeDouble: (value: Any?) -> Double? = { null }): T {
fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?): T {
require(value != null || strategy.descriptor.isNullable) { "Value was null for non-nullable type ${strategy.descriptor.serialName}" }
return FirebaseDecoder(value).decodeSerializableValue(strategy)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.modules.EmptySerializersModule

fun <T> encode(strategy: SerializationStrategy<T>, value: T, shouldEncodeElementDefault: Boolean, positiveInfinity: Any = Double.POSITIVE_INFINITY): Any? =
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply { encodeSerializableValue(strategy, value) }.value
fun <T> encode(strategy: SerializationStrategy<T>, value: T, shouldEncodeElementDefault: Boolean): Any? =
FirebaseEncoder(shouldEncodeElementDefault).apply { encodeSerializableValue(strategy, value) }.value

inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean, positiveInfinity: Any = Double.POSITIVE_INFINITY): Any? = value?.let {
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean): Any? = value?.let {
FirebaseEncoder(shouldEncodeElementDefault).apply {
if (it is ValueWithSerializer<*> && it.value is T) {
@Suppress("UNCHECKED_CAST")
(it as ValueWithSerializer<T>).let {
Expand All @@ -35,7 +35,7 @@ data class ValueWithSerializer<T>(val value: T, val serializer: SerializationStr

expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder

class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positiveInfinity: Any) : TimestampEncoder(positiveInfinity), Encoder {
class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean) : Encoder {

var value: Any? = null

Expand All @@ -55,7 +55,7 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
}

override fun encodeDouble(value: Double) {
this.value = encodeTimestamp(value)
this.value = value
}

override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
Expand Down Expand Up @@ -92,22 +92,14 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive

@ExperimentalSerializationApi
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
}

abstract class TimestampEncoder(internal val positiveInfinity: Any) {
fun encodeTimestamp(value: Double) = when(value) {
Double.POSITIVE_INFINITY -> positiveInfinity
else -> value
}
FirebaseEncoder(shouldEncodeElementDefault)
}

open class FirebaseCompositeEncoder constructor(
private val shouldEncodeElementDefault: Boolean,
positiveInfinity: Any,
private val end: () -> Unit = {},
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit
): TimestampEncoder(positiveInfinity), CompositeEncoder {
): CompositeEncoder {

override val serializersModule = EmptySerializersModule

Expand All @@ -130,7 +122,7 @@ open class FirebaseCompositeEncoder constructor(
descriptor,
index,
value?.let {
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
FirebaseEncoder(shouldEncodeElementDefault).apply {
encodeSerializableValue(serializer, value)
}.value
}
Expand All @@ -144,7 +136,7 @@ open class FirebaseCompositeEncoder constructor(
) = set(
descriptor,
index,
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
FirebaseEncoder(shouldEncodeElementDefault).apply {
encodeSerializableValue(serializer, value)
}.value
)
Expand All @@ -157,7 +149,7 @@ open class FirebaseCompositeEncoder constructor(

override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = set(descriptor, index, value)

override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, encodeTimestamp(value))
override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, value)

override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = set(descriptor, index, value)

Expand All @@ -171,7 +163,7 @@ open class FirebaseCompositeEncoder constructor(

@ExperimentalSerializationApi
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder =
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
FirebaseEncoder(shouldEncodeElementDefault)
}


Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,19 @@ class EncodersTest {
@Test
fun testEncodeDecodedSealedClass() {
val test = SealedClass.Test("Foo")
val encoded = encode(test, false)
val decoded = decode(encoded) as? SealedClass.Test
val serializer = SealedClass.Test.serializer() // has to be used because of JS issue
val encoded = encode(serializer, test, false)
val decoded = decode(serializer, encoded)
assertEquals(test, decoded)
}

@Test
fun testEncodeDecodeGenericClass() {
val test = SealedClass.Test("Foo")
val generic = GenericClass(test)
val encoded = encode(generic, false)
val decoded = decode(encoded) as? GenericClass<SealedClass.Test>
val serializer = GenericClass.serializer(SealedClass.Test.serializer())
val encoded = encode(serializer, generic, false)
val decoded = decode(serializer, encoded)
assertEquals(generic, decoded)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import kotlin.collections.set
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
StructureKind.LIST, is PolymorphicKind -> mutableListOf<Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf<Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS -> mutableMapOf<Any?, Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
StructureKind.OBJECT -> FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, _, obj -> value = obj }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[descriptor.getElementName(index)] = value } }
else -> TODO("Not implemented ${descriptor.kind}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import kotlin.js.json
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind) {
StructureKind.LIST, is PolymorphicKind -> Array<Any?>(descriptor.elementsCount) { null }
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[index] = value } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[index] = value } }
StructureKind.MAP -> {
val map = json()
var lastKey: String = ""
value = map
FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> if(index % 2 == 0) lastKey = value as String else map[lastKey] = value }
FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> if(index % 2 == 0) lastKey = value as String else map[lastKey] = value }
}
StructureKind.CLASS, StructureKind.OBJECT -> json()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it[descriptor.getElementName(index)] = value } }
else -> TODO("Not implemented ${descriptor.kind}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,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
}

open class WriteBatch {
Expand All @@ -442,13 +444,15 @@ external object firebase {
fun delete(documentReference: DocumentReference): Transaction
}

open class Timestamp(seconds: Long, nanoseconds: Int) {
open class Timestamp(seconds: Double, nanoseconds: Double) {
companion object {
fun now(): Timestamp
}

val seconds: Long
val nanoseconds: Int
val seconds: Double
val nanoseconds: Double

fun isEqual(other: Timestamp): Boolean
}
open class FieldPath(vararg fieldNames: String) {
companion object {
Expand All @@ -459,6 +463,8 @@ external object firebase {
open class GeoPoint(latitude: Double, longitude: Double) {
val latitude: Double
val longitude: Double

fun isEqual(other: GeoPoint): Boolean
}

abstract class FieldValue {
Expand All @@ -468,6 +474,7 @@ external object firebase {
fun arrayUnion(vararg elements: Any): FieldValue
fun serverTimestamp(): FieldValue
}
fun isEqual(other: FieldValue): Boolean
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ package dev.gitlive.firebase.database
import com.google.android.gms.tasks.Task
import com.google.firebase.database.ChildEventListener
import com.google.firebase.database.Logger
import com.google.firebase.database.ServerValue
import com.google.firebase.database.ValueEventListener
import dev.gitlive.firebase.encode
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.database.ChildEvent.Type
import dev.gitlive.firebase.decode
import dev.gitlive.firebase.safeOffer
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
Expand All @@ -27,13 +26,6 @@ import kotlinx.coroutines.tasks.await
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy

@PublishedApi
internal inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean) =
dev.gitlive.firebase.encode(value, shouldEncodeElementDefault, ServerValue.TIMESTAMP)

internal fun <T> encode(strategy: SerializationStrategy<T> , value: T, shouldEncodeElementDefault: Boolean): Any? =
dev.gitlive.firebase.encode(strategy, value, shouldEncodeElementDefault, ServerValue.TIMESTAMP)

suspend fun <T> Task<T>.awaitWhileOnline(): T = coroutineScope {

val notConnected = Firebase.database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ expect class DataSnapshot {
val children: Iterable<DataSnapshot>
}

object ServerValue {
val TIMESTAMP = Double.POSITIVE_INFINITY
}

expect class DatabaseException(message: String?, cause: Throwable?) : RuntimeException

expect class OnDisconnect {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package dev.gitlive.firebase.database

import cocoapods.FirebaseDatabase.*
import cocoapods.FirebaseDatabase.FIRDataEventType.*
import dev.gitlive.firebase.encode
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.database.ChildEvent.Type
Expand All @@ -14,7 +15,6 @@ import dev.gitlive.firebase.decode
import dev.gitlive.firebase.safeOffer
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.callbackFlow
Expand All @@ -27,13 +27,6 @@ import platform.Foundation.*
import kotlin.collections.component1
import kotlin.collections.component2

@PublishedApi
internal inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean) =
dev.gitlive.firebase.encode(value, shouldEncodeElementDefault, FIRServerValue.timestamp())

internal fun <T> encode(strategy: SerializationStrategy<T> , value: T, shouldEncodeElementDefault: Boolean): Any? =
dev.gitlive.firebase.encode(strategy, value, shouldEncodeElementDefault, FIRServerValue.timestamp())

actual val Firebase.database
by lazy { FirebaseDatabase(FIRDatabase.database()) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlin.js.Promise

@PublishedApi
internal inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean) =
encode(value, shouldEncodeElementDefault, firebase.database.ServerValue.TIMESTAMP)

internal fun <T> encode(strategy: SerializationStrategy<T>, value: T, shouldEncodeElementDefault: Boolean): Any? =
encode(strategy, value, shouldEncodeElementDefault, firebase.database.ServerValue.TIMESTAMP)


actual val Firebase.database
get() = rethrow { dev.gitlive.firebase.database; FirebaseDatabase(firebase.database()) }

Expand Down
Loading

0 comments on commit 86797ab

Please sign in to comment.