diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt index 9d5b098..7f2b49f 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt @@ -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, + ) } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt index 6505b33..1eba998 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt @@ -28,15 +28,15 @@ class AlterationStore( val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { - updateAlterations() + updateAlterationFlow() } } fun alterationsFlow(): StateFlow> = 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 { + private fun load( + directory: File = this.directory, + ): List { 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(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. try { - val data = this.json.decodeFromString(json) factory.convertFromJson(data) } catch (exception: Exception) { throw JsonConversionException(root = exception) @@ -70,67 +78,76 @@ 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) { - alterationFlow.update { alterations -> - alterations.toMutableList().also { alteration -> - alteration.removeIf { it.id == id } - } + // 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 { @@ -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) } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/DELETE_Alteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/DELETE_Alteration.kt index c347afc..99fa222 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/DELETE_Alteration.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/DELETE_Alteration.kt @@ -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 ?: "?", + ) ) } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt index 0414d46..a5e4df7 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt @@ -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 ?: "?", + ) ) } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_AlterationTags.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_AlterationTags.kt index 23baeeb..76ac1d9 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_AlterationTags.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_AlterationTags.kt @@ -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 { - call.respond( - message = alterationService.tags(), - ) + try { + call.respond( + message = alterationService.tags(), + ) + } catch (exception: Exception) { + call.respond( + message = ResultJson.Error( + status = ResultJson.Error.GENERIC, + message = exception.message ?: "?", + ) + ) + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alterations.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alterations.kt index 43316d2..e0c029b 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alterations.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alterations.kt @@ -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 { - call.respond( - message = alterationService.alterations(), - ) + try { + call.respond( + message = alterationService.alterations(), + ) + } catch (exception: Exception) { + call.respond( + message = ResultJson.Error( + status = ResultJson.Error.GENERIC, + message = exception.message ?: "?", + ) + ) + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_Alateration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_Alateration.kt index 6885a81..141c60c 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_Alateration.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_Alateration.kt @@ -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() + 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 ?: "?", + ) ) } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt index ef7f6da..4901cdb 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt @@ -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.") \ No newline at end of file + 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.") diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt index a7a9956..e5047ac 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt @@ -31,6 +31,7 @@ val toolsDependencies get() = module { factory { Json { + encodeDefaults = true explicitNulls = false prettyPrint = true } diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/PutResultJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/PutResultJson.kt new file mode 100644 index 0000000..c646c0f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/PutResultJson.kt @@ -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 +} \ No newline at end of file