Skip to content

Commit

Permalink
feat: add port and http adapter of the chess game service and add che…
Browse files Browse the repository at this point in the history
…ss engine dependency
  • Loading branch information
madina9229 authored and jahrim committed Aug 26, 2023
1 parent 5a69b56 commit cd88691
Show file tree
Hide file tree
Showing 22 changed files with 526 additions and 1 deletion.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
implementation(libs.scallop)
implementation(libs.hexarc)
implementation(libs.vertx.web)
implementation(libs.chess.engine)
testImplementation(libs.scalatest)
testImplementation(libs.scalatestplusjunit)
}
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ publish-on-central-version = "5.0.7" # simplifies publishing arti
hexarc-version = "0.4.1" # hexagonal architecture framework
vertx-web-version = "4.4.4" # event-loop architecture framework
scallop-version = "4.1.0" # cli argument parser for scala
chess-engine-version = "0.6.0" # chess engine
# insert the version of the dependencies here...

[libraries]
Expand All @@ -21,6 +22,7 @@ scalafmt-config = { module = "org.scalameta:scalafmt-config_2.13", version.ref =
scallop = { module = "org.rogach:scallop_3", version.ref = "scallop-version" }
vertx-web = { module = "io.vertx:vertx-web", version.ref = "vertx-web-version" }
hexarc = { module = "io.github.jahrim:hexarc", version.ref = "hexarc-version" }
chess-engine = { module = "io.github.jahrim.chess:chess", version.ref = "chess-engine-version" }
# insert the dependencies here...

[bundles]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.github.jahrim.chess.game.service.components.adapters.http

import io.github.chess.engine.model.configuration.{GameConfiguration, GameMode, TimeConstraint, WhitePlayer}
import io.github.jahrim.chess.game.service.components.adapters.http.handlers.LogHandler
import io.vertx.core.http.HttpServerOptions
import io.github.jahrim.hexarc.architecture.vertx.core.components.{Adapter, AdapterContext}
import io.github.jahrim.chess.game.service.components.ports.ChessGamePort
import io.github.jahrim.chess.game.service.main.TimeConstraint
import io.vertx.ext.web.handler.BodyHandler
import io.vertx.ext.web.{Router, RoutingContext}
import io.github.jahrim.chess.game.service.util.extension.VertxFutureExtension.*
import io.github.jahrim.chess.game.service.util.extension.JsonObjectExtension.*
import io.github.jahrim.chess.game.service.util.extension.RoutingContextExtension.*
import io.github.jahrim.chess.game.service.components.data.GameConfigurationData
import io.github.jahrim.chess.game.service.components.data.codecs.TimeConstraintCodec

class ChessGameHttpAdapter(
options: HttpServerOptions = new HttpServerOptions:
setHost("localhost")
setPort(8080)
) extends Adapter[ChessGamePort]:


override protected def init(context: AdapterContext[ChessGamePort]): Unit =
val router = Router.router(context.vertx)

router
.route()
.handler(BodyHandler.create())
.handler(LogHandler(context.log.info))

router
.get("/")
.handler { message => message.response().send("Ciao, questo e' il chess game service") }

router
.post("/game")
.handler { message =>
future { message.requireBodyParam("gameConfiguration").as[GameConfigurationData] }
.compose { context.api.createGame(_) }
.onSuccess { ok(message) }
.onFailure { fail(message) }
}

router
.get("/game")
.handler { message =>
context.api.findPublicGame()
.map { gameId =>
bsonToJson(bson {
"connection" :# {
"websocket" :: s"game/$gameId"
}
}).encode()
}
.onSuccess { json => message.sendJson(json) }
.onFailure { fail(message) }
}

router
.get("game/:gameId")
.handler { message =>
future { message.requirePathParam("gameId") }
.compose { context.api.joinPrivateGame(_) }
.map { gameId =>
bsonToJson(bson {
"connection" :# {
"websocket" :: s"game/$gameId"
}
}).encode()
}
.onSuccess { json => message.sendJson(json) }
.onFailure { fail(message) }
}

context.vertx
.createHttpServer(options)
.requestHandler(router)
.listen(_ => context.log.info("The server is up"))

end init

private def ok[T](message: RoutingContext): Handler[T] = _ => message.ok.send()

private def fail(message: RoutingContext): Handler[Throwable] = e =>
e.printStackTrace()
val response: HttpServerResponse = e match
case e: MalformedInputException => message.error(400, e)
case e: Exception => message.error(500, e)
response.send()


Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.jahrim.chess.game.service.components.adapters.http

import io.vertx.core.Handler
import io.vertx.ext.web
import io.vertx.ext.web.RoutingContext

/** An handler for http requests. */
@FunctionalInterface
trait HttpHandler extends Handler[RoutingContext]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.jahrim.chess.game.service.components.adapters.http.handlers

import io.github.jahrim.chess.authentication.service.components.adapters.http.HttpHandler
import io.github.jahrim.chess.game.service.components.adapters.http.handlers.LogHandler.*
import io.vertx.ext.web.RoutingContext

import scala.jdk.CollectionConverters.{IterableHasAsScala, MapHasAsScala}

/**
* An [[HttpHandler]] that logs all incoming requests through the specified [[Logger]].
* @param logger the specified logger.
* @param verbosityLevel the specified verbosity level (default: maximum verbosity).
* <br/> - =0: disables logging.
* <br/> - >0: logs the `date`, `request number` and `route` of the request.
* <br/> - >1: logs the `body` of the request.
* <br/> - >2: logs the `query parameters` of the request.
*/
case class LogHandler(
private val logger: String => Unit,
private val verbosityLevel: Int = Int.MaxValue
) extends HttpHandler:
private var requestCount: Int = -1

override def handle(c: RoutingContext): Unit =
var logString = ""
if this.verbosityLevel > 0 then
this.requestCount = this.requestCount + 1
logString =
logString + s"\nRequest $requestCount:\n${c.request.method}: ${c.request.absoluteURI}"
if this.verbosityLevel > 1 then
logString =
logString + s"\nWith body:\n${stringOf(Option(c.body).flatMap(b => Option(b.asString)))}"
if this.verbosityLevel > 2 then
logString = logString + s"\nWith query params:\n${stringOf(c.queryParams.asScala)}\n"
logger(logString)
c.next()

/** Companion object of [[LogHandler]]. */
object LogHandler:
/**
* @param iterable the specified [[Iterable]].
* @return the string representation of the specified [[Iterable]].
*/
private def stringOf(iterable: Iterable[?]): String =
if iterable.isEmpty then "{}" else iterable.mkString(",")
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.github.jahrim.chess.game.service.components.data

import io.github.chess.engine.model.moves.Move
import io.github.chess.engine.model.pieces.Piece
import io.github.jahrim.hexarc.persistence.bson.codecs.{BsonDocumentDecoder, BsonDocumentEncoder}

case class ChessboardStatusData(
pieces: Seq[Piece],
moves: Seq[Move]
)

object ChessboardStatusCodec:
given BsonDocumentDecoder[ChessboardStatusData] = bson =>
ChessboardStatusData(
pieces = bson.require("pieces").as[Seq[Piece]],
moves = bson.require("moves").as[Seq[Move]]
)

given BsonDocumentEncoder[ChessboardStatusData] = csd =>
bson {
"pieces" :: csd.pieces
"moves" :: csd.moves
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.jahrim.chess.game.service.components.data

import io.github.chess.engine.model.configuration.TimeConstraint

case class GameConfigurationData(
timeConstraint: Option[TimeConstraint],
gameId: Option[String],
isPrivate: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.github.jahrim.chess.game.service.components.data

enum PieceTypeData:
case Pawn, Knight, Bishop, Rook, Queen, King
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.jahrim.chess.game.service.components.data.codecs

import org.bson.conversions.Bson

object Codecs:
export TimeConstraintCodec.given
export GameConfigurationCodec.given
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.jahrim.chess.game.service.components.data.codecs

import io.github.chess.engine.model.configuration.TimeConstraint
import io.github.chess.util.general.timer.Time
import io.github.jahrim.hexarc.persistence.bson.codecs.{BsonDocumentDecoder, BsonDocumentEncoder}
import io.github.jahrim.hexarc.persistence.bson.dsl.BsonDSL.{*, given}
import org.bson.conversions.Bson

import java.util.concurrent.TimeUnit
import scala.concurrent.duration.Duration

/** [[Bson]] codec for [[Duration]]. */
object DurationCodec:
/** A given [[BsonDocumentDecoder]] for [[Duration]]. */
given durationDecoder: BsonDocumentDecoder[Duration] = bson =>
Duration(
bson.require("value").as[Long],
TimeUnit.valueOf(bson.require("unit").as[String].toUpperCase)
)

/** A given [[BsonDocumentEncoder]] for [[Duration]]. */
given durationEncoder: BsonDocumentEncoder[Duration] = duration =>
bson {
"value" :: duration.length
"unit" :: duration.unit.toString
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.jahrim.chess.game.service.components.data.codecs

import io.github.jahrim.hexarc.persistence.bson.codecs.{BsonDocumentDecoder, BsonDocumentEncoder}
import io.github.jahrim.hexarc.persistence.bson.dsl.BsonDSL.{*, given}
import org.bson.conversions.Bson
import io.github.jahrim.chess.game.service.components.data.GameConfigurationData
import TimeConstraintCodec.given
import io.github.chess.engine.model.configuration.TimeConstraint


/** [[Bson]] codec for [[GameConfigurationData]]. */
object GameConfigurationCodec:
/** A given [[BsonDocumentDecoder]] for [[GameConfigurationData]]. */
given bsonToGameConfiguration: BsonDocumentDecoder[GameConfigurationData] = bson =>
GameConfigurationData(
timeConstraint = bson("gameConfiguration.timeConstraint").map(_.as[TimeConstraint]),
gameId = bson("gameConfiguration.gameId").map(_.as[String]),
isPrivate = bson.require("gameConfiguration.isPrivate").as[Boolean]
)

/** A given [[BsonDocumentEncoder]] for [[GameConfigurationData]]. */
given gameConfigurationToBson: BsonDocumentEncoder[GameConfigurationData] = gc =>
bson {
gc.timeConstraint.foreach { "timeConstraint" :: tc }
gc.gameId.foreach { "gameId" :: tc }
"private" :: gc.isPrivate
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.jahrim.chess.game.service.components.data.codecs

import io.github.jahrim.chess.game.service.components.data.PieceTypeData
import io.github.jahrim.hexarc.persistence.bson.codecs.{BsonDecoder, BsonEncoder}
import org.bson.BsonString

object PieceTypeDataCodec:

given BsonDecoder[PieceTypeData] = bson =>
PieceTypeData(bson.asString.getValue)

given BsonEncoder[PieceTypeData] = pt =>
BsonString(pt.toString)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.jahrim.chess.game.service.components.data.codecs

import DurationCodec.given
import io.github.jahrim.hexarc.persistence.bson.codecs.{BsonDocumentDecoder, BsonDocumentEncoder}
import io.github.jahrim.hexarc.persistence.bson.dsl.BsonDSL.{*, given}
import org.bson.conversions.Bson
import io.github.chess.engine.model.configuration.TimeConstraint

import java.util.concurrent.TimeUnit
import scala.concurrent.duration.Duration

/** [[Bson]] codec for [[TimeConstraint]]. */
object TimeConstraintCodec:
/** A given [[BsonDocumentDecoder]] for [[TimeConstraint]]. */
given bsonToTimeConstraint: BsonDocumentDecoder[TimeConstraint] = bson =>
val constraint = bson.require("type").as[String] match
case "MoveLimit" => TimeConstraint.MoveLimit
case "PlayerLimit" => TimeConstraint.PlayerLimit
case _ => TimeConstraint.NoLimit
constraint.minutes = bson.require("time").as[Duration].toMinutes.asInstanceOf[Int] //TODO make Long
constraint

/** A given [[BsonDocumentEncoder]] for [[TimeConstraint]]. */
given timeConstraintToBson: BsonDocumentEncoder[TimeConstraint] = ts =>
bson {
"type" :: ts match
case _: TimeConstraint.MoveLimit => "MoveLimit"
case _: TimeConstraint.PlayerLimit => "PlayerLimit"
case _ => "NoLimit"
"time" :: Duration(ts.minutes, TimeUnit.MINUTES)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.jahrim.chess.game.service.components.exceptions

class MalformedInputException extends Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.jahrim.chess.game.service.components.ports

class ChessGameModel {

}

0 comments on commit cd88691

Please sign in to comment.