fix(rules): Serializers
Build & Test (NowChessSystems) TeamCity build failed

Added serializers like in IO
This commit is contained in:
LQ63
2026-04-21 20:42:24 +02:00
parent 4a69198e3d
commit 74f846c95c
15 changed files with 251 additions and 383 deletions
@@ -1,14 +1,30 @@
package de.nowchess.rules.config
import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.api.board.{File, Rank, Square}
import de.nowchess.api.move.MoveType
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JacksonConfigTest extends AnyFunSuite with Matchers:
test("customize registers DefaultScalaModule enabling Option serialization"):
val config = new JacksonConfig()
val mapper = new ObjectMapper()
config.customize(mapper)
private val mapper: ObjectMapper =
val m = new ObjectMapper()
new JacksonConfig().customize(m)
m
test("customize enables Option serialization via DefaultScalaModule"):
mapper.writeValueAsString(None) shouldBe "null"
mapper.writeValueAsString(Some("hello")) shouldBe """"hello""""
test("customize registers SquareSerializer"):
mapper.writeValueAsString(Square(File.E, Rank.R4)) shouldBe """"e4""""
test("customize registers MoveTypeSerializer"):
mapper.writeValueAsString(MoveType.CastleKingside) shouldBe """{"type":"castleKingside"}"""
test("customize registers SquareDeserializer"):
mapper.readValue(""""e4"""", classOf[Square]) shouldBe Square(File.E, Rank.R4)
test("customize registers MoveTypeDeserializer"):
mapper.readValue("""{"type":"enPassant"}""", classOf[MoveType]) shouldBe MoveType.EnPassant
@@ -1,174 +0,0 @@
package de.nowchess.rules.dto
import de.nowchess.api.board.{Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class DtoMapperTest extends AnyFunSuite with Matchers:
// ── toColor ────────────────────────────────────────────────────────
test("toColor converts White"):
DtoMapper.toColor("White") shouldBe Right(Color.White)
test("toColor converts Black"):
DtoMapper.toColor("Black") shouldBe Right(Color.Black)
test("toColor rejects unknown"):
DtoMapper.toColor("Red") shouldBe a[Left[?, ?]]
// ── toPieceType ────────────────────────────────────────────────────
test("toPieceType converts all piece types"):
DtoMapper.toPieceType("Pawn") shouldBe Right(PieceType.Pawn)
DtoMapper.toPieceType("Knight") shouldBe Right(PieceType.Knight)
DtoMapper.toPieceType("Bishop") shouldBe Right(PieceType.Bishop)
DtoMapper.toPieceType("Rook") shouldBe Right(PieceType.Rook)
DtoMapper.toPieceType("Queen") shouldBe Right(PieceType.Queen)
DtoMapper.toPieceType("King") shouldBe Right(PieceType.King)
test("toPieceType rejects unknown"):
DtoMapper.toPieceType("Dragon") shouldBe a[Left[?, ?]]
// ── toSquare ───────────────────────────────────────────────────────
test("toSquare converts valid algebraic"):
DtoMapper.toSquare("e4") shouldBe Right(Square(File.E, Rank.R4))
test("toSquare rejects invalid"):
DtoMapper.toSquare("z9") shouldBe a[Left[?, ?]]
// ── toMoveType ────────────────────────────────────────────────────
test("toMoveType converts normal non-capture"):
DtoMapper.toMoveType(MoveDto("e2", "e4", "normal", None)) shouldBe Right(MoveType.Normal(false))
test("toMoveType converts capture"):
DtoMapper.toMoveType(MoveDto("e2", "d3", "capture", None)) shouldBe Right(MoveType.Normal(true))
test("toMoveType converts castleKingside"):
DtoMapper.toMoveType(MoveDto("e1", "g1", "castleKingside", None)) shouldBe Right(MoveType.CastleKingside)
test("toMoveType converts castleQueenside"):
DtoMapper.toMoveType(MoveDto("e1", "c1", "castleQueenside", None)) shouldBe Right(MoveType.CastleQueenside)
test("toMoveType converts enPassant"):
DtoMapper.toMoveType(MoveDto("e5", "d6", "enPassant", None)) shouldBe Right(MoveType.EnPassant)
test("toMoveType converts all promotion pieces"):
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Queen"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Queen),
)
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Rook"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Rook),
)
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Bishop"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Bishop),
)
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Knight"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Knight),
)
test("toMoveType rejects promotion without piece"):
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", None)) shouldBe a[Left[?, ?]]
test("toMoveType rejects promotion with unknown piece"):
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Pawn"))) shouldBe a[Left[?, ?]]
test("toMoveType rejects unknown type"):
DtoMapper.toMoveType(MoveDto("e2", "e4", "unknown", None)) shouldBe a[Left[?, ?]]
// ── toBoard ───────────────────────────────────────────────────────
test("toBoard builds valid board"):
val pieces = List(
PieceOnSquareDto("e1", "White", "King"),
PieceOnSquareDto("e8", "Black", "King"),
)
val result = DtoMapper.toBoard(pieces)
result.isRight shouldBe true
result.map(_.pieceAt(Square(File.E, Rank.R1))) shouldBe Right(Some(Piece(Color.White, PieceType.King)))
test("toBoard rejects invalid square"):
DtoMapper.toBoard(List(PieceOnSquareDto("z9", "White", "King"))) shouldBe a[Left[?, ?]]
test("toBoard rejects invalid color"):
DtoMapper.toBoard(List(PieceOnSquareDto("e1", "Red", "King"))) shouldBe a[Left[?, ?]]
test("toBoard rejects invalid piece type"):
DtoMapper.toBoard(List(PieceOnSquareDto("e1", "White", "Dragon"))) shouldBe a[Left[?, ?]]
test("toBoard with multiple invalid pieces covers all sequenceList branches"):
val pieces = List(
PieceOnSquareDto("z9", "White", "King"),
PieceOnSquareDto("z8", "White", "Queen"),
)
DtoMapper.toBoard(pieces) shouldBe a[Left[?, ?]]
// ── toGameContext ─────────────────────────────────────────────────
test("toGameContext round-trips initial position"):
val ctx = GameContext.initial
val dto = DtoMapper.fromGameContext(ctx)
DtoMapper.toGameContext(dto) shouldBe Right(ctx)
test("toGameContext rejects invalid turn"):
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(turn = "Red")
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
test("toGameContext rejects invalid en passant square"):
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(enPassantSquare = Some("z9"))
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
test("toGameContext rejects invalid initial board"):
val badBoard = List(PieceOnSquareDto("z9", "White", "King"))
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(initialBoard = badBoard)
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
test("toGameContext rejects invalid move"):
val badMove = MoveDto("z9", "e4", "normal", None)
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(moves = List(badMove))
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
// ── fromGameContext ───────────────────────────────────────────────
test("fromGameContext includes en passant square when present"):
val ctx = GameContext.initial.copy(enPassantSquare = Some(Square(File.E, Rank.R3)))
DtoMapper.fromGameContext(ctx).enPassantSquare shouldBe Some("e3")
test("toGameContext round-trips a valid en passant square"):
val ctx = GameContext.initial.copy(enPassantSquare = Some(Square(File.E, Rank.R3)))
DtoMapper.toGameContext(DtoMapper.fromGameContext(ctx)).map(_.enPassantSquare) shouldBe Right(
Some(Square(File.E, Rank.R3)),
)
// ── fromMove ──────────────────────────────────────────────────────
test("fromMove converts all move types"):
DtoMapper.fromMove(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))).moveType shouldBe "normal"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R2), Square(File.D, Rank.R3), MoveType.Normal(true)))
.moveType shouldBe "capture"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside))
.moveType shouldBe "castleKingside"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside))
.moveType shouldBe "castleQueenside"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant))
.moveType shouldBe "enPassant"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen)))
.promotionPiece shouldBe Some("Queen")
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Rook)))
.promotionPiece shouldBe Some("Rook")
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Bishop)))
.promotionPiece shouldBe Some("Bishop")
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Knight)))
.promotionPiece shouldBe Some("Knight")
@@ -0,0 +1,98 @@
package de.nowchess.rules.json
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 de.nowchess.api.move.{MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonSerializersTest extends AnyFunSuite with Matchers:
private val mapper: ObjectMapper =
val m = new ObjectMapper()
val mod = new SimpleModule()
m.registerModule(DefaultScalaModule)
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
mod.addSerializer(classOf[Square], new SquareSerializer())
mod.addDeserializer(classOf[Square], new SquareDeserializer())
mod.addSerializer(classOf[MoveType], new MoveTypeSerializer())
mod.addDeserializer(classOf[MoveType], new MoveTypeDeserializer())
m.registerModule(mod)
m
private val e4 = Square(File.E, Rank.R4)
// ── SquareKeySerializer ───────────────────────────────────────────
test("SquareKeySerializer writes square as map field name"):
mapper.writeValueAsString(Map(e4 -> "piece")) shouldBe """{"e4":"piece"}"""
// ── SquareKeyDeserializer ─────────────────────────────────────────
test("SquareKeyDeserializer returns square for valid key"):
new SquareKeyDeserializer().deserializeKey("e4", null) shouldBe e4
test("SquareKeyDeserializer returns null for invalid key"):
new SquareKeyDeserializer().deserializeKey("z9", null) shouldBe null
// ── SquareSerializer/Deserializer ─────────────────────────────────
test("SquareSerializer writes square as string"):
mapper.writeValueAsString(e4) shouldBe """"e4""""
test("SquareDeserializer reads valid square string"):
mapper.readValue(""""e4"""", classOf[Square]) shouldBe e4
test("SquareDeserializer returns null for invalid square string"):
mapper.readValue(""""z9"""", classOf[Square]) shouldBe null
// ── MoveTypeSerializer ────────────────────────────────────────────
test("MoveTypeSerializer serializes Normal non-capture"):
mapper.writeValueAsString(MoveType.Normal(false)) shouldBe """{"type":"normal","isCapture":false}"""
test("MoveTypeSerializer serializes Normal capture"):
mapper.writeValueAsString(MoveType.Normal(true)) shouldBe """{"type":"normal","isCapture":true}"""
test("MoveTypeSerializer serializes CastleKingside"):
mapper.writeValueAsString(MoveType.CastleKingside) shouldBe """{"type":"castleKingside"}"""
test("MoveTypeSerializer serializes CastleQueenside"):
mapper.writeValueAsString(MoveType.CastleQueenside) shouldBe """{"type":"castleQueenside"}"""
test("MoveTypeSerializer serializes EnPassant"):
mapper.writeValueAsString(MoveType.EnPassant) shouldBe """{"type":"enPassant"}"""
test("MoveTypeSerializer serializes Promotion"):
mapper.writeValueAsString(MoveType.Promotion(PromotionPiece.Queen)) shouldBe
"""{"type":"promotion","piece":"Queen"}"""
// ── MoveTypeDeserializer ──────────────────────────────────────────
test("MoveTypeDeserializer deserializes normal non-capture"):
mapper.readValue("""{"type":"normal","isCapture":false}""", classOf[MoveType]) shouldBe
MoveType.Normal(false)
test("MoveTypeDeserializer deserializes normal capture"):
mapper.readValue("""{"type":"normal","isCapture":true}""", classOf[MoveType]) shouldBe
MoveType.Normal(true)
test("MoveTypeDeserializer deserializes castleKingside"):
mapper.readValue("""{"type":"castleKingside"}""", classOf[MoveType]) shouldBe MoveType.CastleKingside
test("MoveTypeDeserializer deserializes castleQueenside"):
mapper.readValue("""{"type":"castleQueenside"}""", classOf[MoveType]) shouldBe MoveType.CastleQueenside
test("MoveTypeDeserializer deserializes enPassant"):
mapper.readValue("""{"type":"enPassant"}""", classOf[MoveType]) shouldBe MoveType.EnPassant
test("MoveTypeDeserializer deserializes promotion"):
mapper.readValue("""{"type":"promotion","piece":"Rook"}""", classOf[MoveType]) shouldBe
MoveType.Promotion(PromotionPiece.Rook)
test("MoveTypeDeserializer throws for unknown type"):
an[Exception] should be thrownBy
mapper.readValue("""{"type":"unknown"}""", classOf[MoveType])
@@ -1,11 +1,11 @@
package de.nowchess.rules.resource
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest, DtoMapper}
import de.nowchess.rules.config.JacksonConfig
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest}
import de.nowchess.rules.sets.DefaultRules
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
@@ -16,21 +16,25 @@ import org.junit.jupiter.api.Test
@QuarkusTest
class RuleSetResourceTest:
private val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
private val rules = DefaultRules
private val mapper: ObjectMapper =
val m = new ObjectMapper()
new JacksonConfig().customize(m)
m
private val rules = DefaultRules
private def request() = RestAssured.`given`()
private def toJson(value: AnyRef): String = mapper.writeValueAsString(value)
private def contextBody(ctx: GameContext): String =
toJson(ContextRequest(DtoMapper.fromGameContext(ctx)))
toJson(ContextRequest(ctx))
private def contextSquareBody(ctx: GameContext, square: String): String =
toJson(ContextSquareRequest(DtoMapper.fromGameContext(ctx), square))
toJson(ContextSquareRequest(ctx, square))
private def contextMoveBody(ctx: GameContext, move: Move): String =
toJson(ContextMoveRequest(DtoMapper.fromGameContext(ctx), DtoMapper.fromMove(move)))
toJson(ContextMoveRequest(ctx, move))
// ── all-legal-moves ───────────────────────────────────────────────
@@ -238,7 +242,7 @@ class RuleSetResourceTest:
def invalidSquare_returns400(): Unit =
request()
.contentType(ContentType.JSON)
.body(toJson(ContextSquareRequest(DtoMapper.fromGameContext(GameContext.initial), "z9")))
.body(toJson(ContextSquareRequest(GameContext.initial, "z9")))
.when()
.post("/api/rules/legal-moves")
.`then`()
@@ -2,8 +2,8 @@ package de.nowchess.rules.resource
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType}
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest, DtoMapper}
import de.nowchess.api.move.Move
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest}
import de.nowchess.rules.sets.DefaultRules
import jakarta.ws.rs.BadRequestException
import org.scalatest.funsuite.AnyFunSuite
@@ -14,9 +14,9 @@ class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
private val resource = new RuleSetResource()
private val rules = DefaultRules
private def ctx(g: GameContext) = ContextRequest(DtoMapper.fromGameContext(g))
private def ctxSq(g: GameContext, sq: String) = ContextSquareRequest(DtoMapper.fromGameContext(g), sq)
private def ctxMv(g: GameContext, m: Move) = ContextMoveRequest(DtoMapper.fromGameContext(g), DtoMapper.fromMove(m))
private def ctx(g: GameContext) = ContextRequest(g)
private def ctxSq(g: GameContext, sq: String) = ContextSquareRequest(g, sq)
private def ctxMv(g: GameContext, m: Move) = ContextMoveRequest(g, m)
// ── position builders ─────────────────────────────────────────────
@@ -104,12 +104,6 @@ class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
test("isCheck returns true when king is attacked"):
resource.isCheck(ctx(checkContext())).result shouldBe true
test("isCheck throws BadRequestException for invalid context"):
an[BadRequestException] should be thrownBy
resource.isCheck(
ctx(GameContext.initial).copy(context = DtoMapper.fromGameContext(GameContext.initial).copy(turn = "Red")),
)
// ── isCheckmate ───────────────────────────────────────────────────
test("isCheckmate returns false for initial position"):
@@ -157,11 +151,4 @@ class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
.legalMoves(GameContext.initial)(Square(File.E, Rank.R2))
.find(_.to == Square(File.E, Rank.R4))
.get
resource.applyMove(ctxMv(GameContext.initial, move)).turn shouldBe "Black"
test("applyMove throws BadRequestException for invalid move"):
val badMove = DtoMapper
.fromMove(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)))
.copy(moveType = "unknown")
an[BadRequestException] should be thrownBy
resource.applyMove(ContextMoveRequest(DtoMapper.fromGameContext(GameContext.initial), badMove))
resource.applyMove(ctxMv(GameContext.initial, move)).turn shouldBe Color.Black