Add Store RxJava3 module (#171)
This commit is contained in:
parent
0afefdb0b0
commit
10da116ea6
16 changed files with 779 additions and 3 deletions
|
@ -35,6 +35,7 @@ ext.versions = [
|
|||
navigation : '2.2.1',
|
||||
constraintLayout : '1.1.3',
|
||||
rx2 : '2.2.19',
|
||||
rx3 : '3.0.3',
|
||||
|
||||
// Testing.
|
||||
junit : '4.13',
|
||||
|
@ -69,6 +70,7 @@ ext.libraries = [
|
|||
roomRuntime : "androidx.room:room-ktx:$versions.room",
|
||||
coreKtx : "androidx.core:core-ktx:$versions.coreKtx",
|
||||
rx2 : "io.reactivex.rxjava2:rxjava:$versions.rx2",
|
||||
rx3 : "io.reactivex.rxjava3:rxjava:$versions.rx3",
|
||||
|
||||
// Testing.
|
||||
junit : "junit:junit:$versions.junit",
|
||||
|
@ -76,7 +78,8 @@ ext.libraries = [
|
|||
mockito : "org.mockito:mockito-core:$versions.mockito",
|
||||
mockitoKotlin : "com.nhaarman.mockitokotlin2:mockito-kotlin:$versions.mockitoKotlin",
|
||||
coroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines",
|
||||
coroutinesRx : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$versions.coroutines",
|
||||
coroutinesRx2 : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$versions.coroutines",
|
||||
coroutinesRx3 : "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$versions.coroutines",
|
||||
coroutinesReactive : "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$versions.coroutines",
|
||||
coroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines",
|
||||
coroutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines",
|
||||
|
|
|
@ -1 +1 @@
|
|||
include ':app', ':store', ':cache', ':filesystem', ':multicast',':store-rx2'
|
||||
include ':app', ':store', ':cache', ':filesystem', ':multicast',':store-rx2', ':store-rx3'
|
||||
|
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
dependencies {
|
||||
implementation libraries.kotlinStdLib
|
||||
implementation libraries.coroutinesCore
|
||||
implementation libraries.coroutinesRx
|
||||
implementation libraries.coroutinesRx2
|
||||
implementation libraries.coroutinesReactive
|
||||
implementation project(path: ':store')
|
||||
implementation libraries.rx2
|
||||
|
|
1
store-rx3/.gitignore
vendored
Normal file
1
store-rx3/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
26
store-rx3/api/store-rx3.api
Normal file
26
store-rx3/api/store-rx3.api
Normal file
|
@ -0,0 +1,26 @@
|
|||
public final class com/dropbox/store/rx3/RxFetcherKt {
|
||||
public static final fun flowableFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
|
||||
public static final fun flowableValueFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
|
||||
public static final fun singleFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
|
||||
public static final fun singleValueFetcher (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
|
||||
}
|
||||
|
||||
public final class com/dropbox/store/rx3/RxSourceOfTruthKt {
|
||||
public static final fun fromFlowable (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lcom/dropbox/android/external/store4/SourceOfTruth;
|
||||
public static synthetic fun fromFlowable$default (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/dropbox/android/external/store4/SourceOfTruth;
|
||||
public static final fun fromMaybe (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Lcom/dropbox/android/external/store4/SourceOfTruth;
|
||||
public static synthetic fun fromMaybe$default (Lcom/dropbox/android/external/store4/SourceOfTruth$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/dropbox/android/external/store4/SourceOfTruth;
|
||||
}
|
||||
|
||||
public final class com/dropbox/store/rx3/RxStoreBuilderKt {
|
||||
public static final fun withScheduler (Lcom/dropbox/android/external/store4/StoreBuilder;Lio/reactivex/rxjava3/core/Scheduler;)Lcom/dropbox/android/external/store4/StoreBuilder;
|
||||
}
|
||||
|
||||
public final class com/dropbox/store/rx3/RxStoreKt {
|
||||
public static final fun freshSingle (Lcom/dropbox/android/external/store4/Store;Ljava/lang/Object;)Lio/reactivex/rxjava3/core/Single;
|
||||
public static final fun getSingle (Lcom/dropbox/android/external/store4/Store;Ljava/lang/Object;)Lio/reactivex/rxjava3/core/Single;
|
||||
public static final fun observe (Lcom/dropbox/android/external/store4/Store;Lcom/dropbox/android/external/store4/StoreRequest;)Lio/reactivex/rxjava3/core/Flowable;
|
||||
public static final fun observeClear (Lcom/dropbox/android/external/store4/Store;Ljava/lang/Object;)Lio/reactivex/rxjava3/core/Completable;
|
||||
public static final fun observeClearAll (Lcom/dropbox/android/external/store4/Store;)Lio/reactivex/rxjava3/core/Completable;
|
||||
}
|
||||
|
39
store-rx3/build.gradle
Normal file
39
store-rx3/build.gradle
Normal file
|
@ -0,0 +1,39 @@
|
|||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
id 'org.jetbrains.dokka'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libraries.kotlinStdLib
|
||||
implementation libraries.coroutinesCore
|
||||
implementation libraries.coroutinesRx3
|
||||
implementation libraries.coroutinesReactive
|
||||
implementation project(path: ':store')
|
||||
implementation libraries.rx3
|
||||
|
||||
testImplementation libraries.junit
|
||||
testImplementation libraries.truth
|
||||
testImplementation libraries.mockito
|
||||
testImplementation libraries.coroutinesTest
|
||||
|
||||
}
|
||||
group = GROUP
|
||||
version = VERSION_NAME
|
||||
apply from: rootProject.file("gradle/maven-push.gradle")
|
||||
apply from: rootProject.file("gradle/jacoco.gradle")
|
||||
targetCompatibility = 1.8
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += [
|
||||
'-Xopt-in=kotlin.RequiresOptIn',
|
||||
]
|
||||
}
|
||||
}
|
18
store-rx3/gradle.properties
Normal file
18
store-rx3/gradle.properties
Normal file
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# Copyright 2020 Dropbox LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
POM_NAME=com.dropbox.mobile.store
|
||||
POM_ARTIFACT_ID=store-rx3
|
||||
POM_PACKAGING=jar
|
71
store-rx3/src/main/kotlin/com/dropbox/store/rx3/RxFetcher.kt
Normal file
71
store-rx3/src/main/kotlin/com/dropbox/store/rx3/RxFetcher.kt
Normal file
|
@ -0,0 +1,71 @@
|
|||
package com.dropbox.store.rx3
|
||||
|
||||
import com.dropbox.android.external.store4.Fetcher
|
||||
import com.dropbox.android.external.store4.FetcherResult
|
||||
import com.dropbox.android.external.store4.Store
|
||||
import com.dropbox.android.external.store4.valueFetcher
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
|
||||
/**
|
||||
* Creates a new [Fetcher] from a [flowableFactory].
|
||||
*
|
||||
* [Store] does not catch exception thrown in [flowableFactory] or in the returned [Flowable]. These
|
||||
* exception will be propagated to the caller.
|
||||
*
|
||||
* Use when creating a [Store] that fetches objects in a multiple responses per request
|
||||
* network protocol (e.g Web Sockets).
|
||||
*
|
||||
* @param flowableFactory a factory for a [Flowable] source of network records.
|
||||
*/
|
||||
fun <Key : Any, Output : Any> flowableFetcher(
|
||||
flowableFactory: (key: Key) -> Flowable<FetcherResult<Output>>
|
||||
): Fetcher<Key, Output> = { key: Key -> flowableFactory(key).asFlow() }
|
||||
|
||||
/**
|
||||
* "Creates" a [Fetcher] from a [singleFactory].
|
||||
*
|
||||
* [Store] does not catch exception thrown in [singleFactory] or in the returned [Single]. These
|
||||
* exception will be propagated to the caller.
|
||||
*
|
||||
* Use when creating a [Store] that fetches objects in a single response per request network
|
||||
* protocol (e.g Http).
|
||||
*
|
||||
* @param singleFactory a factory for a [Single] source of network records.
|
||||
*/
|
||||
fun <Key : Any, Output : Any> singleFetcher(
|
||||
singleFactory: (key: Key) -> Single<FetcherResult<Output>>
|
||||
): Fetcher<Key, Output> = { key: Key -> singleFactory(key).toFlowable().asFlow() }
|
||||
|
||||
/**
|
||||
* "Creates" a [Fetcher] from a [flowableFactory] and translate the results to a [FetcherResult].
|
||||
*
|
||||
* Emitted values will be wrapped in [FetcherResult.Data]. if an exception disrupts the stream then
|
||||
* it will be wrapped in [FetcherResult.Error]. Exceptions thrown in [flowableFactory] itself are
|
||||
* not caught and will be returned to the caller.
|
||||
*
|
||||
* Use when creating a [Store] that fetches objects in a multiple responses per request
|
||||
* network protocol (e.g Web Sockets).
|
||||
*
|
||||
* @param flowFactory a factory for a [Flowable] source of network records.
|
||||
*/
|
||||
fun <Key : Any, Output : Any> flowableValueFetcher(
|
||||
flowableFactory: (key: Key) -> Flowable<Output>
|
||||
): Fetcher<Key, Output> = valueFetcher { key: Key -> flowableFactory(key).asFlow() }
|
||||
|
||||
/**
|
||||
* Creates a new [Fetcher] from a [singleFactory] and translate the results to a [FetcherResult].
|
||||
*
|
||||
* The emitted value will be wrapped in [FetcherResult.Data]. if an exception is returned then
|
||||
* it will be wrapped in [FetcherResult.Error]. Exceptions thrown in [singleFactory] itself are
|
||||
* not caught and will be returned to the caller.
|
||||
*
|
||||
* Use when creating a [Store] that fetches objects in a single response per request network
|
||||
* protocol (e.g Http).
|
||||
*
|
||||
* @param singleFactory a factory for a [Single] source of network records.
|
||||
*/
|
||||
fun <Key : Any, Output : Any> singleValueFetcher(
|
||||
singleFactory: (key: Key) -> Single<Output>
|
||||
): Fetcher<Key, Output> = flowableValueFetcher { key: Key -> singleFactory(key).toFlowable() }
|
|
@ -0,0 +1,62 @@
|
|||
package com.dropbox.store.rx3
|
||||
|
||||
import com.dropbox.android.external.store4.SourceOfTruth
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import kotlinx.coroutines.rx3.await
|
||||
|
||||
/**
|
||||
* Creates a [Maybe] source of truth that is accessible via [reader], [writer], [delete] and
|
||||
* [deleteAll].
|
||||
*
|
||||
* @param reader function for reading records from the source of truth
|
||||
* @param writer function for writing updates to the backing source of truth
|
||||
* @param delete function for deleting records in the source of truth for the given key
|
||||
* @param deleteAll function for deleting all records in the source of truth
|
||||
*
|
||||
*/
|
||||
fun <Key : Any, Input : Any, Output : Any> SourceOfTruth.Companion.fromMaybe(
|
||||
reader: (Key) -> Maybe<Output>,
|
||||
writer: (Key, Input) -> Completable,
|
||||
delete: ((Key) -> Completable)? = null,
|
||||
deleteAll: (() -> Completable)? = null
|
||||
): SourceOfTruth<Key, Input, Output> {
|
||||
val deleteFun: (suspend (Key) -> Unit)? =
|
||||
if (delete != null) { key -> delete(key).await() } else null
|
||||
val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } }
|
||||
return fromNonFlow(
|
||||
reader = { key -> reader.invoke(key).await() },
|
||||
writer = { key, output -> writer.invoke(key, output).await() },
|
||||
delete = deleteFun,
|
||||
deleteAll = deleteAllFun
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ([Flowable]) source of truth that is accessed via [reader], [writer], [delete] and
|
||||
* [deleteAll].
|
||||
*
|
||||
* @param reader function for reading records from the source of truth
|
||||
* @param writer function for writing updates to the backing source of truth
|
||||
* @param delete function for deleting records in the source of truth for the given key
|
||||
* @param deleteAll function for deleting all records in the source of truth
|
||||
*
|
||||
*/
|
||||
fun <Key : Any, Input : Any, Output : Any> SourceOfTruth.Companion.fromFlowable(
|
||||
reader: (Key) -> Flowable<Output>,
|
||||
writer: (Key, Input) -> Completable,
|
||||
delete: ((Key) -> Completable)? = null,
|
||||
deleteAll: (() -> Completable)? = null
|
||||
): SourceOfTruth<Key, Input, Output> {
|
||||
val deleteFun: (suspend (Key) -> Unit)? =
|
||||
if (delete != null) { key -> delete(key).await() } else null
|
||||
val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } }
|
||||
return from(
|
||||
reader = { key -> reader.invoke(key).asFlow() },
|
||||
writer = { key, output -> writer.invoke(key, output).await() },
|
||||
delete = deleteFun,
|
||||
deleteAll = deleteAllFun
|
||||
)
|
||||
}
|
52
store-rx3/src/main/kotlin/com/dropbox/store/rx3/RxStore.kt
Normal file
52
store-rx3/src/main/kotlin/com/dropbox/store/rx3/RxStore.kt
Normal file
|
@ -0,0 +1,52 @@
|
|||
package com.dropbox.store.rx3
|
||||
|
||||
import com.dropbox.android.external.store4.ExperimentalStoreApi
|
||||
import com.dropbox.android.external.store4.Store
|
||||
import com.dropbox.android.external.store4.StoreBuilder
|
||||
import com.dropbox.android.external.store4.StoreRequest
|
||||
import com.dropbox.android.external.store4.StoreResponse
|
||||
import com.dropbox.android.external.store4.fresh
|
||||
import com.dropbox.android.external.store4.get
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.rx3.asFlowable
|
||||
import kotlinx.coroutines.rx3.rxCompletable
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
|
||||
/**
|
||||
* Return a [Flowable] for the given key
|
||||
* @param request - see [StoreRequest] for configurations
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
fun <Key : Any, Output : Any> Store<Key, Output>.observe(request: StoreRequest<Key>): Flowable<StoreResponse<Output>> =
|
||||
stream(request).asFlowable()
|
||||
|
||||
/**
|
||||
* Purge a particular entry from memory and disk cache.
|
||||
* Persistent storage will only be cleared if a delete function was passed to
|
||||
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
|
||||
*/
|
||||
fun <Key : Any, Output : Any> Store<Key, Output>.observeClear(key: Key): Completable =
|
||||
rxCompletable { clear(key) }
|
||||
|
||||
/**
|
||||
* Purge all entries from memory and disk cache.
|
||||
* Persistent storage will only be cleared if a deleteAll function was passed to
|
||||
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
|
||||
*/
|
||||
@ExperimentalStoreApi
|
||||
fun <Key : Any, Output : Any> Store<Key, Output>.observeClearAll(): Completable =
|
||||
rxCompletable { clearAll() }
|
||||
|
||||
/**
|
||||
* Helper factory that will return data as a [Single] for [key] if it is cached otherwise will return fresh/network data (updating your caches)
|
||||
*/
|
||||
fun <Key : Any, Output : Any> Store<Key, Output>.getSingle(key: Key) =
|
||||
rxSingle { this@getSingle.get(key) }
|
||||
|
||||
/**
|
||||
* Helper factory that will return fresh data as a [Single] for [key] while updating your caches
|
||||
*/
|
||||
fun <Key : Any, Output : Any> Store<Key, Output>.freshSingle(key: Key) =
|
||||
rxSingle { this@freshSingle.fresh(key) }
|
|
@ -0,0 +1,21 @@
|
|||
package com.dropbox.store.rx3
|
||||
|
||||
import com.dropbox.android.external.store4.StoreBuilder
|
||||
import io.reactivex.rxjava3.core.Scheduler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.rx3.asCoroutineDispatcher
|
||||
|
||||
/**
|
||||
* Define what scheduler fetcher requests will be called on,
|
||||
* if a scheduler is not set Store will use [GlobalScope]
|
||||
*/
|
||||
@FlowPreview
|
||||
@ExperimentalCoroutinesApi
|
||||
fun <Key : Any, Output : Any> StoreBuilder<Key, Output>.withScheduler(
|
||||
scheduler: Scheduler
|
||||
): StoreBuilder<Key, Output> {
|
||||
return scope(CoroutineScope(scheduler.asCoroutineDispatcher()))
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2019 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.dropbox.store.rx3.test
|
||||
|
||||
import com.google.common.truth.FailureMetadata
|
||||
import com.google.common.truth.Subject
|
||||
import com.google.common.truth.Truth
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal fun <T> TestCoroutineScope.assertThat(flow: Flow<T>): FlowSubject<T> {
|
||||
return Truth.assertAbout(
|
||||
FlowSubject.Factory<T>(
|
||||
this
|
||||
)
|
||||
).that(flow)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class FlowSubject<T> constructor(
|
||||
failureMetadata: FailureMetadata,
|
||||
private val testCoroutineScope: TestCoroutineScope,
|
||||
private val actual: Flow<T>
|
||||
) : Subject(failureMetadata, actual) {
|
||||
/**
|
||||
* Takes all items in the flow that are available by collecting on it as long as there are
|
||||
* active jobs in the given [TestCoroutineScope].
|
||||
*
|
||||
* It ensures all expected items are dispatched as well as no additional unexpected items are
|
||||
* dispatched.
|
||||
*/
|
||||
suspend fun emitsExactly(vararg expected: T) {
|
||||
val collectedSoFar = mutableListOf<T>()
|
||||
val collectionCoroutine = testCoroutineScope.async {
|
||||
actual.collect {
|
||||
collectedSoFar.add(it)
|
||||
if (collectedSoFar.size > expected.size) {
|
||||
assertWithMessage("Too many emissions in the flow (only first additional item is shown)")
|
||||
.that(collectedSoFar)
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
testCoroutineScope.advanceUntilIdle()
|
||||
if (!collectionCoroutine.isActive) {
|
||||
collectionCoroutine.getCompletionExceptionOrNull()?.let {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
collectionCoroutine.cancelAndJoin()
|
||||
assertWithMessage("Flow didn't exactly emit expected items")
|
||||
.that(collectedSoFar)
|
||||
.isEqualTo(expected.toList())
|
||||
}
|
||||
|
||||
class Factory<T>(
|
||||
private val testCoroutineScope: TestCoroutineScope
|
||||
) : Subject.Factory<FlowSubject<T>, Flow<T>> {
|
||||
override fun createSubject(metadata: FailureMetadata, actual: Flow<T>): FlowSubject<T> {
|
||||
return FlowSubject(
|
||||
failureMetadata = metadata,
|
||||
actual = actual,
|
||||
testCoroutineScope = testCoroutineScope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package com.dropbox.store.rx3.test
|
||||
|
||||
import com.dropbox.android.external.store4.FetcherResult
|
||||
import com.dropbox.android.external.store4.ResponseOrigin
|
||||
import com.dropbox.android.external.store4.StoreBuilder
|
||||
import com.dropbox.android.external.store4.StoreRequest
|
||||
import com.dropbox.android.external.store4.StoreResponse
|
||||
import com.dropbox.store.rx3.singleFetcher
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FlowPreview
|
||||
@ExperimentalCoroutinesApi
|
||||
class HotRxSingleStoreTest {
|
||||
private val testScope = TestCoroutineScope()
|
||||
|
||||
@Test
|
||||
fun `GIVEN a hot fetcher WHEN two cached and one fresh call THEN fetcher is only called twice`() =
|
||||
testScope.runBlockingTest {
|
||||
val fetcher: FakeRxFetcher<Int, FetcherResult<String>> =
|
||||
FakeRxFetcher(
|
||||
3 to FetcherResult.Data("three-1"),
|
||||
3 to FetcherResult.Data("three-2")
|
||||
)
|
||||
val pipeline = StoreBuilder.from(singleFetcher<Int, String> { fetcher.fetch(it) })
|
||||
.scope(testScope)
|
||||
.build()
|
||||
|
||||
assertThat(pipeline.stream(StoreRequest.cached(3, refresh = false)))
|
||||
.emitsExactly(
|
||||
StoreResponse.Loading<String>(
|
||||
origin = ResponseOrigin.Fetcher
|
||||
), StoreResponse.Data(
|
||||
value = "three-1",
|
||||
origin = ResponseOrigin.Fetcher
|
||||
)
|
||||
)
|
||||
assertThat(
|
||||
pipeline.stream(StoreRequest.cached(3, refresh = false))
|
||||
).emitsExactly(
|
||||
StoreResponse.Data(
|
||||
value = "three-1",
|
||||
origin = ResponseOrigin.Cache
|
||||
)
|
||||
)
|
||||
|
||||
assertThat(pipeline.stream(StoreRequest.fresh(3)))
|
||||
.emitsExactly(
|
||||
StoreResponse.Loading<String>(
|
||||
origin = ResponseOrigin.Fetcher
|
||||
),
|
||||
StoreResponse.Data(
|
||||
value = "three-2",
|
||||
origin = ResponseOrigin.Fetcher
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRxFetcher<Key, Output>(
|
||||
vararg val responses: Pair<Key, Output>
|
||||
) {
|
||||
private var index = 0
|
||||
|
||||
@Suppress("RedundantSuspendModifier") // needed for function reference
|
||||
fun fetch(key: Key): Single<Output> {
|
||||
// will throw if fetcher called more than twice
|
||||
if (index >= responses.size) {
|
||||
throw AssertionError("unexpected fetch request")
|
||||
}
|
||||
val pair = responses[index++]
|
||||
assertThat(pair.first).isEqualTo(key)
|
||||
return Single.just(pair.second)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package com.dropbox.store.rx3.test
|
||||
|
||||
import com.dropbox.android.external.store4.FetcherResult
|
||||
import com.dropbox.android.external.store4.ResponseOrigin
|
||||
import com.dropbox.android.external.store4.SourceOfTruth
|
||||
import com.dropbox.android.external.store4.StoreBuilder
|
||||
import com.dropbox.android.external.store4.StoreRequest
|
||||
import com.dropbox.android.external.store4.StoreResponse
|
||||
import com.dropbox.store.rx3.flowableFetcher
|
||||
import com.dropbox.store.rx3.fromFlowable
|
||||
import com.dropbox.store.rx3.observe
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import io.reactivex.rxjava3.subscribers.TestSubscriber
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FlowPreview
|
||||
@ExperimentalCoroutinesApi
|
||||
class RxFlowableStoreTest {
|
||||
private val testScheduler = TestScheduler()
|
||||
private val atomicInteger = AtomicInteger(0)
|
||||
private val fakeDisk = mutableMapOf<Int, String>()
|
||||
private val store =
|
||||
StoreBuilder.from<Int, String, String>(
|
||||
flowableFetcher {
|
||||
Flowable.create({ emitter ->
|
||||
emitter.onNext(
|
||||
FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence")
|
||||
)
|
||||
emitter.onNext(
|
||||
FetcherResult.Data("$it ${atomicInteger.incrementAndGet()} occurrence")
|
||||
)
|
||||
emitter.onComplete()
|
||||
}, BackpressureStrategy.LATEST)
|
||||
},
|
||||
sourceOfTruth = SourceOfTruth.fromFlowable(
|
||||
reader = {
|
||||
if (fakeDisk[it] != null)
|
||||
Flowable.fromCallable { fakeDisk[it]!! }
|
||||
else
|
||||
Flowable.empty<String>()
|
||||
},
|
||||
writer = { key, value ->
|
||||
Completable.fromAction { fakeDisk[key] = value }
|
||||
}
|
||||
))
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun simpleTest() {
|
||||
var testSubscriber = TestSubscriber<StoreResponse<String>>()
|
||||
store.observe(StoreRequest.fresh(3))
|
||||
.subscribeOn(testScheduler)
|
||||
.subscribe(testSubscriber)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber
|
||||
.awaitCount(3)
|
||||
.assertValues(
|
||||
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 1 occurrence", ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 2 occurrence", ResponseOrigin.Fetcher)
|
||||
)
|
||||
|
||||
testSubscriber = TestSubscriber<StoreResponse<String>>()
|
||||
store.observe(StoreRequest.cached(3, false))
|
||||
.subscribeOn(testScheduler)
|
||||
.subscribe(testSubscriber)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Data("3 2 occurrence", ResponseOrigin.Cache),
|
||||
StoreResponse.Data("3 2 occurrence", ResponseOrigin.SourceOfTruth)
|
||||
)
|
||||
|
||||
testSubscriber = TestSubscriber<StoreResponse<String>>()
|
||||
store.observe(StoreRequest.fresh(3))
|
||||
.subscribeOn(testScheduler)
|
||||
.subscribe(testSubscriber)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber
|
||||
.awaitCount(3)
|
||||
.assertValues(
|
||||
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 3 occurrence", ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 4 occurrence", ResponseOrigin.Fetcher)
|
||||
)
|
||||
|
||||
testSubscriber = TestSubscriber<StoreResponse<String>>()
|
||||
store.observe(StoreRequest.cached(3, false))
|
||||
.subscribeOn(testScheduler)
|
||||
.subscribe(testSubscriber)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Data("3 4 occurrence", ResponseOrigin.Cache),
|
||||
StoreResponse.Data("3 4 occurrence", ResponseOrigin.SourceOfTruth)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package com.dropbox.store.rx3.test
|
||||
|
||||
import com.dropbox.android.external.store4.ExperimentalStoreApi
|
||||
import com.dropbox.android.external.store4.FetcherResult
|
||||
import com.dropbox.android.external.store4.SourceOfTruth
|
||||
import com.dropbox.android.external.store4.StoreBuilder
|
||||
import com.dropbox.store.rx3.freshSingle
|
||||
import com.dropbox.store.rx3.fromMaybe
|
||||
import com.dropbox.store.rx3.getSingle
|
||||
import com.dropbox.store.rx3.singleFetcher
|
||||
import com.dropbox.store.rx3.withScheduler
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@ExperimentalStoreApi
|
||||
@RunWith(JUnit4::class)
|
||||
@FlowPreview
|
||||
@ExperimentalCoroutinesApi
|
||||
class RxSingleStoreExtensionsTest {
|
||||
private val atomicInteger = AtomicInteger(0)
|
||||
private var fakeDisk = mutableMapOf<Int, String>()
|
||||
private val store =
|
||||
StoreBuilder.from<Int, String, String>(
|
||||
fetcher = singleFetcher {
|
||||
Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") }
|
||||
},
|
||||
sourceOfTruth = SourceOfTruth.fromMaybe(
|
||||
reader = { Maybe.fromCallable<String> { fakeDisk[it] } },
|
||||
writer = { key, value ->
|
||||
Completable.fromAction { fakeDisk[key] = value }
|
||||
},
|
||||
delete = { key ->
|
||||
Completable.fromAction { fakeDisk.remove(key) }
|
||||
},
|
||||
deleteAll = {
|
||||
Completable.fromAction { fakeDisk.clear() }
|
||||
}
|
||||
)
|
||||
)
|
||||
.withScheduler(Schedulers.trampoline())
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun `store rx extension tests`() {
|
||||
// Return from cache - after initial fetch
|
||||
store.getSingle(3)
|
||||
.test()
|
||||
.await()
|
||||
.assertValue("3 1")
|
||||
|
||||
// Return from cache
|
||||
store.getSingle(3)
|
||||
.test()
|
||||
.await()
|
||||
.assertValue("3 1")
|
||||
|
||||
// Return from fresh - forcing a new fetch
|
||||
store.freshSingle(3)
|
||||
.test()
|
||||
.await()
|
||||
.assertValue("3 2")
|
||||
|
||||
// Return from cache - different to initial
|
||||
store.getSingle(3)
|
||||
.test()
|
||||
.await()
|
||||
.assertValue("3 2")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package com.dropbox.store.rx3.test
|
||||
|
||||
import com.dropbox.android.external.store4.ExperimentalStoreApi
|
||||
import com.dropbox.android.external.store4.FetcherResult
|
||||
import com.dropbox.android.external.store4.ResponseOrigin
|
||||
import com.dropbox.android.external.store4.SourceOfTruth
|
||||
import com.dropbox.android.external.store4.StoreBuilder
|
||||
import com.dropbox.android.external.store4.StoreRequest
|
||||
import com.dropbox.android.external.store4.StoreResponse
|
||||
import com.dropbox.store.rx3.fromMaybe
|
||||
import com.dropbox.store.rx3.observe
|
||||
import com.dropbox.store.rx3.observeClear
|
||||
import com.dropbox.store.rx3.observeClearAll
|
||||
import com.dropbox.store.rx3.singleFetcher
|
||||
import com.dropbox.store.rx3.withScheduler
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@ExperimentalStoreApi
|
||||
@RunWith(JUnit4::class)
|
||||
@FlowPreview
|
||||
@ExperimentalCoroutinesApi
|
||||
class RxSingleStoreTest {
|
||||
private val atomicInteger = AtomicInteger(0)
|
||||
private var fakeDisk = mutableMapOf<Int, String>()
|
||||
private val store =
|
||||
StoreBuilder.from<Int, String, String>(
|
||||
fetcher = singleFetcher {
|
||||
Single.fromCallable { FetcherResult.Data("$it ${atomicInteger.incrementAndGet()}") }
|
||||
},
|
||||
sourceOfTruth = SourceOfTruth.fromMaybe(
|
||||
reader = { Maybe.fromCallable<String> { fakeDisk[it] } },
|
||||
writer = { key, value ->
|
||||
Completable.fromAction { fakeDisk[key] = value }
|
||||
},
|
||||
delete = { key ->
|
||||
Completable.fromAction { fakeDisk.remove(key) }
|
||||
},
|
||||
deleteAll = {
|
||||
Completable.fromAction { fakeDisk.clear() }
|
||||
}
|
||||
)
|
||||
)
|
||||
.withScheduler(Schedulers.trampoline())
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun simpleTest() {
|
||||
store.observe(StoreRequest.cached(3, false))
|
||||
.test()
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 1", ResponseOrigin.Fetcher)
|
||||
)
|
||||
|
||||
store.observe(StoreRequest.cached(3, false))
|
||||
.test()
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Data("3 1", ResponseOrigin.Cache),
|
||||
StoreResponse.Data("3 1", ResponseOrigin.SourceOfTruth)
|
||||
)
|
||||
|
||||
store.observe(StoreRequest.fresh(3))
|
||||
.test()
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 2", ResponseOrigin.Fetcher)
|
||||
)
|
||||
|
||||
store.observe(StoreRequest.cached(3, false))
|
||||
.test()
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Data("3 2", ResponseOrigin.Cache),
|
||||
StoreResponse.Data("3 2", ResponseOrigin.SourceOfTruth)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a store with persister values WHEN observeClear is Called THEN next Store get hits network`() {
|
||||
fakeDisk[3] = "seeded occurrence"
|
||||
|
||||
store.observeClear(3).blockingAwait()
|
||||
|
||||
store.observe(StoreRequest.cached(3, false))
|
||||
.test()
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 1", ResponseOrigin.Fetcher)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a store with persister values WHEN observeClearAll is called THEN next Store get calls both hit network`() {
|
||||
fakeDisk[3] = "seeded occurrence"
|
||||
fakeDisk[4] = "another seeded occurrence"
|
||||
|
||||
store.observeClearAll().blockingAwait()
|
||||
|
||||
store.observe(StoreRequest.cached(3, false))
|
||||
.test()
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("3 1", ResponseOrigin.Fetcher)
|
||||
)
|
||||
|
||||
store.observe(StoreRequest.cached(4, false))
|
||||
.test()
|
||||
.awaitCount(2)
|
||||
.assertValues(
|
||||
StoreResponse.Loading<String>(ResponseOrigin.Fetcher),
|
||||
StoreResponse.Data("4 2", ResponseOrigin.Fetcher)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue