mirror of
https://github.com/openflocon/Flocon.git
synced 2026-05-19 15:56:36 +00:00
Feat badnetwork exeptions (#122)
* feat: [BADNETWORK] exceptions * added errors on okhttp * added errors on okhttp * feat: [BADNETWORK] exceptions * feat: [BADNETWORK] exceptions --------- Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
parent
cae29db95c
commit
db3c8a118c
13 changed files with 491 additions and 191 deletions
|
|
@ -12,8 +12,17 @@ data class BadQualityConfig(
|
|||
)
|
||||
class Error(
|
||||
val weight: Float, // increase the probability of being triggered vs all others errors
|
||||
val errorCode: Int,
|
||||
val errorBody: String,
|
||||
val errorContentType: String, // "application/json"
|
||||
)
|
||||
val type: Type,
|
||||
) {
|
||||
sealed interface Type {
|
||||
data class Body(
|
||||
val errorCode: Int,
|
||||
val errorBody: String,
|
||||
val errorContentType: String, // "application/json"
|
||||
) : Type
|
||||
data class ErrorThrow(
|
||||
val classPath: String,
|
||||
) : Type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,16 @@ fun toJsonObject(config: BadQualityConfig): JSONObject {
|
|||
config.errors.forEach { error ->
|
||||
val errorObject = JSONObject()
|
||||
errorObject.put("weight", error.weight)
|
||||
errorObject.put("errorCode", error.errorCode)
|
||||
errorObject.put("errorBody", error.errorBody)
|
||||
errorObject.put("errorContentType", error.errorContentType)
|
||||
when(val t = error.type) {
|
||||
is BadQualityConfig.Error.Type.Body -> {
|
||||
errorObject.put("errorCode", t.errorCode)
|
||||
errorObject.put("errorBody", t.errorBody)
|
||||
errorObject.put("errorContentType", t.errorContentType)
|
||||
}
|
||||
is BadQualityConfig.Error.Type.ErrorThrow -> {
|
||||
errorObject.put("errorException", t.classPath)
|
||||
}
|
||||
}
|
||||
errorsArray.put(errorObject)
|
||||
}
|
||||
jsonObject.put("errors", errorsArray)
|
||||
|
|
@ -54,13 +61,27 @@ fun parseBadQualityConfig(jsonString: String): BadQualityConfig? {
|
|||
val errorsList = mutableListOf<BadQualityConfig.Error>()
|
||||
for (i in 0 until errorsArray.length()) {
|
||||
val errorObject = errorsArray.getJSONObject(i)
|
||||
val error = BadQualityConfig.Error(
|
||||
weight = errorObject.getDouble("weight").toFloat(),
|
||||
errorCode = errorObject.getInt("errorCode"),
|
||||
errorBody = errorObject.getString("errorBody"),
|
||||
errorContentType = errorObject.getString("errorContentType")
|
||||
)
|
||||
errorsList.add(error)
|
||||
try {
|
||||
val errorException = errorObject.optString("errorException")
|
||||
val errorCode = errorObject.optInt("errorCode")
|
||||
val errorBody = errorObject.optString("errorBody")
|
||||
val errorContentType = errorObject.optString("errorContentType")
|
||||
val error = BadQualityConfig.Error(
|
||||
weight = errorObject.getDouble("weight").toFloat(),
|
||||
type = if (errorException.isNotEmpty()) {
|
||||
BadQualityConfig.Error.Type.ErrorThrow(errorException)
|
||||
} else {
|
||||
BadQualityConfig.Error.Type.Body(
|
||||
errorCode = errorCode,
|
||||
errorBody = errorBody,
|
||||
errorContentType = errorContentType
|
||||
)
|
||||
}
|
||||
)
|
||||
errorsList.add(error)
|
||||
} catch (t: Throwable) {
|
||||
FloconLogger.logError(t.message ?: "bad connection network parsing issue", t)
|
||||
}
|
||||
}
|
||||
|
||||
BadQualityConfig(
|
||||
|
|
|
|||
|
|
@ -84,80 +84,112 @@ class FloconOkhttpInterceptor() : Interceptor {
|
|||
)
|
||||
)
|
||||
|
||||
val response = if(isMocked) {
|
||||
executeMock(request = request, mock = mockConfig)
|
||||
} else {
|
||||
val badQualityConfig = floconNetworkPlugin.badQualityConfig // Supposons que cette propriété existe
|
||||
|
||||
if (badQualityConfig != null) {
|
||||
val latencyProbability = badQualityConfig.latency.latencyTriggerProbability
|
||||
val shouldSimulateLatency = latencyProbability > 0f && (latencyProbability == 1f || Math.random() < latencyProbability)
|
||||
if (shouldSimulateLatency) {
|
||||
try {
|
||||
val latencyMs = Random.nextLong(badQualityConfig.latency.minLatencyMs, badQualityConfig.latency.maxLatencyMs + 1)
|
||||
Thread.sleep(latencyMs)
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
failResponseIfNeeded(
|
||||
badQualityConfig = badQualityConfig,
|
||||
request = request,
|
||||
) ?: chain.proceed(request) // if no need to trigger an error
|
||||
try {
|
||||
val response = if (isMocked) {
|
||||
executeMock(request = request, mock = mockConfig)
|
||||
} else {
|
||||
chain.proceed(request)
|
||||
val badQualityConfig =
|
||||
floconNetworkPlugin.badQualityConfig // Supposons que cette propriété existe
|
||||
|
||||
if (badQualityConfig != null) {
|
||||
val latencyProbability = badQualityConfig.latency.latencyTriggerProbability
|
||||
val shouldSimulateLatency =
|
||||
latencyProbability > 0f && (latencyProbability == 1f || Math.random() < latencyProbability)
|
||||
if (shouldSimulateLatency) {
|
||||
try {
|
||||
val latencyMs = Random.nextLong(
|
||||
badQualityConfig.latency.minLatencyMs,
|
||||
badQualityConfig.latency.maxLatencyMs + 1
|
||||
)
|
||||
Thread.sleep(latencyMs)
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
failResponseIfNeeded(
|
||||
badQualityConfig = badQualityConfig,
|
||||
request = request,
|
||||
) ?: chain.proceed(request) // if no need to trigger an error
|
||||
} else {
|
||||
chain.proceed(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val endTime = System.nanoTime()
|
||||
val endTime = System.nanoTime()
|
||||
|
||||
val durationMs: Double = (endTime - startTime) / 1e6
|
||||
val durationMs: Double = (endTime - startTime) / 1e6
|
||||
|
||||
// To get the response body, be careful
|
||||
// because the body can only be read once.
|
||||
// It must be duplicated so that the chain can continue normally.
|
||||
val responseBody = response.body
|
||||
var responseBodyString: String? = null
|
||||
var responseSize: Long? = null
|
||||
val responseContentType: MediaType? = responseBody?.contentType()
|
||||
// To get the response body, be careful
|
||||
// because the body can only be read once.
|
||||
// It must be duplicated so that the chain can continue normally.
|
||||
val responseBody = response.body
|
||||
var responseBodyString: String? = null
|
||||
var responseSize: Long? = null
|
||||
val responseContentType: MediaType? = responseBody?.contentType()
|
||||
|
||||
if (responseBody != null) {
|
||||
// Use response.peekBody() to safely read the body without consuming it
|
||||
// Note: peekBody has a max size, adjust as needed.
|
||||
responseBodyString = response.peekBody(Long.MAX_VALUE).string() // Reads the body safely
|
||||
responseSize = responseBody.contentLength().let {
|
||||
if (it != -1L) it else responseBodyString?.toByteArray(StandardCharsets.UTF_8)?.size?.toLong()
|
||||
if (responseBody != null) {
|
||||
// Use response.peekBody() to safely read the body without consuming it
|
||||
// Note: peekBody has a max size, adjust as needed.
|
||||
responseBodyString =
|
||||
response.peekBody(Long.MAX_VALUE).string() // Reads the body safely
|
||||
responseSize = responseBody.contentLength().let {
|
||||
if (it != -1L) it else responseBodyString?.toByteArray(StandardCharsets.UTF_8)?.size?.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
val responseHeadersMap =
|
||||
response.headers.toMultimap().mapValues { it.value.joinToString(",") }
|
||||
val responseHeadersMap =
|
||||
response.headers.toMultimap().mapValues { it.value.joinToString(",") }
|
||||
|
||||
val isImage = responseContentType?.toString()?.startsWith("image/") == true
|
||||
val isImage = responseContentType?.toString()?.startsWith("image/") == true
|
||||
|
||||
val floconCallResponse = FloconNetworkResponse(
|
||||
httpCode = response.code,
|
||||
contentType = responseContentType?.toString(),
|
||||
body = responseBodyString.takeUnless { isImage }, // dont send images responses bytes
|
||||
headers = responseHeadersMap,
|
||||
size = responseSize,
|
||||
grpcStatus = null,
|
||||
)
|
||||
|
||||
floconNetworkPlugin.logResponse(
|
||||
FloconNetworkCallResponse(
|
||||
floconCallId = floconCallId,
|
||||
durationMs = durationMs,
|
||||
floconNetworkType = floconNetworkType,
|
||||
isMocked = isMocked,
|
||||
response = floconCallResponse,
|
||||
val floconCallResponse = FloconNetworkResponse(
|
||||
httpCode = response.code,
|
||||
contentType = responseContentType?.toString(),
|
||||
body = responseBodyString.takeUnless { isImage }, // dont send images responses bytes
|
||||
headers = responseHeadersMap,
|
||||
size = responseSize,
|
||||
grpcStatus = null,
|
||||
)
|
||||
)
|
||||
|
||||
// Rebuild the response with a new body so that the chain can continue
|
||||
// The original response body is already consumed by peekBody, so no need to rebuild with it.
|
||||
// Just return the original response if you don't modify the body itself.
|
||||
return response
|
||||
floconNetworkPlugin.logResponse(
|
||||
FloconNetworkCallResponse(
|
||||
floconCallId = floconCallId,
|
||||
durationMs = durationMs,
|
||||
floconNetworkType = floconNetworkType,
|
||||
isMocked = isMocked,
|
||||
response = floconCallResponse,
|
||||
)
|
||||
)
|
||||
|
||||
// Rebuild the response with a new body so that the chain can continue
|
||||
// The original response body is already consumed by peekBody, so no need to rebuild with it.
|
||||
// Just return the original response if you don't modify the body itself.
|
||||
return response
|
||||
} catch (e: IOException) {
|
||||
val endTime = System.nanoTime()
|
||||
|
||||
val durationMs: Double = (endTime - startTime) / 1e6
|
||||
|
||||
val floconCallResponse = FloconNetworkResponse(
|
||||
httpCode = 0,
|
||||
contentType = "error",
|
||||
body = e.message, // TODO better handle of errors
|
||||
headers = emptyMap(),
|
||||
size = null,
|
||||
grpcStatus = null,
|
||||
)
|
||||
|
||||
floconNetworkPlugin.logResponse(
|
||||
FloconNetworkCallResponse(
|
||||
floconCallId = floconCallId,
|
||||
durationMs = durationMs,
|
||||
floconNetworkType = floconNetworkType,
|
||||
isMocked = isMocked,
|
||||
response = floconCallResponse,
|
||||
)
|
||||
)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun failResponseIfNeeded(
|
||||
|
|
@ -168,15 +200,28 @@ class FloconOkhttpInterceptor() : Interceptor {
|
|||
if (shouldFail) {
|
||||
val selectedError = selectRandomError(badQualityConfig.errors)
|
||||
if (selectedError != null) {
|
||||
val errorBody = selectedError.errorBody.toResponseBody(selectedError.errorContentType.toMediaTypeOrNull())
|
||||
when(val t = selectedError.type) {
|
||||
is BadQualityConfig.Error.Type.Body -> {
|
||||
val errorBody = t.errorBody.toResponseBody(t.errorContentType.toMediaTypeOrNull())
|
||||
|
||||
return Response.Builder()
|
||||
.request(request)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(selectedError.errorCode) // Utiliser le code d'erreur configuré
|
||||
.message(getHttpMessage(selectedError.errorCode))
|
||||
.body(errorBody)
|
||||
.build()
|
||||
return Response.Builder()
|
||||
.request(request)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(t.errorCode) // Utiliser le code d'erreur configuré
|
||||
.message(getHttpMessage(t.errorCode))
|
||||
.body(errorBody)
|
||||
.build()
|
||||
}
|
||||
is BadQualityConfig.Error.Type.ErrorThrow -> {
|
||||
val errorClass = Class.forName(t.classPath)
|
||||
val error = errorClass.newInstance() as? Throwable
|
||||
if(error is IOException) {
|
||||
throw error //okhttp accepts only IOException
|
||||
} else if(error is Throwable){
|
||||
throw IOException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -18,9 +18,16 @@ fun BadQualityConfigDomainModel.toUi() = BadQualityConfigUiModel(
|
|||
|
||||
private fun BadQualityConfigDomainModel.Error.toUi() = BadQualityConfigUiModel.Error(
|
||||
weight = weight,
|
||||
httpCode = httpCode,
|
||||
body = body,
|
||||
contentType = contentType,
|
||||
type = when(val t = type) {
|
||||
is BadQualityConfigDomainModel.Error.Type.Body -> BadQualityConfigUiModel.Error.Type.Body(
|
||||
httpCode = t.httpCode,
|
||||
body = t.body,
|
||||
contentType = t.contentType,
|
||||
)
|
||||
is BadQualityConfigDomainModel.Error.Type.Exception -> BadQualityConfigUiModel.Error.Type.Exception(
|
||||
classPath = t.classPath,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private fun BadQualityConfigDomainModel.LatencyConfig.toUi() = BadQualityConfigUiModel.LatencyConfig(
|
||||
|
|
@ -43,9 +50,16 @@ fun BadQualityConfigUiModel.toDomain() = BadQualityConfigDomainModel(
|
|||
|
||||
private fun BadQualityConfigUiModel.Error.toDomain() = BadQualityConfigDomainModel.Error(
|
||||
weight = weight,
|
||||
httpCode = httpCode,
|
||||
body = body,
|
||||
contentType = contentType,
|
||||
type = when(val t = type) {
|
||||
is BadQualityConfigUiModel.Error.Type.Body -> BadQualityConfigDomainModel.Error.Type.Body(
|
||||
httpCode = t.httpCode,
|
||||
body = t.body,
|
||||
contentType = t.contentType,
|
||||
)
|
||||
is BadQualityConfigUiModel.Error.Type.Exception -> BadQualityConfigDomainModel.Error.Type.Exception(
|
||||
classPath = t.classPath,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private fun BadQualityConfigUiModel.LatencyConfig.toDomain() = BadQualityConfigDomainModel.LatencyConfig(
|
||||
|
|
|
|||
|
|
@ -19,10 +19,19 @@ data class BadQualityConfigUiModel(
|
|||
data class Error(
|
||||
val uuid: String = UUID.randomUUID().toString(),
|
||||
val weight: Float, // increase the probability of being triggered vs all others errors
|
||||
val httpCode: Int,
|
||||
val body: String,
|
||||
val contentType: String, // "application/json"
|
||||
)
|
||||
val type: Type,
|
||||
) {
|
||||
sealed interface Type {
|
||||
data class Body(
|
||||
val httpCode: Int,
|
||||
val body: String,
|
||||
val contentType: String, // "application/json"
|
||||
) : Type
|
||||
data class Exception(
|
||||
val classPath: String,
|
||||
) : Type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun previewBadQualityConfigUiModel(errorCount: Int) = BadQualityConfigUiModel(
|
||||
|
|
@ -39,9 +48,11 @@ fun previewBadQualityConfigUiModel(errorCount: Int) = BadQualityConfigUiModel(
|
|||
errors = List(errorCount) {
|
||||
BadQualityConfigUiModel.Error(
|
||||
weight = 1f,
|
||||
httpCode = 500,
|
||||
body = "{\"error\":\"...\"}",
|
||||
contentType = "application/json",
|
||||
type = BadQualityConfigUiModel.Error.Type.Body(
|
||||
httpCode = 500,
|
||||
body = "{\"error\":\"...\"}",
|
||||
contentType = "application/json",
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package io.github.openflocon.flocondesktop.features.network.badquality.edition.model
|
||||
|
||||
data class PossibleExceptionUiModel(
|
||||
val classPath : String,
|
||||
val description: String,
|
||||
)
|
||||
|
||||
// warning : be sure it's only IOExceptions
|
||||
val possibleExceptions = listOf(
|
||||
PossibleExceptionUiModel(
|
||||
"java.net.SocketTimeoutException",
|
||||
"The network operation timed out (connection, read, or write)."
|
||||
),
|
||||
PossibleExceptionUiModel(
|
||||
"java.net.UnknownHostException",
|
||||
"Unable to resolve the host name or server address."
|
||||
),
|
||||
PossibleExceptionUiModel(
|
||||
"java.net.ConnectException",
|
||||
"Connection to the server was refused or couldn't be established."
|
||||
),
|
||||
PossibleExceptionUiModel(
|
||||
"javax.net.ssl.SSLHandshakeException",
|
||||
"Failed to complete the TLS/SSL handshake (often a certificate issue)."
|
||||
),
|
||||
PossibleExceptionUiModel(
|
||||
"java.io.IOException",
|
||||
"A generic I/O error; an unspecified network problem."
|
||||
)
|
||||
)
|
||||
|
|
@ -35,8 +35,14 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import io.github.openflocon.flocondesktop.features.network.badquality.edition.model.BadQualityConfigUiModel
|
||||
import io.github.openflocon.flocondesktop.features.network.badquality.edition.model.SelectedBadQualityUiModel
|
||||
import io.github.openflocon.flocondesktop.features.network.badquality.edition.model.possibleExceptions
|
||||
import io.github.openflocon.flocondesktop.features.network.list.view.components.getMethodBackground
|
||||
import io.github.openflocon.flocondesktop.features.network.list.view.components.getMethodText
|
||||
import io.github.openflocon.flocondesktop.features.network.list.view.components.postMethodBackground
|
||||
import io.github.openflocon.flocondesktop.features.network.list.view.components.postMethodText
|
||||
import io.github.openflocon.flocondesktop.features.network.mock.edition.view.NetworkMockFieldView
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconButton
|
||||
|
|
@ -179,21 +185,45 @@ fun BadNetworkQualityEditionContent(
|
|||
},
|
||||
) {
|
||||
FloconSurface {
|
||||
ErrorsEditor(
|
||||
error = selectedError,
|
||||
onErrorsChange = { error ->
|
||||
errors = if (errors.any { it.uuid == selectedError.uuid }) {
|
||||
errors.map {
|
||||
if (it.uuid == selectedError.uuid) {
|
||||
error
|
||||
} else it
|
||||
}
|
||||
} else {
|
||||
errors + error
|
||||
}
|
||||
selectedErrorToEdit = null
|
||||
},
|
||||
)
|
||||
when (val t = selectedError.type) {
|
||||
is BadQualityConfigUiModel.Error.Type.Body -> {
|
||||
ErrorsEditor(
|
||||
error = selectedError,
|
||||
httpType = t,
|
||||
onErrorsChange = { error ->
|
||||
errors = if (errors.any { it.uuid == selectedError.uuid }) {
|
||||
errors.map {
|
||||
if (it.uuid == selectedError.uuid) {
|
||||
error
|
||||
} else it
|
||||
}
|
||||
} else {
|
||||
errors + error
|
||||
}
|
||||
selectedErrorToEdit = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is BadQualityConfigUiModel.Error.Type.Exception -> {
|
||||
ErrorExceptionEditor(
|
||||
error = selectedError,
|
||||
errorException = t,
|
||||
onErrorsChange = { error ->
|
||||
errors = if (errors.any { it.uuid == selectedError.uuid }) {
|
||||
errors.map {
|
||||
if (it.uuid == selectedError.uuid) {
|
||||
error
|
||||
} else it
|
||||
}
|
||||
} else {
|
||||
errors + error
|
||||
}
|
||||
selectedErrorToEdit = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -209,7 +239,8 @@ fun BadNetworkQualityEditionContent(
|
|||
id = config?.id ?: UUID.randomUUID().toString(), // generate a new one
|
||||
name = name,
|
||||
isEnabled = config?.isEnabled ?: false, // disabled by default
|
||||
createdAt = config?.createdAt ?: System.currentTimeMillis(), // generate a new date
|
||||
createdAt = config?.createdAt
|
||||
?: System.currentTimeMillis(), // generate a new date
|
||||
latency = BadQualityConfigUiModel.LatencyConfig(
|
||||
triggerProbability = triggerProbability
|
||||
.toDoubleOrNull()
|
||||
|
|
@ -257,15 +288,34 @@ fun ErrorsListView(
|
|||
BadQualityConfigUiModel.Error(
|
||||
// new error
|
||||
weight = 1f,
|
||||
httpCode = 500,
|
||||
body = "",
|
||||
contentType = "application/json",
|
||||
type = BadQualityConfigUiModel.Error.Type.Body(
|
||||
httpCode = 500,
|
||||
body = "{\"error\":\"...\"}",
|
||||
contentType = "application/json",
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = "Add",
|
||||
text = "Add http error",
|
||||
)
|
||||
}
|
||||
FloconButton(
|
||||
onClick = {
|
||||
onErrorsClicked(
|
||||
BadQualityConfigUiModel.Error(
|
||||
// new error
|
||||
weight = 1f,
|
||||
type = BadQualityConfigUiModel.Error.Type.Exception(
|
||||
classPath = possibleExceptions.first().classPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = "Add Exception",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -290,9 +340,24 @@ fun ErrorsListView(
|
|||
)
|
||||
|
||||
Text("Weight : ${error.weight}", style = textStyle)
|
||||
Text("HttpCode : ${error.httpCode}", style = textStyle) // or throwable ?
|
||||
Text(error.contentType, style = textStyle)
|
||||
Text("Body : ${error.body}", maxLines = 2, style = textStyle)
|
||||
when (val t = error.type) {
|
||||
is BadQualityConfigUiModel.Error.Type.Body -> {
|
||||
Text("HttpCode : ${t.httpCode}", style = textStyle) // or throwable ?
|
||||
Text(t.contentType, style = textStyle)
|
||||
Text("Body : ${t.body}", maxLines = 2, style = textStyle)
|
||||
}
|
||||
|
||||
is BadQualityConfigUiModel.Error.Type.Exception -> {
|
||||
Text(
|
||||
"Exception",
|
||||
style = textStyle
|
||||
)
|
||||
Text(
|
||||
t.classPath,
|
||||
style = textStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// bouton supprimer
|
||||
Row(
|
||||
|
|
@ -318,15 +383,70 @@ fun ErrorsListView(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorExceptionEditor(
|
||||
error: BadQualityConfigUiModel.Error,
|
||||
errorException: BadQualityConfigUiModel.Error.Type.Exception,
|
||||
onErrorsChange: (BadQualityConfigUiModel.Error) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
possibleExceptions.fastForEach { exception ->
|
||||
val (backgroundColor, textColor) = if (exception.classPath == errorException.classPath) {
|
||||
FloconTheme.colorPalette.onSurface to FloconTheme.colorPalette.panel
|
||||
} else {
|
||||
FloconTheme.colorPalette.panel to FloconTheme.colorPalette.onSurface
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
)
|
||||
.then(
|
||||
Modifier.clickable(
|
||||
onClick = {
|
||||
onErrorsChange(
|
||||
error.copy(
|
||||
weight = 1f,
|
||||
type = errorException.copy(
|
||||
classPath = exception.classPath
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
).padding(all = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = exception.description,
|
||||
style = FloconTheme.typography.bodySmall,
|
||||
color = textColor,
|
||||
)
|
||||
Text(
|
||||
text = exception.classPath,
|
||||
style = FloconTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorsEditor(
|
||||
error: BadQualityConfigUiModel.Error,
|
||||
httpType: BadQualityConfigUiModel.Error.Type.Body,
|
||||
onErrorsChange: (BadQualityConfigUiModel.Error) -> Unit,
|
||||
) {
|
||||
var weight by remember(error) { mutableStateOf<String>(error.weight.toString()) }
|
||||
var httpCode by remember(error) { mutableStateOf<String>(error.httpCode.toString()) }
|
||||
var contentType by remember { mutableStateOf<String>(error.contentType) }
|
||||
var body by remember(error) { mutableStateOf<String>(error.body) }
|
||||
var httpCode by remember(error) { mutableStateOf<String>(httpType.httpCode.toString()) }
|
||||
var contentType by remember { mutableStateOf<String>(httpType.contentType) }
|
||||
var body by remember(error) { mutableStateOf<String>(httpType.body) }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// bouton ajouter
|
||||
|
|
@ -338,9 +458,11 @@ fun ErrorsEditor(
|
|||
onErrorsChange(
|
||||
error.copy(
|
||||
weight = weight.toFloatOrNull() ?: error.weight,
|
||||
httpCode = httpCode.toIntOrNull() ?: error.httpCode,
|
||||
contentType = contentType,
|
||||
body = body,
|
||||
type = httpType.copy(
|
||||
httpCode = httpCode.toIntOrNull() ?: httpType.httpCode,
|
||||
contentType = contentType,
|
||||
body = body,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,8 @@ class BadQualityConfigLocalDataSourceImpl(
|
|||
config: BadQualityConfigDomainModel
|
||||
) {
|
||||
networkBadQualityConfigDao.save(
|
||||
toEntity(
|
||||
config.toEntity(
|
||||
json = json,
|
||||
config = config,
|
||||
deviceIdAndPackageName = deviceIdAndPackageName
|
||||
)
|
||||
)
|
||||
|
|
@ -39,9 +38,8 @@ class BadQualityConfigLocalDataSourceImpl(
|
|||
packageName = deviceIdAndPackageName.packageName,
|
||||
configId = configId
|
||||
)?.let {
|
||||
toDomain(
|
||||
json = json,
|
||||
entity = it
|
||||
it.toDomain(
|
||||
json = json
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,9 +54,8 @@ class BadQualityConfigLocalDataSourceImpl(
|
|||
configId = configId,
|
||||
).map {
|
||||
it?.let {
|
||||
toDomain(
|
||||
json = json,
|
||||
entity = it
|
||||
it.toDomain(
|
||||
json = json
|
||||
)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
|
@ -73,9 +70,8 @@ class BadQualityConfigLocalDataSourceImpl(
|
|||
packageName = deviceIdAndPackageName.packageName,
|
||||
).map { list ->
|
||||
list.map {
|
||||
toDomain(
|
||||
json = json,
|
||||
entity = it
|
||||
it.toDomain(
|
||||
json = json
|
||||
)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
|
@ -99,9 +95,8 @@ class BadQualityConfigLocalDataSourceImpl(
|
|||
deviceId = deviceIdAndPackageName.deviceId,
|
||||
packageName = deviceIdAndPackageName.packageName,
|
||||
)?.let {
|
||||
toDomain(
|
||||
json = json,
|
||||
entity = it
|
||||
it.toDomain(
|
||||
json = json
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,46 +8,57 @@ import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel
|
|||
import kotlinx.serialization.json.Json
|
||||
import kotlin.time.Instant
|
||||
|
||||
fun toDomain(json: Json, entity: BadQualityConfigEntity): BadQualityConfigDomainModel {
|
||||
fun BadQualityConfigEntity.toDomain(json: Json): BadQualityConfigDomainModel {
|
||||
val errors = try {
|
||||
json.decodeFromString<List<ErrorEmbedded>>(entity.errors)
|
||||
.map { toDomain(it) }
|
||||
json.decodeFromString<List<ErrorEmbedded>>(errors)
|
||||
.map { it.toDomain() }
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
return BadQualityConfigDomainModel(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
createdAt = Instant.fromEpochMilliseconds(entity.createdAt),
|
||||
isEnabled = entity.isEnabled,
|
||||
id = id,
|
||||
name = name,
|
||||
createdAt = Instant.fromEpochMilliseconds(createdAt),
|
||||
isEnabled = isEnabled,
|
||||
latency = BadQualityConfigDomainModel.LatencyConfig(
|
||||
triggerProbability = entity.latency.triggerProbability,
|
||||
minLatencyMs = entity.latency.minLatencyMs,
|
||||
maxLatencyMs = entity.latency.maxLatencyMs,
|
||||
triggerProbability = latency.triggerProbability,
|
||||
minLatencyMs = latency.minLatencyMs,
|
||||
maxLatencyMs = latency.maxLatencyMs,
|
||||
),
|
||||
errorProbability = entity.errorProbability,
|
||||
errorProbability = errorProbability,
|
||||
errors = errors,
|
||||
)
|
||||
}
|
||||
|
||||
fun toDomain(error: ErrorEmbedded): BadQualityConfigDomainModel.Error {
|
||||
fun ErrorEmbedded.toDomain(): BadQualityConfigDomainModel.Error {
|
||||
return BadQualityConfigDomainModel.Error(
|
||||
weight = error.weight,
|
||||
httpCode = error.httpCode,
|
||||
body = error.body,
|
||||
contentType = error.contentType,
|
||||
weight = weight,
|
||||
type = type.toDomain(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun ErrorEmbedded.Type.toDomain(): BadQualityConfigDomainModel.Error.Type {
|
||||
return when (this) {
|
||||
is ErrorEmbedded.Type.Body -> BadQualityConfigDomainModel.Error.Type.Body(
|
||||
httpCode = httpCode,
|
||||
body = body,
|
||||
contentType = contentType,
|
||||
)
|
||||
|
||||
fun toEntity(
|
||||
is ErrorEmbedded.Type.Exception -> BadQualityConfigDomainModel.Error.Type.Exception(
|
||||
classPath = classPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun BadQualityConfigDomainModel.toEntity(
|
||||
json: Json,
|
||||
config: BadQualityConfigDomainModel,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel
|
||||
): BadQualityConfigEntity {
|
||||
val errorsEmbedded = config.errors.map {
|
||||
toEntity(it)
|
||||
val errorsEmbedded = errors.map {
|
||||
it.toEntity()
|
||||
}
|
||||
val errors = try {
|
||||
json.encodeToString<List<ErrorEmbedded>>(errorsEmbedded)
|
||||
|
|
@ -56,25 +67,35 @@ fun toEntity(
|
|||
"[]"
|
||||
}
|
||||
return BadQualityConfigEntity(
|
||||
id = config.id,
|
||||
name = config.name,
|
||||
createdAt = config.createdAt.toEpochMilliseconds(),
|
||||
id = id,
|
||||
name = name,
|
||||
createdAt = createdAt.toEpochMilliseconds(),
|
||||
deviceId = deviceIdAndPackageName.deviceId,
|
||||
packageName = deviceIdAndPackageName.packageName,
|
||||
isEnabled = config.isEnabled,
|
||||
isEnabled = isEnabled,
|
||||
latency = LatencyConfigEmbedded(
|
||||
triggerProbability = config.latency.triggerProbability,
|
||||
minLatencyMs = config.latency.minLatencyMs,
|
||||
maxLatencyMs = config.latency.maxLatencyMs,
|
||||
triggerProbability = latency.triggerProbability,
|
||||
minLatencyMs = latency.minLatencyMs,
|
||||
maxLatencyMs = latency.maxLatencyMs,
|
||||
),
|
||||
errorProbability = config.errorProbability,
|
||||
errorProbability = errorProbability,
|
||||
errors = errors,
|
||||
)
|
||||
}
|
||||
|
||||
private fun toEntity(error: BadQualityConfigDomainModel.Error): ErrorEmbedded = ErrorEmbedded(
|
||||
weight = error.weight,
|
||||
httpCode = error.httpCode,
|
||||
body = error.body,
|
||||
contentType = error.contentType,
|
||||
private fun BadQualityConfigDomainModel.Error.toEntity(): ErrorEmbedded = ErrorEmbedded(
|
||||
weight = weight,
|
||||
type = this.type.toEntity(),
|
||||
)
|
||||
|
||||
private fun BadQualityConfigDomainModel.Error.Type.toEntity() = when (this) {
|
||||
is BadQualityConfigDomainModel.Error.Type.Body -> ErrorEmbedded.Type.Body(
|
||||
httpCode = httpCode,
|
||||
body = body,
|
||||
contentType = contentType,
|
||||
)
|
||||
|
||||
is BadQualityConfigDomainModel.Error.Type.Exception -> ErrorEmbedded.Type.Exception(
|
||||
classPath = classPath,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,19 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class ErrorEmbedded(
|
||||
val weight: Float,
|
||||
val httpCode: Int,
|
||||
val body: String,
|
||||
val contentType: String,
|
||||
)
|
||||
val type: Type,
|
||||
) {
|
||||
@Serializable
|
||||
sealed interface Type {
|
||||
@Serializable
|
||||
data class Body(
|
||||
val httpCode: Int,
|
||||
val body: String,
|
||||
val contentType: String,
|
||||
) : Type
|
||||
@Serializable
|
||||
data class Exception(
|
||||
val classPath: String,
|
||||
) : Type
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,11 +134,21 @@ fun toRemote(domain: BadQualityConfigDomainModel): BadQualityConfigDataModel = B
|
|||
},
|
||||
errorProbability = domain.errorProbability,
|
||||
errors = domain.errors.map {
|
||||
BadQualityConfigDataModel.Error(
|
||||
weight = it.weight,
|
||||
errorCode = it.httpCode,
|
||||
errorBody = it.body,
|
||||
errorContentType = it.contentType,
|
||||
)
|
||||
when(val t = it.type) {
|
||||
is BadQualityConfigDomainModel.Error.Type.Body -> BadQualityConfigDataModel.Error(
|
||||
weight = it.weight,
|
||||
errorCode = t.httpCode,
|
||||
errorBody = t.body,
|
||||
errorContentType = t.contentType,
|
||||
errorException = null,
|
||||
)
|
||||
is BadQualityConfigDomainModel.Error.Type.Exception -> BadQualityConfigDataModel.Error(
|
||||
weight = it.weight,
|
||||
errorException = t.classPath,
|
||||
errorCode = null,
|
||||
errorBody = null,
|
||||
errorContentType = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ data class BadQualityConfigDataModel(
|
|||
@Serializable
|
||||
data class Error(
|
||||
val weight: Float, // increase the probability of being triggered vs all others errors
|
||||
val errorCode: Int,
|
||||
val errorBody: String,
|
||||
val errorContentType: String, // "application/json"
|
||||
val errorException: String?,
|
||||
val errorCode: Int?,
|
||||
val errorBody: String?,
|
||||
val errorContentType: String?, // "application/json"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,17 @@ data class BadQualityConfigDomainModel(
|
|||
)
|
||||
data class Error(
|
||||
val weight: Float, // increase the probability of being triggered vs all others errors
|
||||
val httpCode: Int,
|
||||
val body: String,
|
||||
val contentType: String, // "application/json"
|
||||
)
|
||||
val type: Type,
|
||||
) {
|
||||
sealed interface Type {
|
||||
data class Body(
|
||||
val httpCode: Int,
|
||||
val body: String,
|
||||
val contentType: String,
|
||||
) : Type
|
||||
data class Exception(
|
||||
val classPath: String,
|
||||
) : Type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue