Skip to content

Commit

Permalink
Feat: move from Scala2 to Scala3 (#6)
Browse files Browse the repository at this point in the history
* feat(api): add

* feat(api): separate server creation

* feat(all): add `http4s` & introduce `scala3` fixes
  • Loading branch information
sobakavosne authored Nov 1, 2024
1 parent 2f45484 commit 4e13b81
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 133 deletions.
6 changes: 3 additions & 3 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version = "3.7.15"
runner.dialect = scala213
runner.dialect = scala3
align.preset = more

assumeStandardLibraryStripMargin = false
Expand All @@ -18,8 +18,8 @@ danglingParentheses.preset = true
verticalMultiline.arityThreshold = 1
newlines.source = keep
indentOperator.preset = spray
maxColumn = 100
maxColumn = 120
project.excludeFilters = [".*\\.sbt"]
rewrite.rules = [RedundantParens, SortModifiers, AsciiSortImports]
spaces.inImportCurlyBraces = false
newlines.topLevelStatements = [before]
# newlines.topLevelStatements = [before]
29 changes: 12 additions & 17 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Dependencies._

ThisBuild / scalaVersion := "2.12.19"
ThisBuild / scalaVersion := "3.3.3"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.chemist.flow"
ThisBuild / organizationName := "chemist.flow"
Expand All @@ -22,24 +22,19 @@ lazy val root = (project in file("."))
name := ".",
libraryDependencies ++= Seq(
scalaLogging,
scalaTest,
circeGeneric,
circeParser,
circeCore,
// circeCore,
catsEffect,
akkaStream,
pureconfig,
scalaTest,
akkaActor,
akkaHttp,
akkaTest,
docker,
spray
http4sEmberClient,
http4sEmberServer,
http4sCirce,
http4sDSL,
pureconfig.cross(CrossVersion.for3Use2_13),
akkaStream.cross(CrossVersion.for3Use2_13),
// akkaActor.cross(CrossVersion.for3Use2_13),
// akkaTest.cross(CrossVersion.for3Use2_13),
// docker
)
)

scalacOptions ++= Seq(
"-deprecation", // Warn about the usage of deprecated features
"-feature", // Warn about features that should be enabled explicitly
"-unchecked", // Enable additional warnings where generated code depends on assumptions
"-Xlint:unused" // Enable warnings for unused imports
)
34 changes: 18 additions & 16 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import sbt._

object Dependencies {
lazy val akkaVersion = "2.6.20"
lazy val akkaHttpVersion = "10.2.10"
lazy val akkaVersion = "2.7.0"
lazy val akkaHttpVersion = "10.4.0"
lazy val scalaTestVersion = "3.2.15"
lazy val scalaLogVersion = "1.2.11"
lazy val sprayVersion = "1.3.6"
lazy val dockerVersion = "8.9.0"
lazy val catsEffectVersion = "3.3.11"
lazy val circeVersion = "0.14.5"
lazy val pureconfigVersion = "0.17.1"
lazy val http4sVersion = "0.23.29"

lazy val akkaActor = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion
lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % akkaVersion
lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % akkaHttpVersion
lazy val scalaLogging = "ch.qos.logback" % "logback-classic" % scalaLogVersion
lazy val spray = "io.spray" %% "spray-json" % sprayVersion
lazy val docker = "com.spotify" % "docker-client" % dockerVersion
lazy val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion
lazy val circeCore = "io.circe" %% "circe-core" % circeVersion
lazy val circeGeneric = "io.circe" %% "circe-generic" % circeVersion
lazy val circeParser = "io.circe" %% "circe-parser" % circeVersion
lazy val pureconfig = "com.github.pureconfig" %% "pureconfig" % pureconfigVersion
lazy val akkaTest = "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test
lazy val scalaTest = "org.scalatest" %% "scalatest" % scalaTestVersion % Test
lazy val akkaActor = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion
lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % akkaVersion
lazy val scalaLogging = "ch.qos.logback" % "logback-classic" % scalaLogVersion
lazy val docker = "com.spotify" % "docker-client" % dockerVersion
lazy val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion
lazy val circeCore = "io.circe" %% "circe-core" % circeVersion
lazy val circeGeneric = "io.circe" %% "circe-generic" % circeVersion
lazy val circeParser = "io.circe" %% "circe-parser" % circeVersion
lazy val pureconfig = "com.github.pureconfig" %% "pureconfig" % pureconfigVersion
lazy val http4sEmberClient = "org.http4s" %% "http4s-ember-client" % http4sVersion
lazy val http4sEmberServer = "org.http4s" %% "http4s-ember-server" % http4sVersion
lazy val http4sCirce = "org.http4s" %% "http4s-circe" % http4sVersion
lazy val http4sDSL = "org.http4s" %% "http4s-dsl" % http4sVersion
lazy val akkaTest = "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test
lazy val scalaTest = "org.scalatest" %% "scalatest" % scalaTestVersion % Test
}
3 changes: 2 additions & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.4")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4")
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.17.5")
resolvers += "Scalafmt Releases" at "https://oss.sonatype.org/content/repositories/releases"
31 changes: 18 additions & 13 deletions src/main/scala/app/Main.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package app

import akka.actor.ActorSystem
import api.Endpoints
import cats.effect.{ExitCode, IO, IOApp, Resource}
import com.comcast.ip4s.{Host, Port}
import resource.api.{Endpoints, ServerBuilder}
import resource.core.util.ConfigLoader
import org.slf4j.LoggerFactory
import scala.concurrent.ExecutionContext
Expand All @@ -29,31 +30,35 @@ object Main extends IOApp {
)
)

def endpointsResource: Resource[IO, Endpoints] =
Resource.make(IO(new Endpoints))(endpoints =>
IO(logger.info("Shutting down Endpoints")).handleErrorWith(_ => IO.unit)
def serverBuilderResource(
implicit endpoints: Endpoints
): Resource[IO, ServerBuilder] =
Resource.make(IO(new ServerBuilder))(endpoints =>
IO(logger.info("Shutting down ServerBuilder")).handleErrorWith(_ => IO.unit)
)

def runApp(
host: String,
port: Int
host: Host,
port: Port
)(
implicit
ec: ExecutionContext,
system: ActorSystem
system: ActorSystem,
endpoints: Endpoints
): Resource[IO, Unit] =
for {
_ <- Resource.eval(IO(logger.info("Creating Actor system resource")))
system <- actorSystemResource
_ <- Resource.eval(IO(logger.info("Creating Endpoints resource")))
endpoints <- endpointsResource
_ <- endpoints.startServer(host, port)(system)
_ <- Resource.eval(IO(scala.io.StdIn.readLine))
_ <- Resource.eval(IO(logger.info("Creating Actor system resource")))
system <- actorSystemResource
_ <- Resource.eval(IO(logger.info("Creating ServerBuilder resource")))
serverBuilder <- serverBuilderResource
_ <- serverBuilder.startServer(host, port)
_ <- Resource.eval(IO(scala.io.StdIn.readLine))
} yield ()

override def run(
args: List[String]
): IO[ExitCode] = {
implicit val endpoints: Endpoints = new Endpoints
implicit val system: ActorSystem = ActorSystem("ChemistFlowActorSystem")
implicit val ec: ExecutionContext = system.dispatcher

Expand Down
89 changes: 29 additions & 60 deletions src/main/scala/resource/api/Endpoints.scala
Original file line number Diff line number Diff line change
@@ -1,72 +1,41 @@
package api

import akka.actor.ActorSystem
import cats.effect.{IO, Resource}
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
package resource.api

import cats.effect.IO
import cats.syntax.semigroupk.toSemigroupKOps
import org.http4s.HttpRoutes
import org.http4s.dsl.io._
import org.http4s.server.Router
import org.http4s.circe.CirceEntityEncoder._
import org.slf4j.LoggerFactory

class Endpoints {
private val logger = LoggerFactory.getLogger(getClass)

private def healthRoute: Route =
path("health")(get {
private val healthRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "health" =>
logger.info("[Request]: get health check")
complete("Health check response")
})
Ok("Health check response")
}

private def getReactionRoute: Route =
path("reaction" / Segment) { id =>
get {
logger.info(s"[Request]: get reaction details for ID: $id")
complete(s"Get reaction details for ID: $id")
}
}
private val getReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "reaction" / id =>
logger.info(s"[Request]: get reaction details for ID: $id")
Ok(s"Get reaction details for ID: $id")
}

private def postReactionRoute: Route =
path("reaction")(post {
private val postReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case POST -> Root / "reaction" =>
logger.info("[Request]: create new reaction")
complete("Create new reaction")
})

private def deleteReactionRoute: Route =
path("reaction" / Segment) { id =>
delete {
logger.info(s"[Request]: delete reaction with ID: $id")
complete(s"Delete reaction with ID: $id")
}
}

private val routes: Route =
pathPrefix("api") {
healthRoute ~
getReactionRoute ~
postReactionRoute ~
deleteReactionRoute
}
Ok("Create new reaction")
}

def startServer(
interface: String,
port: Int
)(
implicit system: ActorSystem
): Resource[IO, Http.ServerBinding] = {
Resource.make {
IO.fromFuture(IO(Http().newServerAt(interface, port).bind(routes)))
.flatTap { binding =>
IO(
logger.info(
s"Server online at http://${binding.localAddress.getHostName}:${binding.localAddress.getPort}/" +
"\nPress ENTER to terminate..."
)
)
}
} { binding =>
IO.fromFuture(IO(binding.unbind))
.flatTap(_ => IO(logger.info("Server unbound successfully")))
.handleErrorWith(ex => IO(logger.error(s"Failed to unbind server: ${ex.getMessage}")))
.as(())
}
private val deleteReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case DELETE -> Root / "reaction" / id =>
logger.info(s"[Request]: delete reaction with ID: $id")
Ok(s"Delete reaction with ID: $id")
}

val routes: HttpRoutes[IO] = Router(
"/api" -> (healthRoute <+> getReactionRoute <+> postReactionRoute <+> deleteReactionRoute)
)
}
38 changes: 38 additions & 0 deletions src/main/scala/resource/api/ErrorHandler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package resource.api

import cats.effect.IO
import org.http4s._
import io.circe.syntax._
import io.circe.generic.auto._
import org.http4s.circe.CirceEntityEncoder._

final case class ErrorResponse(error: String, message: String)

object ErrorHandler {
def apply(routes: HttpRoutes[IO]): HttpRoutes[IO] = HttpRoutes.of[IO] { request =>
routes(request).getOrElseF(
IO(Response(Status.NotFound).withEntity(ErrorResponse(
"NotFound",
"Resource not found"
).asJson))
).handleErrorWith {
case _: NoSuchElementException =>
IO.pure(Response(Status.NotFound).withEntity(ErrorResponse(
"NotFound",
"Resource not found"
).asJson))

case ex: IllegalArgumentException =>
IO.pure(Response(Status.BadRequest).withEntity(ErrorResponse(
"BadRequest",
ex.getMessage
).asJson))

case ex: Exception =>
IO.pure(Response(Status.InternalServerError).withEntity(ErrorResponse(
"InternalServerError",
"An unexpected error occurred."
).asJson))
}
}
}
32 changes: 32 additions & 0 deletions src/main/scala/resource/api/Server.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package resource.api

import cats.effect.{IO, Resource}
import com.comcast.ip4s.{Host, Port}
import cats.syntax.flatMap.toFlatMapOps
import org.http4s.server.Server
import org.http4s.ember.server.EmberServerBuilder
import org.slf4j.LoggerFactory

class ServerBuilder(
implicit endpoints: Endpoints
) {
private val logger = LoggerFactory.getLogger(getClass)

def startServer(
interface: Host,
port: Port
): Resource[IO, Server] = {
EmberServerBuilder
.default[IO]
.withHost(interface)
.withPort(port)
.withHttpApp(endpoints.routes.orNotFound)
.build
.flatTap { server =>
Resource.eval(IO(logger.info(
s"Server online at http://${server.address.getHostName}:${server.address.getPort}/" +
s"\nPress ENTER to terminate..."
)))
}
}
}
2 changes: 1 addition & 1 deletion src/main/scala/resource/core/configs/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ akka {

# Kafka configuration
kafka {
bootstrap-servers = "localhost:9092" # Kafka broker address
bootstrapServers = "localhost:9092" # Kafka broker address
client.id = "chemist-flow-client"
topic {
reactions = "reactions-topic" # Topic for reaction messages
Expand Down
Loading

0 comments on commit 4e13b81

Please sign in to comment.