Add Validator (#500)

* Resolve conflicts

Signed-off-by: mramotar <mramotar@dropbox.com>

* Fix rebase issues

Signed-off-by: mramotar <mramotar@dropbox.com>

* Fix tests

Signed-off-by: mramotar <mramotar@dropbox.com>

Signed-off-by: mramotar <mramotar@dropbox.com>
This commit is contained in:
Matt 2022-12-27 18:00:49 -05:00 committed by GitHub
parent 728d0e5792
commit eeb2bf7850
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 389 additions and 100 deletions

View file

@ -2,29 +2,29 @@ package org.mobilenativefoundation.store.store5
interface Converter<Network : Any, Output : Any, Local : Any> {
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<Network : Any, Output : Any, Local : Any> {
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<Network, Output, Local> =
RealConverter(fromOutputToLocal, fromNetworkToOutput, fromLocalToOutput)
fun fromOutputToLocal(converter: (value: Output) -> Local): Builder<Network, Output, Local> {
fun fromOutputToLocal(converter: (output: Output) -> Local): Builder<Network, Output, Local> {
fromOutputToLocal = converter
return this
}
fun fromLocalToOutput(converter: (value: Local) -> Output): Builder<Network, Output, Local> {
fun fromLocalToOutput(converter: (local: Local) -> Output): Builder<Network, Output, Local> {
fromLocalToOutput = converter
return this
}
fun fromNetworkToOutput(converter: (value: Network) -> Output): Builder<Network, Output, Local> {
fun fromNetworkToOutput(converter: (network: Network) -> Output): Builder<Network, Output, Local> {
fromNetworkToOutput = converter
return this
}
@ -32,16 +32,16 @@ interface Converter<Network : Any, Output : Any, Local : Any> {
}
private class RealConverter<Network : Any, Output : Any, Local : Any>(
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<Network, Output, Local> {
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)
}

View file

@ -54,6 +54,8 @@ interface StoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any> {
fun converter(converter: Converter<Network, Output, Local>):
StoreBuilder<Key, Network, Output, Local>
fun validator(validator: Validator<Output>): StoreBuilder<Key, Network, Output, Local>
companion object {
/**

View file

@ -93,11 +93,9 @@ internal class FetcherController<Key : Any, Network : Any, Output : Any, Local :
piggybackingDownstream = true,
onEach = { response ->
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)
}
}
)

View file

@ -203,10 +203,7 @@ internal class RealMutableStore<Key : Any, Network : Any, Output : Any, Local :
return lastFailedSync != null || writeRequestsQueueIsEmpty(key).not()
}
@AnyThread
private suspend fun writeRequestsQueueIsEmpty(key: Key): Boolean = withThreadSafety(key) {
keyToWriteRequestQueue[key].isNullOrEmpty()
}
private fun writeRequestsQueueIsEmpty(key: Key): Boolean = keyToWriteRequestQueue[key].isNullOrEmpty()
private suspend fun <Response : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, Output, Response>) =
withWriteRequestQueueLock(writeRequest.key) {

View file

@ -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<Key : Any, Network : Any, Output : Any, Local : Any>(
scope: CoroutineScope,
fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, Local>? = null,
converter: Converter<Network, Output, Local>? = null,
private val converter: Converter<Network, Output, Local>? = null,
private val validator: Validator<Output>?,
private val memoryPolicy: MemoryPolicy<Key, Output>?
) : Store<Key, Output> {
/**
@ -88,12 +90,18 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
converter = converter
)
@Suppress("UNCHECKED_CAST")
override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>> =
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<Key : Any, Network : Any, Output : Any, Local : Any>(
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))
}
}
}
}

View file

@ -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 <Key : Any, Network : Any, Output : Any> storeBuilderFromFetcher(
@ -31,6 +32,7 @@ internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local :
private var scope: CoroutineScope? = null
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
private var converter: Converter<Network, Output, Local>? = null
private var validator: Validator<Output>? = null
override fun scope(scope: CoroutineScope): StoreBuilder<Key, Network, Output, Local> {
this.scope = scope
@ -47,6 +49,11 @@ internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local :
return this
}
override fun validator(validator: Validator<Output>): StoreBuilder<Key, Network, Output, Local> {
this.validator = validator
return this
}
override fun converter(converter: Converter<Network, Output, Local>): StoreBuilder<Key, Network, Output, Local> {
this.converter = converter
return this
@ -57,7 +64,8 @@ internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local :
sourceOfTruth = sourceOfTruth,
fetcher = fetcher,
memoryPolicy = cachePolicy,
converter = converter
converter = converter,
validator = validator
)
override fun <UpdaterResult : Any> build(

View file

@ -76,7 +76,7 @@ internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any,
}
val readFlow: Flow<StoreReadResponse<Output?>> = 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<Key : Any, Network : Any, Output : Any,
StoreReadResponseOrigin.SourceOfTruth
}
val value = sourceOfTruth as? Output ?: if (sourceOfTruth != null) {
converter?.fromLocalToOutput(sourceOfTruth)
} else {
null
val output = when {
local != null -> 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<Output?>
}
}.catch { throwable ->
@ -151,10 +153,9 @@ internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any,
try {
barrier.emit(BarrierMsg.Blocked(versionCounter.incrementAndGet()))
val writeError = try {
val input = value as? Local ?: converter?.fromOutputToLocal(value)
if (input != null) {
delegate.write(key, input)
}
val local = converter?.fromOutputToLocal(value)
val input = local ?: value
delegate.write(key, input as Local)
null
} catch (throwable: Throwable) {
if (throwable !is CancellationException) {

View file

@ -1,5 +1,7 @@
package org.mobilenativefoundation.store.store5.impl.extensions
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.hours
internal fun now() = Clock.System.now().toEpochMilliseconds()
internal fun inHours(n: Int) = Clock.System.now().plus(n.hours).toEpochMilliseconds()

View file

@ -1,46 +1,53 @@
package org.mobilenativefoundation.store.store5
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
import org.mobilenativefoundation.store.store5.impl.extensions.inHours
import org.mobilenativefoundation.store.store5.util.assertEmitsExactly
import org.mobilenativefoundation.store.store5.util.fake.Notes
import org.mobilenativefoundation.store.store5.util.fake.NotesApi
import org.mobilenativefoundation.store.store5.util.fake.NotesBookkeeping
import org.mobilenativefoundation.store.store5.util.fake.NotesConverterProvider
import org.mobilenativefoundation.store.store5.util.fake.NotesDatabase
import org.mobilenativefoundation.store.store5.util.fake.NotesUpdaterProvider
import org.mobilenativefoundation.store.store5.util.fake.NotesValidator
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
import org.mobilenativefoundation.store.store5.util.model.SOTNote
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStoreApi::class)
class UpdaterTests {
private val testScope = TestScope()
private lateinit var api: NotesApi
private lateinit var bookkeeping: NotesBookkeeping
private lateinit var notes: NotesDatabase
@BeforeTest
fun before() {
api = NotesApi()
bookkeeping = NotesBookkeeping()
notes = NotesDatabase()
}
@Test
fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest {
val updater = Updater.by<String, CommonNote, NotesWriteResponse>(
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<String, NetworkNote, CommonNote>(
fetcher = Fetcher.ofFlow { key ->
val network = NetworkNote(NoteData.Single(Note("$key-id", "$key-title", "$key-content")))
flow { emit(network) }
}
val store = StoreBuilder.from<String, NetworkNote, CommonNote, SOTNote>(
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<String, NetworkNote, CommonNote, SOTNote, NotesWriteResponse>(
.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<NotesWriteResponse>(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<String, CommonNote, NotesWriteResponse>(
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<NotesWriteResponse>(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<StoreReadResponse.Data<CommonNote>>(secondResponse)
val data = secondResponse.value.data
assertIs<NoteData.Single>(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<String, NetworkNote, CommonNote, SOTNote>(
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<NotesWriteResponse>(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<NotesWriteResponse>(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<String, NetworkNote, CommonNote, SOTNote>(
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<String, CommonNote, NotesWriteResponse>(
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])
}
}

View file

@ -1,6 +1,6 @@
package org.mobilenativefoundation.store.store5.util
internal interface TestApi<Key : Any, Network : Any, Output : Any, Response : Any> {
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
}

View file

@ -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")
}

View file

@ -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<String, NetworkNote, CommonNote, NotesWriteRes
seed()
}
override fun get(key: String, fail: Boolean): NetworkNote? {
override fun get(key: String, fail: Boolean, ttl: Long?): NetworkNote {
if (fail) {
throw Exception()
}
return db[key]
val networkNote = db[key]!!
return if (ttl != null) {
networkNote.copy(ttl = ttl)
} else {
networkNote
}
}
override fun post(key: String, value: CommonNote, fail: Boolean): NotesWriteResponse {
@ -33,8 +37,15 @@ internal class NotesApi : TestApi<String, NetworkNote, CommonNote, NotesWriteRes
}
private fun seed() {
db["1-id"] = NetworkNote(NoteData.Single(Note("1-id", "1-title", "1-content")))
db["2-id"] = NetworkNote(NoteData.Single(Note("2-id", "2-title", "2-content")))
db["3-id"] = NetworkNote(NoteData.Single(Note("3-id", "3-title", "3-content")))
db[Notes.One.id] = NetworkNote(NoteData.Single(Notes.One))
db[Notes.Two.id] = NetworkNote(NoteData.Single(Notes.Two))
db[Notes.Three.id] = NetworkNote(NoteData.Single(Notes.Three))
db[Notes.Four.id] = NetworkNote(NoteData.Single(Notes.Four))
db[Notes.Five.id] = NetworkNote(NoteData.Single(Notes.Five))
db[Notes.Six.id] = NetworkNote(NoteData.Single(Notes.Six))
db[Notes.Seven.id] = NetworkNote(NoteData.Single(Notes.Seven))
db[Notes.Eight.id] = NetworkNote(NoteData.Single(Notes.Eight))
db[Notes.Nine.id] = NetworkNote(NoteData.Single(Notes.Nine))
db[Notes.Ten.id] = NetworkNote(NoteData.Single(Notes.Ten))
}
}

View file

@ -0,0 +1,15 @@
package org.mobilenativefoundation.store.store5.util.fake
import org.mobilenativefoundation.store.store5.Converter
import org.mobilenativefoundation.store.store5.impl.extensions.inHours
import org.mobilenativefoundation.store.store5.util.model.CommonNote
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
import org.mobilenativefoundation.store.store5.util.model.SOTNote
internal class NotesConverterProvider {
fun provide(): Converter<NetworkNote, CommonNote, SOTNote> = Converter.Builder<NetworkNote, CommonNote, SOTNote>()
.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()
}

View file

@ -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<String, SOTNote?> = 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
}
}

View file

@ -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<String, CommonNote, NotesWriteResponse> = 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")
}
}
)
}

View file

@ -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<CommonNote> {
override suspend fun isValid(item: CommonNote): Boolean = when {
item.ttl == null -> true
else -> item.ttl > expiration
}
}

View file

@ -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(