diff --git a/src/main/kotlin/configuration/SecurityConfiguration.kt b/src/main/kotlin/configuration/SecurityConfiguration.kt index 1e3ab668f17eb298da828379c48b7dd909c190de..a3f508ed1e58876e02e01b1ff24d9f7e2d8f826c 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 c74c97780011de86ae509d187427228fca0990f9..0000000000000000000000000000000000000000 --- 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 3a15fd9d5651b934a14d5ee86db5f5f491beec48..4360500d0803da9398e3823ea6281f8a02fafad0 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 c55907f13bf01397c78d71667cd13ca1bf952fcb..b52c4c3bec27f7cf348019b36793c8db71221e50 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 2b12d176de181fbc0acd999ecfa12a200c8f79b2..3214fd689dbf001d357ab2e3fab42d9c331a0f76 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 a9fc7a42c878c77d919b3e5631e0800835c59130..7e652196c5ec4b055afa48186c2340bda3362bef 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 0000000000000000000000000000000000000000..46568cf02ccce1602fe30a7fee29b0ba82750455 --- /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 0000000000000000000000000000000000000000..abeff58a21c33077025e4bd4eddef6029caef79f --- /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 0000000000000000000000000000000000000000..b209e4cf36d66173d46542a82d773a09916d0572 --- /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 0000000000000000000000000000000000000000..4deded5743a854df69d28a0ac238d4125af152f8 --- /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