Add zip helpers for Result and use in VaultRepository (#383)

This commit is contained in:
Brian Yencho 2023-12-13 08:49:56 -06:00 committed by Álison Fernandes
parent f0bd9f54d6
commit 4d3899e6f9
3 changed files with 205 additions and 22 deletions

View file

@ -1,5 +1,8 @@
package com.x8bit.bitwarden.data.platform.util
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
/**
* Flat maps a successful [Result] with the given [transform] to another [Result], and leaves
* failures untouched.
@ -20,3 +23,62 @@ fun <T> T.asSuccess(): Result<T> =
*/
fun Throwable.asFailure(): Result<Nothing> =
Result.failure(this)
/**
* Retrieves results from [firstResultProvider] and [secondResultProvider] by first running in
* parallel and then combining successful results using the given [zipper].
*/
suspend fun <T1, T2, R> zip(
firstResultProvider: suspend () -> Result<T1>,
secondResultProvider: suspend () -> Result<T2>,
zipper: suspend (first: T1, second: T2) -> R,
): Result<R> = coroutineScope {
val firstResultDeferred = async { firstResultProvider() }
val secondResultDeferred = async { secondResultProvider() }
val firstResult = firstResultDeferred.await()
val secondResult = secondResultDeferred.await()
val errorOrNull = firstResult.exceptionOrNull()
?: secondResult.exceptionOrNull()
errorOrNull
?.asFailure()
?: zipper(
firstResult.getOrThrow(),
secondResult.getOrThrow(),
)
.asSuccess()
}
/**
* Retrieves results from [firstResultProvider], [secondResultProvider], and [thirdResultProvider]
* by first running in parallel and then combining successful results using the given [zipper].
*/
suspend fun <T1, T2, T3, R> zip(
firstResultProvider: suspend () -> Result<T1>,
secondResultProvider: suspend () -> Result<T2>,
thirdResultProvider: suspend () -> Result<T3>,
zipper: suspend (first: T1, second: T2, third: T3) -> R,
): Result<R> = coroutineScope {
val firstResultDeferred = async { firstResultProvider() }
val secondResultDeferred = async { secondResultProvider() }
val thirdResultDeferred = async { thirdResultProvider() }
val firstResult = firstResultDeferred.await()
val secondResult = secondResultDeferred.await()
val thirdResult = thirdResultDeferred.await()
val errorOrNull = firstResult.exceptionOrNull()
?: secondResult.exceptionOrNull()
?: thirdResult.exceptionOrNull()
errorOrNull
?.asFailure()
?: zipper(
firstResult.getOrThrow(),
secondResult.getOrThrow(),
thirdResult.getOrThrow(),
)
.asSuccess()
}

View file

@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.map
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.platform.util.zip
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
@ -44,6 +45,7 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Default implementation of [VaultRepository].
@ -54,7 +56,7 @@ class VaultRepositoryImpl constructor(
private val ciphersService: CiphersService,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
dispatcherManager: DispatcherManager,
private val dispatcherManager: DispatcherManager,
) : VaultRepository {
private val scope = CoroutineScope(dispatcherManager.io)
@ -374,15 +376,21 @@ class VaultRepositoryImpl constructor(
sendDataMutableStateFlow.update { newState }
}
private suspend fun decryptSyncResponseAndUpdateVaultDataState(syncResponse: SyncResponseJson) {
val newState = vaultSdkSource
.decryptCipherList(
cipherList = syncResponse
.ciphers
.orEmpty()
.toEncryptedSdkCipherList(),
)
.flatMap { decryptedCipherList ->
private suspend fun decryptSyncResponseAndUpdateVaultDataState(
syncResponse: SyncResponseJson,
) = withContext(dispatcherManager.default) {
// Allow decryption of various types in parallel.
val newState = zip(
{
vaultSdkSource
.decryptCipherList(
cipherList = syncResponse
.ciphers
.orEmpty()
.toEncryptedSdkCipherList(),
)
},
{
vaultSdkSource
.decryptFolderList(
folderList = syncResponse
@ -390,19 +398,15 @@ class VaultRepositoryImpl constructor(
.orEmpty()
.toEncryptedSdkFolderList(),
)
.map { decryptedFolderList ->
decryptedCipherList to decryptedFolderList
}
}
},
) { decryptedCipherList, decryptedFolderList ->
VaultData(
cipherViewList = decryptedCipherList,
folderViewList = decryptedFolderList,
)
}
.fold(
onSuccess = { (decryptedCipherList, decryptedFolderList) ->
DataState.Loaded(
data = VaultData(
cipherViewList = decryptedCipherList,
folderViewList = decryptedFolderList,
),
)
},
onSuccess = { DataState.Loaded(data = it) },
onFailure = { DataState.Error(error = it) },
)
vaultDataMutableStateFlow.update { newState }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.util
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@ -66,4 +67,120 @@ class ResultTest {
throwable.asFailure(),
)
}
@Test
fun `zip with two arguments should return a success when both are successes`() = runTest {
assertEquals(
("A" to 1).asSuccess(),
zip(
{ "A".asSuccess() },
{ 1.asSuccess() },
) { first, second ->
first to second
},
)
}
@Test
fun `zip with two arguments should return a failure when the first is a failure`() = runTest {
val throwable = Throwable()
assertEquals(
throwable.asFailure(),
zip(
{
@Suppress("USELESS_CAST")
throwable.asFailure() as Result<String>
},
{ 1.asSuccess() },
) { first, second ->
first to second
},
)
}
@Test
fun `zip with two arguments should return a failure when the second is a failure`() = runTest {
val throwable = Throwable()
assertEquals(
throwable.asFailure(),
zip(
{ "A".asSuccess() },
{
@Suppress("USELESS_CAST")
throwable.asFailure() as Result<Int>
},
) { first, second ->
first to second
},
)
}
@Test
fun `zip with three arguments should return a success when all are successes`() = runTest {
assertEquals(
Triple("A", 1, true).asSuccess(),
zip(
{ "A".asSuccess() },
{ 1.asSuccess() },
{ true.asSuccess() },
) { first, second, third ->
Triple(first, second, third)
},
)
}
@Test
fun `zip with three arguments should return a failure when the first is a failure`() = runTest {
val throwable = Throwable()
assertEquals(
throwable.asFailure(),
zip(
{
@Suppress("USELESS_CAST")
throwable.asFailure() as Result<String>
},
{ 1.asSuccess() },
{ true.asSuccess() },
) { first, second, third ->
Triple(first, second, third)
},
)
}
@Test
fun `zip with three arguments should return a failure when the second is a failure`() =
runTest {
val throwable = Throwable()
assertEquals(
throwable.asFailure(),
zip(
{ "A".asSuccess() },
{
@Suppress("USELESS_CAST")
throwable.asFailure() as Result<Int>
},
{ true.asSuccess() },
) { first, second, third ->
Triple(first, second, third)
},
)
}
@Test
fun `zip with three arguments should return a failure when the third is a failure`() = runTest {
val throwable = Throwable()
assertEquals(
throwable.asFailure(),
zip(
{ "A".asSuccess() },
{ 1.asSuccess() },
{
@Suppress("USELESS_CAST")
throwable.asFailure() as Result<Boolean>
},
) { first, second, third ->
Triple(first, second, third)
},
)
}
}