fix: Better formatting of error messages from the API

This commit is contained in:
Artem Chepurnoy 2024-01-25 12:19:35 +02:00
parent 55fd6b246a
commit 553d40c45b
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
8 changed files with 82 additions and 28 deletions

View File

@ -1,15 +1,18 @@
package com.artemchep.keyguard.common.exception
import com.artemchep.keyguard.feature.localization.TextHolder
import com.artemchep.keyguard.provider.bitwarden.model.TwoFactorProviderArgument
import io.ktor.http.HttpStatusCode
class ApiException(
override val title: TextHolder,
override val text: TextHolder?,
val exception: Exception,
val code: HttpStatusCode,
val error: String,
val type: Type?,
message: String?,
) : HttpException(code, message, exception) {
) : HttpException(code, message, exception), Readable {
sealed interface Type {
data class CaptchaRequired(
val siteKey: String,

View File

@ -1,12 +1,12 @@
package com.artemchep.keyguard.common.exception
import com.artemchep.keyguard.feature.localization.TextHolder
import com.artemchep.keyguard.res.Res
import dev.icerock.moko.resources.StringResource
open class OutOfMemoryKdfException(
m: String?,
e: Throwable?,
) : Exception(m, e), Readable {
override val title: StringResource
get() = Res.strings.error_failed_generate_kdf_hash_oom
override val title: TextHolder
get() = TextHolder.Res(Res.strings.error_failed_generate_kdf_hash_oom)
}

View File

@ -1,13 +1,13 @@
package com.artemchep.keyguard.common.exception
import com.artemchep.keyguard.common.model.NoAnalytics
import com.artemchep.keyguard.feature.localization.TextHolder
import com.artemchep.keyguard.res.Res
import dev.icerock.moko.resources.StringResource
class PasswordMismatchException(
) : RuntimeException("Invalid password"),
Readable,
NoAnalytics {
override val title: StringResource
get() = Res.strings.error_incorrect_password
override val title: TextHolder
get() = TextHolder.Res(Res.strings.error_incorrect_password)
}

View File

@ -1,7 +1,8 @@
package com.artemchep.keyguard.common.exception
import dev.icerock.moko.resources.StringResource
import com.artemchep.keyguard.feature.localization.TextHolder
interface Readable {
val title: StringResource
val title: TextHolder
val text: TextHolder? get() = null
}

View File

@ -1908,7 +1908,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
val title = data.override.name
data.contentOrException.fold(
ifLeft = { e ->
val text = getErrorReadableMessage(
val parsedMessage = getErrorReadableMessage(
e,
translator = this,
)
@ -1935,7 +1935,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
style = MaterialTheme.typography.labelLarge,
)
Text(
text = text,
text = parsedMessage.title,
style = MaterialTheme.typography.labelMedium,
color = LocalContentColor.current
.combineAlpha(MediumEmphasisAlpha),
@ -1945,7 +1945,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
}
VaultViewItem.Uri.Override(
title = title,
text = text,
text = parsedMessage.title,
error = true,
dropdown = dropdown,
)

View File

@ -7,6 +7,7 @@ import com.artemchep.keyguard.common.io.attempt
import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.util.flow.EventFlow
import com.artemchep.keyguard.feature.navigation.state.TranslatorScope
import com.artemchep.keyguard.feature.navigation.state.translate
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@ -21,7 +22,7 @@ class LoadingTask(
* Exception handler that's responsible for parsing the
* error messages as user-readable messages.
*/
private val exceptionHandler: (Throwable) -> String = { e ->
private val exceptionHandler: (Throwable) -> ReadableExceptionMessage = { e ->
getErrorReadableMessage(e, translator)
},
) {
@ -31,11 +32,12 @@ class LoadingTask(
val isExecutingFlow get() = isWorkingSink
val errorFlow: Flow<String> = errorSink.map { it.text }
val errorFlow: Flow<Failure> = errorSink
data class Failure(
val tag: String?,
val text: String,
val title: String,
val text: String?,
)
/**
@ -54,9 +56,11 @@ class LoadingTask(
try {
val result = io.attempt().bind()
if (result is Either.Left<Throwable>) {
val parsedMessage = exceptionHandler(result.value)
val message = Failure(
tag = tag,
text = exceptionHandler(result.value),
title = parsedMessage.title,
text = parsedMessage.text,
)
result.value.printStackTrace()
errorSink.emit(message)
@ -73,8 +77,26 @@ class LoadingTask(
}
}
data class ReadableExceptionMessage(
val title: String,
val text: String? = null,
)
fun getErrorReadableMessage(e: Throwable, translator: TranslatorScope) =
when (e) {
is Readable -> translator.translate(e.title)
else -> e.message.orEmpty()
is Readable -> {
val title = e.title.let(translator::translate)
val text = e.text?.let(translator::translate)
ReadableExceptionMessage(
title = title,
text = text,
)
}
else -> {
val title = e.message.orEmpty()
ReadableExceptionMessage(
title = title,
)
}
}

View File

@ -118,10 +118,11 @@ class RememberStateFlowScopeImpl(
override fun message(
exception: Throwable,
) {
val title = getErrorReadableMessage(exception, this)
val parsedMessage = getErrorReadableMessage(exception, this)
val message = ToastMessage(
type = ToastMessage.Type.ERROR,
title = title,
title = parsedMessage.title,
text = parsedMessage.text,
)
message(message)
}
@ -144,7 +145,8 @@ class RememberStateFlowScopeImpl(
.errorFlow
.onEach { message ->
val model = ToastMessage(
title = message,
title = message.title,
text = message.text,
type = ToastMessage.Type.ERROR,
)
message(model)

View File

@ -2,6 +2,7 @@ package com.artemchep.keyguard.provider.bitwarden.entity
import com.artemchep.keyguard.common.exception.ApiException
import com.artemchep.keyguard.common.service.state.impl.toMap
import com.artemchep.keyguard.feature.localization.TextHolder
import com.artemchep.keyguard.provider.bitwarden.model.TwoFactorProviderArgument
import com.artemchep.keyguard.provider.bitwarden.model.TwoFactorProviderType
import com.artemchep.keyguard.provider.bitwarden.model.toObj
@ -196,14 +197,31 @@ fun ErrorEntity.toException(
}
// Auto-format the validation error
// messages to something user-friendly.
val validationError = validationErrors?.toMap()?.format()
val errorTitle = kotlin.run {
errorModel?.message?.takeIf { it.isNotBlank() }
?: error.takeIf { it.isNotBlank() }
// We usually should get an error message, but
// just in case resort to the basic message.
?: code.description
}.trim()
val errorText = kotlin.run {
val validation = validationErrors?.toMap()?.format()?.takeIf { it.isNotBlank() }
val message = listOfNotNull(
errorDescription?.takeIf { it.isNotBlank() },
validation,
)
.joinToString(separator = "\n")
.takeIf { it.isNotBlank() }
?.trim()
message?.takeIf { it != errorTitle }
}
val message = listOfNotNull(
errorModel?.message,
errorDescription,
error,
validationError
errorTitle,
errorText,
).joinToString(separator = "\n")
ApiException(
title = errorTitle.let(TextHolder::Value),
text = errorText?.let(TextHolder::Value),
exception = exception,
code = code,
error = error,
@ -222,10 +240,18 @@ private fun Any?.format(): String {
.joinToString(
separator = "\n\n",
) { entry ->
"${entry.key}:\n${entry.value.format()}"
val formattedValue = entry.value
.format()
if (entry.key.isBlank()) {
formattedValue
} else {
"${entry.key}:\n$formattedValue"
}
}
is Collection<*> -> this
is Collection<*> -> if (this.size == 1) {
this.first().format()
} else this
.joinToString(separator = "\n") { value ->
"- " + value.format()
}