feat: NCS-53 changed IO to MicroService for easier scaling (#37)
Reviewed-on: #37 Reviewed-by: Shahd Lala <shosho996@blackhole.local>
This commit is contained in:
Generated
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<option name="deprecationWarnings" value="true" />
|
<option name="deprecationWarnings" value="true" />
|
||||||
<option name="uncheckedWarnings" value="true" />
|
<option name="uncheckedWarnings" value="true" />
|
||||||
</profile>
|
</profile>
|
||||||
<profile name="Gradle 2" modules="NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.main,NowChessSystems.modules.io.scoverage,NowChessSystems.modules.io.test,NowChessSystems.modules.rule.main,NowChessSystems.modules.rule.scoverage,NowChessSystems.modules.rule.test,NowChessSystems.modules.ui.main,NowChessSystems.modules.ui.scoverage,NowChessSystems.modules.ui.test">
|
<profile name="Gradle 2" modules="NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.integrationTest,NowChessSystems.modules.io.main,NowChessSystems.modules.io.native-test,NowChessSystems.modules.io.quarkus-generated-sources,NowChessSystems.modules.io.quarkus-test-generated-sources,NowChessSystems.modules.io.scoverage,NowChessSystems.modules.io.test,NowChessSystems.modules.rule.main,NowChessSystems.modules.rule.scoverage,NowChessSystems.modules.rule.test,NowChessSystems.modules.ui.main,NowChessSystems.modules.ui.scoverage,NowChessSystems.modules.ui.test">
|
||||||
<option name="deprecationWarnings" value="true" />
|
<option name="deprecationWarnings" value="true" />
|
||||||
<option name="uncheckedWarnings" value="true" />
|
<option name="uncheckedWarnings" value="true" />
|
||||||
<parameters>
|
<parameters>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Scala 3.5.1 · Gradle 9
|
|||||||
./compile # Compile all modules — always run
|
./compile # Compile all modules — always run
|
||||||
./test # Run all tests
|
./test # Run all tests
|
||||||
./coverage # Check coverage
|
./coverage # Check coverage
|
||||||
|
./lint # Run linters
|
||||||
```
|
```
|
||||||
Use consistently.
|
Use consistently.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Scala 3.5.1 · Gradle 9
|
|||||||
./compile # Compile all modules — always run
|
./compile # Compile all modules — always run
|
||||||
./test # Run all tests
|
./test # Run all tests
|
||||||
./coverage # Check coverage
|
./coverage # Check coverage
|
||||||
|
./lint # Run linters
|
||||||
```
|
```
|
||||||
Try to stick to these commands for consistency.
|
Try to stick to these commands for consistency.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
vars {
|
vars {
|
||||||
baseUrl: http://localhost:8080
|
baseUrl: http://localhost:8080
|
||||||
|
ioBaseUrl: http://localhost:8081
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
meta {
|
||||||
|
name: Export FEN
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
method: POST
|
||||||
|
url: {{ioBaseUrl}}/io/export/fen
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"board": {
|
||||||
|
"a1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"b1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"d1": {"color": "White", "pieceType": "Queen"},
|
||||||
|
"e1": {"color": "White", "pieceType": "King"},
|
||||||
|
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"g1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"h1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||||
|
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||||
|
"e8": {"color": "Black", "pieceType": "King"},
|
||||||
|
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||||
|
},
|
||||||
|
"turn": "White",
|
||||||
|
"castlingRights": {
|
||||||
|
"whiteKingSide": true,
|
||||||
|
"whiteQueenSide": true,
|
||||||
|
"blackKingSide": true,
|
||||||
|
"blackQueenSide": true
|
||||||
|
},
|
||||||
|
"enPassantSquare": null,
|
||||||
|
"halfMoveClock": 0,
|
||||||
|
"moves": [],
|
||||||
|
"result": null,
|
||||||
|
"initialBoard": {
|
||||||
|
"a1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"b1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"d1": {"color": "White", "pieceType": "Queen"},
|
||||||
|
"e1": {"color": "White", "pieceType": "King"},
|
||||||
|
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"g1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"h1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||||
|
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||||
|
"e8": {"color": "Black", "pieceType": "King"},
|
||||||
|
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
meta {
|
||||||
|
name: Export PGN
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
method: POST
|
||||||
|
url: {{ioBaseUrl}}/io/export/pgn
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"board": {
|
||||||
|
"a1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"b1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"d1": {"color": "White", "pieceType": "Queen"},
|
||||||
|
"e1": {"color": "White", "pieceType": "King"},
|
||||||
|
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"g1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"h1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||||
|
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||||
|
"e8": {"color": "Black", "pieceType": "King"},
|
||||||
|
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||||
|
},
|
||||||
|
"turn": "White",
|
||||||
|
"castlingRights": {
|
||||||
|
"whiteKingSide": true,
|
||||||
|
"whiteQueenSide": true,
|
||||||
|
"blackKingSide": true,
|
||||||
|
"blackQueenSide": true
|
||||||
|
},
|
||||||
|
"enPassantSquare": null,
|
||||||
|
"halfMoveClock": 0,
|
||||||
|
"moves": [],
|
||||||
|
"result": null,
|
||||||
|
"initialBoard": {
|
||||||
|
"a1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"b1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"d1": {"color": "White", "pieceType": "Queen"},
|
||||||
|
"e1": {"color": "White", "pieceType": "King"},
|
||||||
|
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||||
|
"g1": {"color": "White", "pieceType": "Knight"},
|
||||||
|
"h1": {"color": "White", "pieceType": "Rook"},
|
||||||
|
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||||
|
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||||
|
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||||
|
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||||
|
"e8": {"color": "Black", "pieceType": "King"},
|
||||||
|
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||||
|
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||||
|
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
meta {
|
||||||
|
name: export
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
meta {
|
||||||
|
name: Import FEN
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
method: POST
|
||||||
|
url: {{ioBaseUrl}}/io/import/fen
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
meta {
|
||||||
|
name: Import PGN
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
method: POST
|
||||||
|
url: {{ioBaseUrl}}/io/import/pgn
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"pgn": "1. e4 e5 2. Nf3 Nc6 *"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
meta {
|
||||||
|
name: import
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
+6
-2
@@ -36,7 +36,11 @@ val coverageExclusions = listOf(
|
|||||||
"**/core/src/main/scala/de/nowchess/chess/registry/GameEntry.scala",
|
"**/core/src/main/scala/de/nowchess/chess/registry/GameEntry.scala",
|
||||||
"**/core/src/main/scala/de/nowchess/chess/registry/GameRegistryImpl.scala",
|
"**/core/src/main/scala/de/nowchess/chess/registry/GameRegistryImpl.scala",
|
||||||
// GameResource — REST integration layer with @Inject var fields; mocking dependencies for unit tests is infeasible with Quarkus DI; integration tests would require @QuarkusTest which Scoverage doesn't instrument
|
// GameResource — REST integration layer with @Inject var fields; mocking dependencies for unit tests is infeasible with Quarkus DI; integration tests would require @QuarkusTest which Scoverage doesn't instrument
|
||||||
"**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala"
|
"**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala",
|
||||||
|
// IoResource — same rationale as GameResource; @QuarkusTest not instrumented by Scoverage
|
||||||
|
"**/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala",
|
||||||
|
// JacksonConfig — Quarkus lifecycle hook, no testable logic beyond ObjectMapper registration
|
||||||
|
"**/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Converts a Sonar-style glob to a scoverage regex (matched against full source path).
|
// Converts a Sonar-style glob to a scoverage regex (matched against full source path).
|
||||||
@@ -72,7 +76,7 @@ sonar {
|
|||||||
val versions = mapOf(
|
val versions = mapOf(
|
||||||
"QUARKUS_SCALA3" to "1.0.0",
|
"QUARKUS_SCALA3" to "1.0.0",
|
||||||
"SCALA3" to "3.5.1",
|
"SCALA3" to "3.5.1",
|
||||||
"SCALA_LIBRARY" to "2.13.18",
|
"SCALA_LIBRARY" to "2.13.16",
|
||||||
"SCALATEST" to "3.2.19",
|
"SCALATEST" to "3.2.19",
|
||||||
"SCALATEST_JUNIT" to "0.1.11",
|
"SCALATEST_JUNIT" to "0.1.11",
|
||||||
"SCOVERAGE" to "2.1.1",
|
"SCOVERAGE" to "2.1.1",
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ dependencies {
|
|||||||
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||||
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
||||||
testImplementation("io.quarkus:quarkus-junit5")
|
testImplementation("io.quarkus:quarkus-junit5")
|
||||||
|
testImplementation("io.quarkus:quarkus-junit5-mockito")
|
||||||
testImplementation("io.rest-assured:rest-assured")
|
testImplementation("io.rest-assured:rest-assured")
|
||||||
|
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
greeting:
|
quarkus:
|
||||||
message: "hello"
|
http:
|
||||||
|
port: 8080
|
||||||
|
application:
|
||||||
|
name: nowchess-core
|
||||||
|
rest-client:
|
||||||
|
io-service:
|
||||||
|
url: http://localhost:8081
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package de.nowchess.chess.client
|
||||||
|
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
|
||||||
|
import jakarta.ws.rs.*
|
||||||
|
import jakarta.ws.rs.core.MediaType
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||||
|
|
||||||
|
@Path("/io")
|
||||||
|
@RegisterRestClient(configKey = "io-service")
|
||||||
|
trait IoServiceClient:
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/import/fen")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def importFen(body: ImportFenRequest): GameContext
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/import/pgn")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def importPgn(body: ImportPgnRequest): GameContext
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/export/fen")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.TEXT_PLAIN))
|
||||||
|
def exportFen(ctx: GameContext): String
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/export/pgn")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array("application/x-chess-pgn"))
|
||||||
|
def exportPgn(ctx: GameContext): String
|
||||||
@@ -2,7 +2,10 @@ package de.nowchess.chess.config
|
|||||||
|
|
||||||
import com.fasterxml.jackson.core.Version
|
import com.fasterxml.jackson.core.Version
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
|
import de.nowchess.api.board.Square
|
||||||
|
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
|
||||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||||
import jakarta.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@@ -15,3 +18,7 @@ class JacksonConfig extends ObjectMapperCustomizer:
|
|||||||
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||||
// scalafix:on DisableSyntax.null
|
// scalafix:on DisableSyntax.null
|
||||||
})
|
})
|
||||||
|
val squareModule = new SimpleModule()
|
||||||
|
squareModule.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||||
|
squareModule.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||||
|
mapper.registerModule(squareModule)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package de.nowchess.chess.config
|
package de.nowchess.chess.config
|
||||||
|
|
||||||
|
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||||
import de.nowchess.api.dto.*
|
import de.nowchess.api.dto.*
|
||||||
|
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||||
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||||
|
|
||||||
@RegisterForReflection(
|
@RegisterForReflection(
|
||||||
@@ -18,6 +21,19 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
|||||||
classOf[LegalMovesResponseDto],
|
classOf[LegalMovesResponseDto],
|
||||||
classOf[OkResponseDto],
|
classOf[OkResponseDto],
|
||||||
classOf[PlayerInfoDto],
|
classOf[PlayerInfoDto],
|
||||||
|
classOf[GameContext],
|
||||||
|
classOf[Color],
|
||||||
|
classOf[Piece],
|
||||||
|
classOf[PieceType],
|
||||||
|
classOf[CastlingRights],
|
||||||
|
classOf[Square],
|
||||||
|
classOf[File],
|
||||||
|
classOf[Rank],
|
||||||
|
classOf[Move],
|
||||||
|
classOf[MoveType],
|
||||||
|
classOf[PromotionPiece],
|
||||||
|
classOf[GameResult],
|
||||||
|
classOf[DrawReason],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
class NativeReflectionConfig
|
class NativeReflectionConfig
|
||||||
|
|||||||
@@ -6,18 +6,21 @@ import de.nowchess.api.dto.*
|
|||||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||||
|
import de.nowchess.chess.client.IoServiceClient
|
||||||
import de.nowchess.chess.controller.Parser
|
import de.nowchess.chess.controller.Parser
|
||||||
import de.nowchess.chess.engine.GameEngine
|
import de.nowchess.chess.engine.GameEngine
|
||||||
import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException}
|
import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException}
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
|
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
|
||||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
import de.nowchess.io.fen.FenExporter
|
||||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
import de.nowchess.io.pgn.PgnExporter
|
||||||
|
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
|
||||||
import io.smallrye.mutiny.Multi
|
import io.smallrye.mutiny.Multi
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import jakarta.ws.rs.*
|
import jakarta.ws.rs.*
|
||||||
import jakarta.ws.rs.core.{MediaType, Response}
|
import jakarta.ws.rs.core.{MediaType, Response}
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
@@ -32,6 +35,10 @@ class GameResource:
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
var objectMapper: ObjectMapper = uninitialized
|
var objectMapper: ObjectMapper = uninitialized
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@RestClient
|
||||||
|
var ioClient: IoServiceClient = uninitialized
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
|
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
|
||||||
@@ -142,7 +149,6 @@ class GameResource:
|
|||||||
val black = playerInfoFrom(req.black, DefaultBlack)
|
val black = playerInfoFrom(req.black, DefaultBlack)
|
||||||
val entry = newEntry(GameContext.initial, white, black)
|
val entry = newEntry(GameContext.initial, white, black)
|
||||||
registry.store(entry)
|
registry.store(entry)
|
||||||
println(s"Created game ${entry.gameId}")
|
|
||||||
created(toGameFullDto(entry))
|
created(toGameFullDto(entry))
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -264,9 +270,7 @@ class GameResource:
|
|||||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
def importFen(body: ImportFenRequestDto): Response =
|
def importFen(body: ImportFenRequestDto): Response =
|
||||||
val ctx = FenParser.parseFen(body.fen) match
|
val ctx = ioClient.importFen(ImportFenRequest(body.fen))
|
||||||
case Left(err) => throw BadRequestException("INVALID_FEN", err, Some("fen"))
|
|
||||||
case Right(ctx) => ctx
|
|
||||||
val white = playerInfoFrom(body.white, DefaultWhite)
|
val white = playerInfoFrom(body.white, DefaultWhite)
|
||||||
val black = playerInfoFrom(body.black, DefaultBlack)
|
val black = playerInfoFrom(body.black, DefaultBlack)
|
||||||
val entry = newEntry(ctx, white, black)
|
val entry = newEntry(ctx, white, black)
|
||||||
@@ -278,11 +282,8 @@ class GameResource:
|
|||||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
def importPgn(body: ImportPgnRequestDto): Response =
|
def importPgn(body: ImportPgnRequestDto): Response =
|
||||||
val engine = GameEngine()
|
val ctx = ioClient.importPgn(ImportPgnRequest(body.pgn))
|
||||||
engine.loadGame(PgnParser, body.pgn) match
|
val entry = newEntry(ctx, DefaultWhite, DefaultBlack)
|
||||||
case Left(err) => throw BadRequestException("INVALID_PGN", err, Some("pgn"))
|
|
||||||
case Right(_) => ()
|
|
||||||
val entry = GameEntry(registry.generateId(), engine, DefaultWhite, DefaultBlack)
|
|
||||||
registry.store(entry)
|
registry.store(entry)
|
||||||
created(toGameFullDto(entry))
|
created(toGameFullDto(entry))
|
||||||
|
|
||||||
@@ -291,21 +292,12 @@ class GameResource:
|
|||||||
@Produces(Array(MediaType.TEXT_PLAIN))
|
@Produces(Array(MediaType.TEXT_PLAIN))
|
||||||
def exportFen(@PathParam("gameId") gameId: String): Response =
|
def exportFen(@PathParam("gameId") gameId: String): Response =
|
||||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||||
ok(FenExporter.exportGameContext(entry.engine.context))
|
ok(ioClient.exportFen(entry.engine.context))
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{gameId}/export/pgn")
|
@Path("/{gameId}/export/pgn")
|
||||||
@Produces(Array("application/x-chess-pgn"))
|
@Produces(Array("application/x-chess-pgn"))
|
||||||
def exportPgn(@PathParam("gameId") gameId: String): Response =
|
def exportPgn(@PathParam("gameId") gameId: String): Response =
|
||||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||||
val pgn = PgnExporter.exportGame(
|
ok(ioClient.exportPgn(entry.engine.context))
|
||||||
Map(
|
|
||||||
"Event" -> "NowChess game",
|
|
||||||
"White" -> entry.white.displayName,
|
|
||||||
"Black" -> entry.black.displayName,
|
|
||||||
"Result" -> "*",
|
|
||||||
),
|
|
||||||
entry.engine.context.moves,
|
|
||||||
)
|
|
||||||
ok(pgn)
|
|
||||||
// scalafix:on DisableSyntax.throw
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|||||||
+22
-1
@@ -1,11 +1,19 @@
|
|||||||
package de.nowchess.chess.resource
|
package de.nowchess.chess.resource
|
||||||
|
|
||||||
import de.nowchess.api.dto.*
|
import de.nowchess.api.dto.*
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.chess.client.IoServiceClient
|
||||||
import de.nowchess.chess.exception.BadRequestException
|
import de.nowchess.chess.exception.BadRequestException
|
||||||
|
import de.nowchess.io.fen.FenExporter
|
||||||
|
import de.nowchess.io.pgn.PgnParser
|
||||||
|
import io.quarkus.test.InjectMock
|
||||||
import io.quarkus.test.junit.QuarkusTest
|
import io.quarkus.test.junit.QuarkusTest
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import org.junit.jupiter.api.{DisplayName, Test}
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
|
import org.junit.jupiter.api.{BeforeEach, DisplayName, Test}
|
||||||
import org.junit.jupiter.api.Assertions.*
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.mockito.ArgumentMatchers.any
|
||||||
|
import org.mockito.Mockito.when
|
||||||
|
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
|
|
||||||
@@ -17,6 +25,19 @@ class GameResourceIntegrationTest:
|
|||||||
@Inject
|
@Inject
|
||||||
var resource: GameResource = uninitialized
|
var resource: GameResource = uninitialized
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
@RestClient
|
||||||
|
var ioClient: IoServiceClient = uninitialized
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
def setupMocks(): Unit =
|
||||||
|
when(ioClient.importFen(any())).thenReturn(GameContext.initial)
|
||||||
|
when(ioClient.importPgn(any())).thenReturn(
|
||||||
|
PgnParser.importGameContext("1. e4 c5").toOption.get,
|
||||||
|
)
|
||||||
|
when(ioClient.exportFen(any())).thenReturn(FenExporter.exportGameContext(GameContext.initial))
|
||||||
|
when(ioClient.exportPgn(any())).thenReturn("1. e4 c5")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("createGame returns 201")
|
@DisplayName("createGame returns 201")
|
||||||
def testCreateGame(): Unit =
|
def testCreateGame(): Unit =
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("scala")
|
id("scala")
|
||||||
id("org.scoverage") version "8.1"
|
id("org.scoverage") version "8.1"
|
||||||
|
id("io.quarkus")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "de.nowchess"
|
group = "de.nowchess"
|
||||||
@@ -28,6 +29,10 @@ tasks.withType<ScalaCompile> {
|
|||||||
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val quarkusPlatformGroupId: String by project
|
||||||
|
val quarkusPlatformArtifactId: String by project
|
||||||
|
val quarkusPlatformVersion: String by project
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
compileOnly("org.scala-lang:scala3-compiler_3") {
|
compileOnly("org.scala-lang:scala3-compiler_3") {
|
||||||
@@ -53,19 +58,51 @@ dependencies {
|
|||||||
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
||||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions["JACKSON"]!!}")
|
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions["JACKSON"]!!}")
|
||||||
|
|
||||||
|
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
||||||
|
implementation("io.quarkus:quarkus-rest")
|
||||||
|
implementation("io.quarkus:quarkus-rest-jackson")
|
||||||
|
implementation("io.quarkus:quarkus-arc")
|
||||||
|
implementation("io.quarkus:quarkus-config-yaml")
|
||||||
|
implementation("io.quarkus:quarkus-smallrye-health")
|
||||||
|
implementation("io.quarkus:quarkus-smallrye-openapi")
|
||||||
|
|
||||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||||
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
||||||
|
testImplementation("io.quarkus:quarkus-junit5")
|
||||||
|
testImplementation("io.rest-assured:rest-assured")
|
||||||
|
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
|
||||||
|
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
|
||||||
|
}
|
||||||
|
configurations.scoverage {
|
||||||
|
resolutionStrategy.eachDependency {
|
||||||
|
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
|
||||||
|
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<JavaCompile> {
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
options.compilerArgs.add("-parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Jar>().configureEach {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform {
|
useJUnitPlatform {
|
||||||
includeEngines("scalatest")
|
includeEngines("scalatest", "junit-jupiter")
|
||||||
testLogging {
|
testLogging {
|
||||||
events("skipped", "failed")
|
events("passed", "skipped", "failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalizedBy(tasks.reportScoverage)
|
finalizedBy(tasks.reportScoverage)
|
||||||
@@ -73,3 +110,6 @@ tasks.test {
|
|||||||
tasks.reportScoverage {
|
tasks.reportScoverage {
|
||||||
dependsOn(tasks.test)
|
dependsOn(tasks.test)
|
||||||
}
|
}
|
||||||
|
tasks.jar {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
quarkus:
|
||||||
|
http:
|
||||||
|
port: 8081
|
||||||
|
application:
|
||||||
|
name: nowchess-io
|
||||||
|
smallrye-openapi:
|
||||||
|
info-title: NowChess IO Service
|
||||||
|
info-version: 1.0.0
|
||||||
|
info-description: Chess notation import and export — FEN and PGN
|
||||||
|
path: /openapi
|
||||||
|
swagger-ui:
|
||||||
|
always-include: true
|
||||||
|
path: /swagger-ui
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.nowchess.io.json
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer}
|
||||||
|
import de.nowchess.api.board.Square
|
||||||
|
|
||||||
|
class SquareKeyDeserializer extends KeyDeserializer:
|
||||||
|
override def deserializeKey(key: String, ctx: DeserializationContext): AnyRef =
|
||||||
|
Square.fromAlgebraic(key).orNull
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.nowchess.io.json
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
|
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
|
||||||
|
import de.nowchess.api.board.Square
|
||||||
|
|
||||||
|
class SquareKeySerializer extends JsonSerializer[Square]:
|
||||||
|
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
|
||||||
|
gen.writeFieldName(value.toString)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.nowchess.io.service.config
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.Version
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
|
import de.nowchess.api.board.Square
|
||||||
|
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
|
||||||
|
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||||
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class JacksonConfig extends ObjectMapperCustomizer:
|
||||||
|
def customize(mapper: ObjectMapper): Unit =
|
||||||
|
mapper.registerModule(new DefaultScalaModule() {
|
||||||
|
override def version(): Version =
|
||||||
|
// scalafix:off DisableSyntax.null
|
||||||
|
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||||
|
// scalafix:on DisableSyntax.null
|
||||||
|
})
|
||||||
|
val squareModule = new SimpleModule()
|
||||||
|
squareModule.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||||
|
squareModule.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||||
|
mapper.registerModule(squareModule)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package de.nowchess.io.service.config
|
||||||
|
|
||||||
|
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||||
|
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||||
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
|
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||||
|
|
||||||
|
@RegisterForReflection(
|
||||||
|
targets = Array(
|
||||||
|
classOf[ImportFenRequest],
|
||||||
|
classOf[ImportPgnRequest],
|
||||||
|
classOf[IoErrorDto],
|
||||||
|
classOf[GameContext],
|
||||||
|
classOf[GameResult],
|
||||||
|
classOf[DrawReason],
|
||||||
|
classOf[Color],
|
||||||
|
classOf[Piece],
|
||||||
|
classOf[PieceType],
|
||||||
|
classOf[CastlingRights],
|
||||||
|
classOf[Square],
|
||||||
|
classOf[File],
|
||||||
|
classOf[Rank],
|
||||||
|
classOf[Move],
|
||||||
|
classOf[MoveType],
|
||||||
|
classOf[PromotionPiece],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class NativeReflectionConfig
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.nowchess.io.service.dto
|
||||||
|
|
||||||
|
case class ImportFenRequest(fen: String)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.nowchess.io.service.dto
|
||||||
|
|
||||||
|
case class ImportPgnRequest(pgn: String)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.nowchess.io.service.dto
|
||||||
|
|
||||||
|
case class IoErrorDto(code: String, message: String)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package de.nowchess.io.service.resource
|
||||||
|
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||||
|
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||||
|
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||||
|
import io.smallrye.mutiny.Uni
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
|
import jakarta.ws.rs.*
|
||||||
|
import jakarta.ws.rs.core.{MediaType, Response}
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.media.{Content, Schema}
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.responses.{APIResponse, APIResponses}
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag
|
||||||
|
|
||||||
|
@Path("/io")
|
||||||
|
@ApplicationScoped
|
||||||
|
@Tag(name = "IO", description = "Chess notation import and export")
|
||||||
|
class IoResource:
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/import/fen")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Operation(summary = "Import FEN", description = "Parse a FEN string into a GameContext")
|
||||||
|
@APIResponses(
|
||||||
|
Array(
|
||||||
|
new APIResponse(responseCode = "200", description = "Parsed GameContext"),
|
||||||
|
new APIResponse(responseCode = "400", description = "Invalid FEN"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def importFen(body: ImportFenRequest): Uni[Response] =
|
||||||
|
Uni.createFrom().item {
|
||||||
|
FenParser.parseFen(body.fen) match
|
||||||
|
case Left(err) =>
|
||||||
|
Response.status(400).entity(IoErrorDto("INVALID_FEN", err)).build()
|
||||||
|
case Right(ctx) =>
|
||||||
|
Response.ok(ctx).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/import/pgn")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Operation(summary = "Import PGN", description = "Parse a PGN string into a GameContext")
|
||||||
|
@APIResponses(
|
||||||
|
Array(
|
||||||
|
new APIResponse(responseCode = "200", description = "Parsed GameContext"),
|
||||||
|
new APIResponse(responseCode = "400", description = "Invalid PGN"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def importPgn(body: ImportPgnRequest): Uni[Response] =
|
||||||
|
Uni.createFrom().item {
|
||||||
|
PgnParser.importGameContext(body.pgn) match
|
||||||
|
case Left(err) =>
|
||||||
|
Response.status(400).entity(IoErrorDto("INVALID_PGN", err)).build()
|
||||||
|
case Right(ctx) =>
|
||||||
|
Response.ok(ctx).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/export/fen")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.TEXT_PLAIN))
|
||||||
|
@Operation(summary = "Export FEN", description = "Serialize a GameContext to FEN notation")
|
||||||
|
@APIResponse(responseCode = "200", description = "FEN string")
|
||||||
|
def exportFen(ctx: GameContext): Uni[Response] =
|
||||||
|
Uni.createFrom().item(Response.ok(FenExporter.exportGameContext(ctx)).build())
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/export/pgn")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array("application/x-chess-pgn"))
|
||||||
|
@Operation(summary = "Export PGN", description = "Serialize a GameContext to PGN notation")
|
||||||
|
@APIResponse(responseCode = "200", description = "PGN text")
|
||||||
|
def exportPgn(ctx: GameContext): Uni[Response] =
|
||||||
|
Uni.createFrom().item(Response.ok(PgnExporter.exportGameContext(ctx)).build())
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package de.nowchess.io.json
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.`type`.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
|
import de.nowchess.api.board.{File, Rank, Square}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class SquareKeyDeserializerTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
private def mapper: ObjectMapper =
|
||||||
|
val m = new ObjectMapper()
|
||||||
|
val mod = new SimpleModule()
|
||||||
|
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||||
|
m.registerModule(DefaultScalaModule)
|
||||||
|
m.registerModule(mod)
|
||||||
|
m
|
||||||
|
|
||||||
|
private def readMap(json: String): Map[Square, Int] =
|
||||||
|
mapper.readValue(json, new TypeReference[Map[Square, Int]] {})
|
||||||
|
|
||||||
|
test("deserializes valid algebraic key") {
|
||||||
|
val result = readMap("""{"e4":1}""")
|
||||||
|
result(Square(File.E, Rank.R4)) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
test("deserializes a1 corner") {
|
||||||
|
val result = readMap("""{"a1":1}""")
|
||||||
|
result(Square(File.A, Rank.R1)) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
test("deserializes h8 corner") {
|
||||||
|
val result = readMap("""{"h8":1}""")
|
||||||
|
result(Square(File.H, Rank.R8)) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
test("deserializes multiple squares") {
|
||||||
|
val result = readMap("""{"a1":1,"h8":2,"e4":3}""")
|
||||||
|
result(Square(File.A, Rank.R1)) shouldBe 1
|
||||||
|
result(Square(File.H, Rank.R8)) shouldBe 2
|
||||||
|
result(Square(File.E, Rank.R4)) shouldBe 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// scalafix:off DisableSyntax.null
|
||||||
|
test("deserializeKey returns null for invalid square") {
|
||||||
|
new SquareKeyDeserializer().deserializeKey("invalid", null) shouldBe null
|
||||||
|
}
|
||||||
|
|
||||||
|
test("deserializeKey returns null for wrong-length key") {
|
||||||
|
new SquareKeyDeserializer().deserializeKey("e44", null) shouldBe null
|
||||||
|
}
|
||||||
|
|
||||||
|
test("deserializeKey returns null for bad file") {
|
||||||
|
new SquareKeyDeserializer().deserializeKey("z4", null) shouldBe null
|
||||||
|
}
|
||||||
|
|
||||||
|
test("deserializeKey returns null for bad rank") {
|
||||||
|
new SquareKeyDeserializer().deserializeKey("e9", null) shouldBe null
|
||||||
|
}
|
||||||
|
// scalafix:on DisableSyntax.null
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package de.nowchess.io.json
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.`type`.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
|
import de.nowchess.api.board.{File, Rank, Square}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class SquareKeySerializerTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
private def mapper: ObjectMapper =
|
||||||
|
val m = new ObjectMapper()
|
||||||
|
val mod = new SimpleModule()
|
||||||
|
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||||
|
m.registerModule(DefaultScalaModule)
|
||||||
|
m.registerModule(mod)
|
||||||
|
m
|
||||||
|
|
||||||
|
test("serializes square as algebraic notation") {
|
||||||
|
val json = mapper.writeValueAsString(Map(Square(File.E, Rank.R4) -> 1))
|
||||||
|
json should include("\"e4\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
test("serializes a1 corner") {
|
||||||
|
val json = mapper.writeValueAsString(Map(Square(File.A, Rank.R1) -> 1))
|
||||||
|
json should include("\"a1\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
test("serializes h8 corner") {
|
||||||
|
val json = mapper.writeValueAsString(Map(Square(File.H, Rank.R8) -> 1))
|
||||||
|
json should include("\"h8\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
test("round-trips with SquareKeyDeserializer") {
|
||||||
|
val rt = {
|
||||||
|
val m = new ObjectMapper()
|
||||||
|
val mod = new SimpleModule()
|
||||||
|
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||||
|
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||||
|
m.registerModule(DefaultScalaModule)
|
||||||
|
m.registerModule(mod)
|
||||||
|
m
|
||||||
|
}
|
||||||
|
val original = Map(Square(File.D, Rank.R5) -> 99)
|
||||||
|
val json = rt.writeValueAsString(original)
|
||||||
|
val result = rt.readValue(json, new TypeReference[Map[Square, Int]] {})
|
||||||
|
result shouldBe original
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package de.nowchess.io.service.resource
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
|
import de.nowchess.api.board.Square
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
|
||||||
|
import io.quarkus.test.junit.QuarkusTest
|
||||||
|
import io.restassured.RestAssured
|
||||||
|
import io.restassured.http.ContentType
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
class IoResourceTest:
|
||||||
|
private lazy val testMapper: ObjectMapper =
|
||||||
|
val m = new ObjectMapper()
|
||||||
|
val mod = new SimpleModule()
|
||||||
|
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||||
|
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||||
|
m.registerModule(new DefaultScalaModule())
|
||||||
|
m.registerModule(mod)
|
||||||
|
m
|
||||||
|
|
||||||
|
private def contextJson(ctx: GameContext): String = testMapper.writeValueAsString(ctx)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def importFenReturns200(): Unit =
|
||||||
|
val resp = RestAssured
|
||||||
|
.`given`()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"}""")
|
||||||
|
.post("/io/import/fen")
|
||||||
|
assertEquals(200, resp.statusCode())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def importFenInvalidReturns400(): Unit =
|
||||||
|
val resp = RestAssured
|
||||||
|
.`given`()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""{"fen":"not-a-fen"}""")
|
||||||
|
.post("/io/import/fen")
|
||||||
|
assertEquals(400, resp.statusCode())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def importPgnReturns200(): Unit =
|
||||||
|
val resp = RestAssured
|
||||||
|
.`given`()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""{"pgn":"1. e4 e5"}""")
|
||||||
|
.post("/io/import/pgn")
|
||||||
|
assertEquals(200, resp.statusCode())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def importPgnInvalidReturns400(): Unit =
|
||||||
|
val resp = RestAssured
|
||||||
|
.`given`()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""{"pgn":"not valid pgn !!!###"}""")
|
||||||
|
.post("/io/import/pgn")
|
||||||
|
assertEquals(400, resp.statusCode())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def exportFenReturns200WithFen(): Unit =
|
||||||
|
val resp = RestAssured
|
||||||
|
.`given`()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(contextJson(GameContext.initial))
|
||||||
|
.post("/io/export/fen")
|
||||||
|
assertEquals(200, resp.statusCode())
|
||||||
|
assertTrue(resp.getBody.asString().contains("rnbqkbnr"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def exportPgnReturns200(): Unit =
|
||||||
|
val resp = RestAssured
|
||||||
|
.`given`()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(contextJson(GameContext.initial))
|
||||||
|
.post("/io/export/pgn")
|
||||||
|
assertEquals(200, resp.statusCode())
|
||||||
Reference in New Issue
Block a user