From a496a644ffe939187dce86f8431d2e2214157c9b Mon Sep 17 00:00:00 2001 From: ccornu <ccornu@takima.fr> Date: Mon, 10 Feb 2025 15:38:57 +0100 Subject: [PATCH] feat: dto, exceptions and better database calls --- .../configuration/SecurityConfiguration.kt | 1 + src/main/kotlin/player/PlayerInfoDTO.kt | 7 ---- src/main/kotlin/player/PlayerRepository.kt | 23 ++++++++++- src/main/kotlin/player/PlayerRoute.kt | 40 +++++++++++++++---- src/main/kotlin/player/PlayerService.kt | 8 +++- src/main/kotlin/player/PlayerServiceImpl.kt | 25 ++++++++---- .../kotlin/player/dtos/PlayerCreationDTO.kt | 9 +++++ src/main/kotlin/player/dtos/PlayerInfoDTO.kt | 17 ++++++++ .../kotlin/player/dtos/PlayerUpdateDTO.kt | 13 ++++++ .../exceptions/AlreadyExistingException.kt | 4 ++ 10 files changed, 120 insertions(+), 27 deletions(-) delete mode 100644 src/main/kotlin/player/PlayerInfoDTO.kt create mode 100644 src/main/kotlin/player/dtos/PlayerCreationDTO.kt create mode 100644 src/main/kotlin/player/dtos/PlayerInfoDTO.kt create mode 100644 src/main/kotlin/player/dtos/PlayerUpdateDTO.kt create mode 100644 src/main/kotlin/player/exceptions/AlreadyExistingException.kt diff --git a/src/main/kotlin/configuration/SecurityConfiguration.kt b/src/main/kotlin/configuration/SecurityConfiguration.kt index 1e3ab66..a3f508e 100644 --- a/src/main/kotlin/configuration/SecurityConfiguration.kt +++ b/src/main/kotlin/configuration/SecurityConfiguration.kt @@ -3,4 +3,5 @@ package betclic.test.configuration import io.ktor.server.application.* fun Application.configureSecurity() { + //TODO Add comment to say what I would do in term of security } diff --git a/src/main/kotlin/player/PlayerInfoDTO.kt b/src/main/kotlin/player/PlayerInfoDTO.kt deleted file mode 100644 index c74c977..0000000 --- a/src/main/kotlin/player/PlayerInfoDTO.kt +++ /dev/null @@ -1,7 +0,0 @@ -package betclic.test.player - -class PlayerInfoDTO( - val pseudo: String, - val pointsNumber: Int, - val ranking: Int, -) \ No newline at end of file diff --git a/src/main/kotlin/player/PlayerRepository.kt b/src/main/kotlin/player/PlayerRepository.kt index 3a15fd9..4360500 100644 --- a/src/main/kotlin/player/PlayerRepository.kt +++ b/src/main/kotlin/player/PlayerRepository.kt @@ -7,7 +7,10 @@ import kotlinx.coroutines.future.await import kotlinx.coroutines.reactive.asFlow import org.slf4j.LoggerFactory import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient +import software.amazon.awssdk.enhanced.dynamodb.Expression import software.amazon.awssdk.enhanced.dynamodb.Key +import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest +import software.amazon.awssdk.services.dynamodb.model.AttributeValue class PlayerRepository(dynamoDbEnhancedClient: DynamoDbEnhancedAsyncClient) { private val tableName = PlayerEntity::class.simpleName @@ -16,11 +19,13 @@ class PlayerRepository(dynamoDbEnhancedClient: DynamoDbEnhancedAsyncClient) { private val logger = LoggerFactory.getLogger(Application::class.java) suspend fun createNewPlayer(player: Player) = coroutineScope { - val createdPlayer = table.putItem(player.toPlayerEntity()).await() - logger.info("Successfully created new player $createdPlayer") + table.putItem(player.toPlayerEntity()).await() + logger.info("Successfully created new player $player") + return@coroutineScope player } suspend fun updatePlayer(player: Player): Player { + val updatedPlayer = table.updateItem(player.toPlayerEntity()).await() return updatedPlayer.toPlayer() } @@ -32,6 +37,20 @@ class PlayerRepository(dynamoDbEnhancedClient: DynamoDbEnhancedAsyncClient) { return foundPlayer?.toPlayer() } + suspend fun getRank(player: Player): Int { + val scan = ScanEnhancedRequest.builder() + .filterExpression( + Expression.builder() + .expression("pointsNumber > :pointsNumber") + .putExpressionValue( + ":pointsNumber", AttributeValue.builder().n(player.pointsNumber.toString()).build() + ).build() + ).build() + return buildList { + table.scan(scan).asFlow().collect { it.items().stream().forEach { t -> add(t) } } + }.size + 1 + } + suspend fun findAll(): List<Player> { return buildList { table.scan().asFlow().collect { it.items().stream().forEach { item -> add(item.toPlayer()) } } diff --git a/src/main/kotlin/player/PlayerRoute.kt b/src/main/kotlin/player/PlayerRoute.kt index c55907f..b52c4c3 100644 --- a/src/main/kotlin/player/PlayerRoute.kt +++ b/src/main/kotlin/player/PlayerRoute.kt @@ -1,34 +1,58 @@ package betclic.test.player +import betclic.test.player.dtos.PlayerCreationDTO +import betclic.test.player.dtos.PlayerInfoDTO +import betclic.test.player.dtos.PlayerUpdateDTO +import betclic.test.player.exceptions.AlreadyExistingPlayerException import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.plugins.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.inject +private const val SOMETHING_WENT_WRONG = "Something went wrong." + fun Routing.playerRoutes() { val playerService by inject<PlayerService>() + // const route("/players") { post { - val request = call.receive<String>() - playerService.createNewPlayer(request) - call.respond(HttpStatusCode.Created) + val request = call.receive<PlayerCreationDTO>() + try { + call.respond(HttpStatusCode.Created, playerService.createNewPlayer(request)) + } catch (e: AlreadyExistingPlayerException) { + call.respond(status = HttpStatusCode.BadRequest, message = e.message ?: SOMETHING_WENT_WRONG) + } } put { - val request = call.receive<Player>() - call.respond(playerService.updatePlayer(request)) + val request = call.receive<PlayerUpdateDTO>() + try { + call.respond(HttpStatusCode.OK, playerService.updatePlayer(request)) + } catch (e: NotFoundException) { + call.respond(status = HttpStatusCode.NotFound, message = e.message ?: SOMETHING_WENT_WRONG) + } + } get("/{pseudo}") { val pseudo = call.parameters["pseudo"] ?: return@get call.respond(HttpStatusCode.BadRequest) - call.respond<Player>(playerService.findPlayerByPseudo(pseudo)) + try { + call.respond<Player>(playerService.findPlayerByPseudo(pseudo)) + } catch (e: NotFoundException) { + call.respond(status = HttpStatusCode.NotFound, message = e.message ?: SOMETHING_WENT_WRONG) + } } get("/{pseudo}/info") { val pseudo = call.parameters["pseudo"] ?: return@get call.respond(HttpStatusCode.BadRequest) - call.respond<PlayerInfoDTO>(playerService.getPlayerInfoByPseudo(pseudo)) + try { + call.respond<PlayerInfoDTO>(playerService.getPlayerInfoByPseudo(pseudo)) + } catch (e: NotFoundException) { + call.respond(status = HttpStatusCode.NotFound, message = e.message ?: SOMETHING_WENT_WRONG) + } } get("/ranking") { @@ -39,6 +63,6 @@ fun Routing.playerRoutes() { playerService.deleteAllPlayers() call.respond(HttpStatusCode.NoContent) } - } + } } \ No newline at end of file diff --git a/src/main/kotlin/player/PlayerService.kt b/src/main/kotlin/player/PlayerService.kt index 2b12d17..3214fd6 100644 --- a/src/main/kotlin/player/PlayerService.kt +++ b/src/main/kotlin/player/PlayerService.kt @@ -1,9 +1,13 @@ package betclic.test.player +import betclic.test.player.dtos.PlayerCreationDTO +import betclic.test.player.dtos.PlayerInfoDTO +import betclic.test.player.dtos.PlayerUpdateDTO + interface PlayerService { - suspend fun createNewPlayer(pseudo: String) + suspend fun createNewPlayer(playerCreationDTO: PlayerCreationDTO): Player - suspend fun updatePlayer(player: Player): Player + suspend fun updatePlayer(playerUpdateDTO: PlayerUpdateDTO): Player suspend fun findPlayerByPseudo(pseudo: String): Player diff --git a/src/main/kotlin/player/PlayerServiceImpl.kt b/src/main/kotlin/player/PlayerServiceImpl.kt index a9fc7a4..7e65219 100644 --- a/src/main/kotlin/player/PlayerServiceImpl.kt +++ b/src/main/kotlin/player/PlayerServiceImpl.kt @@ -1,15 +1,25 @@ package betclic.test.player +import betclic.test.player.dtos.PlayerCreationDTO +import betclic.test.player.dtos.PlayerInfoDTO +import betclic.test.player.dtos.PlayerUpdateDTO +import betclic.test.player.dtos.toPlayer +import betclic.test.player.dtos.toPlayerInfoDTO +import betclic.test.player.exceptions.AlreadyExistingPlayerException import io.ktor.server.plugins.* class PlayerServiceImpl(private val playerRepository: PlayerRepository) : PlayerService { - override suspend fun createNewPlayer(pseudo: String) { - playerRepository.createNewPlayer(Player(pseudo = pseudo)) + override suspend fun createNewPlayer(playerCreationDTO: PlayerCreationDTO): Player { + if (playerRepository.findPlayerByPseudo(playerCreationDTO.pseudo) != null) { + throw AlreadyExistingPlayerException(playerCreationDTO.pseudo) + } + return playerRepository.createNewPlayer(playerCreationDTO.toPlayer()) } - override suspend fun updatePlayer(player: Player): Player { - return playerRepository.updatePlayer(player) + override suspend fun updatePlayer(playerUpdateDTO: PlayerUpdateDTO): Player { + findPlayerByPseudo(playerUpdateDTO.pseudo) + return playerRepository.updatePlayer(playerUpdateDTO.toPlayer()) } override suspend fun findPlayerByPseudo(pseudo: String): Player { @@ -18,10 +28,9 @@ class PlayerServiceImpl(private val playerRepository: PlayerRepository) : Player } override suspend fun getPlayerInfoByPseudo(pseudo: String): PlayerInfoDTO { - val allPlayersRanked = getPlayersRanked() - val player = allPlayersRanked.find { it.pseudo == pseudo } - ?: throw NotFoundException("Player $pseudo not found in ranking") - return player + val player = findPlayerByPseudo(pseudo) + val rank = playerRepository.getRank(player) + return player.toPlayerInfoDTO(rank) } override suspend fun getPlayersRanked(): List<PlayerInfoDTO> { diff --git a/src/main/kotlin/player/dtos/PlayerCreationDTO.kt b/src/main/kotlin/player/dtos/PlayerCreationDTO.kt new file mode 100644 index 0000000..46568cf --- /dev/null +++ b/src/main/kotlin/player/dtos/PlayerCreationDTO.kt @@ -0,0 +1,9 @@ +package betclic.test.player.dtos + +import betclic.test.player.Player + +data class PlayerCreationDTO( + val pseudo: String, +) + +fun PlayerCreationDTO.toPlayer() = Player(pseudo) \ No newline at end of file diff --git a/src/main/kotlin/player/dtos/PlayerInfoDTO.kt b/src/main/kotlin/player/dtos/PlayerInfoDTO.kt new file mode 100644 index 0000000..abeff58 --- /dev/null +++ b/src/main/kotlin/player/dtos/PlayerInfoDTO.kt @@ -0,0 +1,17 @@ +package betclic.test.player.dtos + +import betclic.test.player.Player + +data class PlayerInfoDTO( + val pseudo: String, + val pointsNumber: Int, + val ranking: Int, +) + +fun Player.toPlayerInfoDTO(rank: Int): PlayerInfoDTO { + return PlayerInfoDTO( + pseudo = this.pseudo, + pointsNumber = this.pointsNumber, + ranking = rank + ) +} \ No newline at end of file diff --git a/src/main/kotlin/player/dtos/PlayerUpdateDTO.kt b/src/main/kotlin/player/dtos/PlayerUpdateDTO.kt new file mode 100644 index 0000000..b209e4c --- /dev/null +++ b/src/main/kotlin/player/dtos/PlayerUpdateDTO.kt @@ -0,0 +1,13 @@ +package betclic.test.player.dtos + +import betclic.test.player.Player + +data class PlayerUpdateDTO( + val pseudo: String, + val pointsNumber: Int, +) + +fun PlayerUpdateDTO.toPlayer(): Player = Player( + pseudo = this.pseudo, + pointsNumber = this.pointsNumber +) \ No newline at end of file diff --git a/src/main/kotlin/player/exceptions/AlreadyExistingException.kt b/src/main/kotlin/player/exceptions/AlreadyExistingException.kt new file mode 100644 index 0000000..4deded5 --- /dev/null +++ b/src/main/kotlin/player/exceptions/AlreadyExistingException.kt @@ -0,0 +1,4 @@ +package betclic.test.player.exceptions + +class AlreadyExistingPlayerException(val pseudo: String) : + RuntimeException("$pseudo already exists. You still can update this player points") \ No newline at end of file -- GitLab