@@ -0,0 +1,87 @@
|
||||
syntax = "proto3";
|
||||
option java_package = "de.nowchess.io.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "ChessTypesProto";
|
||||
|
||||
enum ProtoColor {
|
||||
WHITE = 0;
|
||||
BLACK = 1;
|
||||
}
|
||||
|
||||
enum ProtoPieceType {
|
||||
PAWN = 0;
|
||||
KNIGHT = 1;
|
||||
BISHOP = 2;
|
||||
ROOK = 3;
|
||||
QUEEN = 4;
|
||||
KING = 5;
|
||||
}
|
||||
|
||||
enum ProtoMoveKind {
|
||||
QUIET = 0;
|
||||
CAPTURE = 1;
|
||||
CASTLE_KINGSIDE = 2;
|
||||
CASTLE_QUEENSIDE = 3;
|
||||
EN_PASSANT = 4;
|
||||
PROMO_QUEEN = 5;
|
||||
PROMO_ROOK = 6;
|
||||
PROMO_BISHOP = 7;
|
||||
PROMO_KNIGHT = 8;
|
||||
}
|
||||
|
||||
enum ProtoGameResultKind {
|
||||
ONGOING = 0;
|
||||
WIN_CHECKMATE_W = 1;
|
||||
WIN_CHECKMATE_B = 2;
|
||||
WIN_RESIGN_W = 3;
|
||||
WIN_RESIGN_B = 4;
|
||||
WIN_TIME_W = 5;
|
||||
WIN_TIME_B = 6;
|
||||
DRAW_STALEMATE = 7;
|
||||
DRAW_INSUFFICIENT = 8;
|
||||
DRAW_FIFTY_MOVE = 9;
|
||||
DRAW_THREEFOLD = 10;
|
||||
DRAW_AGREEMENT = 11;
|
||||
}
|
||||
|
||||
message ProtoPiece {
|
||||
ProtoColor color = 1;
|
||||
ProtoPieceType piece_type = 2;
|
||||
}
|
||||
|
||||
message ProtoSquarePiece {
|
||||
string square = 1;
|
||||
ProtoPiece piece = 2;
|
||||
}
|
||||
|
||||
message ProtoMove {
|
||||
string from = 1;
|
||||
string to = 2;
|
||||
ProtoMoveKind move_kind = 3;
|
||||
}
|
||||
|
||||
message ProtoCastlingRights {
|
||||
bool white_king_side = 1;
|
||||
bool white_queen_side = 2;
|
||||
bool black_king_side = 3;
|
||||
bool black_queen_side = 4;
|
||||
}
|
||||
|
||||
message ProtoGameContext {
|
||||
repeated ProtoSquarePiece board = 1;
|
||||
ProtoColor turn = 2;
|
||||
ProtoCastlingRights castling_rights = 3;
|
||||
string en_passant_square = 4;
|
||||
int32 half_move_clock = 5;
|
||||
repeated ProtoMove moves = 6;
|
||||
ProtoGameResultKind result = 7;
|
||||
repeated ProtoSquarePiece initial_board = 8;
|
||||
}
|
||||
|
||||
message ProtoPostMoveStatus {
|
||||
bool is_checkmate = 1;
|
||||
bool is_stalemate = 2;
|
||||
bool is_insufficient_material = 3;
|
||||
bool is_check = 4;
|
||||
bool is_threefold_repetition = 5;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
syntax = "proto3";
|
||||
option java_package = "de.nowchess.io.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "IoServiceProto";
|
||||
|
||||
import "chess_types.proto";
|
||||
|
||||
message ProtoImportFenRequest {
|
||||
string fen = 1;
|
||||
}
|
||||
|
||||
message ProtoImportPgnRequest {
|
||||
string pgn = 1;
|
||||
}
|
||||
|
||||
message ProtoCombinedExport {
|
||||
string fen = 1;
|
||||
string pgn = 2;
|
||||
}
|
||||
|
||||
message ProtoStringResult {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
service IoService {
|
||||
rpc ImportFen (ProtoImportFenRequest) returns (ProtoGameContext);
|
||||
rpc ImportPgn (ProtoImportPgnRequest) returns (ProtoGameContext);
|
||||
rpc ExportCombined (ProtoGameContext) returns (ProtoCombinedExport);
|
||||
rpc ExportFen (ProtoGameContext) returns (ProtoStringResult);
|
||||
rpc ExportPgn (ProtoGameContext) returns (ProtoStringResult);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"reflection": [
|
||||
{ "type": "scala.Tuple1[]" },
|
||||
{ "type": "scala.Tuple2[]" },
|
||||
{ "type": "scala.Tuple3[]" },
|
||||
{ "type": "scala.Tuple4[]" },
|
||||
{ "type": "scala.Tuple5[]" },
|
||||
{ "type": "scala.Tuple6[]" },
|
||||
{ "type": "scala.Tuple7[]" },
|
||||
{ "type": "scala.Tuple8[]" },
|
||||
{ "type": "scala.Tuple9[]" },
|
||||
{ "type": "scala.Tuple10[]" },
|
||||
{ "type": "scala.Tuple11[]" },
|
||||
{ "type": "scala.Tuple12[]" },
|
||||
{ "type": "scala.Tuple13[]" },
|
||||
{ "type": "scala.Tuple14[]" },
|
||||
{ "type": "scala.Tuple15[]" },
|
||||
{ "type": "scala.Tuple16[]" },
|
||||
{ "type": "scala.Tuple17[]" },
|
||||
{ "type": "scala.Tuple18[]" },
|
||||
{ "type": "scala.Tuple19[]" },
|
||||
{ "type": "scala.Tuple20[]" },
|
||||
{ "type": "scala.Tuple21[]" },
|
||||
{ "type": "scala.Tuple22[]" },
|
||||
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
quarkus:
|
||||
http:
|
||||
port: 8081
|
||||
grpc:
|
||||
server:
|
||||
use-separate-server: false
|
||||
application:
|
||||
name: nowchess-io
|
||||
|
||||
nowchess:
|
||||
internal:
|
||||
secret: 123abc
|
||||
smallrye-openapi:
|
||||
info-title: NowChess IO Service
|
||||
info-version: 1.0.0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.nowchess.io
|
||||
|
||||
import de.nowchess.api.error.GameError
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.io.{GameContextExport, GameContextImport}
|
||||
|
||||
@@ -12,29 +13,29 @@ import scala.util.Try
|
||||
* Abstracts file I/O operations away from the UI layer. Handles both reading and writing game files.
|
||||
*/
|
||||
trait GameFileService:
|
||||
def saveGameToFile(context: GameContext, path: Path, exporter: GameContextExport): Either[String, Unit]
|
||||
def loadGameFromFile(path: Path, importer: GameContextImport): Either[String, GameContext]
|
||||
def saveGameToFile(context: GameContext, path: Path, exporter: GameContextExport): Either[GameError, Unit]
|
||||
def loadGameFromFile(path: Path, importer: GameContextImport): Either[GameError, GameContext]
|
||||
|
||||
/** Default implementation using the file system. */
|
||||
object FileSystemGameService extends GameFileService:
|
||||
|
||||
/** Save a game context to a file using the specified exporter. */
|
||||
def saveGameToFile(context: GameContext, path: Path, exporter: GameContextExport): Either[String, Unit] =
|
||||
def saveGameToFile(context: GameContext, path: Path, exporter: GameContextExport): Either[GameError, Unit] =
|
||||
Try {
|
||||
val json = exporter.exportGameContext(context)
|
||||
Files.write(path, json.getBytes(StandardCharsets.UTF_8))
|
||||
()
|
||||
}.fold(
|
||||
ex => Left(s"Failed to save file: ${ex.getMessage}"),
|
||||
ex => Left(GameError.FileWriteError(s"Failed to save file: ${ex.getMessage}")),
|
||||
_ => Right(()),
|
||||
)
|
||||
|
||||
/** Load a game context from a file using the specified importer. */
|
||||
def loadGameFromFile(path: Path, importer: GameContextImport): Either[String, GameContext] =
|
||||
def loadGameFromFile(path: Path, importer: GameContextImport): Either[GameError, GameContext] =
|
||||
Try {
|
||||
val json = new String(Files.readAllBytes(path), StandardCharsets.UTF_8)
|
||||
importer.importGameContext(json)
|
||||
}.fold(
|
||||
ex => Left(s"Failed to load file: ${ex.getMessage}"),
|
||||
ex => Left(GameError.FileReadError(s"Failed to load file: ${ex.getMessage}")),
|
||||
result => result,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.nowchess.io.fen
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.error.GameError
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.io.GameContextImport
|
||||
|
||||
@@ -8,18 +9,29 @@ object FenParser extends GameContextImport:
|
||||
|
||||
/** Parse a complete FEN string into a GameContext. Returns Left with error message if the format is invalid.
|
||||
*/
|
||||
def parseFen(fen: String): Either[String, GameContext] =
|
||||
def parseFen(fen: String): Either[GameError, GameContext] =
|
||||
val parts = fen.trim.split("\\s+")
|
||||
if parts.length != 6 then Left(s"Invalid FEN: expected 6 space-separated fields, got ${parts.length}")
|
||||
if parts.length != 6 then
|
||||
Left(GameError.ParseError(s"Invalid FEN: expected 6 space-separated fields, got ${parts.length}"))
|
||||
else
|
||||
for
|
||||
board <- parseBoard(parts(0)).toRight("Invalid FEN: invalid board position")
|
||||
activeColor <- parseColor(parts(1)).toRight("Invalid FEN: invalid active color (expected 'w' or 'b')")
|
||||
castlingRights <- parseCastling(parts(2)).toRight("Invalid FEN: invalid castling rights")
|
||||
enPassant <- parseEnPassant(parts(3)).toRight("Invalid FEN: invalid en passant square")
|
||||
halfMoveClock <- parts(4).toIntOption.toRight("Invalid FEN: invalid half-move clock (expected integer)")
|
||||
fullMoveNumber <- parts(5).toIntOption.toRight("Invalid FEN: invalid full move number (expected integer)")
|
||||
_ <- Either.cond(halfMoveClock >= 0 && fullMoveNumber >= 1, (), "Invalid FEN: invalid move counts")
|
||||
board <- parseBoard(parts(0)).toRight(GameError.ParseError("Invalid FEN: invalid board position"))
|
||||
activeColor <- parseColor(parts(1)).toRight(
|
||||
GameError.ParseError("Invalid FEN: invalid active color (expected 'w' or 'b')"),
|
||||
)
|
||||
castlingRights <- parseCastling(parts(2)).toRight(GameError.ParseError("Invalid FEN: invalid castling rights"))
|
||||
enPassant <- parseEnPassant(parts(3)).toRight(GameError.ParseError("Invalid FEN: invalid en passant square"))
|
||||
halfMoveClock <- parts(4).toIntOption.toRight(
|
||||
GameError.ParseError("Invalid FEN: invalid half-move clock (expected integer)"),
|
||||
)
|
||||
fullMoveNumber <- parts(5).toIntOption.toRight(
|
||||
GameError.ParseError("Invalid FEN: invalid full move number (expected integer)"),
|
||||
)
|
||||
_ <- Either.cond(
|
||||
halfMoveClock >= 0 && fullMoveNumber >= 1,
|
||||
(),
|
||||
GameError.ParseError("Invalid FEN: invalid move counts"),
|
||||
)
|
||||
yield GameContext(
|
||||
board = board,
|
||||
turn = activeColor,
|
||||
@@ -29,7 +41,7 @@ object FenParser extends GameContextImport:
|
||||
moves = List.empty,
|
||||
)
|
||||
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
def importGameContext(input: String): Either[GameError, GameContext] =
|
||||
parseFen(input)
|
||||
|
||||
/** Parse active color ("w" or "b"). */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.nowchess.io.fen
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.error.GameError
|
||||
import de.nowchess.api.game.GameContext
|
||||
|
||||
import scala.util.parsing.combinator.RegexParsers
|
||||
@@ -107,15 +108,15 @@ object FenParserCombinators extends RegexParsers with GameContextImport:
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
def parseFen(fen: String): Either[String, GameContext] =
|
||||
def parseFen(fen: String): Either[GameError, GameContext] =
|
||||
parseAll(fenParser, fen) match
|
||||
case Success(ctx, _) => Right(ctx)
|
||||
case other => Left(s"Invalid FEN: ${other.toString}")
|
||||
case other => Left(GameError.ParseError(s"Invalid FEN: ${other.toString}"))
|
||||
|
||||
def parseBoard(fen: String): Option[Board] =
|
||||
parseAll(boardParser, fen) match
|
||||
case Success(board, _) => Some(board)
|
||||
case _ => None
|
||||
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
def importGameContext(input: String): Either[GameError, GameContext] =
|
||||
parseFen(input)
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.nowchess.io.fen
|
||||
import fastparse.*
|
||||
import fastparse.NoWhitespace.*
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.error.GameError
|
||||
import de.nowchess.api.game.GameContext
|
||||
import FenParserSupport.*
|
||||
import de.nowchess.api.io.GameContextImport
|
||||
@@ -103,10 +104,10 @@ object FenParserFastParse extends GameContextImport:
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
def parseFen(fen: String): Either[String, GameContext] =
|
||||
def parseFen(fen: String): Either[GameError, GameContext] =
|
||||
parse(fen, fenParser(using _)) match
|
||||
case Parsed.Success(ctx, _) => Right(ctx)
|
||||
case f: Parsed.Failure => Left(s"Invalid FEN: ${f.msg}")
|
||||
case f: Parsed.Failure => Left(GameError.ParseError(s"Invalid FEN: ${f.msg}"))
|
||||
|
||||
private def boardParserFull(using P[Any]): P[Board] =
|
||||
boardParser ~ End
|
||||
@@ -116,5 +117,5 @@ object FenParserFastParse extends GameContextImport:
|
||||
case Parsed.Success(board, _) => Some(board)
|
||||
case _ => None
|
||||
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
def importGameContext(input: String): Either[GameError, GameContext] =
|
||||
parseFen(input)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.nowchess.io.grpc
|
||||
|
||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||
import de.nowchess.io.proto.*
|
||||
import io.grpc.stub.StreamObserver
|
||||
import io.grpc.Status
|
||||
import io.quarkus.grpc.GrpcService
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
@GrpcService
|
||||
class IoGrpcService extends IoServiceGrpc.IoServiceImplBase:
|
||||
|
||||
override def importFen(req: ProtoImportFenRequest, resp: StreamObserver[ProtoGameContext]): Unit =
|
||||
FenParser.parseFen(req.getFen) match
|
||||
case Left(err) =>
|
||||
resp.onError(Status.INVALID_ARGUMENT.withDescription(err.message).asRuntimeException())
|
||||
case Right(ctx) =>
|
||||
respond(resp, IoProtoMapper.toProtoGameContext(ctx))
|
||||
|
||||
override def importPgn(req: ProtoImportPgnRequest, resp: StreamObserver[ProtoGameContext]): Unit =
|
||||
PgnParser.importGameContext(req.getPgn) match
|
||||
case Left(err) =>
|
||||
resp.onError(Status.INVALID_ARGUMENT.withDescription(err.message).asRuntimeException())
|
||||
case Right(ctx) =>
|
||||
respond(resp, IoProtoMapper.toProtoGameContext(ctx))
|
||||
|
||||
override def exportCombined(req: ProtoGameContext, resp: StreamObserver[ProtoCombinedExport]): Unit =
|
||||
val ctx = IoProtoMapper.fromProtoGameContext(req)
|
||||
respond(
|
||||
resp,
|
||||
ProtoCombinedExport
|
||||
.newBuilder()
|
||||
.setFen(FenExporter.exportGameContext(ctx))
|
||||
.setPgn(PgnExporter.exportGameContext(ctx))
|
||||
.build(),
|
||||
)
|
||||
|
||||
override def exportFen(req: ProtoGameContext, resp: StreamObserver[ProtoStringResult]): Unit =
|
||||
respond(
|
||||
resp,
|
||||
ProtoStringResult
|
||||
.newBuilder()
|
||||
.setValue(FenExporter.exportGameContext(IoProtoMapper.fromProtoGameContext(req)))
|
||||
.build(),
|
||||
)
|
||||
|
||||
override def exportPgn(req: ProtoGameContext, resp: StreamObserver[ProtoStringResult]): Unit =
|
||||
respond(
|
||||
resp,
|
||||
ProtoStringResult
|
||||
.newBuilder()
|
||||
.setValue(PgnExporter.exportGameContext(IoProtoMapper.fromProtoGameContext(req)))
|
||||
.build(),
|
||||
)
|
||||
|
||||
private def respond[T](obs: StreamObserver[T], value: T): Unit =
|
||||
obs.onNext(value)
|
||||
obs.onCompleted()
|
||||
@@ -0,0 +1,161 @@
|
||||
package de.nowchess.io.grpc
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.board.CastlingRights as DomainCastlingRights
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult, WinReason}
|
||||
import de.nowchess.api.grpc.ProtoMapperBase
|
||||
import de.nowchess.api.move.{Move as DomainMove, MoveType}
|
||||
import de.nowchess.io.proto.*
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
object IoProtoMapper
|
||||
extends ProtoMapperBase[
|
||||
ProtoColor,
|
||||
ProtoPieceType,
|
||||
ProtoMoveKind,
|
||||
ProtoMove,
|
||||
ProtoSquarePiece,
|
||||
java.util.List[ProtoSquarePiece],
|
||||
ProtoCastlingRights,
|
||||
ProtoGameResultKind,
|
||||
ProtoGameContext,
|
||||
]:
|
||||
private val (colorTo, colorFrom) = ProtoMapperBase.colorConversions(ProtoColor.WHITE, ProtoColor.BLACK)
|
||||
private val (pieceTypeTo, pieceTypeFrom) = ProtoMapperBase.pieceTypeConversions(
|
||||
ProtoPieceType.PAWN,
|
||||
ProtoPieceType.KNIGHT,
|
||||
ProtoPieceType.BISHOP,
|
||||
ProtoPieceType.ROOK,
|
||||
ProtoPieceType.QUEEN,
|
||||
ProtoPieceType.KING,
|
||||
)
|
||||
private val (moveKindTo, moveKindFrom) = ProtoMapperBase.moveKindConversions(
|
||||
ProtoMoveKind.QUIET,
|
||||
ProtoMoveKind.CAPTURE,
|
||||
ProtoMoveKind.CASTLE_KINGSIDE,
|
||||
ProtoMoveKind.CASTLE_QUEENSIDE,
|
||||
ProtoMoveKind.EN_PASSANT,
|
||||
ProtoMoveKind.PROMO_QUEEN,
|
||||
ProtoMoveKind.PROMO_ROOK,
|
||||
ProtoMoveKind.PROMO_BISHOP,
|
||||
ProtoMoveKind.PROMO_KNIGHT,
|
||||
)
|
||||
|
||||
override def toProtoColor(c: Color): ProtoColor = colorTo(c)
|
||||
override def fromProtoColor(c: ProtoColor): Color = colorFrom(c)
|
||||
override def toProtoPieceType(pt: PieceType): ProtoPieceType = pieceTypeTo(pt)
|
||||
override def fromProtoPieceType(pt: ProtoPieceType): PieceType = pieceTypeFrom(pt)
|
||||
override def toProtoMoveKind(mt: MoveType): ProtoMoveKind = moveKindTo(mt)
|
||||
override def fromProtoMoveKind(k: ProtoMoveKind): MoveType = moveKindFrom(k)
|
||||
|
||||
override def toProtoMove(m: DomainMove): ProtoMove =
|
||||
ProtoMove
|
||||
.newBuilder()
|
||||
.setFrom(m.from.toString)
|
||||
.setTo(m.to.toString)
|
||||
.setMoveKind(toProtoMoveKind(m.moveType))
|
||||
.build()
|
||||
|
||||
override def fromProtoMove(m: ProtoMove): Option[DomainMove] =
|
||||
for
|
||||
from <- Square.fromAlgebraic(m.getFrom)
|
||||
to <- Square.fromAlgebraic(m.getTo)
|
||||
yield DomainMove(from, to, fromProtoMoveKind(m.getMoveKind))
|
||||
|
||||
override def toProtoSquarePiece(sq: Square, piece: Piece): ProtoSquarePiece =
|
||||
ProtoSquarePiece
|
||||
.newBuilder()
|
||||
.setSquare(sq.toString)
|
||||
.setPiece(
|
||||
ProtoPiece
|
||||
.newBuilder()
|
||||
.setColor(toProtoColor(piece.color))
|
||||
.setPieceType(toProtoPieceType(piece.pieceType))
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
override def fromProtoSquarePiece(sp: ProtoSquarePiece): Option[(Square, Piece)] =
|
||||
Square
|
||||
.fromAlgebraic(sp.getSquare)
|
||||
.map(_ -> Piece(fromProtoColor(sp.getPiece.getColor), fromProtoPieceType(sp.getPiece.getPieceType)))
|
||||
|
||||
override def toProtoBoard(board: Board): java.util.List[ProtoSquarePiece] =
|
||||
board.pieces
|
||||
.map((sq, piece) => toProtoSquarePiece(sq, piece))
|
||||
.toSeq
|
||||
.asJava
|
||||
|
||||
override def fromProtoBoard(pieces: java.util.List[ProtoSquarePiece]): Board =
|
||||
Board(
|
||||
pieces.asScala
|
||||
.flatMap(fromProtoSquarePiece)
|
||||
.toMap,
|
||||
)
|
||||
|
||||
override def toProtoResultKind(r: Option[GameResult]): ProtoGameResultKind = r match
|
||||
case None => ProtoGameResultKind.ONGOING
|
||||
case Some(GameResult.Win(Color.White, WinReason.Checkmate)) => ProtoGameResultKind.WIN_CHECKMATE_W
|
||||
case Some(GameResult.Win(Color.Black, WinReason.Checkmate)) => ProtoGameResultKind.WIN_CHECKMATE_B
|
||||
case Some(GameResult.Win(Color.White, WinReason.Resignation)) => ProtoGameResultKind.WIN_RESIGN_W
|
||||
case Some(GameResult.Win(Color.Black, WinReason.Resignation)) => ProtoGameResultKind.WIN_RESIGN_B
|
||||
case Some(GameResult.Win(Color.White, WinReason.TimeControl)) => ProtoGameResultKind.WIN_TIME_W
|
||||
case Some(GameResult.Win(Color.Black, WinReason.TimeControl)) => ProtoGameResultKind.WIN_TIME_B
|
||||
case Some(GameResult.Draw(DrawReason.Stalemate)) => ProtoGameResultKind.DRAW_STALEMATE
|
||||
case Some(GameResult.Draw(DrawReason.InsufficientMaterial)) => ProtoGameResultKind.DRAW_INSUFFICIENT
|
||||
case Some(GameResult.Draw(DrawReason.FiftyMoveRule)) => ProtoGameResultKind.DRAW_FIFTY_MOVE
|
||||
case Some(GameResult.Draw(DrawReason.ThreefoldRepetition)) => ProtoGameResultKind.DRAW_THREEFOLD
|
||||
case Some(GameResult.Draw(DrawReason.Agreement)) => ProtoGameResultKind.DRAW_AGREEMENT
|
||||
|
||||
override def fromProtoResultKind(k: ProtoGameResultKind): Option[GameResult] = k match
|
||||
case ProtoGameResultKind.ONGOING => None
|
||||
case ProtoGameResultKind.WIN_CHECKMATE_W => Some(GameResult.Win(Color.White, WinReason.Checkmate))
|
||||
case ProtoGameResultKind.WIN_CHECKMATE_B => Some(GameResult.Win(Color.Black, WinReason.Checkmate))
|
||||
case ProtoGameResultKind.WIN_RESIGN_W => Some(GameResult.Win(Color.White, WinReason.Resignation))
|
||||
case ProtoGameResultKind.WIN_RESIGN_B => Some(GameResult.Win(Color.Black, WinReason.Resignation))
|
||||
case ProtoGameResultKind.WIN_TIME_W => Some(GameResult.Win(Color.White, WinReason.TimeControl))
|
||||
case ProtoGameResultKind.WIN_TIME_B => Some(GameResult.Win(Color.Black, WinReason.TimeControl))
|
||||
case ProtoGameResultKind.DRAW_STALEMATE => Some(GameResult.Draw(DrawReason.Stalemate))
|
||||
case ProtoGameResultKind.DRAW_INSUFFICIENT => Some(GameResult.Draw(DrawReason.InsufficientMaterial))
|
||||
case ProtoGameResultKind.DRAW_FIFTY_MOVE => Some(GameResult.Draw(DrawReason.FiftyMoveRule))
|
||||
case ProtoGameResultKind.DRAW_THREEFOLD => Some(GameResult.Draw(DrawReason.ThreefoldRepetition))
|
||||
case ProtoGameResultKind.DRAW_AGREEMENT => Some(GameResult.Draw(DrawReason.Agreement))
|
||||
case _ => None
|
||||
|
||||
override def toProtoCastlingRights(cr: DomainCastlingRights): ProtoCastlingRights =
|
||||
ProtoCastlingRights
|
||||
.newBuilder()
|
||||
.setWhiteKingSide(cr.whiteKingSide)
|
||||
.setWhiteQueenSide(cr.whiteQueenSide)
|
||||
.setBlackKingSide(cr.blackKingSide)
|
||||
.setBlackQueenSide(cr.blackQueenSide)
|
||||
.build()
|
||||
|
||||
override def fromProtoCastlingRights(pcr: ProtoCastlingRights): DomainCastlingRights =
|
||||
DomainCastlingRights(pcr.getWhiteKingSide, pcr.getWhiteQueenSide, pcr.getBlackKingSide, pcr.getBlackQueenSide)
|
||||
|
||||
override def toProtoGameContext(ctx: GameContext): ProtoGameContext =
|
||||
ProtoGameContext
|
||||
.newBuilder()
|
||||
.addAllBoard(toProtoBoard(ctx.board))
|
||||
.setTurn(toProtoColor(ctx.turn))
|
||||
.setCastlingRights(toProtoCastlingRights(ctx.castlingRights))
|
||||
.setEnPassantSquare(ctx.enPassantSquare.map(_.toString).getOrElse(""))
|
||||
.setHalfMoveClock(ctx.halfMoveClock)
|
||||
.addAllMoves(ctx.moves.map(toProtoMove).asJava)
|
||||
.setResult(toProtoResultKind(ctx.result))
|
||||
.addAllInitialBoard(toProtoBoard(ctx.initialBoard))
|
||||
.build()
|
||||
|
||||
override def fromProtoGameContext(p: ProtoGameContext): GameContext =
|
||||
GameContext(
|
||||
board = fromProtoBoard(p.getBoardList),
|
||||
turn = fromProtoColor(p.getTurn),
|
||||
castlingRights = fromProtoCastlingRights(p.getCastlingRights),
|
||||
enPassantSquare = Option(p.getEnPassantSquare).filter(_.nonEmpty).flatMap(Square.fromAlgebraic),
|
||||
halfMoveClock = p.getHalfMoveClock,
|
||||
moves = p.getMovesList.asScala.flatMap(fromProtoMove).toList,
|
||||
result = fromProtoResultKind(p.getResult),
|
||||
initialBoard = fromProtoBoard(p.getInitialBoardList),
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package de.nowchess.io.json
|
||||
import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.error.GameError
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.io.GameContextImport
|
||||
@@ -27,9 +28,9 @@ object JsonParser extends GameContextImport:
|
||||
.registerModule(DefaultScalaModule)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
def importGameContext(input: String): Either[GameError, GameContext] =
|
||||
Try(mapper.readValue(input, classOf[JsonGameRecord])).toEither.left
|
||||
.map(e => "JSON parsing error: " + e.getMessage)
|
||||
.map(e => GameError.ParseError("JSON parsing error: " + e.getMessage))
|
||||
.flatMap { data =>
|
||||
val gs = data.gameState.getOrElse(JsonGameState())
|
||||
val rawBoard = gs.board.getOrElse(Nil)
|
||||
@@ -54,7 +55,7 @@ object JsonParser extends GameContextImport:
|
||||
)
|
||||
}
|
||||
|
||||
private def parseBoard(pieces: List[JsonPiece]): Either[String, Board] =
|
||||
private def parseBoard(pieces: List[JsonPiece]): Either[GameError, Board] =
|
||||
val parsedPieces = pieces.flatMap { p =>
|
||||
for
|
||||
sq <- p.square.flatMap(Square.fromAlgebraic)
|
||||
@@ -64,8 +65,8 @@ object JsonParser extends GameContextImport:
|
||||
}
|
||||
Right(Board(parsedPieces.toMap))
|
||||
|
||||
private def parseTurn(color: String): Either[String, Color] =
|
||||
parseColor(color).toRight(s"Invalid turn color: $color")
|
||||
private def parseTurn(color: String): Either[GameError, Color] =
|
||||
parseColor(color).toRight(GameError.ParseError(s"Invalid turn color: $color"))
|
||||
|
||||
private def parseColor(color: String): Option[Color] =
|
||||
if color == "White" then Some(Color.White)
|
||||
@@ -90,7 +91,7 @@ object JsonParser extends GameContextImport:
|
||||
cr.blackQueenSide.getOrElse(false),
|
||||
)
|
||||
|
||||
private def parseMoves(moves: List[JsonMove]): Either[String, List[Move]] =
|
||||
private def parseMoves(moves: List[JsonMove]): Either[GameError, List[Move]] =
|
||||
Right(moves.flatMap { m =>
|
||||
for
|
||||
from <- m.from.flatMap(Square.fromAlgebraic)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
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)
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.nowchess.io.pgn
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.error.GameError
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.io.GameContextImport
|
||||
@@ -17,7 +18,7 @@ object PgnParser extends GameContextImport:
|
||||
/** Strictly validate a PGN text. Returns Right(PgnGame) if every move token is a legal move in the evolving position.
|
||||
* Returns Left(error message) on the first illegal or impossible move, or any unrecognised token.
|
||||
*/
|
||||
def validatePgn(pgn: String): Either[String, PgnGame] =
|
||||
def validatePgn(pgn: String): Either[GameError, PgnGame] =
|
||||
val lines = pgn.split("\n").map(_.trim)
|
||||
val (headerLines, rest) = lines.span(_.startsWith("["))
|
||||
val headers = parseHeaders(headerLines)
|
||||
@@ -28,7 +29,7 @@ object PgnParser extends GameContextImport:
|
||||
* moves applied and .moves populated. Returns Left(error message) if validation fails or move replay encounters an
|
||||
* issue.
|
||||
*/
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
def importGameContext(input: String): Either[GameError, GameContext] =
|
||||
validatePgn(input).flatMap { game =>
|
||||
Right(game.moves.foldLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)))
|
||||
}
|
||||
@@ -173,17 +174,17 @@ object PgnParser extends GameContextImport:
|
||||
// ── Strict validation helpers ─────────────────────────────────────────────
|
||||
|
||||
/** Walk all move tokens, failing immediately on any unresolvable or illegal move. */
|
||||
private def validateMovesText(moveText: String): Either[String, List[Move]] =
|
||||
private def validateMovesText(moveText: String): Either[GameError, List[Move]] =
|
||||
val tokens = moveText.split("\\s+").filter(_.nonEmpty)
|
||||
tokens
|
||||
.foldLeft(
|
||||
Right((GameContext.initial, Color.White, List.empty[Move])): Either[String, (GameContext, Color, List[Move])],
|
||||
Right((GameContext.initial, Color.White, List.empty[Move])): Either[GameError, (GameContext, Color, List[Move])],
|
||||
) { case (acc, token) =>
|
||||
acc.flatMap { case (ctx, color, moves) =>
|
||||
if isMoveNumberOrResult(token) then Right((ctx, color, moves))
|
||||
else
|
||||
parseAlgebraicMove(token, ctx, color) match
|
||||
case None => Left(s"Illegal or impossible move: '$token'")
|
||||
case None => Left(GameError.ParseError(s"Illegal or impossible move: '$token'"))
|
||||
case Some(move) =>
|
||||
val nextCtx = DefaultRules.applyMove(ctx)(move)
|
||||
Right((nextCtx, color.opposite, moves :+ move))
|
||||
|
||||
@@ -2,10 +2,8 @@ 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 de.nowchess.json.ChessJacksonModule
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@@ -18,7 +16,4 @@ 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)
|
||||
mapper.registerModule(new ChessJacksonModule())
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.nowchess.io.service.dto
|
||||
|
||||
final case class CombinedExportResponse(fen: String, pgn: String)
|
||||
@@ -1,9 +1,10 @@
|
||||
package de.nowchess.io.service.resource
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.security.InternalOnly
|
||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||
import de.nowchess.io.service.dto.{CombinedExportResponse, ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||
import io.smallrye.mutiny.Uni
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.ws.rs.*
|
||||
@@ -15,6 +16,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag
|
||||
|
||||
@Path("/io")
|
||||
@ApplicationScoped
|
||||
@InternalOnly
|
||||
@Tag(name = "IO", description = "Chess notation import and export")
|
||||
class IoResource:
|
||||
|
||||
@@ -33,7 +35,7 @@ class IoResource:
|
||||
Uni.createFrom().item {
|
||||
FenParser.parseFen(body.fen) match
|
||||
case Left(err) =>
|
||||
Response.status(400).entity(IoErrorDto("INVALID_FEN", err)).build()
|
||||
Response.status(400).entity(IoErrorDto("INVALID_FEN", err.message)).build()
|
||||
case Right(ctx) =>
|
||||
Response.ok(ctx).build()
|
||||
}
|
||||
@@ -53,7 +55,7 @@ class IoResource:
|
||||
Uni.createFrom().item {
|
||||
PgnParser.importGameContext(body.pgn) match
|
||||
case Left(err) =>
|
||||
Response.status(400).entity(IoErrorDto("INVALID_PGN", err)).build()
|
||||
Response.status(400).entity(IoErrorDto("INVALID_PGN", err.message)).build()
|
||||
case Right(ctx) =>
|
||||
Response.ok(ctx).build()
|
||||
}
|
||||
@@ -75,3 +77,18 @@ class IoResource:
|
||||
@APIResponse(responseCode = "200", description = "PGN text")
|
||||
def exportPgn(ctx: GameContext): Uni[Response] =
|
||||
Uni.createFrom().item(Response.ok(PgnExporter.exportGameContext(ctx)).build())
|
||||
|
||||
@POST
|
||||
@Path("/export/combined")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
@Operation(summary = "Export FEN and PGN", description = "Serialize a GameContext to both FEN and PGN in one call")
|
||||
@APIResponse(responseCode = "200", description = "FEN and PGN")
|
||||
def exportCombined(ctx: GameContext): Uni[Response] =
|
||||
Uni
|
||||
.createFrom()
|
||||
.item(
|
||||
Response
|
||||
.ok(CombinedExportResponse(FenExporter.exportGameContext(ctx), PgnExporter.exportGameContext(ctx)))
|
||||
.build(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
nowchess:
|
||||
internal:
|
||||
secret: test-secret
|
||||
auth:
|
||||
enabled: false
|
||||
@@ -5,6 +5,7 @@ import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.io.GameContextExport
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.io.json.{JsonExporter, JsonParser}
|
||||
import org.scalactic.Prettifier.default
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
@@ -128,6 +129,6 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
val result = FileSystemGameService.saveGameToFile(context, tmpFile, faultyExporter)
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("Failed to save file"))
|
||||
assert(result.left.toOption.get.message.contains("Failed to save file"))
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
val fen = FenExporter.gameContextToFen(gameContext)
|
||||
FenParser.parseFen(fen) match
|
||||
case Right(ctx) => ctx.halfMoveClock shouldBe 42
|
||||
case Left(err) => fail(s"FEN parsing failed: $err")
|
||||
case Left(err) => fail(s"FEN parsing failed: ${err.message}")
|
||||
|
||||
test("exportGameContext forwards to gameContextToFen"):
|
||||
val ctx = GameContext.initial
|
||||
|
||||
@@ -95,13 +95,13 @@ class JsonParserTest extends AnyFunSuite with Matchers:
|
||||
val invalidJson = "{ this is not valid json at all }"
|
||||
val result = JsonParser.importGameContext(invalidJson)
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("JSON parsing error"))
|
||||
assert(result.left.toOption.get.message.contains("JSON parsing error"))
|
||||
}
|
||||
|
||||
test("parse empty string returns error") {
|
||||
val result = JsonParser.importGameContext("")
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("JSON parsing error"))
|
||||
assert(result.left.toOption.get.message.contains("JSON parsing error"))
|
||||
}
|
||||
|
||||
test("parse number value returns error") {
|
||||
@@ -113,7 +113,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
|
||||
val malformed = """{"metadata": {"unclosed": """
|
||||
val result = JsonParser.importGameContext(malformed)
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("JSON parsing error"))
|
||||
assert(result.left.toOption.get.message.contains("JSON parsing error"))
|
||||
}
|
||||
|
||||
test("parse invalid JSON array returns error") {
|
||||
@@ -137,7 +137,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
|
||||
}"""
|
||||
val result = JsonParser.importGameContext(json)
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("Invalid turn color"))
|
||||
assert(result.left.toOption.get.message.contains("Invalid turn color"))
|
||||
}
|
||||
|
||||
test("parse invalid piece type filters it out") {
|
||||
|
||||
@@ -2,61 +2,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 de.nowchess.io.service.config.JacksonConfig
|
||||
import de.nowchess.json.SquareKeyDeserializer
|
||||
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)
|
||||
val m = new ObjectMapper()
|
||||
new JacksonConfig().customize(m)
|
||||
m
|
||||
|
||||
private def readMap(json: String): Map[Square, Int] =
|
||||
mapper.readValue(json, new TypeReference[Map[Square, Int]] {})
|
||||
|
||||
test("deserializes valid algebraic key") {
|
||||
test("deserializes valid algebraic key"):
|
||||
val result = readMap("""{"e4":1}""")
|
||||
result(Square(File.E, Rank.R4)) shouldBe 1
|
||||
}
|
||||
|
||||
test("deserializes a1 corner") {
|
||||
test("deserializes a1 corner"):
|
||||
val result = readMap("""{"a1":1}""")
|
||||
result(Square(File.A, Rank.R1)) shouldBe 1
|
||||
}
|
||||
|
||||
test("deserializes h8 corner") {
|
||||
test("deserializes h8 corner"):
|
||||
val result = readMap("""{"h8":1}""")
|
||||
result(Square(File.H, Rank.R8)) shouldBe 1
|
||||
}
|
||||
|
||||
test("deserializes multiple squares") {
|
||||
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") {
|
||||
test("deserializeKey returns null for invalid square"):
|
||||
new SquareKeyDeserializer().deserializeKey("invalid", null) shouldBe null
|
||||
}
|
||||
|
||||
test("deserializeKey returns null for wrong-length key") {
|
||||
test("deserializeKey returns null for wrong-length key"):
|
||||
new SquareKeyDeserializer().deserializeKey("e44", null) shouldBe null
|
||||
}
|
||||
|
||||
test("deserializeKey returns null for bad file") {
|
||||
test("deserializeKey returns null for bad file"):
|
||||
new SquareKeyDeserializer().deserializeKey("z4", null) shouldBe null
|
||||
}
|
||||
|
||||
test("deserializeKey returns null for bad rank") {
|
||||
test("deserializeKey returns null for bad rank"):
|
||||
new SquareKeyDeserializer().deserializeKey("e9", null) shouldBe null
|
||||
}
|
||||
// scalafix:on DisableSyntax.null
|
||||
|
||||
@@ -2,49 +2,32 @@ 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 de.nowchess.io.service.config.JacksonConfig
|
||||
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)
|
||||
val m = new ObjectMapper()
|
||||
new JacksonConfig().customize(m)
|
||||
m
|
||||
|
||||
test("serializes square as algebraic notation") {
|
||||
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") {
|
||||
test("serializes a1 corner"):
|
||||
val json = mapper.writeValueAsString(Map(Square(File.A, Rank.R1) -> 1))
|
||||
json should include("\"a1\"")
|
||||
}
|
||||
|
||||
test("serializes h8 corner") {
|
||||
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
|
||||
}
|
||||
test("round-trips with SquareKeyDeserializer"):
|
||||
val original = Map(Square(File.D, Rank.R5) -> 99)
|
||||
val json = rt.writeValueAsString(original)
|
||||
val result = rt.readValue(json, new TypeReference[Map[Square, Int]] {})
|
||||
val json = mapper.writeValueAsString(original)
|
||||
val result = mapper.readValue(json, new TypeReference[Map[Square, Int]] {})
|
||||
result shouldBe original
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 de.nowchess.json.{SquareKeyDeserializer, SquareKeySerializer}
|
||||
import io.quarkus.test.junit.QuarkusTest
|
||||
import io.restassured.RestAssured
|
||||
import io.restassured.http.ContentType
|
||||
|
||||
Reference in New Issue
Block a user