feat: NCS-53 changed IO to MicroService for easier scaling
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-21 14:15:44 +02:00
parent 5ad5efb41e
commit 9cb14e26e1
45 changed files with 547 additions and 29 deletions
+41 -2
View File
@@ -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<ScalaCompile> {
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<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<Jar>().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
}
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -0,0 +1,3 @@
package de.nowchess.io.service.dto
case class ImportFenRequest(fen: String)
@@ -0,0 +1,3 @@
package de.nowchess.io.service.dto
case class ImportPgnRequest(pgn: String)
@@ -0,0 +1,3 @@
package de.nowchess.io.service.dto
case class IoErrorDto(code: String, message: String)
@@ -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())