Skip to content

Commit

Permalink
Refactor all V1 (#17)
Browse files Browse the repository at this point in the history
* fix(githooks): git diff check

* refactor(all): add tests for InMemoryMechanismRepositorySpec and InMemoryReactionRepositorySpec
  • Loading branch information
sobakavosne authored Nov 23, 2024
1 parent 56fa116 commit 5200066
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 69 deletions.
16 changes: 16 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
#!/bin/sh

echo "Checking for unstaged changes..."
git diff --exit-code
EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
echo ""
echo "======================================"
echo "Unstaged changes detected. Please stage or discard changes before running this script."
echo "======================================"
echo ""
exit 1
fi

echo "No unstaged changes found."
echo ""

echo "Running scalafmt check..."
echo ""
sbt scalafmtCheckAll
Expand Down
14 changes: 9 additions & 5 deletions src/main/scala/app/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import akka.util.Timeout
import cats.effect.{ExitCode, IO, IOApp, Resource}
import cats.implicits.toSemigroupKOps

import com.comcast.ip4s.Host
import com.comcast.ip4s.Port

import config.ConfigLoader
import config.ConfigLoader.DefaultConfigLoader

import org.http4s.Uri
import org.typelevel.log4cats.slf4j.Slf4jLogger
import org.typelevel.log4cats.Logger

Expand Down Expand Up @@ -81,10 +85,10 @@ object Main extends IOApp {
ttl: Timeout,
logger: Logger[IO]
): Resource[IO, Unit] =
val preprocessorBaseUri = config.preprocessorHttpClientConfig.baseUri
val engineBaseUri = config.engineHttpClientConfig.baseUri
val host = config.httpConfig.host
val port = config.httpConfig.port
val preprocessorBaseUri: Uri = config.preprocessorHttpClientConfig.baseUri
val engineBaseUri: Uri = config.engineHttpClientConfig.baseUri
val host: Host = config.httpConfig.host
val port: Port = config.httpConfig.port

for {
system <- actorSystemResource
Expand Down Expand Up @@ -125,7 +129,7 @@ object Main extends IOApp {
implicit val ec: ExecutionContext = system.dispatcher
implicit val distributedData = DistributedData(system)
implicit val selfUniqueAddress: SelfUniqueAddress = distributedData.selfUniqueAddress
implicit val cacheExpiration: Timeout = Timeout(5.minutes)
implicit val ttl: Timeout = Timeout(5.minutes)

runApp(config)
.use(_ => IO.unit)
Expand Down
33 changes: 18 additions & 15 deletions src/main/scala/core/repositories/InMemoryMechanismRepository.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package core.repositories

import cats.effect.{Ref, Sync}
import cats.implicits.{catsSyntaxApplicativeId, catsSyntaxEitherId, catsSyntaxEq, toFlatMapOps, toFunctorOps}

import core.domain.preprocessor.{Mechanism, MechanismId}
import core.errors.http.preprocessor.MechanismError
import core.errors.http.preprocessor.MechanismError.CreationError
import cats.implicits.toFunctorOps

import types.MechanismRepository

/**
Expand Down Expand Up @@ -70,19 +72,20 @@ class InMemoryMechanismRepository[F[_]: Sync](state: Ref[F, Map[MechanismId, Mec
* - The `copy` method in Scala can be thought of as `record syntax` in Haskell, creating a new `Mechanism` with an
* updated `id`.
*/
def create(mechanism: Mechanism): F[Either[MechanismError, Mechanism]] = {
state.modify { mechanisms =>
val id = generateId(mechanisms)

if (mechanisms.values.exists(_.mechanismName == mechanism.mechanismName)) {
// Returns Left if a mechanism with the same name already exists
(mechanisms, Left(CreationError(s"Mechanism with name '${mechanism.mechanismName}' already exists")))
} else {
val newMechanism = mechanism.copy(id)
(mechanisms + (id -> newMechanism), Right(newMechanism))
}
def create(mechanism: Mechanism): F[Either[MechanismError, Mechanism]] =
state.get.flatMap { mechanisms =>
mechanisms.values
.find(_.mechanismName === mechanism.mechanismName)
.fold {
val newId = generateId(mechanisms)
val newMechanism = mechanism.copy(newId)
state.update(_ + (newId -> newMechanism)).as(newMechanism.asRight[MechanismError])
} { _ =>
CreationError(s"Mechanism with name '${mechanism.mechanismName}' already exists")
.asLeft[Mechanism]
.pure[F]
}
}
}

/**
* Deletes a Mechanism from the state by its identifier.
Expand All @@ -96,8 +99,8 @@ class InMemoryMechanismRepository[F[_]: Sync](state: Ref[F, Map[MechanismId, Mec
*/
def delete(id: MechanismId): F[Boolean] =
state.modify { mechanisms =>
if (mechanisms.contains(id)) (mechanisms - id, true)
else (mechanisms, false)
mechanisms.get(id)
.fold((mechanisms, false))(_ => (mechanisms - id, true))
}

}
65 changes: 34 additions & 31 deletions src/main/scala/core/repositories/InMemoryReactionRepository.scala
Original file line number Diff line number Diff line change
@@ -1,80 +1,83 @@
package core.repositories

import cats.effect.{Ref, Sync}
import cats.implicits.toFunctorOps
import cats.syntax.all._

import core.domain.preprocessor.{Reaction, ReactionId}
import types.ReactionRepository
import core.errors.http.preprocessor.ReactionError
import core.errors.http.preprocessor.ReactionError.CreationError

import types.ReactionRepository

/**
* An in-memory implementation of the `ReactionRepository` for testing and local use.
* An in-memory implementation of `ReactionRepository` for testing and local use.
*
* @param state
* A reference to a mutable map representing the current state of stored reactions.
* A `Ref` encapsulating the mutable map of reactions.
* @tparam F
* The effect type (e.g., `IO`, `SyncIO`, etc.).
* Effect type, such as `IO` or `SyncIO`.
*/
class InMemoryReactionRepository[F[_]: Sync](state: Ref[F, Map[ReactionId, Reaction]])
class FunctionalInMemoryReactionRepository[F[_]: Sync](state: Ref[F, Map[ReactionId, Reaction]])
extends ReactionRepository[F] {

/**
* Generates a new unique ID for a reaction.
*
* @param currentState
* @param reactions
* The current state of the stored reactions.
* @return
* The next available integer ID.
*/
private def generateId(currentState: Map[ReactionId, Reaction]): Int =
currentState.keys.maxOption.getOrElse(0) + 1
private def generateId(reactions: Map[ReactionId, Reaction]): ReactionId =
reactions.keys.maxOption.fold(1)(_ + 1)

/**
* Retrieves a reaction by its ID.
*
* @param id
* The ID of the reaction to retrieve.
* @return
* An effect wrapping an optional `Reaction`. Returns `None` if the reaction is not found.
* Effectful optional `Reaction`. Returns `None` if not found.
*/
def get(id: ReactionId): F[Option[Reaction]] =
state.get.map(_.get(id))

/**
* Creates a new reaction and stores it in the repository.
* Creates a new reaction.
*
* @param reaction
* The `Reaction` instance to create.
* The `Reaction` instance to add.
* @return
* An effect wrapping either a `ReactionError` if the creation fails or the created `Reaction` on success.
* - If a reaction with the same name already exists, returns a `CreationError`.
* Effectful result of `Either` with an error or the new reaction.
*/
def create(reaction: Reaction): F[Either[ReactionError, Reaction]] = {
state.modify { reactions =>
val id = generateId(reactions)
if (reactions.values.exists(_.reactionName == reaction.reactionName)) {
(reactions, Left(CreationError(s"Reaction with name '${reaction.reactionName}' already exists")))
} else {
val newReaction = reaction.copy(id)
(reactions + (id -> newReaction), Right(newReaction))
}
def create(reaction: Reaction): F[Either[ReactionError, Reaction]] =
state.get.flatMap { reactions =>
reactions.values
.find(_.reactionName === reaction.reactionName)
.fold {
val newId = generateId(reactions)
val newReaction = reaction.copy(newId)
state.update(_ + (newId -> newReaction)).as(newReaction.asRight[ReactionError])
} { _ =>
CreationError(s"Reaction with name '${reaction.reactionName}' already exists")
.asLeft[Reaction]
.pure[F]
}
}
}

/**
* Deletes a reaction by its ID.
*
* @param id
* The ID of the reaction to delete.
* @return
* An effect wrapping a `Boolean` indicating whether the deletion was successful.
* - Returns `true` if the reaction was successfully deleted.
* - Returns `false` if the reaction ID was not found.
* Effectful `Boolean` indicating if the deletion was successful.
*/
def delete(id: ReactionId): F[Boolean] =
state.modify { reactions =>
if (reactions.contains(id)) (reactions - id, true)
else (reactions, false)
}
state.modify(reactions =>
reactions
.get(id)
.fold((reactions, false))(_ => (reactions - id, true))
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ package core.repositories

import cats.effect.Sync
import cats.implicits.{toFlatMapOps, toFunctorOps}

import core.domain.preprocessor.{Reaction, ReactionId}
import core.errors.http.preprocessor.ReactionError
import core.errors.http.preprocessor.ReactionError.CreationError

import io.circe.parser.decode
import io.circe.syntax.EncoderOps

import infrastructure.http.HttpClient

import org.http4s.Uri

import types.ReactionRepository

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import core.domain.preprocessor.{Mechanism, MechanismDetails, MechanismId, React
import core.services.cache.types.CacheServiceTrait

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

/**
* A distributed cache service for managing mechanisms and reactions using Akka Distributed Data.
Expand Down Expand Up @@ -89,7 +88,7 @@ class DistributedCacheService[F[_]: Async](
private def getFromCache[K, V](key: LWWMapKey[K, V], id: K): F[Option[V]] =
Async[F].fromFuture {
Async[F].delay {
(replicator ? Get(key, ReadAll(5.seconds))).map {
(replicator ? Get(key, ReadAll(ttl.duration))).map {
case response: Replicator.GetSuccess[LWWMap[K, V]] @unchecked =>
response.dataValue.get(id)
case _: Replicator.NotFound[V] @unchecked =>
Expand All @@ -104,9 +103,9 @@ class DistributedCacheService[F[_]: Async](
private def putInCache[K, V](key: LWWMapKey[K, V], id: K, value: V): F[Unit] =
Async[F].fromFuture {
Async[F].delay {
(replicator ? Update(key, LWWMap.empty[K, V], WriteAll(5.seconds)) {
(replicator ? Update(key, LWWMap.empty[K, V], WriteAll(ttl.duration)) {
_.put(selfUniqueAddress, id, value)
}).map(_ => ())
}).void
}
}

Expand Down
25 changes: 15 additions & 10 deletions src/main/scala/core/services/cache/LocalCacheService.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package core.services.cache

import scala.concurrent.duration._
import core.domain.preprocessor.{Mechanism, MechanismDetails, MechanismId, Reaction, ReactionDetails, ReactionId}
import scala.collection.concurrent.TrieMap
import cats.effect.Sync
import cats.implicits.{toFlatMapOps, toFunctorOps}

import core.domain.preprocessor.{Mechanism, MechanismDetails, MechanismId, Reaction, ReactionDetails, ReactionId}
import core.services.cache.types.CacheServiceTrait

import scala.collection.concurrent.TrieMap
import scala.concurrent.duration._

/**
* A local, in-memory service for caching mechanisms and reactions with a time-to-live (TTL) mechanism.
*
Expand All @@ -32,8 +35,8 @@ class LocalCacheService[F[_]: Sync](
private def isExpired(entryTime: Long): Boolean =
currentTime - entryTime > ttl.toMillis

private def cleanCache[K, V](cache: TrieMap[K, (V, Long)]): Unit =
cache.filterInPlace { case (_, (_, timestamp)) => !isExpired(timestamp) }
private def cleanCache[K, V](cache: TrieMap[K, (V, Long)]): F[Unit] =
Sync[F].delay { cache.filterInPlace { case (_, (_, timestamp)) => !isExpired(timestamp) } }

private def getFromCache[K, V](cache: TrieMap[K, (V, Long)], id: K): F[Option[V]] =
Sync[F].delay {
Expand Down Expand Up @@ -94,11 +97,13 @@ class LocalCacheService[F[_]: Sync](
* @return
* An effectful computation that completes when all expired entries have been removed.
*/
def cleanExpiredEntries: F[Unit] = Sync[F].delay {
cleanCache(mechanismCache)
cleanCache(mechanismDetailsCache)
cleanCache(reactionCache)
cleanCache(reactionDetailsCache)
def cleanExpiredEntries: F[Unit] = {
for {
_ <- cleanCache(mechanismCache)
_ <- cleanCache(mechanismDetailsCache)
_ <- cleanCache(reactionCache)
_ <- cleanCache(reactionDetailsCache)
} yield ()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import core.errors.http.preprocessor.MechanismError
import core.errors.http.preprocessor.MechanismError._
import core.services.cache.DistributedCacheService

import org.http4s.circe._
import org.http4s.client.Client
import org.http4s.{Method, Request, Status, Uri}

import io.circe.syntax._
import io.circe.parser.decode

import org.http4s.circe._

/**
* Service for managing mechanisms using both a distributed cache and remote HTTP service.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package core.services.preprocessor

import cats.effect.Concurrent
import cats.implicits._
import cats.implicits.{catsSyntaxApplicativeError, catsSyntaxApplicativeId, toBifunctorOps, toFlatMapOps, toFunctorOps}

import core.domain.preprocessor.{Reaction, ReactionDetails, ReactionId}
import core.errors.http.preprocessor.ReactionError
import core.errors.http.preprocessor.ReactionError._
import core.services.cache.DistributedCacheService

import org.http4s.circe._
import org.http4s.client.Client
import org.http4s.{Method, Request, Status, Uri}

import io.circe.syntax._
import io.circe.parser.decode
import org.http4s.circe._

/**
* Service for managing reactions using both a distributed cache and remote HTTP service.
Expand Down
Loading

0 comments on commit 5200066

Please sign in to comment.