diff --git a/.idea/scala_compiler.xml b/.idea/scala_compiler.xml
index a99d21e..5c60cfe 100644
--- a/.idea/scala_compiler.xml
+++ b/.idea/scala_compiler.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/bruno/draw/01 Offer Draw.bru b/bruno/board/draw/01 Offer Draw.bru
similarity index 100%
rename from bruno/draw/01 Offer Draw.bru
rename to bruno/board/draw/01 Offer Draw.bru
diff --git a/bruno/draw/02 Accept Draw.bru b/bruno/board/draw/02 Accept Draw.bru
similarity index 100%
rename from bruno/draw/02 Accept Draw.bru
rename to bruno/board/draw/02 Accept Draw.bru
diff --git a/bruno/draw/03 Decline Draw.bru b/bruno/board/draw/03 Decline Draw.bru
similarity index 100%
rename from bruno/draw/03 Decline Draw.bru
rename to bruno/board/draw/03 Decline Draw.bru
diff --git a/bruno/draw/04 Claim Draw.bru b/bruno/board/draw/04 Claim Draw.bru
similarity index 100%
rename from bruno/draw/04 Claim Draw.bru
rename to bruno/board/draw/04 Claim Draw.bru
diff --git a/bruno/draw/folder.bru b/bruno/board/draw/folder.bru
similarity index 100%
rename from bruno/draw/folder.bru
rename to bruno/board/draw/folder.bru
diff --git a/bruno/export/01 Export FEN.bru b/bruno/board/export/01 Export FEN.bru
similarity index 100%
rename from bruno/export/01 Export FEN.bru
rename to bruno/board/export/01 Export FEN.bru
diff --git a/bruno/export/02 Export PGN.bru b/bruno/board/export/02 Export PGN.bru
similarity index 100%
rename from bruno/export/02 Export PGN.bru
rename to bruno/board/export/02 Export PGN.bru
diff --git a/bruno/export/folder.bru b/bruno/board/export/folder.bru
similarity index 100%
rename from bruno/export/folder.bru
rename to bruno/board/export/folder.bru
diff --git a/bruno/game/01 Create Game.bru b/bruno/board/game/01 Create Game.bru
similarity index 100%
rename from bruno/game/01 Create Game.bru
rename to bruno/board/game/01 Create Game.bru
diff --git a/bruno/game/02 Get Game.bru b/bruno/board/game/02 Get Game.bru
similarity index 100%
rename from bruno/game/02 Get Game.bru
rename to bruno/board/game/02 Get Game.bru
diff --git a/bruno/game/03 Stream Game.bru b/bruno/board/game/03 Stream Game.bru
similarity index 100%
rename from bruno/game/03 Stream Game.bru
rename to bruno/board/game/03 Stream Game.bru
diff --git a/bruno/game/04 Resign.bru b/bruno/board/game/04 Resign.bru
similarity index 100%
rename from bruno/game/04 Resign.bru
rename to bruno/board/game/04 Resign.bru
diff --git a/bruno/game/folder.bru b/bruno/board/game/folder.bru
similarity index 100%
rename from bruno/game/folder.bru
rename to bruno/board/game/folder.bru
diff --git a/bruno/import/01 Import FEN.bru b/bruno/board/import/01 Import FEN.bru
similarity index 100%
rename from bruno/import/01 Import FEN.bru
rename to bruno/board/import/01 Import FEN.bru
diff --git a/bruno/import/02 Import PGN.bru b/bruno/board/import/02 Import PGN.bru
similarity index 100%
rename from bruno/import/02 Import PGN.bru
rename to bruno/board/import/02 Import PGN.bru
diff --git a/bruno/import/folder.bru b/bruno/board/import/folder.bru
similarity index 100%
rename from bruno/import/folder.bru
rename to bruno/board/import/folder.bru
diff --git a/bruno/move/01 Make Move.bru b/bruno/board/move/01 Make Move.bru
similarity index 100%
rename from bruno/move/01 Make Move.bru
rename to bruno/board/move/01 Make Move.bru
diff --git a/bruno/move/02 Get Legal Moves.bru b/bruno/board/move/02 Get Legal Moves.bru
similarity index 100%
rename from bruno/move/02 Get Legal Moves.bru
rename to bruno/board/move/02 Get Legal Moves.bru
diff --git a/bruno/move/03 Undo Move.bru b/bruno/board/move/03 Undo Move.bru
similarity index 100%
rename from bruno/move/03 Undo Move.bru
rename to bruno/board/move/03 Undo Move.bru
diff --git a/bruno/move/04 Redo Move.bru b/bruno/board/move/04 Redo Move.bru
similarity index 100%
rename from bruno/move/04 Redo Move.bru
rename to bruno/board/move/04 Redo Move.bru
diff --git a/bruno/move/folder.bru b/bruno/board/move/folder.bru
similarity index 100%
rename from bruno/move/folder.bru
rename to bruno/board/move/folder.bru
diff --git a/bruno/environments/local.bru b/bruno/environments/local.bru
index 85aff34..d0f7638 100644
--- a/bruno/environments/local.bru
+++ b/bruno/environments/local.bru
@@ -1,3 +1,4 @@
vars {
baseUrl: http://localhost:8080
+ ioBaseUrl: http://localhost:8081
}
diff --git a/bruno/io/export/01 Export FEN.bru b/bruno/io/export/01 Export FEN.bru
new file mode 100644
index 0000000..27ab0d7
--- /dev/null
+++ b/bruno/io/export/01 Export FEN.bru
@@ -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"}
+ }
+ }
+}
diff --git a/bruno/io/export/02 Export PGN.bru b/bruno/io/export/02 Export PGN.bru
new file mode 100644
index 0000000..e1395a3
--- /dev/null
+++ b/bruno/io/export/02 Export PGN.bru
@@ -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"}
+ }
+ }
+}
diff --git a/bruno/io/export/folder.bru b/bruno/io/export/folder.bru
new file mode 100644
index 0000000..ba07dba
--- /dev/null
+++ b/bruno/io/export/folder.bru
@@ -0,0 +1,4 @@
+meta {
+ name: export
+ seq: 2
+}
diff --git a/bruno/io/import/01 Import FEN.bru b/bruno/io/import/01 Import FEN.bru
new file mode 100644
index 0000000..ec901c6
--- /dev/null
+++ b/bruno/io/import/01 Import FEN.bru
@@ -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"
+ }
+}
diff --git a/bruno/io/import/02 Import PGN.bru b/bruno/io/import/02 Import PGN.bru
new file mode 100644
index 0000000..b7e038d
--- /dev/null
+++ b/bruno/io/import/02 Import PGN.bru
@@ -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 *"
+ }
+}
diff --git a/bruno/io/import/folder.bru b/bruno/io/import/folder.bru
new file mode 100644
index 0000000..86dcb7c
--- /dev/null
+++ b/bruno/io/import/folder.bru
@@ -0,0 +1,4 @@
+meta {
+ name: import
+ seq: 1
+}
diff --git a/modules/bot/build.gradle.kts b/modules/bot/build.gradle.kts
index 8041789..b111ec5 100644
--- a/modules/bot/build.gradle.kts
+++ b/modules/bot/build.gradle.kts
@@ -49,10 +49,10 @@ dependencies {
}
implementation(project(":modules:api"))
- implementation(project(":modules:io"))
implementation(project(":modules:rule"))
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
+ testImplementation(project(":modules:io"))
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
diff --git a/modules/core/src/main/resources/application.yml b/modules/core/src/main/resources/application.yml
index 0d92446..1663bc0 100644
--- a/modules/core/src/main/resources/application.yml
+++ b/modules/core/src/main/resources/application.yml
@@ -1,3 +1,8 @@
-greeting:
- message: "hello"
-
+quarkus:
+ http:
+ port: 8080
+ application:
+ name: nowchess-core
+ rest-client:
+ io-service:
+ url: http://localhost:8081
diff --git a/modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala b/modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala
new file mode 100644
index 0000000..dac4c3d
--- /dev/null
+++ b/modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala
@@ -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
diff --git a/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala b/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala
index b252cec..816cb0b 100644
--- a/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala
+++ b/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala
@@ -2,7 +2,10 @@ package de.nowchess.chess.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
@@ -15,3 +18,7 @@ class JacksonConfig extends ObjectMapperCustomizer:
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)
diff --git a/modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala b/modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala
index c8ce0fe..90f7241 100644
--- a/modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala
+++ b/modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala
@@ -1,6 +1,9 @@
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.game.{DrawReason, GameContext, GameResult}
+import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
@@ -18,6 +21,21 @@ import io.quarkus.runtime.annotations.RegisterForReflection
classOf[LegalMovesResponseDto],
classOf[OkResponseDto],
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
diff --git a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala
index e8258bb..ece9752 100644
--- a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala
+++ b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala
@@ -6,18 +6,21 @@ import de.nowchess.api.dto.*
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
+import de.nowchess.chess.client.IoServiceClient
import de.nowchess.chess.controller.Parser
import de.nowchess.chess.engine.GameEngine
import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException}
import de.nowchess.chess.observer.*
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
-import de.nowchess.io.fen.{FenExporter, FenParser}
-import de.nowchess.io.pgn.{PgnExporter, PgnParser}
+import de.nowchess.io.fen.FenExporter
+import de.nowchess.io.pgn.PgnExporter
+import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
import io.smallrye.mutiny.Multi
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
+import org.eclipse.microprofile.rest.client.inject.RestClient
import java.util.concurrent.atomic.AtomicReference
import scala.compiletime.uninitialized
@@ -32,6 +35,10 @@ class GameResource:
@Inject
var objectMapper: ObjectMapper = uninitialized
+
+ @Inject
+ @RestClient
+ var ioClient: IoServiceClient = uninitialized
// scalafix:on DisableSyntax.var
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
@@ -142,7 +149,6 @@ class GameResource:
val black = playerInfoFrom(req.black, DefaultBlack)
val entry = newEntry(GameContext.initial, white, black)
registry.store(entry)
- println(s"Created game ${entry.gameId}")
created(toGameFullDto(entry))
@GET
@@ -264,9 +270,7 @@ class GameResource:
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def importFen(body: ImportFenRequestDto): Response =
- val ctx = FenParser.parseFen(body.fen) match
- case Left(err) => throw BadRequestException("INVALID_FEN", err, Some("fen"))
- case Right(ctx) => ctx
+ val ctx = ioClient.importFen(ImportFenRequest(body.fen))
val white = playerInfoFrom(body.white, DefaultWhite)
val black = playerInfoFrom(body.black, DefaultBlack)
val entry = newEntry(ctx, white, black)
@@ -278,11 +282,8 @@ class GameResource:
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def importPgn(body: ImportPgnRequestDto): Response =
- val engine = GameEngine()
- engine.loadGame(PgnParser, body.pgn) match
- case Left(err) => throw BadRequestException("INVALID_PGN", err, Some("pgn"))
- case Right(_) => ()
- val entry = GameEntry(registry.generateId(), engine, DefaultWhite, DefaultBlack)
+ val ctx = ioClient.importPgn(ImportPgnRequest(body.pgn))
+ val entry = newEntry(ctx, DefaultWhite, DefaultBlack)
registry.store(entry)
created(toGameFullDto(entry))
@@ -291,21 +292,12 @@ class GameResource:
@Produces(Array(MediaType.TEXT_PLAIN))
def exportFen(@PathParam("gameId") gameId: String): Response =
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
- ok(FenExporter.exportGameContext(entry.engine.context))
+ ok(ioClient.exportFen(entry.engine.context))
@GET
@Path("/{gameId}/export/pgn")
@Produces(Array("application/x-chess-pgn"))
def exportPgn(@PathParam("gameId") gameId: String): Response =
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
- val pgn = PgnExporter.exportGame(
- Map(
- "Event" -> "NowChess game",
- "White" -> entry.white.displayName,
- "Black" -> entry.black.displayName,
- "Result" -> "*",
- ),
- entry.engine.context.moves,
- )
- ok(pgn)
+ ok(ioClient.exportPgn(entry.engine.context))
// scalafix:on DisableSyntax.throw
diff --git a/modules/io/build.gradle.kts b/modules/io/build.gradle.kts
index 84475e4..9480fa1 100644
--- a/modules/io/build.gradle.kts
+++ b/modules/io/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
+ id("io.quarkus")
}
group = "de.nowchess"
@@ -28,6 +29,10 @@ tasks.withType {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
+val quarkusPlatformGroupId: String by project
+val quarkusPlatformArtifactId: String by project
+val quarkusPlatformVersion: String by project
+
dependencies {
compileOnly("org.scala-lang:scala3-compiler_3") {
@@ -53,19 +58,50 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
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("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
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.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 {
+ options.encoding = "UTF-8"
+ options.compilerArgs.add("-parameters")
+}
+
+tasks.withType().configureEach {
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.test {
useJUnitPlatform {
- includeEngines("scalatest")
+ includeEngines("scalatest", "junit-jupiter")
testLogging {
- events("skipped", "failed")
+ events("passed", "skipped", "failed")
}
}
finalizedBy(tasks.reportScoverage)
@@ -73,3 +109,6 @@ tasks.test {
tasks.reportScoverage {
dependsOn(tasks.test)
}
+tasks.jar {
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+}
diff --git a/modules/io/src/main/resources/application.yml b/modules/io/src/main/resources/application.yml
new file mode 100644
index 0000000..b80a867
--- /dev/null
+++ b/modules/io/src/main/resources/application.yml
@@ -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
diff --git a/modules/io/src/main/scala/de/nowchess/io/json/SquareKeyDeserializer.scala b/modules/io/src/main/scala/de/nowchess/io/json/SquareKeyDeserializer.scala
new file mode 100644
index 0000000..223f077
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/json/SquareKeyDeserializer.scala
@@ -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
diff --git a/modules/io/src/main/scala/de/nowchess/io/json/SquareKeySerializer.scala b/modules/io/src/main/scala/de/nowchess/io/json/SquareKeySerializer.scala
new file mode 100644
index 0000000..93bcbad
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/json/SquareKeySerializer.scala
@@ -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)
diff --git a/modules/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala b/modules/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala
new file mode 100644
index 0000000..b0a0c5c
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala
@@ -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)
diff --git a/modules/io/src/main/scala/de/nowchess/io/service/config/NativeReflectionConfig.scala b/modules/io/src/main/scala/de/nowchess/io/service/config/NativeReflectionConfig.scala
new file mode 100644
index 0000000..a9a15e8
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/service/config/NativeReflectionConfig.scala
@@ -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
diff --git a/modules/io/src/main/scala/de/nowchess/io/service/dto/ImportFenRequest.scala b/modules/io/src/main/scala/de/nowchess/io/service/dto/ImportFenRequest.scala
new file mode 100644
index 0000000..fe7a139
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/service/dto/ImportFenRequest.scala
@@ -0,0 +1,3 @@
+package de.nowchess.io.service.dto
+
+case class ImportFenRequest(fen: String)
diff --git a/modules/io/src/main/scala/de/nowchess/io/service/dto/ImportPgnRequest.scala b/modules/io/src/main/scala/de/nowchess/io/service/dto/ImportPgnRequest.scala
new file mode 100644
index 0000000..83ed7f7
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/service/dto/ImportPgnRequest.scala
@@ -0,0 +1,3 @@
+package de.nowchess.io.service.dto
+
+case class ImportPgnRequest(pgn: String)
diff --git a/modules/io/src/main/scala/de/nowchess/io/service/dto/IoErrorDto.scala b/modules/io/src/main/scala/de/nowchess/io/service/dto/IoErrorDto.scala
new file mode 100644
index 0000000..04c9805
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/service/dto/IoErrorDto.scala
@@ -0,0 +1,3 @@
+package de.nowchess.io.service.dto
+
+case class IoErrorDto(code: String, message: String)
diff --git a/modules/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala b/modules/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala
new file mode 100644
index 0000000..8b08ff2
--- /dev/null
+++ b/modules/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala
@@ -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())