Skip to content

Commit

Permalink
feat(all): integrate ReactionService & fix comments
Browse files Browse the repository at this point in the history
  • Loading branch information
sobakavosne committed Nov 12, 2024
1 parent 85d87c3 commit 4e9f3e2
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 113 deletions.
62 changes: 47 additions & 15 deletions src/main/scala/api/Endpoints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,79 @@ package api

import cats.effect.IO
import cats.syntax.semigroupk.toSemigroupKOps
import io.circe.syntax.EncoderOps
import org.http4s.HttpRoutes
import org.http4s.dsl.io._
import org.http4s.server.Router
import org.http4s.server.middleware.Logger
import org.http4s.circe.CirceEntityEncoder.circeEntityEncoder
import io.circe.syntax.EncoderOps
import io.circe.generic.auto._
import org.http4s.circe.CirceSensitiveDataEntityDecoder.circeEntityDecoder
import core.services.{MechanismService, ReactionService}
import core.domain.{MechanismDetails, Reaction, ReactionDetails}
import core.errors.http.ReactionError

class Endpoints {

private val healthRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "health" =>
Ok("Health check response")
}
class Endpoints(
reactionService: ReactionService[IO],
mechanismService: MechanismService[IO]
){

private val getReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "reaction" / id =>
validateId(id) match {
case Some(validId) => Ok(s"Get reaction details for ID: $validId")
case None => BadRequest(ErrorResponse("BadRequest", "ID must be an integer").asJson)
case Some(validId) =>
reactionService.getReaction(validId).flatMap {
(reactionDetails: ReactionDetails) => Ok(reactionDetails.asJson)
}.handleErrorWith {
case _: ReactionError.NotFoundError => NotFound(("NotFound", s"Reaction with ID $validId not found"))
case ex => InternalServerError(("InternalError", ex.getMessage))
}
case None => BadRequest(("BadRequest", "ID must be an integer"))
}
}

private val postReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case POST -> Root / "reaction" =>
Ok("Create new reaction")
case req @ POST -> Root / "reaction" =>
req.as[Reaction].flatMap { reaction =>
reactionService.createReaction(reaction).flatMap {
createdReaction => Created(createdReaction.asJson)
}.handleErrorWith {
case _: ReactionError.CreationError => BadRequest(("CreationError", "Failed to create reaction"))
case ex => InternalServerError(("InternalError", ex.getMessage))
}
}
}

private val deleteReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case DELETE -> Root / "reaction" / id =>
validateId(id) match {
case Some(validId) => Ok(s"Delete reaction with ID: $validId")
case None => BadRequest(ErrorResponse("BadRequest", "ID must be an integer").asJson)
case Some(validId) =>
reactionService.deleteReaction(validId).flatMap {
case Right(_) => NoContent()
case Left(error) => BadRequest(("DeletionError", error.message))
}
case None => BadRequest(("BadRequest", "ID must be an integer"))
}
}

private val getMechanismRoute: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "mechanism" / id =>
validateId(id) match {
case Some(validId) =>
mechanismService.getMechanism(validId).flatMap {
(mechanismDetails: MechanismDetails) => Ok(mechanismDetails.asJson)
}.handleErrorWith {
case _: ReactionError.NotFoundError => NotFound(("NotFound", s"Mechanism with ID $validId not found"))
case ex => InternalServerError(("InternalError", ex.getMessage))
}
case None => BadRequest(("BadRequest", "ID must be an integer"))
}
}

private def validateId(id: String): Option[Int] = id.toIntOption

val routes: HttpRoutes[IO] = Logger.httpRoutes(logHeaders = false, logBody = true)(
Router(
"/api" -> (healthRoute <+> getReactionRoute <+> postReactionRoute <+> deleteReactionRoute)
"/api" -> (getReactionRoute <+> postReactionRoute <+> deleteReactionRoute <+> getMechanismRoute)
)
)

Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/api/ServerBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.http4s.server.Server
import org.http4s.ember.server.EmberServerBuilder

class ServerBuilder(
implicit endpoints: Endpoints
endpoints: Endpoints
) {

def startServer(
Expand Down
113 changes: 88 additions & 25 deletions src/main/scala/app/Main.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package app

import api.{Endpoints, ServerBuilder}
import akka.actor.ActorSystem
import cats.effect.{ExitCode, IO, IOApp, Resource}
import com.comcast.ip4s.{Host, Port}
import api.{Endpoints, ServerBuilder}
import core.services.{CacheService, MechanismService, ReactionService}
import config.ConfigLoader
import org.typelevel.log4cats.slf4j.Slf4jLogger
import org.typelevel.log4cats.Logger
import org.http4s.client.Client
import org.http4s.ember.client.EmberClientBuilder
import scala.concurrent.ExecutionContext
import org.http4s.Uri
import config.ConfigLoader.httpClientConfig

object Main extends IOApp {

Expand All @@ -17,56 +22,114 @@ object Main extends IOApp {
system: ActorSystem,
logger: Logger[IO]
): Resource[IO, ActorSystem] =
Resource.make(IO(system)) { system =>
Resource.make(
logger.info("Creating Actor System") *> IO(system)
)(system =>
IO.fromFuture(IO(system.terminate())).attempt.flatMap {
case Right(_) => logger.info("Actor system terminated successfully")
case Left(ex) => logger.error(s"Actor system termination failed: ${ex.getMessage}")
}
}
)

def serverBuilderResource(
implicit
endpoints: Endpoints,
serverBuilder: ServerBuilder,
logger: Logger[IO]
endpoints: Endpoints
)(
implicit logger: Logger[IO]
): Resource[IO, ServerBuilder] =
Resource.make(IO(serverBuilder))(endpoints =>
Resource.make(
logger.info("Creating Server Builder") *> IO(new ServerBuilder(endpoints))
)(endpoints =>
logger.info("Shutting down ServerBuilder").handleErrorWith(_ => IO.unit)
)

def endpointsResource(
reactionService: ReactionService[IO],
mechanismService: MechanismService[IO]
)(
implicit logger: Logger[IO]
): Resource[IO, Endpoints] =
Resource.make(
logger.info("Creating Endpoints") *> IO(new Endpoints(reactionService, mechanismService))
)(endpoints =>
logger.info("Shutting down Endpoints").handleErrorWith(_ => IO.unit)
)

def clientResource(
implicit logger: Logger[IO]
): Resource[IO, Client[IO]] =
EmberClientBuilder.default[IO].build

def mechanismServiceResource(
client: Client[IO],
cacheService: CacheService[IO],
baseUri: Uri
)(
implicit logger: Logger[IO]
): Resource[IO, MechanismService[IO]] =
Resource.make(
logger.info("Creating Mechanism Service") *>
IO(new MechanismService[IO](client, cacheService, baseUri))
)(_ =>
logger.info("Shutting down Mechanism Service").handleErrorWith(_ => IO.unit)
)

def reactionServiceResource(
client: Client[IO],
cacheService: CacheService[IO],
baseUri: Uri
)(
implicit logger: Logger[IO]
): Resource[IO, ReactionService[IO]] =
Resource.make(
logger.info("Creating Reaction Service") *>
IO(new ReactionService[IO](client, cacheService, baseUri))
)(_ =>
logger.info("Shutting down Reaction Service").handleErrorWith(_ => IO.unit)
)

def cacheServiceResource(
implicit logger: Logger[IO]
): Resource[IO, CacheService[IO]] =
Resource.make(
logger.info("Creating Cache Service") *> IO(new CacheService[IO])
)(_ =>
logger.info("Shutting down Cache Service").handleErrorWith(_ => IO.unit)
)

def runApp(
host: Host,
port: Port
port: Port,
baseUri: Uri
)(
implicit
ec: ExecutionContext,
system: ActorSystem,
endpoints: Endpoints,
serverBuilder: ServerBuilder,
logger: Logger[IO]
): Resource[IO, Unit] =
for {
_ <- Resource.eval(logger.info("Creating Actor system resource"))
system <- actorSystemResource
_ <- Resource.eval(logger.info("Creating ServerBuilder resource"))
serverBuilder <- serverBuilderResource
_ <- serverBuilder.startServer(host, port)
_ <- Resource.eval(logger.info("Press ENTER to terminate..."))
_ <- Resource.eval(IO(scala.io.StdIn.readLine))
system <- actorSystemResource
client <- clientResource
cacheService <- cacheServiceResource
mechanismService <- mechanismServiceResource(client, cacheService, baseUri / "mechanism")
reactionService <- reactionServiceResource(client, cacheService, baseUri / "reaction")
endpoints <- endpointsResource(reactionService, mechanismService)
serverBuilder <- serverBuilderResource(endpoints)
server <- serverBuilder.startServer(host, port)
_ <- Resource.eval(logger.info("Press ENTER to terminate..."))
_ <- Resource.eval(IO(scala.io.StdIn.readLine))
} yield ()

override def run(
args: List[String]
): IO[ExitCode] = {
val httpConfig = ConfigLoader.httpConfig
val httpConfig = ConfigLoader.httpConfig
val httpClientConfig = ConfigLoader.httpClientConfig

implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO]
implicit val endpoints: Endpoints = new Endpoints
implicit val serverBuilder: ServerBuilder = new ServerBuilder
implicit val system: ActorSystem = ActorSystem("ChemistFlowActorSystem")
implicit val ec: ExecutionContext = system.dispatcher
implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO]
implicit val system: ActorSystem = ActorSystem("ChemistFlowActorSystem")
implicit val ec: ExecutionContext = system.dispatcher

runApp(httpConfig.host, httpConfig.port).use(_ => IO.unit).as(ExitCode.Success)
runApp(httpConfig.host, httpConfig.port, httpClientConfig.baseUri).use(_ => IO.unit).as(ExitCode.Success)
}

}
7 changes: 6 additions & 1 deletion src/main/scala/config/ConfigLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import pureconfig.{ConfigReader, ConfigSource}
import pureconfig.error.CannotConvert
import java.io.File
import scala.concurrent.duration.FiniteDuration
import org.http4s.Uri

case class KafkaTopics(
reactions: String,
Expand Down Expand Up @@ -65,7 +66,7 @@ object DatabaseConfig {
}

case class HttpClientConfig(
baseUri: String,
baseUri: Uri,
timeout: HttpClientTimeout,
retries: Int,
pool: HttpClientPool
Expand All @@ -89,6 +90,10 @@ object HttpClientConfig {
implicit val httpClientPoolReader: ConfigReader[HttpClientPool] =
ConfigReader.forProduct2("max-connections", "max-idle-time")(HttpClientPool.apply)

implicit val baseUriReader: ConfigReader[Uri] = ConfigReader.fromString { str =>
Uri.fromString(str).left.map(failure => CannotConvert(str, "Uri", failure.sanitized))
}

implicit val httpClientConfigReader: ConfigReader[HttpClientConfig] =
ConfigReader.forProduct4("baseUri", "timeout", "retries", "pool")(HttpClientConfig.apply)

Expand Down
48 changes: 38 additions & 10 deletions src/main/scala/core/domain/Chemical.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,24 @@ import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}

type MoleculeId = Int

type ReactionId = Int

type CatalystId = Int

case class Molecule(
id: MoleculeId,
smiles: String,
iupacName: String
moleculeId: MoleculeId,
moleculeSmiles: String,
moleculeIupacName: String
)

case class Reaction(
id: ReactionId,
name: String
reactionId: ReactionId,
reactionName: String
)

case class Catalyst(
id: CatalystId,
smiles: String,
name: Option[String]
catalystId: CatalystId,
catalystSmiles: String,
catalystName: Option[String]
)

case class PRODUCT_FROM(productAmount: Float)
Expand All @@ -35,6 +33,21 @@ case class ACCELERATE(
pressure: List[Float]
)

case class InboundReagent(
reagentIn: REAGENT_IN,
molecule: Molecule
)

case class OutboundProduct(
productFrom: PRODUCT_FROM,
molecule: Molecule
)

case class Condition(
accelerate: ACCELERATE,
catalyst: Catalyst
)

object Molecule {
implicit val moleculeEncoder: Encoder[Molecule] = deriveEncoder[Molecule]
implicit val moleculeDecoder: Decoder[Molecule] = deriveDecoder[Molecule]
Expand Down Expand Up @@ -64,3 +77,18 @@ object ACCELERATE {
implicit val accelerateEncoder: Encoder[ACCELERATE] = deriveEncoder[ACCELERATE]
implicit val accelerateDecoder: Decoder[ACCELERATE] = deriveDecoder[ACCELERATE]
}

object InboundReagent {
implicit val inboundReagentEncoder: Encoder[InboundReagent] = deriveEncoder[InboundReagent]
implicit val inboundReagentDecoder: Decoder[InboundReagent] = deriveDecoder[InboundReagent]
}

object OutboundProduct {
implicit val outboundProductEncoder: Encoder[OutboundProduct] = deriveEncoder[OutboundProduct]
implicit val outboundProductDecoder: Decoder[OutboundProduct] = deriveDecoder[OutboundProduct]
}

object Condition {
implicit val conditionEncoder: Encoder[Condition] = deriveEncoder[Condition]
implicit val conditionDecoder: Decoder[Condition] = deriveDecoder[Condition]
}
Loading

0 comments on commit 4e9f3e2

Please sign in to comment.