feat: NCS-53 changed IO to MicroService for easier scaling

This commit is contained in:
2026-04-21 14:15:44 +02:00
parent d7f7c37111
commit e5a6cc30eb
45 changed files with 547 additions and 29 deletions
@@ -1,3 +1,8 @@
greeting:
message: "hello"
quarkus:
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.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)
@@ -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
@@ -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