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:
Florent CHAMPIGNY 2025-08-20 16:45:53 +02:00 committed by GitHub
parent cae29db95c
commit db3c8a118c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 491 additions and 191 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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