From e050a15afc21c22ffea10a6a7d5f1b436ee34a6a Mon Sep 17 00:00:00 2001 From: Matt Ramotar Date: Wed, 12 Apr 2023 22:11:02 -0400 Subject: [PATCH] Separate MutableStoreBuilder from StoreBuilder (#542) * Separate MutableStoreBuilder from StoreBuilder Signed-off-by: Matt Ramotar * Enable conversion from StoreBuilder to MutableStoreBuilder Signed-off-by: Matt Ramotar * Clean up Signed-off-by: Matt Ramotar * Fix tests Signed-off-by: Matt Ramotar --------- Signed-off-by: Matt Ramotar --- .../store/store5/MutableStoreBuilder.kt | 52 +++++++++++++ .../store/store5/StoreBuilder.kt | 34 ++++---- .../store5/impl/RealMutableStoreBuilder.kt | 78 +++++++++++++++++++ .../store/store5/impl/RealStoreBuilder.kt | 64 ++++++++------- .../store/store5/ClearAllStoreTests.kt | 2 +- .../store/store5/ClearStoreByKeyTests.kt | 4 +- .../store/store5/FetcherResponseTests.kt | 2 +- .../store/store5/FlowStoreTests.kt | 20 ++--- .../store/store5/SourceOfTruthErrorsTests.kt | 10 +-- .../store/store5/UpdaterTests.kt | 6 +- 10 files changed, 200 insertions(+), 72 deletions(-) create mode 100644 store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt create mode 100644 store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStoreBuilder.kt diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt new file mode 100644 index 000000000..b665f40cf --- /dev/null +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/MutableStoreBuilder.kt @@ -0,0 +1,52 @@ +package org.mobilenativefoundation.store.store5 + +import kotlinx.coroutines.CoroutineScope +import org.mobilenativefoundation.store.store5.impl.mutableStoreBuilderFromFetcherAndSourceOfTruth + +interface MutableStoreBuilder { + + fun build( + updater: Updater, + bookkeeper: Bookkeeper? = null + ): MutableStore + + /** + * A store multicasts same [Output] value to many consumers (Similar to RxJava.share()), by default + * [Store] will open a global scope for management of shared responses, if instead you'd like to control + * the scope that sharing/multicasting happens in you can pass a @param [scope] + * + * @param scope - scope to use for sharing + */ + fun scope(scope: CoroutineScope): MutableStoreBuilder + + /** + * controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL + * or size based eviction + * Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build() + */ + fun cachePolicy(memoryPolicy: MemoryPolicy?): MutableStoreBuilder + + /** + * by default a Store caches in memory with a default policy of max items = 100 + */ + fun disableCache(): MutableStoreBuilder + + fun converter(converter: Converter): + MutableStoreBuilder + + fun validator(validator: Validator): MutableStoreBuilder + + companion object { + /** + * Creates a new [MutableStoreBuilder] from a [Fetcher] and a [SourceOfTruth]. + * + * @param fetcher a function for fetching a flow of network records. + * @param sourceOfTruth a [SourceOfTruth] for the store. + */ + fun from( + fetcher: Fetcher, + sourceOfTruth: SourceOfTruth + ): MutableStoreBuilder = + mutableStoreBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth) + } +} diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt index da467d3c8..e46cb52d0 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt @@ -22,13 +22,10 @@ import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSo /** * Main entry point for creating a [Store]. */ -interface StoreBuilder { +interface StoreBuilder { fun build(): Store - fun build( - updater: Updater, - bookkeeper: Bookkeeper? = null - ): MutableStore + fun toMutableStoreBuilder(): MutableStoreBuilder /** * A store multicasts same [Output] value to many consumers (Similar to RxJava.share()), by default @@ -37,35 +34,31 @@ interface StoreBuilder { * * @param scope - scope to use for sharing */ - fun scope(scope: CoroutineScope): StoreBuilder + fun scope(scope: CoroutineScope): StoreBuilder /** * controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL * or size based eviction * Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build() */ - fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder + fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder /** * by default a Store caches in memory with a default policy of max items = 100 */ - fun disableCache(): StoreBuilder + fun disableCache(): StoreBuilder - fun converter(converter: Converter): - StoreBuilder - - fun validator(validator: Validator): StoreBuilder + fun validator(validator: Validator): StoreBuilder companion object { - /** * Creates a new [StoreBuilder] from a [Fetcher]. * * @param fetcher a [Fetcher] flow of network records. */ - fun from( - fetcher: Fetcher, - ): StoreBuilder = storeBuilderFromFetcher(fetcher = fetcher) + fun from( + fetcher: Fetcher, + ): StoreBuilder = storeBuilderFromFetcher(fetcher = fetcher) /** * Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth]. @@ -73,10 +66,9 @@ interface StoreBuilder { * @param fetcher a function for fetching a flow of network records. * @param sourceOfTruth a [SourceOfTruth] for the store. */ - fun from( - fetcher: Fetcher, - sourceOfTruth: SourceOfTruth - ): StoreBuilder = - storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth) + fun from( + fetcher: Fetcher, + sourceOfTruth: SourceOfTruth + ): StoreBuilder = storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth) } } diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStoreBuilder.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStoreBuilder.kt new file mode 100644 index 000000000..a62c97f92 --- /dev/null +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStoreBuilder.kt @@ -0,0 +1,78 @@ +package org.mobilenativefoundation.store.store5.impl + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import org.mobilenativefoundation.store.store5.Bookkeeper +import org.mobilenativefoundation.store.store5.Converter +import org.mobilenativefoundation.store.store5.Fetcher +import org.mobilenativefoundation.store.store5.MemoryPolicy +import org.mobilenativefoundation.store.store5.MutableStore +import org.mobilenativefoundation.store.store5.MutableStoreBuilder +import org.mobilenativefoundation.store.store5.SourceOfTruth +import org.mobilenativefoundation.store.store5.Store +import org.mobilenativefoundation.store.store5.StoreDefaults +import org.mobilenativefoundation.store.store5.Updater +import org.mobilenativefoundation.store.store5.Validator +import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore + +fun mutableStoreBuilderFromFetcher( + fetcher: Fetcher, +): MutableStoreBuilder = RealMutableStoreBuilder(fetcher) + +fun mutableStoreBuilderFromFetcherAndSourceOfTruth( + fetcher: Fetcher, + sourceOfTruth: SourceOfTruth, +): MutableStoreBuilder = RealMutableStoreBuilder(fetcher, sourceOfTruth) + +internal class RealMutableStoreBuilder( + private val fetcher: Fetcher, + private val sourceOfTruth: SourceOfTruth? = null, +) : MutableStoreBuilder { + private var scope: CoroutineScope? = null + private var cachePolicy: MemoryPolicy? = StoreDefaults.memoryPolicy + private var converter: Converter? = null + private var validator: Validator? = null + + override fun scope(scope: CoroutineScope): MutableStoreBuilder { + this.scope = scope + return this + } + + override fun cachePolicy(memoryPolicy: MemoryPolicy?): MutableStoreBuilder { + cachePolicy = memoryPolicy + return this + } + + override fun disableCache(): MutableStoreBuilder { + cachePolicy = null + return this + } + + override fun validator(validator: Validator): MutableStoreBuilder { + this.validator = validator + return this + } + + override fun converter(converter: Converter): MutableStoreBuilder { + this.converter = converter + return this + } + + fun build(): Store = RealStore( + scope = scope ?: GlobalScope, + sourceOfTruth = sourceOfTruth, + fetcher = fetcher, + memoryPolicy = cachePolicy, + converter = converter, + validator = validator + ) + + override fun build( + updater: Updater, + bookkeeper: Bookkeeper? + ): MutableStore = + build().asMutableStore( + updater = updater, + bookkeeper = bookkeeper + ) +} diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt index e0cdbc80b..8e4f7e9a7 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt @@ -1,64 +1,58 @@ +@file:Suppress("UNCHECKED_CAST") + package org.mobilenativefoundation.store.store5.impl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope -import org.mobilenativefoundation.store.store5.Bookkeeper import org.mobilenativefoundation.store.store5.Converter import org.mobilenativefoundation.store.store5.Fetcher import org.mobilenativefoundation.store.store5.MemoryPolicy -import org.mobilenativefoundation.store.store5.MutableStore +import org.mobilenativefoundation.store.store5.MutableStoreBuilder import org.mobilenativefoundation.store.store5.SourceOfTruth import org.mobilenativefoundation.store.store5.Store import org.mobilenativefoundation.store.store5.StoreBuilder import org.mobilenativefoundation.store.store5.StoreDefaults -import org.mobilenativefoundation.store.store5.Updater import org.mobilenativefoundation.store.store5.Validator -import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore -fun storeBuilderFromFetcher( - fetcher: Fetcher, +fun storeBuilderFromFetcher( + fetcher: Fetcher, sourceOfTruth: SourceOfTruth? = null, -): StoreBuilder = RealStoreBuilder(fetcher, sourceOfTruth) +): StoreBuilder = RealStoreBuilder(fetcher, sourceOfTruth) -fun storeBuilderFromFetcherAndSourceOfTruth( - fetcher: Fetcher, - sourceOfTruth: SourceOfTruth, -): StoreBuilder = RealStoreBuilder(fetcher, sourceOfTruth) +fun storeBuilderFromFetcherAndSourceOfTruth( + fetcher: Fetcher, + sourceOfTruth: SourceOfTruth, +): StoreBuilder = RealStoreBuilder(fetcher, sourceOfTruth) internal class RealStoreBuilder( private val fetcher: Fetcher, private val sourceOfTruth: SourceOfTruth? = null -) : StoreBuilder { +) : StoreBuilder { private var scope: CoroutineScope? = null private var cachePolicy: MemoryPolicy? = StoreDefaults.memoryPolicy private var converter: Converter? = null private var validator: Validator? = null - override fun scope(scope: CoroutineScope): StoreBuilder { + override fun scope(scope: CoroutineScope): StoreBuilder { this.scope = scope return this } - override fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder { + override fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder { cachePolicy = memoryPolicy return this } - override fun disableCache(): StoreBuilder { + override fun disableCache(): StoreBuilder { cachePolicy = null return this } - override fun validator(validator: Validator): StoreBuilder { + override fun validator(validator: Validator): StoreBuilder { this.validator = validator return this } - override fun converter(converter: Converter): StoreBuilder { - this.converter = converter - return this - } - override fun build(): Store = RealStore( scope = scope ?: GlobalScope, sourceOfTruth = sourceOfTruth, @@ -68,12 +62,24 @@ internal class RealStoreBuilder build( - updater: Updater, - bookkeeper: Bookkeeper? - ): MutableStore = - build().asMutableStore( - updater = updater, - bookkeeper = bookkeeper - ) + override fun toMutableStoreBuilder(): MutableStoreBuilder { + fetcher as Fetcher + return if (sourceOfTruth == null) { + mutableStoreBuilderFromFetcher(fetcher) + } else { + mutableStoreBuilderFromFetcherAndSourceOfTruth(fetcher, sourceOfTruth as SourceOfTruth) + }.apply { + if (this@RealStoreBuilder.scope != null) { + scope(this@RealStoreBuilder.scope!!) + } + + if (this@RealStoreBuilder.cachePolicy != null) { + cachePolicy(this@RealStoreBuilder.cachePolicy) + } + + if (this@RealStoreBuilder.validator != null) { + validator(this@RealStoreBuilder.validator!!) + } + } + } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt index 25fdc5897..c89c47dab 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearAllStoreTests.kt @@ -43,7 +43,7 @@ class ClearAllStoreTests { @Test fun callingClearAllOnStoreWithPersisterAndNoInMemoryCacheDeletesAllEntriesFromThePersister() = testScope.runTest { - val store = StoreBuilder.from( + val store = StoreBuilder.from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ).scope(testScope) diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt index ef9fc8608..083015928 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ClearStoreByKeyTests.kt @@ -24,7 +24,7 @@ class ClearStoreByKeyTests { fun callingClearWithKeyOnStoreWithPersisterWithNoInMemoryCacheDeletesTheEntryAssociatedWithTheKeyFromThePersister() = testScope.runTest { val key = "key" val value = 1 - val store = StoreBuilder.from( + val store = StoreBuilder.from( fetcher = Fetcher.of { value }, sourceOfTruth = persister.asSourceOfTruth() ).scope(testScope) @@ -107,7 +107,7 @@ class ClearStoreByKeyTests { val key2 = "key2" val value1 = 1 val value2 = 2 - val store = StoreBuilder.from( + val store = StoreBuilder.from( fetcher = Fetcher.of { key -> when (key) { key1 -> value1 diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt index 1b7156c72..e1c36c968 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt @@ -211,6 +211,6 @@ class FetcherResponseTests { ) } - private fun StoreBuilder.buildWithTestScope() = + private fun StoreBuilder.buildWithTestScope() = scope(testScope).build() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt index c6008ddd9..68fa92cb0 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt @@ -113,7 +113,7 @@ class FlowStoreTests { 3 to "three-2" ) val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ).buildWithTestScope() @@ -184,7 +184,7 @@ class FlowStoreTests { ) val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ).buildWithTestScope() @@ -309,7 +309,7 @@ class FlowStoreTests { ) val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ) @@ -357,7 +357,7 @@ class FlowStoreTests { @Test fun diskChangeWhileNetworkIsFlowing_simple() = testScope.runTest { val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( Fetcher.ofFlow { flow { delay(20) @@ -395,7 +395,7 @@ class FlowStoreTests { @Test fun diskChangeWhileNetworkIsFlowing_overwrite() = testScope.runTest { val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( fetcher = Fetcher.ofFlow { flow { delay(10) @@ -445,7 +445,7 @@ class FlowStoreTests { fun errorTest() = testScope.runTest { val exception = IllegalArgumentException("wow") val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( Fetcher.of { throw exception }, @@ -496,7 +496,7 @@ class FlowStoreTests { fun givenSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() = testScope.runTest { val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( fetcher = Fetcher.ofFlow { flow {} }, sourceOfTruth = persister.asSourceOfTruth() ) @@ -530,7 +530,7 @@ class FlowStoreTests { @Test fun givenSourceOfTruthWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest { val persister = InMemoryPersister().asFlowable() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( fetcher = Fetcher.ofFlow { flow {} }, sourceOfTruth = persister.asSourceOfTruth() ) @@ -687,7 +687,7 @@ class FlowStoreTests { 3 to "three-2" ) val persister = InMemoryPersister() - val pipeline = StoreBuilder.from( + val pipeline = StoreBuilder.from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ).buildWithTestScope() @@ -865,6 +865,6 @@ class FlowStoreTests { ) ) - private fun StoreBuilder.buildWithTestScope() = + private fun StoreBuilder.buildWithTestScope() = scope(testScope).build() } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt index c1660075f..3c235060c 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt @@ -31,7 +31,7 @@ class SourceOfTruthErrorsTests { 3 to "b" ) val pipeline = StoreBuilder - .from( + .from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ) @@ -65,7 +65,7 @@ class SourceOfTruthErrorsTests { 3 to "b" ) val pipeline = StoreBuilder - .from( + .from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ) @@ -110,7 +110,7 @@ class SourceOfTruthErrorsTests { flowOf("a", "b", "c", "d") } val pipeline = StoreBuilder - .from( + .from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ) @@ -240,7 +240,7 @@ class SourceOfTruthErrorsTests { } } val pipeline = StoreBuilder - .from( + .from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ) @@ -334,7 +334,7 @@ class SourceOfTruthErrorsTests { val persister = InMemoryPersister() val fetcher = Fetcher.of { _: Int -> "a" } val pipeline = StoreBuilder - .from( + .from( fetcher = fetcher, sourceOfTruth = persister.asSourceOfTruth() ) diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt index ce49efcbb..a5502a1a3 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt @@ -55,7 +55,7 @@ class UpdaterTests { clearAll = bookkeeping::clear ) - val store = StoreBuilder.from( + val store = MutableStoreBuilder.from( fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, sourceOfTruth = SourceOfTruth.of( nonFlowReader = { key -> notes.get(key) }, @@ -130,7 +130,7 @@ class UpdaterTests { clearAll = bookkeeping::clear ) - val store = StoreBuilder.from( + val store = MutableStoreBuilder.from( fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, sourceOfTruth = SourceOfTruth.of( nonFlowReader = { key -> notes.get(key) }, @@ -187,7 +187,7 @@ class UpdaterTests { clearAll = bookkeeping::clear ) - val store = StoreBuilder.from( + val store = MutableStoreBuilder.from( fetcher = Fetcher.ofFlow { key -> val network = api.get(key) flow { emit(network) }