mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Add zip helpers for Result and use in VaultRepository (#383)
This commit is contained in:
parent
f0bd9f54d6
commit
4d3899e6f9
3 changed files with 205 additions and 22 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue