Server : Alteration error management

This commit is contained in:
Thomas Andres Gomez 2025-03-30 13:30:22 +02:00
parent 81c6450dbe
commit acb445c480
10 changed files with 220 additions and 80 deletions

View file

@ -46,11 +46,21 @@ class AlterationService(
return alterationHashFlow.value[alterationId]
}
fun update(json: AlterationJson) {
alterationStore.save(alteration = json)
@Throws
fun save(
json: AlterationJson,
create: Boolean,
) {
alterationStore.save(
json = json,
create = create,
)
}
fun delete(alterationId: String): Boolean {
return alterationStore.delete(id = alterationId)
@Throws
fun delete(alterationId: String) {
return alterationStore.delete(
id = alterationId,
)
}
}

View file

@ -28,15 +28,15 @@ class AlterationStore(
val scope = CoroutineScope(Dispatchers.IO + Job())
// load the initial data
scope.launch {
updateAlterations()
updateAlterationFlow()
}
}
fun alterationsFlow(): StateFlow<List<Alteration>> = alterationFlow
private fun updateAlterations() {
private fun updateAlterationFlow() {
alterationFlow.value = try {
loadAlterations()
load()
} catch (exception: Exception) {
println(exception) // TODO proper exception handling
emptyList()
@ -47,7 +47,9 @@ class AlterationStore(
FileReadException::class,
JsonConversionException::class,
)
private fun loadAlterations(): List<Alteration> {
private fun load(
directory: File = this.directory,
): List<Alteration> {
return directory
.listFiles()
?.mapNotNull { file ->
@ -60,8 +62,14 @@ class AlterationStore(
if (json.isBlank()) {
return@mapNotNull null
}
// decode the file
val data = try {
this.json.decodeFromString<AlterationJson>(json)
} catch (exception: Exception) {
throw JsonCodingException(root = exception)
}
// parse the json string.
try {
val data = this.json.decodeFromString<AlterationJson>(json)
factory.convertFromJson(data)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
@ -70,68 +78,77 @@ class AlterationStore(
?: emptyList()
}
@Throws(JsonConversionException::class, FileWriteException::class)
@Throws(
BusinessException::class,
JsonConversionException::class,
JsonCodingException::class,
FileWriteException::class,
)
fun save(
alteration: Alteration,
json: AlterationJson,
create: Boolean,
) {
val json = try {
factory.convertToJson(data = alteration)
val file = alterationFile(id = json.id)
// Guard case on update alteration
if (create && file.exists()) {
val root = Exception("Alteration already exist, creation is impossible.")
throw BusinessException(root = root)
}
// Transform the json into the model.
val alteration = try {
factory.convertFromJson(json)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
save(alteration = json)
}
@Throws(FileWriteException::class)
fun save(
alteration: AlterationJson,
) {
// encode the json into a string
// Encode the json into a string.
val data = try {
json.encodeToString(alteration)
this.json.encodeToString(json)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
throw JsonCodingException(root = exception)
}
// write the alteration into a file.
// Write the alteration into a file.
try {
val file = alterationFile(id = alteration.id)
file.writeText(
text = data,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(
root = exception
)
throw FileWriteException(root = exception)
}
// Update the dataflow.
alterationFlow.update { alterations ->
val index = alterations.indexOfFirst { it.id == alteration.id }
val alt = factory.convertFromJson(alteration)
val index = alterations.indexOfFirst {
it.id == json.id
}
alterations.toMutableList().also {
if (index >= 0) {
it[index] = alt
} else {
it.add(alt)
when {
index >= 0 -> it[index] = alteration
else -> it.add(alteration)
}
}
}
}
fun delete(id: String): Boolean {
@Throws(BusinessException::class)
fun delete(id: String) {
val file = alterationFile(id = id)
val deleted = file.delete()
if (deleted) {
// Guard case on the file existence.
if (file.exists().not()) {
val root = Exception("Alteration doesn't not exist, deletion is impossible.")
throw BusinessException(root = root)
}
// Guard case on the file deletion
if (file.delete().not()) {
val root = Exception("Alteration file have not been deleted for unknown reason.")
throw BusinessException(root = root)
}
// Update the data model with the deleted alteration.
alterationFlow.update { alterations ->
alterations.toMutableList().also { alteration ->
alteration.removeIf { it.id == id }
}
}
}
return deleted
}
private fun alterationFile(id: String): File {
return File("${pathProvider.alterationsPath()}${id}.json")
@ -139,6 +156,8 @@ class AlterationStore(
sealed class AlterationStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : AlterationStoreException(root)
class JsonCodingException(root: Exception) : AlterationStoreException(root)
class BusinessException(root: Exception) : AlterationStoreException(root)
class FileWriteException(root: Exception) : AlterationStoreException(root)
class FileReadException(root: Exception) : AlterationStoreException(root)
}

View file

@ -1,25 +1,23 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.model.alteration.AlterationStore
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.alterationId
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
import io.ktor.server.response.respond
fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
val alterationId = call.parameters.alterationId
val deleted = alterationService.delete(
alterationService.delete(
alterationId = alterationId
)
if (deleted.not()) error("Unexpected error occurred")
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
call.respond(
message = ResultJson.Success(),
)
webSocket.emit(
value = ApiSynchronisation.AlterationDelete(
@ -27,10 +25,26 @@ fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.()
alterationId = alterationId,
),
)
} catch (exception: MissingParameterException) {
call.respond(
message = ResultJson.Error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: AlterationStore.BusinessException) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.FILE_DOES_NOT_EXIST,
message = "Alteration doesn't exist."
)
)
} catch (exception: Exception) {
call.respondText(
text = "${HttpStatusCode.UnprocessableEntity}",
status = HttpStatusCode.UnprocessableEntity,
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -2,9 +2,8 @@ package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.alterationId
import io.ktor.http.HttpStatusCode
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
fun Engine.getAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
@ -20,9 +19,11 @@ fun Engine.getAlteration(): suspend io.ktor.server.routing.RoutingContext.() ->
message = alteration,
)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -1,12 +1,22 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.server.response.respond
fun Engine.getAlterationTags(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
call.respond(
message = alterationService.tags(),
)
} catch (exception: Exception) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}
}

View file

@ -1,12 +1,22 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.server.response.respond
fun Engine.getAlterations(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
call.respond(
message = alterationService.alterations(),
)
} catch (exception: Exception) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}
}

View file

@ -1,22 +1,27 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.model.alteration.AlterationStore
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.create
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.response.respondText
import io.ktor.server.response.respond
fun Engine.putAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
val form = call.receive<AlterationJson>()
val create = call.queryParameters.create
alterationService.update(json = form)
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
alterationService.save(
json = form,
create = create,
)
call.respond(
message = ResultJson.Success(),
)
webSocket.emit(
value = ApiSynchronisation.AlterationUpdate(
@ -24,10 +29,26 @@ fun Engine.putAlteration(): suspend io.ktor.server.routing.RoutingContext.() ->
alterationId = form.id,
),
)
} catch (exception : Exception) {
call.respondText(
text = "${HttpStatusCode.UnprocessableEntity}",
status = HttpStatusCode.UnprocessableEntity,
} catch (exception: MissingParameterException) {
call.respond(
message = ResultJson.Error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: AlterationStore.BusinessException) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.FILE_ALREADY_EXIST,
message = "Alteration file already exist."
)
)
} catch (exception: Exception) {
call.respond(
message = ResultJson.Error(
status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
)
}
}

View file

@ -1,9 +1,30 @@
package com.pixelized.server.lwa.utils.extentions
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.http.Parameters
val Parameters.characterSheetId
get() = this["characterSheetId"] ?: error("Missing characterSheetId parameter.")
get() = this["characterSheetId"]
?: throw MissingParameterException(
name = "characterSheetId",
errorCode = ResultJson.Error.MISSING_CHARACTER_SHEET_ID,
)
val Parameters.alterationId
get() = this["alterationId"] ?: error("Missing alterationId parameter.")
get() = this["alterationId"]
?: throw MissingParameterException(
name = "alterationId",
errorCode = ResultJson.Error.MISSING_ALTERATION_ID,
)
val Parameters.create
get() = this["create"]?.toBooleanStrictOrNull()
?: throw MissingParameterException(
name = "create",
errorCode = ResultJson.Error.MISSING_CREATE
)
class MissingParameterException(
name: String,
val errorCode: Int,
) : Exception("Missing $name parameter.")

View file

@ -31,6 +31,7 @@ val toolsDependencies
get() = module {
factory {
Json {
encodeDefaults = true
explicitNulls = false
prettyPrint = true
}

View file

@ -0,0 +1,33 @@
package com.pixelized.shared.lwa.protocol.rest
import kotlinx.serialization.Serializable
@Serializable
sealed interface ResultJson {
val success: Boolean
@Serializable
data class Error(
override val success: Boolean = false,
val status: Int,
val message: String,
) : ResultJson {
companion object {
const val GENERIC = 500
const val FILE_ALREADY_EXIST = GENERIC + 1
const val FILE_DOES_NOT_EXIST = GENERIC + 2
const val MISSING_PARAMETER = 1000
const val MISSING_CHARACTER_SHEET_ID = MISSING_PARAMETER + 1
const val MISSING_ALTERATION_ID = MISSING_PARAMETER + 2
const val MISSING_CREATE = MISSING_PARAMETER + 3
}
}
@Serializable
data class Success(
override val success: Boolean = true,
val status: Int = 100,
) : ResultJson
}