From 952c7dcc76c586a41b6de058732fb9a223ddeb5f Mon Sep 17 00:00:00 2001
From: ccornu <ccornu@takima.fr>
Date: Fri, 7 Feb 2025 16:15:12 +0100
Subject: [PATCH] feat: new player endpoints (put + get) + update structure and
 readme

---
 README.md                                     | 21 ++++++++++++-----
 build.gradle.kts                              | 15 +++++++++---
 src/main/kotlin/Application.kt                |  5 +++-
 .../ApiConfiguration.kt}                      |  9 +-------
 .../ApiDocumentationConfiguration.kt}         |  2 +-
 .../DynamoDbConfiguration.kt                  |  8 +------
 .../InjectionConfiguration.kt}                |  2 +-
 .../configuration/RoutingConfiguration.kt     | 11 +++++++++
 .../SecurityConfiguration.kt}                 |  2 +-
 src/main/kotlin/player/PlayerRepository.kt    | 19 ++++++++++++---
 src/main/kotlin/player/PlayerRoute.kt         | 23 ++++++++++++-------
 src/main/kotlin/player/PlayerService.kt       |  4 ++++
 src/main/kotlin/player/PlayerServiceImpl.kt   |  8 +++++++
 .../kotlin/player/PlayerIntegrationTest.kt    |  3 +--
 14 files changed, 91 insertions(+), 41 deletions(-)
 rename src/main/kotlin/{Serialization.kt => configuration/ApiConfiguration.kt} (64%)
 rename src/main/kotlin/{HTTP.kt => configuration/ApiDocumentationConfiguration.kt} (85%)
 rename src/main/kotlin/{ => configuration}/DynamoDbConfiguration.kt (88%)
 rename src/main/kotlin/{Koin.kt => configuration/InjectionConfiguration.kt} (94%)
 create mode 100644 src/main/kotlin/configuration/RoutingConfiguration.kt
 rename src/main/kotlin/{Security.kt => configuration/SecurityConfiguration.kt} (69%)

diff --git a/README.md b/README.md
index c185f27..dcd8dbf 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,10 @@ docker compose up -d
 
 ## Available endpoints
 
+## Technical choices
+
+- I chose ... because ...
+
 ## I want to ensure tests are running
 
 ## Left to do
@@ -32,11 +36,16 @@ docker compose up -d
 - [x] faire la connexion avec Dynamo
 - 1er endpoint
     - [x] créer l'entité joueur
-    - [ ] créer le repo et le connecter à la BD
-    - [ ] tester unitairement le service
+    - [x] créer le repo et le connecter à la BD
+    - [x] tester unitairement le service
     - [ ] tester l'intégration complète
-- [ ] endpoint fonctionnel pour l'ajout d'un avec test
-    - [ ] tests d'inté
-    - [ ] tests unitaires
+    - [ ] endpoint fonctionnel pour l'ajout d'un avec test
+        - [ ] tests d'intégration
+        - [ ] tests unitaires
 - [ ] tous les endpoints autres
-- [ ] gestion de la sécurité
\ No newline at end of file
+- gestion de la sécurité
+
+## Going further
+
+- Database migration tool
+- ...
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index ad35518..ade3b6f 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,7 +12,7 @@ plugins {
 }
 
 group = "betclic.test"
-version = "0.0.1"
+version = "0.0.1-SNAPSHOT"
 
 application {
     mainClass.set("io.ktor.server.netty.EngineMain")
@@ -26,19 +26,28 @@ repositories {
 }
 
 dependencies {
+    // Ktor
     implementation("io.ktor:ktor-server-core")
     implementation("io.ktor:ktor-server-auth")
     implementation("io.ktor:ktor-server-openapi")
+    implementation("io.ktor:ktor-server-config-yaml")
+    implementation("io.ktor:ktor-server-netty")
+
+    // Dependency injection
     implementation("io.insert-koin:koin-ktor:$koin_version")
     implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
+
+    // Content negociation
     implementation("io.ktor:ktor-server-content-negotiation")
     implementation("io.ktor:ktor-serialization-jackson")
-    implementation("io.ktor:ktor-server-netty")
     implementation("ch.qos.logback:logback-classic:$logback_version")
-    implementation("io.ktor:ktor-server-config-yaml")
+
+    // Database
     implementation("software.amazon.awssdk:dynamodb-enhanced:$dynamo_version")
     implementation("software.amazon.awssdk:dynamodb:$dynamo_version")
     implementation("dev.andrewohara:dynamokt:$dynamo_kt_version")
+
+    // Tests
     testImplementation("io.ktor:ktor-server-test-host")
     testImplementation("io.mockk:mockk:$mockk_version")
     testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt
index 074276f..6e183ef 100644
--- a/src/main/kotlin/Application.kt
+++ b/src/main/kotlin/Application.kt
@@ -1,6 +1,9 @@
 package betclic.test
 
-import betclic.test.player.configureRouting
+import betclic.test.configuration.configureHTTP
+import betclic.test.configuration.configureKoin
+import betclic.test.configuration.configureRouting
+import betclic.test.configuration.configureSerialization
 import io.ktor.server.application.*
 import kotlinx.coroutines.runBlocking
 
diff --git a/src/main/kotlin/Serialization.kt b/src/main/kotlin/configuration/ApiConfiguration.kt
similarity index 64%
rename from src/main/kotlin/Serialization.kt
rename to src/main/kotlin/configuration/ApiConfiguration.kt
index c3ed7d8..27f2a9c 100644
--- a/src/main/kotlin/Serialization.kt
+++ b/src/main/kotlin/configuration/ApiConfiguration.kt
@@ -1,11 +1,9 @@
-package betclic.test
+package betclic.test.configuration
 
 import com.fasterxml.jackson.databind.SerializationFeature
 import io.ktor.serialization.jackson.*
 import io.ktor.server.application.*
 import io.ktor.server.plugins.contentnegotiation.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
 
 fun Application.configureSerialization() {
     install(ContentNegotiation) {
@@ -13,9 +11,4 @@ fun Application.configureSerialization() {
             enable(SerializationFeature.INDENT_OUTPUT)
         }
     }
-    routing {
-        get("/json/jackson") {
-            call.respond(mapOf("hello" to "world"))
-        }
-    }
 }
diff --git a/src/main/kotlin/HTTP.kt b/src/main/kotlin/configuration/ApiDocumentationConfiguration.kt
similarity index 85%
rename from src/main/kotlin/HTTP.kt
rename to src/main/kotlin/configuration/ApiDocumentationConfiguration.kt
index 62f841d..ed88a96 100644
--- a/src/main/kotlin/HTTP.kt
+++ b/src/main/kotlin/configuration/ApiDocumentationConfiguration.kt
@@ -1,4 +1,4 @@
-package betclic.test
+package betclic.test.configuration
 
 import io.ktor.server.application.*
 import io.ktor.server.plugins.openapi.*
diff --git a/src/main/kotlin/DynamoDbConfiguration.kt b/src/main/kotlin/configuration/DynamoDbConfiguration.kt
similarity index 88%
rename from src/main/kotlin/DynamoDbConfiguration.kt
rename to src/main/kotlin/configuration/DynamoDbConfiguration.kt
index 8c3d3ad..1ef9481 100644
--- a/src/main/kotlin/DynamoDbConfiguration.kt
+++ b/src/main/kotlin/configuration/DynamoDbConfiguration.kt
@@ -1,4 +1,4 @@
-package betclic.test
+package betclic.test.configuration
 
 import betclic.test.player.PlayerEntity
 import dev.andrewohara.dynamokt.DataClassTableSchema
@@ -24,12 +24,6 @@ private fun createDynamoDbClient(): DynamoDbAsyncClient {
     return DynamoDbAsyncClient.builder().endpointOverride(URI("http://localhost:8000")).build()
 }
 
-fun Application.createEnhancedDynamoDbClient(dynamoDbClient: DynamoDbAsyncClient): DynamoDbEnhancedAsyncClient {
-    return DynamoDbEnhancedAsyncClient.builder()
-        .dynamoDbClient(dynamoDbClient)
-        .build()
-}
-
 suspend fun createNecessaryTables(
     dynamoDbClient: DynamoDbAsyncClient,
     dynamoDbEnhancedClient: DynamoDbEnhancedAsyncClient
diff --git a/src/main/kotlin/Koin.kt b/src/main/kotlin/configuration/InjectionConfiguration.kt
similarity index 94%
rename from src/main/kotlin/Koin.kt
rename to src/main/kotlin/configuration/InjectionConfiguration.kt
index e8afee5..4b62325 100644
--- a/src/main/kotlin/Koin.kt
+++ b/src/main/kotlin/configuration/InjectionConfiguration.kt
@@ -1,4 +1,4 @@
-package betclic.test
+package betclic.test.configuration
 
 import betclic.test.player.PlayerRepository
 import betclic.test.player.PlayerService
diff --git a/src/main/kotlin/configuration/RoutingConfiguration.kt b/src/main/kotlin/configuration/RoutingConfiguration.kt
new file mode 100644
index 0000000..2e5b34b
--- /dev/null
+++ b/src/main/kotlin/configuration/RoutingConfiguration.kt
@@ -0,0 +1,11 @@
+package betclic.test.configuration
+
+import betclic.test.player.playerRoutes
+import io.ktor.server.application.*
+import io.ktor.server.routing.*
+
+fun Application.configureRouting() {
+    routing {
+        playerRoutes()
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/Security.kt b/src/main/kotlin/configuration/SecurityConfiguration.kt
similarity index 69%
rename from src/main/kotlin/Security.kt
rename to src/main/kotlin/configuration/SecurityConfiguration.kt
index 93a244c..1e3ab66 100644
--- a/src/main/kotlin/Security.kt
+++ b/src/main/kotlin/configuration/SecurityConfiguration.kt
@@ -1,4 +1,4 @@
-package betclic.test
+package betclic.test.configuration
 
 import io.ktor.server.application.*
 
diff --git a/src/main/kotlin/player/PlayerRepository.kt b/src/main/kotlin/player/PlayerRepository.kt
index 68612ab..c85d2a0 100644
--- a/src/main/kotlin/player/PlayerRepository.kt
+++ b/src/main/kotlin/player/PlayerRepository.kt
@@ -6,6 +6,7 @@ import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.future.await
 import org.slf4j.LoggerFactory
 import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient
+import software.amazon.awssdk.enhanced.dynamodb.Key
 
 class PlayerRepository(dynamoDbEnhancedClient: DynamoDbEnhancedAsyncClient) {
     private val tableName = PlayerEntity::class.simpleName
@@ -13,8 +14,20 @@ class PlayerRepository(dynamoDbEnhancedClient: DynamoDbEnhancedAsyncClient) {
     private val table = dynamoDbEnhancedClient.table(tableName, tableSchema)
     private val logger = LoggerFactory.getLogger(Application::class.java)
 
-    suspend fun createNewPlayer(player: Player): Unit = coroutineScope {
-        val saved = table.putItem(player.toPlayerEntity()).await()
-        logger.info("Successfully created new player $saved")
+    suspend fun createNewPlayer(player: Player) = coroutineScope {
+        val createdPlayer = table.putItem(player.toPlayerEntity()).await()
+        logger.info("Successfully created new player $createdPlayer")
+    }
+
+    suspend fun updatePlayer(player: Player): Player {
+        val updatedPlayer = table.updateItem(player.toPlayerEntity()).await()
+        return updatedPlayer.toPlayer()
+    }
+
+    suspend fun findPlayerByPseudo(pseudo: String): Player {
+        val foundPlayer = table.getItem(
+            Key.builder().partitionValue(pseudo).build()
+        ).await()
+        return foundPlayer.toPlayer()
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/player/PlayerRoute.kt b/src/main/kotlin/player/PlayerRoute.kt
index 43d3ca7..ff94f41 100644
--- a/src/main/kotlin/player/PlayerRoute.kt
+++ b/src/main/kotlin/player/PlayerRoute.kt
@@ -7,14 +7,21 @@ import io.ktor.server.response.*
 import io.ktor.server.routing.*
 import org.koin.ktor.ext.inject
 
-fun Application.configureRouting() {
-    routing {
-        val playerService by inject<PlayerService>()
+fun Routing.playerRoutes() {
+    val playerService by inject<PlayerService>()
+    post("/players") {
+        val request = call.receive<String>()
+        playerService.createNewPlayer(request)
+        call.respond(HttpStatusCode.Created)
+    }
+
+    put("/players") {
+        val request = call.receive<Player>()
+        call.respond(playerService.updatePlayer(request))
+    }
 
-        post("/player") {
-            val request = call.receive<String>()
-            playerService.createNewPlayer(request)
-            call.respond(HttpStatusCode.Created)
-        }
+    get("/players/{pseudo}") {
+        val pseudo = call.parameters["pseudo"] ?: return@get call.respond(HttpStatusCode.BadRequest)
+        call.respond<Player>(playerService.getPlayerByPseudo(pseudo))
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/player/PlayerService.kt b/src/main/kotlin/player/PlayerService.kt
index 222e09b..2f06a29 100644
--- a/src/main/kotlin/player/PlayerService.kt
+++ b/src/main/kotlin/player/PlayerService.kt
@@ -2,4 +2,8 @@ package betclic.test.player
 
 interface PlayerService {
     suspend fun createNewPlayer(pseudo: String)
+
+    suspend fun updatePlayer(player: Player): Player
+
+    suspend fun getPlayerByPseudo(pseudo: String): Player
 }
\ No newline at end of file
diff --git a/src/main/kotlin/player/PlayerServiceImpl.kt b/src/main/kotlin/player/PlayerServiceImpl.kt
index 6cfee70..9af6cdc 100644
--- a/src/main/kotlin/player/PlayerServiceImpl.kt
+++ b/src/main/kotlin/player/PlayerServiceImpl.kt
@@ -5,4 +5,12 @@ class PlayerServiceImpl(private val playerRepository: PlayerRepository) : Player
     override suspend fun createNewPlayer(pseudo: String) {
         playerRepository.createNewPlayer(Player(pseudo = pseudo))
     }
+
+    override suspend fun updatePlayer(player: Player): Player {
+        return playerRepository.updatePlayer(player)
+    }
+
+    override suspend fun getPlayerByPseudo(pseudo: String): Player {
+        return playerRepository.findPlayerByPseudo(pseudo)
+    }
 }
\ No newline at end of file
diff --git a/src/test/kotlin/player/PlayerIntegrationTest.kt b/src/test/kotlin/player/PlayerIntegrationTest.kt
index dae72e7..2aeb263 100644
--- a/src/test/kotlin/player/PlayerIntegrationTest.kt
+++ b/src/test/kotlin/player/PlayerIntegrationTest.kt
@@ -1,4 +1,4 @@
-package betclic.test.player
+package player
 
 import io.ktor.client.request.*
 import io.ktor.http.*
@@ -9,7 +9,6 @@ import kotlin.test.assertEquals
 class PlayerIntegrationTest {
     @Test
     fun `When calling post player, should return created`() = testApplication {
-
         assertEquals(HttpStatusCode.Created, client.post("/player") {
             header(HttpHeaders.ContentType, ContentType.Text.Plain)
             setBody("Test d'intégration")
-- 
GitLab