From eeb2bf7850d155f942a30e595c6836718690a702 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 27 Dec 2022 18:00:49 -0500 Subject: [PATCH] Add Validator (#500) * Resolve conflicts Signed-off-by: mramotar * Fix rebase issues Signed-off-by: mramotar * Fix tests Signed-off-by: mramotar Signed-off-by: mramotar --- .../store/store5/Converter.kt | 30 +-- .../store/store5/StoreBuilder.kt | 2 + .../store/store5/impl/FetcherController.kt | 8 +- .../store/store5/impl/RealMutableStore.kt | 5 +- .../store/store5/impl/RealStore.kt | 70 +++++-- .../store/store5/impl/RealStoreBuilder.kt | 10 +- .../store5/impl/SourceOfTruthWithBarrier.kt | 29 +-- .../store/store5/impl/extensions/clock.kt | 2 + .../store/store5/UpdaterTests.kt | 198 +++++++++++++++--- .../store/store5/util/TestApi.kt | 2 +- .../store/store5/util/fake/Notes.kt | 16 ++ .../store/store5/util/fake/NotesApi.kt | 23 +- .../util/fake/NotesConverterProvider.kt | 15 ++ .../store/store5/util/fake/NotesDatabase.kt | 39 ++++ .../store5/util/fake/NotesUpdaterProvider.kt | 19 ++ .../store/store5/util/fake/NotesValidator.kt | 12 ++ .../store/store5/util/model/NoteData.kt | 9 +- 17 files changed, 389 insertions(+), 100 deletions(-) create mode 100644 store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/Notes.kt create mode 100644 store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesConverterProvider.kt create mode 100644 store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt create mode 100644 store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt create mode 100644 store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt index 3128621..5a9fe68 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/Converter.kt @@ -2,29 +2,29 @@ package org.mobilenativefoundation.store.store5 interface Converter { fun fromNetworkToOutput(network: Network): Output? - fun fromOutputToLocal(common: Output): Local? - fun fromLocalToOutput(sourceOfTruth: Local): Output? + fun fromOutputToLocal(output: Output): Local? + fun fromLocalToOutput(local: Local): Output? class Builder { - private var fromOutputToLocal: ((value: Output) -> Local)? = null - private var fromNetworkToOutput: ((value: Network) -> Output)? = null - private var fromLocalToOutput: ((value: Local) -> Output)? = null + private var fromOutputToLocal: ((output: Output) -> Local)? = null + private var fromNetworkToOutput: ((network: Network) -> Output)? = null + private var fromLocalToOutput: ((local: Local) -> Output)? = null fun build(): Converter = RealConverter(fromOutputToLocal, fromNetworkToOutput, fromLocalToOutput) - fun fromOutputToLocal(converter: (value: Output) -> Local): Builder { + fun fromOutputToLocal(converter: (output: Output) -> Local): Builder { fromOutputToLocal = converter return this } - fun fromLocalToOutput(converter: (value: Local) -> Output): Builder { + fun fromLocalToOutput(converter: (local: Local) -> Output): Builder { fromLocalToOutput = converter return this } - fun fromNetworkToOutput(converter: (value: Network) -> Output): Builder { + fun fromNetworkToOutput(converter: (network: Network) -> Output): Builder { fromNetworkToOutput = converter return this } @@ -32,16 +32,16 @@ interface Converter { } private class RealConverter( - private val fromOutputToLocal: ((value: Output) -> Local)?, - private val fromNetworkToOutput: ((value: Network) -> Output)?, - private val fromLocalToOutput: ((value: Local) -> Output)?, + private val fromOutputToLocal: ((output: Output) -> Local)?, + private val fromNetworkToOutput: ((network: Network) -> Output)?, + private val fromLocalToOutput: ((local: Local) -> Output)?, ) : Converter { override fun fromNetworkToOutput(network: Network): Output? = fromNetworkToOutput?.invoke(network) - override fun fromOutputToLocal(common: Output): Local? = - fromOutputToLocal?.invoke(common) + override fun fromOutputToLocal(output: Output): Local? = + fromOutputToLocal?.invoke(output) - override fun fromLocalToOutput(sourceOfTruth: Local): Output? = - fromLocalToOutput?.invoke(sourceOfTruth) + override fun fromLocalToOutput(local: Local): Output? = + fromLocalToOutput?.invoke(local) } 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 877502c..2f2c763 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt @@ -54,6 +54,8 @@ interface StoreBuilder { fun converter(converter: Converter): StoreBuilder + fun validator(validator: Validator): StoreBuilder + companion object { /** diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt index db66b9f..902342d 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/FetcherController.kt @@ -93,11 +93,9 @@ internal class FetcherController response.dataOrNull()?.let { network -> - val input = - network as? Output ?: converter?.fromNetworkToOutput(network) - if (input != null) { - sourceOfTruth?.write(key, input) - } + val output = converter?.fromNetworkToOutput(network) + val input = output ?: network + sourceOfTruth?.write(key, input as Output) } } ) diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt index 622cc14..1494f05 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt @@ -203,10 +203,7 @@ internal class RealMutableStore addWriteRequestToQueue(writeRequest: StoreWriteRequest) = withWriteRequestQueueLock(writeRequest.key) { diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt index b5dbc3f..8ba6042 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt @@ -36,6 +36,7 @@ import org.mobilenativefoundation.store.store5.Store import org.mobilenativefoundation.store.store5.StoreReadRequest import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin +import org.mobilenativefoundation.store.store5.Validator import org.mobilenativefoundation.store.store5.impl.operators.Either import org.mobilenativefoundation.store.store5.impl.operators.merge import org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWriteResult @@ -44,7 +45,8 @@ internal class RealStore( scope: CoroutineScope, fetcher: Fetcher, sourceOfTruth: SourceOfTruth? = null, - converter: Converter? = null, + private val converter: Converter? = null, + private val validator: Validator?, private val memoryPolicy: MemoryPolicy? ) : Store { /** @@ -88,12 +90,18 @@ internal class RealStore( converter = converter ) + @Suppress("UNCHECKED_CAST") override fun stream(request: StoreReadRequest): Flow> = flow { val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) { null } else { - memCache?.getIfPresent(request.key) + val output = memCache?.getIfPresent(request.key) + when { + output == null -> null + validator?.isValid(output) == false -> null + else -> output + } } cachedToEmit?.let { @@ -114,27 +122,43 @@ internal class RealStore( diskNetworkCombined(request, sourceOfTruth) } emitAll( - stream.transform { - emit(it) - if (it is StoreReadResponse.NoNewData && cachedToEmit == null) { - // In the special case where fetcher returned no new data we actually want to - // serve cache data (even if the request specified skipping cache and/or SoT) - // - // For stream(Request.cached(key, refresh=true)) we will return: - // Cache - // Source of truth - // Fetcher - > Loading - // Fetcher - > NoNewData - // (future Source of truth updates) - // - // For stream(Request.fresh(key)) we will return: - // Fetcher - > Loading - // Fetcher - > NoNewData - // Cache - // Source of truth - // (future Source of truth updates) - memCache?.getIfPresent(request.key)?.let { - emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache)) + stream.transform { output -> + val data = output.dataOrNull() + val shouldSkipValidation = validator == null || data == null || output.origin == StoreReadResponseOrigin.Fetcher + if (data != null && !shouldSkipValidation && validator?.isValid(data) == false) { + fetcherController.getFetcher(request.key, false).collect { storeReadResponse -> + val network = storeReadResponse.dataOrNull() + if (network != null) { + val newOutput = converter?.fromNetworkToOutput(network) ?: network as? Output + if (newOutput != null) { + emit(StoreReadResponse.Data(newOutput, origin = StoreReadResponseOrigin.Fetcher)) + } else { + emit(StoreReadResponse.NoNewData(origin = StoreReadResponseOrigin.Fetcher)) + } + } + } + } else { + emit(output) + if (output is StoreReadResponse.NoNewData && cachedToEmit == null) { + // In the special case where fetcher returned no new data we actually want to + // serve cache data (even if the request specified skipping cache and/or SoT) + // + // For stream(Request.cached(key, refresh=true)) we will return: + // Cache + // Source of truth + // Fetcher - > Loading + // Fetcher - > NoNewData + // (future Source of truth updates) + // + // For stream(Request.fresh(key)) we will return: + // Fetcher - > Loading + // Fetcher - > NoNewData + // Cache + // Source of truth + // (future Source of truth updates) + memCache?.getIfPresent(request.key)?.let { + emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache)) + } } } } 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 18faaa8..71ad56c 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 @@ -12,6 +12,7 @@ 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( @@ -31,6 +32,7 @@ internal class RealStoreBuilder? = StoreDefaults.memoryPolicy private var converter: Converter? = null + private var validator: Validator? = null override fun scope(scope: CoroutineScope): StoreBuilder { this.scope = scope @@ -47,6 +49,11 @@ internal class RealStoreBuilder): StoreBuilder { + this.validator = validator + return this + } + override fun converter(converter: Converter): StoreBuilder { this.converter = converter return this @@ -57,7 +64,8 @@ internal class RealStoreBuilder build( diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt index 2fe4759..1781f38 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/SourceOfTruthWithBarrier.kt @@ -76,7 +76,7 @@ internal class SourceOfTruthWithBarrier> = when (barrierMessage) { is BarrierMsg.Open -> - delegate.reader(key).mapIndexed { index, sourceOfTruth -> + delegate.reader(key).mapIndexed { index, local -> if (index == 0 && messageArrivedAfterMe) { val firstMsgOrigin = if (writeError == null) { // restarted barrier without an error means write succeeded @@ -89,22 +89,24 @@ internal class SourceOfTruthWithBarrier converter?.fromLocalToOutput(local) ?: local as? Output + else -> null } + StoreReadResponse.Data( origin = firstMsgOrigin, - value = value + value = output ) } else { + val output = when { + local != null -> converter?.fromLocalToOutput(local) ?: local as? Output + else -> null + } + StoreReadResponse.Data( origin = StoreReadResponseOrigin.SourceOfTruth, - value = sourceOfTruth as? Output - ?: if (sourceOfTruth != null) converter?.fromLocalToOutput( - sourceOfTruth - ) else null + value = output ) as StoreReadResponse } }.catch { throwable -> @@ -151,10 +153,9 @@ internal class SourceOfTruthWithBarrier( - post = { key, common -> - val response = api.post(key, common) - if (response.ok) { - UpdaterResult.Success.Typed(response) - } else { - UpdaterResult.Error.Message("Failed to sync") - } - } - ) + fun givenNonEmptyMarketWhenWriteThenStoredAndAPIUpdated() = testScope.runTest { + val ttl = inHours(1) + + val converter = NotesConverterProvider().provide() + val validator = NotesValidator() + val updater = NotesUpdaterProvider(api).provide() val bookkeeper = Bookkeeper.by( getLastFailedSync = bookkeeping::getLastFailedSync, setLastFailedSync = bookkeeping::setLastFailedSync, @@ -48,30 +55,165 @@ class UpdaterTests { clearAll = bookkeeping::clear ) - val store = StoreBuilder.from( - fetcher = Fetcher.ofFlow { key -> - val network = NetworkNote(NoteData.Single(Note("$key-id", "$key-title", "$key-content"))) - flow { emit(network) } - } + val store = StoreBuilder.from( + fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, + sourceOfTruth = SourceOfTruth.of( + nonFlowReader = { key -> notes.get(key) }, + writer = { key, sot -> notes.put(key, sot) }, + delete = { key -> notes.clear(key) }, + deleteAll = { notes.clear() } + ) ) - .build() - .asMutableStore( + .converter(converter) + .validator(validator) + .build( updater = updater, bookkeeper = bookkeeper ) - val noteKey = "1-id" - val noteTitle = "1-title" - val noteContent = "1-content" - val noteData = NoteData.Single(Note(noteKey, noteTitle, noteContent)) + val readRequest = StoreReadRequest.fresh(Notes.One.id) + + val stream = store.stream(readRequest) + + // Read is success + assertEmitsExactly( + stream, + listOf( + StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher), + StoreReadResponse.Data(CommonNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher) + ) + ) + + val newNote = Notes.One.copy(title = "New Title-1") val writeRequest = StoreWriteRequest.of( - key = noteKey, - value = CommonNote(noteData) + key = Notes.One.id, + value = CommonNote(NoteData.Single(newNote)) ) val storeWriteResponse = store.write(writeRequest) - assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(noteKey, true)), storeWriteResponse) - assertEquals(NetworkNote(noteData), api.db[noteKey]) + // Write is success + assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(Notes.One.id, true)), storeWriteResponse) + + val cachedReadRequest = StoreReadRequest.cached(Notes.One.id, refresh = false) + val cachedStream = store.stream(cachedReadRequest) + + // Cache + SOT are updated + val firstResponse = cachedStream.first() + assertEquals(StoreReadResponse.Data(CommonNote(NoteData.Single(newNote), ttl = null), StoreReadResponseOrigin.Cache), firstResponse) + + val secondResponse = cachedStream.take(2).last() + assertIs>(secondResponse) + val data = secondResponse.value.data + assertIs(data) + assertNotNull(data) + assertEquals(newNote, data.item) + assertEquals(StoreReadResponseOrigin.SourceOfTruth, secondResponse.origin) + assertNotNull(secondResponse.value.ttl) + + // API is updated + assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(Notes.One.id, true)), storeWriteResponse) + assertEquals(NetworkNote(NoteData.Single(newNote), ttl = null), api.db[Notes.One.id]) + } + + @Test + fun givenNonEmptyMarketWithValidatorWhenInvalidThenSuccessOriginatingFromFetcher() = testScope.runTest { + val ttl = inHours(1) + + val converter = NotesConverterProvider().provide() + val validator = NotesValidator(expiration = inHours(12)) + val updater = NotesUpdaterProvider(api).provide() + val bookkeeper = Bookkeeper.by( + getLastFailedSync = bookkeeping::getLastFailedSync, + setLastFailedSync = bookkeeping::setLastFailedSync, + clear = bookkeeping::clear, + clearAll = bookkeeping::clear + ) + + val store = StoreBuilder.from( + fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) }, + sourceOfTruth = SourceOfTruth.of( + nonFlowReader = { key -> notes.get(key) }, + writer = { key, sot -> notes.put(key, sot) }, + delete = { key -> notes.clear(key) }, + deleteAll = { notes.clear() } + ) + ) + .converter(converter) + .validator(validator) + .build( + updater = updater, + bookkeeper = bookkeeper + ) + + val readRequest = StoreReadRequest.fresh(Notes.One.id) + + val stream = store.stream(readRequest) + + // Fetch is success and validator is not used + assertEmitsExactly( + stream, + listOf( + StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher), + StoreReadResponse.Data(CommonNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher) + ) + ) + + val cachedReadRequest = StoreReadRequest.cached(Notes.One.id, refresh = false) + val cachedStream = store.stream(cachedReadRequest) + + // Cache + SOT are updated + // But item is invalid + // So we do not emit value in cache or SOT + // Instead we get latest from network even though refresh = false + + assertEmitsExactly( + cachedStream, + listOf( + StoreReadResponse.Data(CommonNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher) + ) + ) + } + + @Test + fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest { + val converter = NotesConverterProvider().provide() + val validator = NotesValidator() + val updater = NotesUpdaterProvider(api).provide() + val bookkeeper = Bookkeeper.by( + getLastFailedSync = bookkeeping::getLastFailedSync, + setLastFailedSync = bookkeeping::setLastFailedSync, + clear = bookkeeping::clear, + clearAll = bookkeeping::clear + ) + + val store = StoreBuilder.from( + fetcher = Fetcher.ofFlow { key -> + val network = api.get(key) + flow { emit(network) } + }, + sourceOfTruth = SourceOfTruth.of( + nonFlowReader = { key -> notes.get(key) }, + writer = { key, sot -> notes.put(key, sot) }, + delete = { key -> notes.clear(key) }, + deleteAll = { notes.clear() } + ) + ) + .converter(converter) + .validator(validator) + .build( + updater = updater, + bookkeeper = bookkeeper + ) + + val newNote = Notes.One.copy(title = "New Title-1") + val writeRequest = StoreWriteRequest.of( + key = Notes.One.id, + value = CommonNote(NoteData.Single(newNote)) + ) + val storeWriteResponse = store.write(writeRequest) + + assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(Notes.One.id, true)), storeWriteResponse) + assertEquals(NetworkNote(NoteData.Single(newNote)), api.db[Notes.One.id]) } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt index 57702e8..ca937f9 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/TestApi.kt @@ -1,6 +1,6 @@ package org.mobilenativefoundation.store.store5.util internal interface TestApi { - fun get(key: Key, fail: Boolean = false): Network? + fun get(key: Key, fail: Boolean = false, ttl: Long? = null): Network? fun post(key: Key, value: Output, fail: Boolean = false): Response } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/Notes.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/Notes.kt new file mode 100644 index 0000000..cfc4c6e --- /dev/null +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/Notes.kt @@ -0,0 +1,16 @@ +package org.mobilenativefoundation.store.store5.util.fake + +import org.mobilenativefoundation.store.store5.util.model.Note + +internal object Notes { + val One = Note("1", "Title-1", "Content-1") + val Two = Note("2", "Title-2", "Content-2") + val Three = Note("3", "Title-3", "Content-3") + val Four = Note("4", "Title-4", "Content-4") + val Five = Note("5", "Title-5", "Content-5") + val Six = Note("6", "Title-6", "Content-6") + val Seven = Note("7", "Title-7", "Content-7") + val Eight = Note("8", "Title-8", "Content-8") + val Nine = Note("9", "Title-9", "Content-9") + val Ten = Note("10", "Title-10", "Content-10") +} diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt index df61a83..1ee2b2c 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesApi.kt @@ -3,7 +3,6 @@ package org.mobilenativefoundation.store.store5.util.fake import org.mobilenativefoundation.store.store5.util.TestApi import org.mobilenativefoundation.store.store5.util.model.CommonNote import org.mobilenativefoundation.store.store5.util.model.NetworkNote -import org.mobilenativefoundation.store.store5.util.model.Note import org.mobilenativefoundation.store.store5.util.model.NoteData import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse @@ -14,12 +13,17 @@ internal class NotesApi : TestApi = Converter.Builder() + .fromLocalToOutput { value -> CommonNote(data = value.data, ttl = value.ttl) } + .fromOutputToLocal { value -> SOTNote(data = value.data, ttl = value.ttl ?: inHours(12)) } + .fromNetworkToOutput { value -> CommonNote(data = value.data, ttl = value.ttl) } + .build() +} diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt new file mode 100644 index 0000000..0df4f08 --- /dev/null +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesDatabase.kt @@ -0,0 +1,39 @@ +package org.mobilenativefoundation.store.store5.util.fake + +import org.mobilenativefoundation.store.store5.util.model.SOTNote + +internal class NotesDatabase { + private val db: MutableMap = mutableMapOf() + fun put(key: String, input: SOTNote, fail: Boolean = false): Boolean { + if (fail) { + throw Exception() + } + + db[key] = input + return true + } + + fun get(key: String, fail: Boolean = false): SOTNote? { + if (fail) { + throw Exception() + } + + return db[key] + } + + fun clear(key: String, fail: Boolean = false): Boolean { + if (fail) { + throw Exception() + } + db.remove(key) + return true + } + + fun clear(fail: Boolean = false): Boolean { + if (fail) { + throw Exception() + } + db.clear() + return true + } +} diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt new file mode 100644 index 0000000..f6624ef --- /dev/null +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesUpdaterProvider.kt @@ -0,0 +1,19 @@ +package org.mobilenativefoundation.store.store5.util.fake + +import org.mobilenativefoundation.store.store5.Updater +import org.mobilenativefoundation.store.store5.UpdaterResult +import org.mobilenativefoundation.store.store5.util.model.CommonNote +import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse + +internal class NotesUpdaterProvider(private val api: NotesApi) { + fun provide(): Updater = Updater.by( + post = { key, input -> + val response = api.post(key, input) + if (response.ok) { + UpdaterResult.Success.Typed(response) + } else { + UpdaterResult.Error.Message("Failed to sync") + } + } + ) +} diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt new file mode 100644 index 0000000..4947eca --- /dev/null +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/fake/NotesValidator.kt @@ -0,0 +1,12 @@ +package org.mobilenativefoundation.store.store5.util.fake + +import org.mobilenativefoundation.store.store5.Validator +import org.mobilenativefoundation.store.store5.impl.extensions.now +import org.mobilenativefoundation.store.store5.util.model.CommonNote + +internal class NotesValidator(private val expiration: Long = now()) : Validator { + override suspend fun isValid(item: CommonNote): Boolean = when { + item.ttl == null -> true + else -> item.ttl > expiration + } +} diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt index 64bab3b..65ca826 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/model/NoteData.kt @@ -11,15 +11,18 @@ internal data class NotesWriteResponse( ) internal data class NetworkNote( - val data: NoteData? = null + val data: NoteData? = null, + val ttl: Long? = null, ) internal data class CommonNote( - val data: NoteData? = null + val data: NoteData? = null, + val ttl: Long? = null, ) internal data class SOTNote( - val data: NoteData? = null + val data: NoteData? = null, + val ttl: Long ) internal data class Note(