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] return alterationHashFlow.value[alterationId]
} }
fun update(json: AlterationJson) { @Throws
alterationStore.save(alteration = json) fun save(
json: AlterationJson,
create: Boolean,
) {
alterationStore.save(
json = json,
create = create,
)
} }
fun delete(alterationId: String): Boolean { @Throws
return alterationStore.delete(id = alterationId) fun delete(alterationId: String) {
return alterationStore.delete(
id = alterationId,
)
} }
} }

View file

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

View file

@ -1,25 +1,23 @@
package com.pixelized.server.lwa.server.rest.alteration 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.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.alterationId 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 com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond
import io.ktor.server.response.respondText
fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
try { try {
val alterationId = call.parameters.alterationId val alterationId = call.parameters.alterationId
val deleted = alterationService.delete( alterationService.delete(
alterationId = alterationId alterationId = alterationId
) )
call.respond(
if (deleted.not()) error("Unexpected error occurred") message = ResultJson.Success(),
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
) )
webSocket.emit( webSocket.emit(
value = ApiSynchronisation.AlterationDelete( value = ApiSynchronisation.AlterationDelete(
@ -27,10 +25,26 @@ fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.()
alterationId = alterationId, 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) { } catch (exception: Exception) {
call.respondText( call.respond(
text = "${HttpStatusCode.UnprocessableEntity}", message = ResultJson.Error(
status = HttpStatusCode.UnprocessableEntity, 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.server.Engine
import com.pixelized.server.lwa.utils.extentions.alterationId 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.respond
import io.ktor.server.response.respondText
fun Engine.getAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.getAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
@ -20,9 +19,11 @@ fun Engine.getAlteration(): suspend io.ktor.server.routing.RoutingContext.() ->
message = alteration, message = alteration,
) )
} catch (exception: Exception) { } catch (exception: Exception) {
call.respondText( call.respond(
text = exception.localizedMessage, message = ResultJson.Error(
status = HttpStatusCode.UnprocessableEntity, status = ResultJson.Error.GENERIC,
message = exception.message ?: "?",
)
) )
} }
} }

View file

@ -1,12 +1,22 @@
package com.pixelized.server.lwa.server.rest.alteration package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.server.response.respond import io.ktor.server.response.respond
fun Engine.getAlterationTags(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.getAlterationTags(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
call.respond( try {
message = alterationService.tags(), 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 package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.server.response.respond import io.ktor.server.response.respond
fun Engine.getAlterations(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.getAlterations(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
call.respond( try {
message = alterationService.alterations(), 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 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.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.model.alteration.AlterationJson
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive 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 { fun Engine.putAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
try { try {
val form = call.receive<AlterationJson>() val form = call.receive<AlterationJson>()
val create = call.queryParameters.create
alterationService.update(json = form) alterationService.save(
json = form,
call.respondText( create = create,
text = "${HttpStatusCode.OK}", )
status = HttpStatusCode.OK, call.respond(
message = ResultJson.Success(),
) )
webSocket.emit( webSocket.emit(
value = ApiSynchronisation.AlterationUpdate( value = ApiSynchronisation.AlterationUpdate(
@ -24,10 +29,26 @@ fun Engine.putAlteration(): suspend io.ktor.server.routing.RoutingContext.() ->
alterationId = form.id, alterationId = form.id,
), ),
) )
} catch (exception : Exception) { } catch (exception: MissingParameterException) {
call.respondText( call.respond(
text = "${HttpStatusCode.UnprocessableEntity}", message = ResultJson.Error(
status = HttpStatusCode.UnprocessableEntity, 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 package com.pixelized.server.lwa.utils.extentions
import com.pixelized.shared.lwa.protocol.rest.ResultJson
import io.ktor.http.Parameters import io.ktor.http.Parameters
val Parameters.characterSheetId 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 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 { get() = module {
factory { factory {
Json { Json {
encodeDefaults = true
explicitNulls = false explicitNulls = false
prettyPrint = true 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
}