fix(rules): Serializers

Added serializers like in IO
This commit is contained in:
LQ63
2026-04-21 21:00:36 +02:00
committed by Janis
parent e82010bd22
commit 934568e716
4 changed files with 64 additions and 73 deletions
@@ -3,12 +3,6 @@ package de.nowchess.rules.dto
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move import de.nowchess.api.move.Move
case class ContextRequest(context: GameContext)
case class ContextSquareRequest(context: GameContext, square: String) case class ContextSquareRequest(context: GameContext, square: String)
case class ContextMoveRequest(context: GameContext, move: Move) case class ContextMoveRequest(context: GameContext, move: Move)
case class MovesResponse(moves: List[Move])
case class BooleanResponse(result: Boolean)
@@ -2,6 +2,7 @@ package de.nowchess.rules.resource
import de.nowchess.api.board.Square import de.nowchess.api.board.Square
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move
import de.nowchess.rules.dto.* import de.nowchess.rules.dto.*
import de.nowchess.rules.sets.DefaultRules import de.nowchess.rules.sets.DefaultRules
import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.context.ApplicationScoped
@@ -22,64 +23,64 @@ class RuleSetResource:
@Path("/candidate-moves") @Path("/candidate-moves")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def candidateMoves(req: ContextSquareRequest): MovesResponse = def candidateMoves(req: ContextSquareRequest): List[Move] =
MovesResponse(rules.candidateMoves(req.context)(parseSquare(req.square))) rules.candidateMoves(req.context)(parseSquare(req.square))
@POST @POST
@Path("/legal-moves") @Path("/legal-moves")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def legalMoves(req: ContextSquareRequest): MovesResponse = def legalMoves(req: ContextSquareRequest): List[Move] =
MovesResponse(rules.legalMoves(req.context)(parseSquare(req.square))) rules.legalMoves(req.context)(parseSquare(req.square))
@POST @POST
@Path("/all-legal-moves") @Path("/all-legal-moves")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def allLegalMoves(req: ContextRequest): MovesResponse = def allLegalMoves(ctx: GameContext): List[Move] =
MovesResponse(rules.allLegalMoves(req.context)) rules.allLegalMoves(ctx)
@POST @POST
@Path("/is-check") @Path("/is-check")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def isCheck(req: ContextRequest): BooleanResponse = def isCheck(ctx: GameContext): Boolean =
BooleanResponse(rules.isCheck(req.context)) rules.isCheck(ctx)
@POST @POST
@Path("/is-checkmate") @Path("/is-checkmate")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def isCheckmate(req: ContextRequest): BooleanResponse = def isCheckmate(ctx: GameContext): Boolean =
BooleanResponse(rules.isCheckmate(req.context)) rules.isCheckmate(ctx)
@POST @POST
@Path("/is-stalemate") @Path("/is-stalemate")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def isStalemate(req: ContextRequest): BooleanResponse = def isStalemate(ctx: GameContext): Boolean =
BooleanResponse(rules.isStalemate(req.context)) rules.isStalemate(ctx)
@POST @POST
@Path("/is-insufficient-material") @Path("/is-insufficient-material")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def isInsufficientMaterial(req: ContextRequest): BooleanResponse = def isInsufficientMaterial(ctx: GameContext): Boolean =
BooleanResponse(rules.isInsufficientMaterial(req.context)) rules.isInsufficientMaterial(ctx)
@POST @POST
@Path("/is-fifty-move-rule") @Path("/is-fifty-move-rule")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def isFiftyMoveRule(req: ContextRequest): BooleanResponse = def isFiftyMoveRule(ctx: GameContext): Boolean =
BooleanResponse(rules.isFiftyMoveRule(req.context)) rules.isFiftyMoveRule(ctx)
@POST @POST
@Path("/is-threefold-repetition") @Path("/is-threefold-repetition")
@Consumes(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON))
def isThreefoldRepetition(req: ContextRequest): BooleanResponse = def isThreefoldRepetition(ctx: GameContext): Boolean =
BooleanResponse(rules.isThreefoldRepetition(req.context)) rules.isThreefoldRepetition(ctx)
@POST @POST
@Path("/apply-move") @Path("/apply-move")
@@ -5,7 +5,7 @@ import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceTy
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move import de.nowchess.api.move.Move
import de.nowchess.rules.config.JacksonConfig import de.nowchess.rules.config.JacksonConfig
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest} import de.nowchess.rules.dto.{ContextMoveRequest, ContextSquareRequest}
import de.nowchess.rules.sets.DefaultRules import de.nowchess.rules.sets.DefaultRules
import io.quarkus.test.junit.QuarkusTest import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured import io.restassured.RestAssured
@@ -27,9 +27,6 @@ class RuleSetResourceTest:
private def toJson(value: AnyRef): String = mapper.writeValueAsString(value) private def toJson(value: AnyRef): String = mapper.writeValueAsString(value)
private def contextBody(ctx: GameContext): String =
toJson(ContextRequest(ctx))
private def contextSquareBody(ctx: GameContext, square: String): String = private def contextSquareBody(ctx: GameContext, square: String): String =
toJson(ContextSquareRequest(ctx, square)) toJson(ContextSquareRequest(ctx, square))
@@ -42,12 +39,12 @@ class RuleSetResourceTest:
def allLegalMoves_initialPositionHas20Moves(): Unit = def allLegalMoves_initialPositionHas20Moves(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial)) .body(toJson(GameContext.initial))
.when() .when()
.post("/api/rules/all-legal-moves") .post("/api/rules/all-legal-moves")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("moves.size()", is(20)) .body("size()", is(20))
// ── legal-moves ─────────────────────────────────────────────────── // ── legal-moves ───────────────────────────────────────────────────
@@ -60,7 +57,7 @@ class RuleSetResourceTest:
.post("/api/rules/legal-moves") .post("/api/rules/legal-moves")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("moves.size()", is(2)) .body("size()", is(2))
// ── candidate-moves ─────────────────────────────────────────────── // ── candidate-moves ───────────────────────────────────────────────
@@ -73,7 +70,7 @@ class RuleSetResourceTest:
.post("/api/rules/candidate-moves") .post("/api/rules/candidate-moves")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("moves.size()", is(2)) .body("size()", is(2))
// ── is-check ────────────────────────────────────────────────────── // ── is-check ──────────────────────────────────────────────────────
@@ -81,23 +78,23 @@ class RuleSetResourceTest:
def isCheck_falseForInitialPosition(): Unit = def isCheck_falseForInitialPosition(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial)) .body(toJson(GameContext.initial))
.when() .when()
.post("/api/rules/is-check") .post("/api/rules/is-check")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(false)) .body(is("false"))
@Test @Test
def isCheck_trueWhenKingAttacked(): Unit = def isCheck_trueWhenKingAttacked(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(buildCheckContext())) .body(toJson(buildCheckContext()))
.when() .when()
.post("/api/rules/is-check") .post("/api/rules/is-check")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(true)) .body(is("true"))
// ── is-checkmate ────────────────────────────────────────────────── // ── is-checkmate ──────────────────────────────────────────────────
@@ -105,23 +102,23 @@ class RuleSetResourceTest:
def isCheckmate_falseForInitialPosition(): Unit = def isCheckmate_falseForInitialPosition(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial)) .body(toJson(GameContext.initial))
.when() .when()
.post("/api/rules/is-checkmate") .post("/api/rules/is-checkmate")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(false)) .body(is("false"))
@Test @Test
def isCheckmate_trueForFoolsMate(): Unit = def isCheckmate_trueForFoolsMate(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(buildFoolsMate())) .body(toJson(buildFoolsMate()))
.when() .when()
.post("/api/rules/is-checkmate") .post("/api/rules/is-checkmate")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(true)) .body(is("true"))
// ── is-stalemate ────────────────────────────────────────────────── // ── is-stalemate ──────────────────────────────────────────────────
@@ -129,23 +126,23 @@ class RuleSetResourceTest:
def isStalemate_falseForInitialPosition(): Unit = def isStalemate_falseForInitialPosition(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial)) .body(toJson(GameContext.initial))
.when() .when()
.post("/api/rules/is-stalemate") .post("/api/rules/is-stalemate")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(false)) .body(is("false"))
@Test @Test
def isStalemate_trueForStalematePosition(): Unit = def isStalemate_trueForStalematePosition(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(buildStalemateContext())) .body(toJson(buildStalemateContext()))
.when() .when()
.post("/api/rules/is-stalemate") .post("/api/rules/is-stalemate")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(true)) .body(is("true"))
// ── is-insufficient-material ────────────────────────────────────── // ── is-insufficient-material ──────────────────────────────────────
@@ -153,23 +150,23 @@ class RuleSetResourceTest:
def isInsufficientMaterial_falseForInitialPosition(): Unit = def isInsufficientMaterial_falseForInitialPosition(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial)) .body(toJson(GameContext.initial))
.when() .when()
.post("/api/rules/is-insufficient-material") .post("/api/rules/is-insufficient-material")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(false)) .body(is("false"))
@Test @Test
def isInsufficientMaterial_trueForKingsOnly(): Unit = def isInsufficientMaterial_trueForKingsOnly(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(buildKingsOnlyContext())) .body(toJson(buildKingsOnlyContext()))
.when() .when()
.post("/api/rules/is-insufficient-material") .post("/api/rules/is-insufficient-material")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(true)) .body(is("true"))
// ── is-fifty-move-rule ──────────────────────────────────────────── // ── is-fifty-move-rule ────────────────────────────────────────────
@@ -177,23 +174,23 @@ class RuleSetResourceTest:
def isFiftyMoveRule_falseForInitialPosition(): Unit = def isFiftyMoveRule_falseForInitialPosition(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial)) .body(toJson(GameContext.initial))
.when() .when()
.post("/api/rules/is-fifty-move-rule") .post("/api/rules/is-fifty-move-rule")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(false)) .body(is("false"))
@Test @Test
def isFiftyMoveRule_trueWhenClockAt100(): Unit = def isFiftyMoveRule_trueWhenClockAt100(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial.copy(halfMoveClock = 100))) .body(toJson(GameContext.initial.copy(halfMoveClock = 100)))
.when() .when()
.post("/api/rules/is-fifty-move-rule") .post("/api/rules/is-fifty-move-rule")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(true)) .body(is("true"))
// ── is-threefold-repetition ─────────────────────────────────────── // ── is-threefold-repetition ───────────────────────────────────────
@@ -201,23 +198,23 @@ class RuleSetResourceTest:
def isThreefoldRepetition_falseForInitialPosition(): Unit = def isThreefoldRepetition_falseForInitialPosition(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(GameContext.initial)) .body(toJson(GameContext.initial))
.when() .when()
.post("/api/rules/is-threefold-repetition") .post("/api/rules/is-threefold-repetition")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(false)) .body(is("false"))
@Test @Test
def isThreefoldRepetition_trueAfterRepeatedMoves(): Unit = def isThreefoldRepetition_trueAfterRepeatedMoves(): Unit =
request() request()
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(contextBody(buildThreefoldContext())) .body(toJson(buildThreefoldContext()))
.when() .when()
.post("/api/rules/is-threefold-repetition") .post("/api/rules/is-threefold-repetition")
.`then`() .`then`()
.statusCode(200) .statusCode(200)
.body("result", is(true)) .body(is("true"))
// ── apply-move ──────────────────────────────────────────────────── // ── apply-move ────────────────────────────────────────────────────
@@ -3,7 +3,7 @@ package de.nowchess.rules.resource
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square} import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move import de.nowchess.api.move.Move
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest} import de.nowchess.rules.dto.{ContextMoveRequest, ContextSquareRequest}
import de.nowchess.rules.sets.DefaultRules import de.nowchess.rules.sets.DefaultRules
import jakarta.ws.rs.BadRequestException import jakarta.ws.rs.BadRequestException
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
@@ -14,7 +14,6 @@ class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
private val resource = new RuleSetResource() private val resource = new RuleSetResource()
private val rules = DefaultRules private val rules = DefaultRules
private def ctx(g: GameContext) = ContextRequest(g)
private def ctxSq(g: GameContext, sq: String) = ContextSquareRequest(g, sq) private def ctxSq(g: GameContext, sq: String) = ContextSquareRequest(g, sq)
private def ctxMv(g: GameContext, m: Move) = ContextMoveRequest(g, m) private def ctxMv(g: GameContext, m: Move) = ContextMoveRequest(g, m)
@@ -76,12 +75,12 @@ class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
// ── allLegalMoves ───────────────────────────────────────────────── // ── allLegalMoves ─────────────────────────────────────────────────
test("allLegalMoves returns 20 moves for initial position"): test("allLegalMoves returns 20 moves for initial position"):
resource.allLegalMoves(ctx(GameContext.initial)).moves should have size 20 resource.allLegalMoves(GameContext.initial) should have size 20
// ── legalMoves ──────────────────────────────────────────────────── // ── legalMoves ────────────────────────────────────────────────────
test("legalMoves returns 2 moves for e2 pawn"): test("legalMoves returns 2 moves for e2 pawn"):
resource.legalMoves(ctxSq(GameContext.initial, "e2")).moves should have size 2 resource.legalMoves(ctxSq(GameContext.initial, "e2")) should have size 2
test("legalMoves throws BadRequestException for invalid square"): test("legalMoves throws BadRequestException for invalid square"):
an[BadRequestException] should be thrownBy an[BadRequestException] should be thrownBy
@@ -90,7 +89,7 @@ class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
// ── candidateMoves ──────────────────────────────────────────────── // ── candidateMoves ────────────────────────────────────────────────
test("candidateMoves returns moves for e2 pawn"): test("candidateMoves returns moves for e2 pawn"):
resource.candidateMoves(ctxSq(GameContext.initial, "e2")).moves should not be empty resource.candidateMoves(ctxSq(GameContext.initial, "e2")) should not be empty
test("candidateMoves throws BadRequestException for invalid square"): test("candidateMoves throws BadRequestException for invalid square"):
an[BadRequestException] should be thrownBy an[BadRequestException] should be thrownBy
@@ -99,50 +98,50 @@ class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
// ── isCheck ─────────────────────────────────────────────────────── // ── isCheck ───────────────────────────────────────────────────────
test("isCheck returns false for initial position"): test("isCheck returns false for initial position"):
resource.isCheck(ctx(GameContext.initial)).result shouldBe false resource.isCheck(GameContext.initial) shouldBe false
test("isCheck returns true when king is attacked"): test("isCheck returns true when king is attacked"):
resource.isCheck(ctx(checkContext())).result shouldBe true resource.isCheck(checkContext()) shouldBe true
// ── isCheckmate ─────────────────────────────────────────────────── // ── isCheckmate ───────────────────────────────────────────────────
test("isCheckmate returns false for initial position"): test("isCheckmate returns false for initial position"):
resource.isCheckmate(ctx(GameContext.initial)).result shouldBe false resource.isCheckmate(GameContext.initial) shouldBe false
test("isCheckmate returns true for Fool's mate"): test("isCheckmate returns true for Fool's mate"):
resource.isCheckmate(ctx(foolsMate())).result shouldBe true resource.isCheckmate(foolsMate()) shouldBe true
// ── isStalemate ─────────────────────────────────────────────────── // ── isStalemate ───────────────────────────────────────────────────
test("isStalemate returns false for initial position"): test("isStalemate returns false for initial position"):
resource.isStalemate(ctx(GameContext.initial)).result shouldBe false resource.isStalemate(GameContext.initial) shouldBe false
test("isStalemate returns true for stalemate position"): test("isStalemate returns true for stalemate position"):
resource.isStalemate(ctx(stalemateContext())).result shouldBe true resource.isStalemate(stalemateContext()) shouldBe true
// ── isInsufficientMaterial ──────────────────────────────────────── // ── isInsufficientMaterial ────────────────────────────────────────
test("isInsufficientMaterial returns false for initial position"): test("isInsufficientMaterial returns false for initial position"):
resource.isInsufficientMaterial(ctx(GameContext.initial)).result shouldBe false resource.isInsufficientMaterial(GameContext.initial) shouldBe false
test("isInsufficientMaterial returns true for kings only"): test("isInsufficientMaterial returns true for kings only"):
resource.isInsufficientMaterial(ctx(kingsOnlyContext())).result shouldBe true resource.isInsufficientMaterial(kingsOnlyContext()) shouldBe true
// ── isFiftyMoveRule ─────────────────────────────────────────────── // ── isFiftyMoveRule ───────────────────────────────────────────────
test("isFiftyMoveRule returns false for initial position"): test("isFiftyMoveRule returns false for initial position"):
resource.isFiftyMoveRule(ctx(GameContext.initial)).result shouldBe false resource.isFiftyMoveRule(GameContext.initial) shouldBe false
test("isFiftyMoveRule returns true when halfMoveClock is 100"): test("isFiftyMoveRule returns true when halfMoveClock is 100"):
resource.isFiftyMoveRule(ctx(GameContext.initial.copy(halfMoveClock = 100))).result shouldBe true resource.isFiftyMoveRule(GameContext.initial.copy(halfMoveClock = 100)) shouldBe true
// ── isThreefoldRepetition ───────────────────────────────────────── // ── isThreefoldRepetition ─────────────────────────────────────────
test("isThreefoldRepetition returns false for initial position"): test("isThreefoldRepetition returns false for initial position"):
resource.isThreefoldRepetition(ctx(GameContext.initial)).result shouldBe false resource.isThreefoldRepetition(GameContext.initial) shouldBe false
test("isThreefoldRepetition returns true after repeated moves"): test("isThreefoldRepetition returns true after repeated moves"):
resource.isThreefoldRepetition(ctx(threefoldContext())).result shouldBe true resource.isThreefoldRepetition(threefoldContext()) shouldBe true
// ── applyMove ───────────────────────────────────────────────────── // ── applyMove ─────────────────────────────────────────────────────