From 0aaa56a4aacd1580283e3404c342fd974d2c51fe Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Wed, 2 Apr 2025 15:50:21 +0200 Subject: [PATCH] Server : Add item service. --- .../characterSheet/CharacterSheetStore.kt | 5 +- server/src/main/kotlin/Module.kt | 4 + .../lwa/model/alteration/AlterationService.kt | 22 +-- .../server/lwa/model/item/ItemService.kt | 56 ++++++ .../server/lwa/model/item/ItemStore.kt | 185 ++++++++++++++++++ .../server/lwa/model/tag/TagStore.kt | 9 +- .../com/pixelized/server/lwa/server/Engine.kt | 2 + .../com/pixelized/server/lwa/server/Server.kt | 30 ++- .../server/rest/alteration/GET_Alteration.kt | 3 +- .../lwa/server/rest/item/DELETE_Item.kt | 36 ++++ .../server/lwa/server/rest/item/GET_Item.kt | 30 +++ .../lwa/server/rest/item/GET_ItemTags.kt | 23 +++ .../server/lwa/server/rest/item/GET_Items.kt | 23 +++ .../server/lwa/server/rest/item/PUT_Item.kt | 39 ++++ .../lwa/utils/extentions/ParametersExt.kt | 5 + .../kotlin/com/pixelized/shared/lwa/Module.kt | 4 + .../pixelized/shared/lwa/model/item/Item.kt | 21 ++ .../shared/lwa/model/item/ItemJson.kt | 8 + .../shared/lwa/model/item/ItemJsonV1.kt | 27 +++ .../lwa/model/item/factory/ItemJsonFactory.kt | 19 ++ .../model/item/factory/ItemJsonFactoryV1.kt | 43 ++++ .../shared/lwa/protocol/rest/APIResponse.kt | 2 + .../protocol/websocket/ApiSynchronisation.kt | 29 ++- .../shared/lwa/utils/PathProvider.kt | 10 + 24 files changed, 607 insertions(+), 28 deletions(-) create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/DELETE_Item.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Item.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_ItemTags.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Items.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/PUT_Item.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJson.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactory.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index 163c44d..2877ffb 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -114,7 +114,7 @@ class CharacterSheetStore( private suspend fun handleMessage(message: SocketMessage) { when (message) { - is ApiSynchronisation -> try { + is ApiSynchronisation.CharacterSheetApiSynchronisation -> try { when (message) { is ApiSynchronisation.CharacterSheetUpdate -> { _detailFlow.update( @@ -137,9 +137,6 @@ class CharacterSheetStore( sheets.toMutableMap().also { it.remove(message.characterSheetId) } } } - - is ApiSynchronisation.AlterationUpdate -> Unit - is ApiSynchronisation.AlterationDelete -> Unit } } catch (exception: Exception) { println(exception.message) // TODO proper exception handling diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index a17215f..e71efd5 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -4,6 +4,8 @@ import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.model.campaign.CampaignStore import com.pixelized.server.lwa.model.character.CharacterSheetService import com.pixelized.server.lwa.model.character.CharacterSheetStore +import com.pixelized.server.lwa.model.item.ItemService +import com.pixelized.server.lwa.model.item.ItemStore import com.pixelized.server.lwa.model.tag.TagStore import com.pixelized.server.lwa.server.Engine import com.pixelized.shared.lwa.utils.PathProvider @@ -37,6 +39,7 @@ val storeDependencies singleOf(::CampaignStore) singleOf(::AlterationStore) singleOf(::TagStore) + singleOf(::ItemStore) } val serviceDependencies @@ -44,4 +47,5 @@ val serviceDependencies singleOf(::CharacterSheetService) singleOf(::CampaignService) singleOf(::AlterationService) + singleOf(::ItemService) } 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 7f2b49f..d7f8bd3 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 @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.stateIn class AlterationService( private val alterationStore: AlterationStore, - tagStore: TagStore, + private val tagStore: TagStore, factory: AlterationJsonFactory, ) { private val scope = CoroutineScope(Dispatchers.IO + Job()) @@ -26,26 +26,18 @@ class AlterationService( initialValue = emptyMap() ) - private val alterationTags = tagStore.alterationTags() - .map { it.values.toList() } - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = emptyList() - ) - fun alterations(): List { return alterationHashFlow.value.values.toList() } - fun tags(): List { - return alterationTags.value - } - fun alteration(alterationId: String): AlterationJson? { return alterationHashFlow.value[alterationId] } + fun tags(): List { + return tagStore.alterationTags().value.values.toList() + } + @Throws fun save( json: AlterationJson, @@ -59,8 +51,6 @@ class AlterationService( @Throws fun delete(alterationId: String) { - return alterationStore.delete( - id = alterationId, - ) + alterationStore.delete(id = alterationId) } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt new file mode 100644 index 0000000..744dd48 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt @@ -0,0 +1,56 @@ +package com.pixelized.server.lwa.model.item + +import com.pixelized.server.lwa.model.tag.TagStore +import com.pixelized.shared.lwa.model.item.ItemJson +import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory +import com.pixelized.shared.lwa.model.tag.TagJson +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class ItemService( + private val itemStore: ItemStore, + private val tagStore: TagStore, + factory: ItemJsonFactory, +) { + private val scope = CoroutineScope(Dispatchers.IO + Job()) + + private val itemHashFlow = itemStore.itemsFlow() + .map { items -> items.associate { it.id to factory.convertToJson(it) } } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyMap() + ) + + fun items(): List { + return itemHashFlow.value.values.toList() + } + + fun item(itemId: String): ItemJson? { + return itemHashFlow.value[itemId] + } + + fun tags(): List { + return tagStore.itemTags().value.values.toList() + } + + @Throws + fun save( + json: ItemJson, + create: Boolean, + ) { + itemStore.save( + json = json, + create = create, + ) + } + + @Throws + fun delete(itemId: String) { + itemStore.delete(id = itemId) + } +} diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt new file mode 100644 index 0000000..b2d584c --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt @@ -0,0 +1,185 @@ +package com.pixelized.server.lwa.model.item + +import com.pixelized.server.lwa.server.exception.BusinessException +import com.pixelized.server.lwa.server.exception.FileReadException +import com.pixelized.server.lwa.server.exception.FileWriteException +import com.pixelized.server.lwa.server.exception.JsonCodingException +import com.pixelized.server.lwa.server.exception.JsonConversionException +import com.pixelized.shared.lwa.model.item.Item +import com.pixelized.shared.lwa.model.item.ItemJson +import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.utils.PathProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import java.io.File +import java.text.Collator + +class ItemStore( + private val pathProvider: PathProvider, + private val factory: ItemJsonFactory, + private val json: Json, +) { + private val directory = File(pathProvider.itemsPath()).also { it.mkdirs() } + + private val itemFlow = MutableStateFlow>(emptyList()) + + init { + val scope = CoroutineScope(Dispatchers.IO + Job()) + + scope.launch { + updateItemsFlow() + } + } + + fun itemsFlow(): StateFlow> = itemFlow + + private fun updateItemsFlow() { + itemFlow.value = try { + load() + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + emptyList() + } + } + + @Throws( + FileReadException::class, + JsonConversionException::class, + ) + private fun load( + directory: File = this.directory, + ): List { + return directory + .listFiles() + ?.mapNotNull { file -> + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the json is blank no item have been save, ignore this file. + 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 { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + } + ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name }) + ?: emptyList() + } + + @Throws( + BusinessException::class, + JsonConversionException::class, + JsonCodingException::class, + FileWriteException::class, + ) + fun save( + json: ItemJson, + create: Boolean, + ) { + val file = itemFile(id = json.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException( + message = "Item already exist, creation is impossible.", + ) + } + // Transform the json into the model. + val item = try { + factory.convertFromJson(json) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + if (item.id.isEmpty()) { + throw BusinessException( + message = "Item 'id' is a mandatory field.", + code = APIResponse.ErrorCode.ItemId, + ) + } + if (item.metadata.name.isEmpty()) { + throw BusinessException( + message = "Item 'name' is a mandatory field.", + code = APIResponse.ErrorCode.ItemName, + ) + } + // Encode the json into a string. + val data = try { + this.json.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Write the alteration into a file. + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + itemFlow.update { items -> + val index = items.indexOfFirst { it.id == json.id } + items.toMutableList() + .also { + if (index >= 0) { + it[index] = item + } else { + it.add(item) + } + } + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.name + }) + } + } + + @Throws(BusinessException::class) + fun delete(id: String) { + val file = itemFile(id = id) + // Guard case on the file existence. + if (file.exists().not()) { + throw BusinessException( + message = "Item doesn't not exist, deletion is impossible.", + ) + } + // Guard case on the file deletion + if (file.delete().not()) { + throw BusinessException( + message = "Item file have not been deleted for unknown reason.", + ) + } + // Update the data model with the deleted alteration. + itemFlow.update { items -> + items.toMutableList() + .also { item -> + item.removeIf { it.id == id } + } + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.name + }) + } + } + + private fun itemFile(id: String): File { + return File("${pathProvider.itemsPath()}${id}.json") + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt index 1538552..1486cde 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt @@ -16,6 +16,7 @@ import java.io.File private const val CHARACTER = "character" private const val ALTERATION = "alteration" +private const val ITEM = "item" class TagStore( private val pathProvider: PathProvider, @@ -23,6 +24,7 @@ class TagStore( ) { private val alterationTagsFlow = MutableStateFlow>(emptyMap()) private val characterTagsFlow = MutableStateFlow>(emptyMap()) + private val itemTagsFlow = MutableStateFlow>(emptyMap()) init { // make the file path. @@ -39,11 +41,16 @@ class TagStore( flow = characterTagsFlow, file = characterFile(), ) + update( + flow = itemTagsFlow, + file = itemFile(), + ) } } fun alterationTags(): StateFlow> = alterationTagsFlow fun characterTags(): StateFlow> = characterTagsFlow + fun itemTags(): StateFlow> = itemTagsFlow private fun update( flow: MutableStateFlow>, @@ -101,6 +108,6 @@ class TagStore( } private fun characterFile() = File("${pathProvider.tagsPath()}$CHARACTER.json") - private fun alterationFile() = File("${pathProvider.tagsPath()}$ALTERATION.json") + private fun itemFile() = File("${pathProvider.tagsPath()}$ITEM.json") } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt index fa178ec..e10cca9 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt @@ -3,6 +3,7 @@ package com.pixelized.server.lwa.server import com.pixelized.server.lwa.model.alteration.AlterationService import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.model.character.CharacterSheetService +import com.pixelized.server.lwa.model.item.ItemService import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent @@ -16,6 +17,7 @@ class Engine( val characterService: CharacterSheetService, val campaignService: CampaignService, val alterationService: AlterationService, + val itemService: ItemService, val campaignJsonFactory: CampaignJsonFactory, ) { val webSocket = MutableSharedFlow() diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt index 29f003c..a074acd 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt @@ -21,6 +21,11 @@ import com.pixelized.server.lwa.server.rest.character.putCharacterAlteration import com.pixelized.server.lwa.server.rest.character.putCharacterDamage import com.pixelized.server.lwa.server.rest.character.putCharacterDiminished import com.pixelized.server.lwa.server.rest.character.putCharacterFatigue +import com.pixelized.server.lwa.server.rest.item.deleteItem +import com.pixelized.server.lwa.server.rest.item.getItem +import com.pixelized.server.lwa.server.rest.item.getItemTags +import com.pixelized.server.lwa.server.rest.item.getItems +import com.pixelized.server.lwa.server.rest.item.putItem import com.pixelized.shared.lwa.SERVER_PORT import com.pixelized.shared.lwa.protocol.websocket.SocketMessage import com.pixelized.shared.lwa.sharedModuleDependencies @@ -113,12 +118,13 @@ class LocalServer { } }.onFailure { exception -> // TODO - println("WebSocket exception: ${exception.localizedMessage}") + println("WebSocket exception: ${exception.message}") }.also { job.cancel() } } ) + // TODO Tags. route( path = "/alteration", ) { @@ -217,6 +223,28 @@ class LocalServer { body = engine.putCampaignScene(), ) } + route(path = "item") { + get( + path = "/all", + body = engine.getItems(), + ) + get( + path = "/detail", + body = engine.getItem(), + ) + get( + path = "/tags", + body = engine.getItemTags(), + ) + put( + path = "/update", + body = engine.putItem(), + ) + delete( + path = "/delete", + body = engine.deleteItem(), + ) + } } } ) 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 30f89cd..36b3f62 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 @@ -13,8 +13,7 @@ fun Engine.getAlteration(): suspend RoutingContext.() -> Unit { // get the query parameter val alterationId = call.queryParameters.alterationId // get the alteration of the given id. - val alteration = alterationService - .alteration(alterationId = alterationId) + val alteration = alterationService.alteration(alterationId = alterationId) ?: error("Alteration with id:$alterationId not found.") // send it back to the user. call.respond( diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/DELETE_Item.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/DELETE_Item.kt new file mode 100644 index 0000000..38c3dfb --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/DELETE_Item.kt @@ -0,0 +1,36 @@ +package com.pixelized.server.lwa.server.rest.item + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.server.lwa.utils.extentions.itemId +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.deleteItem(): suspend RoutingContext.() -> Unit { + return { + try { + // get the query parameter + val itemId = call.parameters.itemId + // delete the alteration. + itemService.delete( + itemId = itemId + ) + // API & WebSocket responses. + call.respond( + message = APIResponse.success(), + ) + webSocket.emit( + value = ApiSynchronisation.ItemDelete( + timestamp = System.currentTimeMillis(), + itemId = itemId, + ), + ) + } catch (exception: Exception) { + call.exception( + exception = exception, + ) + } + } +} diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Item.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Item.kt new file mode 100644 index 0000000..61e4230 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Item.kt @@ -0,0 +1,30 @@ +package com.pixelized.server.lwa.server.rest.item + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.server.lwa.utils.extentions.itemId +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.getItem(): suspend RoutingContext.() -> Unit { + return { + try { + // get the query parameter + val itemId = call.queryParameters.itemId + // get the alteration of the given id. + val item = itemService.item(itemId = itemId) + ?: error("Item with id:$itemId not found.") + // send it back to the user. + call.respond( + message = APIResponse.success( + data = item + ) + ) + } catch (exception: Exception) { + call.exception( + exception = exception, + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_ItemTags.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_ItemTags.kt new file mode 100644 index 0000000..bdb959f --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_ItemTags.kt @@ -0,0 +1,23 @@ +package com.pixelized.server.lwa.server.rest.item + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.getItemTags(): suspend RoutingContext.() -> Unit { + return { + try { + call.respond( + message = APIResponse.success( + data = itemService.tags(), + ), + ) + } catch (exception: Exception) { + call.exception( + exception = exception, + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Items.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Items.kt new file mode 100644 index 0000000..046b3f1 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/GET_Items.kt @@ -0,0 +1,23 @@ +package com.pixelized.server.lwa.server.rest.item + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.getItems(): suspend RoutingContext.() -> Unit { + return { + try { + call.respond( + message = APIResponse.success( + data = itemService.items(), + ) + ) + } catch (exception: Exception) { + call.exception( + exception = exception, + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/PUT_Item.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/PUT_Item.kt new file mode 100644 index 0000000..569c97c --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/item/PUT_Item.kt @@ -0,0 +1,39 @@ +package com.pixelized.server.lwa.server.rest.item + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.create +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.model.item.ItemJson +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.putItem(): suspend RoutingContext.() -> Unit { + return { + try { + val form = call.receive() + val create = call.queryParameters.create + + itemService.save( + json = form, + create = create, + ) + call.respond( + message = APIResponse.success(), + ) + webSocket.emit( + value = ApiSynchronisation.ItemUpdate( + timestamp = System.currentTimeMillis(), + itemId = form.id, + ), + ) + } catch (exception: Exception) { + call.exception( + exception = exception, + ) + } + } +} \ No newline at end of file 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 1559b91..df45030 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 @@ -13,6 +13,11 @@ val Parameters.alterationId this[param] ?: throw MissingParameterException(name = param) } +val Parameters.itemId + get() = "itemId".let { param -> + this[param] ?: throw MissingParameterException(name = param) + } + val Parameters.create get() = "create".let { param -> this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(name = param) 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 e5047ac..446d048 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt @@ -7,6 +7,8 @@ import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonV1Factory import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonV2Factory import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonFactory import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonV1Factory +import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory +import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactoryV1 import com.pixelized.shared.lwa.model.tag.TagJsonFactory import com.pixelized.shared.lwa.parser.dice.DiceParser import com.pixelized.shared.lwa.parser.expression.ExpressionParser @@ -48,6 +50,8 @@ val factoryDependencies factoryOf(::AlteredCharacterSheetFactory) factoryOf(::AlterationJsonFactory) factoryOf(::TagJsonFactory) + factoryOf(::ItemJsonFactory) + factoryOf(::ItemJsonFactoryV1) } val parserDependencies diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt new file mode 100644 index 0000000..ecf7ca1 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt @@ -0,0 +1,21 @@ +package com.pixelized.shared.lwa.model.item + +data class Item( + val id: String, + val metadata: MetaData, + val options: Options, + val tags: List, + val alterations: List, +) { + data class MetaData( + val name: String, + val description: String, + val thumbnail: String?, + val image: String?, + ) + + data class Options( + val stackable: Boolean, + val equipable: Boolean, + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJson.kt new file mode 100644 index 0000000..15e568c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJson.kt @@ -0,0 +1,8 @@ +package com.pixelized.shared.lwa.model.item + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface ItemJson { + val id: String +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt new file mode 100644 index 0000000..b8651e7 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt @@ -0,0 +1,27 @@ +package com.pixelized.shared.lwa.model.item + +import kotlinx.serialization.Serializable + +@Serializable +data class ItemJsonV1( + override val id: String, + val metadata: ItemMetadataJsonV1, + val options: ItemOptionJsonV1, + val tags: List, + val alterations: List, +) : ItemJson { + + @Serializable + data class ItemMetadataJsonV1( + val name: String, + val description: String, + val thumbnail: String?, + val image: String?, + ) + + @Serializable + data class ItemOptionJsonV1( + val stackable: Boolean, + val equipable: Boolean, + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactory.kt new file mode 100644 index 0000000..a4411a6 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactory.kt @@ -0,0 +1,19 @@ +package com.pixelized.shared.lwa.model.item.factory + +import com.pixelized.shared.lwa.model.item.Item +import com.pixelized.shared.lwa.model.item.ItemJson +import com.pixelized.shared.lwa.model.item.ItemJsonV1 + +class ItemJsonFactory( + private val v1: ItemJsonFactoryV1, +) { + fun convertFromJson(json: ItemJson): Item { + return when (json) { + is ItemJsonV1 -> v1.convertFromJson(json = json) + } + } + + fun convertToJson(item: Item): ItemJson { + return v1.convertToJson(item = item) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt new file mode 100644 index 0000000..572dfbd --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt @@ -0,0 +1,43 @@ +package com.pixelized.shared.lwa.model.item.factory + +import com.pixelized.shared.lwa.model.item.Item +import com.pixelized.shared.lwa.model.item.ItemJsonV1 + +class ItemJsonFactoryV1 { + + fun convertFromJson(json: ItemJsonV1): Item { + return Item( + id = json.id, + metadata = Item.MetaData( + name = json.metadata.name, + description = json.metadata.description, + image = json.metadata.image, + thumbnail = json.metadata.thumbnail, + ), + options = Item.Options( + stackable = json.options.stackable, + equipable = json.options.equipable, + ), + tags = json.tags, + alterations = json.alterations, + ) + } + + fun convertToJson(item: Item): ItemJsonV1 { + return ItemJsonV1( + id = item.id, + metadata = ItemJsonV1.ItemMetadataJsonV1( + name = item.metadata.name, + description = item.metadata.description, + image = item.metadata.image, + thumbnail = item.metadata.thumbnail, + ), + options = ItemJsonV1.ItemOptionJsonV1( + stackable = item.options.stackable, + equipable = item.options.equipable, + ), + tags = item.tags, + alterations = item.alterations, + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt index a6d2d9e..2a6a627 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt @@ -14,6 +14,8 @@ data class APIResponse( enum class ErrorCode { AlterationId, AlterationName, + ItemId, + ItemName, CharacterSheetId, } diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt index 1394be5..16b7d78 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt @@ -5,27 +5,48 @@ import kotlinx.serialization.Serializable @Serializable sealed interface ApiSynchronisation : SocketMessage { + @Serializable + sealed interface CharacterSheetApiSynchronisation : ApiSynchronisation, CharacterSheetIdMessage + @Serializable data class CharacterSheetDelete( override val timestamp: Long, override val characterSheetId: String, - ) : ApiSynchronisation, CharacterSheetIdMessage + ) : CharacterSheetApiSynchronisation @Serializable data class CharacterSheetUpdate( override val timestamp: Long, override val characterSheetId: String, - ) : ApiSynchronisation, CharacterSheetIdMessage + ) : CharacterSheetApiSynchronisation + + @Serializable + sealed interface AlterationApiSynchronisation : ApiSynchronisation @Serializable data class AlterationUpdate( override val timestamp: Long, val alterationId: String, - ) : ApiSynchronisation + ) : AlterationApiSynchronisation @Serializable data class AlterationDelete( override val timestamp: Long, val alterationId: String, - ) : ApiSynchronisation + ) : AlterationApiSynchronisation + + @Serializable + sealed interface ItemApiSynchronisation : ApiSynchronisation + + @Serializable + data class ItemUpdate( + override val timestamp: Long, + val itemId: String, + ) : ItemApiSynchronisation + + @Serializable + data class ItemDelete( + override val timestamp: Long, + val itemId: String, + ) : ItemApiSynchronisation } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt index 80e5c12..7c275e3 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt @@ -54,6 +54,16 @@ class PathProvider( } } + fun itemsPath( + os: OperatingSystem = this.operatingSystem, + app: String = this.appName, + ): String { + return when (os) { + OperatingSystem.Windows -> "${storePath(os = os, app = app)}items\\" + OperatingSystem.Macintosh -> "${storePath(os = os, app = app)}items/" + } + } + fun tagsPath( os: OperatingSystem = this.operatingSystem, app: String = this.appName,