From 00afa6513f3cae673f46521e8e564be43933ed97 Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Thu, 28 Dec 2023 17:43:56 +0530 Subject: [PATCH 1/9] move readme to local directory --- forex-mtl/Forex.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 forex-mtl/Forex.md diff --git a/forex-mtl/Forex.md b/forex-mtl/Forex.md new file mode 100644 index 00000000..5afcb9fb --- /dev/null +++ b/forex-mtl/Forex.md @@ -0,0 +1,86 @@ + + +# Paidy Take-Home Coding Exercises + +## What to expect? +We understand that your time is valuable, and in anyone's busy schedule solving these exercises may constitute a fairly substantial chunk of time, so we really appreciate any effort you put in to helping us build a solid team. + +## What we are looking for? +**Keep it simple**. Read the requirements and restrictions carefully and focus on solving the problem. + +**Treat it like production code**. That is, develop your software in the same way that you would for any code that is intended to be deployed to production. These may be toy exercises, but we really would like to get an idea of how you build code on a day-to-day basis. + +## How to submit? +You can do this however you see fit - you can email us a tarball, a pointer to download your code from somewhere or just a link to a source control repository. Make sure your submission includes a small **README**, documenting any assumptions, simplifications and/or choices you made, as well as a short description of how to run the code and/or tests. Finally, to help us review your code, please split your commit history in sensible chunks (at least separate the initial provided code from your personal additions). + +# A local proxy for Forex rates + +Build a local proxy for getting Currency Exchange Rates + +## Requirements + +[Forex](forex-mtl) is a simple application that acts as a local proxy for getting exchange rates. It's a service that can be consumed by other internal services to get the exchange rate between a set of currencies, so they don't have to care about the specifics of third-party providers. + +We provide you with an initial scaffold for the application with some dummy interpretations/implementations. For starters we would like you to try and understand the structure of the application, so you can use this as the base to address the following use case: + +* The service returns an exchange rate when provided with 2 supported currencies +* The rate should not be older than 5 minutes +* The service should support at least 10,000 successful requests per day with 1 API token + +Please note the following drawback of the [One-Frame service](https://hub.docker.com/r/paidyinc/one-frame): + +> The One-Frame service supports a maximum of 1000 requests per day for any given authentication token. + +## Guidance + +In practice, this should require the following points: + +1. Create a `live` interpreter for the `oneframe` service. This should consume the [One-Frame API](https://hub.docker.com/r/paidyinc/one-frame). + +2. Adapt the `rates` processes (if necessary) to make sure you cover the requirements of the use case, and work around possible limitations of the third-party provider. + +3. Make sure the service's own API gets updated to reflect the changes you made in point 1 & 2. + +Some notes: +- Don't feel limited by the existing dependencies; you can include others. +- The algebras/interfaces provided act as an example/starting point. Feel free to add to improve or built on it when needed. +- The `rates` processes currently only use a single service. Don't feel limited, and do add others if you see fit. +- It's great for downstream users of the service (your colleagues) if the api returns descriptive errors in case something goes wrong. +- Feel free to fix any unsafe methods you might encounter. + +Some of the traits/specifics we are looking for using this exercise: + +- How can you navigate through an existing codebase; +- How easily do you pick up concepts, techniques and/or libraries you might not have encountered/used before; +- How do you work with third-party APIs that might not be (as) complete (as we would wish them to be); +- How do you work around restrictions; +- What design choices do you make; +- How do you think beyond the happy path. + +### The One-Frame service + +#### How to run locally + +* Pull the docker image with `docker pull paidyinc/one-frame` +* Run the service locally on port 8080 with `docker run -p 8080:8080 paidyinc/one-frame` + +#### Usage +__API__ + +The One-Frame API offers two different APIs, for this exercise please use the `GET /rates` one. + +`GET /rates?pair={currency_pair_0}&pair={currency_pair_1}&...pair={currency_pair_n}` + +pair: Required query parameter that is the concatenation of two different currency codes, e.g. `USDJPY`. One or more pairs per request are allowed. + +token: Header required for authentication. `10dc303535874aeccc86a8251e6992f5` is the only accepted value in the current implementation. + +__Example cURL request__ +``` +$ curl -H "token: 10dc303535874aeccc86a8251e6992f5" 'localhost:8080/rates?pair=USDJPY' + +[{"from":"USD","to":"JPY","bid":0.61,"ask":0.82,"price":0.71,"time_stamp":"2019-01-01T00:00:00.000"}] +``` + +## F.A.Q. +[Please click here for the F.A.Q.](./README.md) From 988735484c116fd8bf52c2e3173be8270a382a6c Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Fri, 29 Dec 2023 01:31:35 +0530 Subject: [PATCH 2/9] make case classes final --- forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala | 4 ++-- forex-mtl/src/main/scala/forex/domain/Price.scala | 2 +- forex-mtl/src/main/scala/forex/domain/Rate.scala | 2 +- forex-mtl/src/main/scala/forex/domain/Timestamp.scala | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala index eff0fad7..5c2a5ed8 100644 --- a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala +++ b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala @@ -2,11 +2,11 @@ package forex.config import scala.concurrent.duration.FiniteDuration -case class ApplicationConfig( +final case class ApplicationConfig( http: HttpConfig, ) -case class HttpConfig( +final case class HttpConfig( host: String, port: Int, timeout: FiniteDuration diff --git a/forex-mtl/src/main/scala/forex/domain/Price.scala b/forex-mtl/src/main/scala/forex/domain/Price.scala index 7faea8c5..a029f869 100644 --- a/forex-mtl/src/main/scala/forex/domain/Price.scala +++ b/forex-mtl/src/main/scala/forex/domain/Price.scala @@ -1,6 +1,6 @@ package forex.domain -case class Price(value: BigDecimal) extends AnyVal +final case class Price(value: BigDecimal) extends AnyVal object Price { def apply(value: Integer): Price = diff --git a/forex-mtl/src/main/scala/forex/domain/Rate.scala b/forex-mtl/src/main/scala/forex/domain/Rate.scala index 4a444003..238a5161 100644 --- a/forex-mtl/src/main/scala/forex/domain/Rate.scala +++ b/forex-mtl/src/main/scala/forex/domain/Rate.scala @@ -1,6 +1,6 @@ package forex.domain -case class Rate( +final case class Rate( pair: Rate.Pair, price: Price, timestamp: Timestamp diff --git a/forex-mtl/src/main/scala/forex/domain/Timestamp.scala b/forex-mtl/src/main/scala/forex/domain/Timestamp.scala index 82fc3fb0..accc53ca 100644 --- a/forex-mtl/src/main/scala/forex/domain/Timestamp.scala +++ b/forex-mtl/src/main/scala/forex/domain/Timestamp.scala @@ -2,7 +2,7 @@ package forex.domain import java.time.OffsetDateTime -case class Timestamp(value: OffsetDateTime) extends AnyVal +final case class Timestamp(value: OffsetDateTime) extends AnyVal object Timestamp { def now: Timestamp = From 02932fb94a704797d4d019ef4fec880a3cdf3c64 Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Fri, 29 Dec 2023 15:10:54 +0530 Subject: [PATCH 3/9] Working code --- forex-mtl/build.sbt | 4 ++ forex-mtl/project/Dependencies.scala | 11 +++ forex-mtl/src/main/resources/application.conf | 8 ++- forex-mtl/src/main/scala/forex/Main.scala | 7 +- forex-mtl/src/main/scala/forex/Module.scala | 20 ++++-- .../forex/client/OneFrameHttpClient.scala | 68 +++++++++++++++++++ .../forex/config/ApplicationConfig.scala | 7 ++ .../main/scala/forex/domain/Currency.scala | 8 +++ .../domain/OneFrameCurrencyInformation.scala | 11 +++ .../src/main/scala/forex/domain/Rate.scala | 4 +- .../forex/services/rates/Interpreters.scala | 5 ++ .../scala/forex/services/rates/algebra.scala | 3 + .../rates/interpreters/OneFrameDummy.scala | 8 ++- .../rates/interpreters/OneFrameService.scala | 49 +++++++++++++ 14 files changed, 201 insertions(+), 12 deletions(-) create mode 100644 forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala create mode 100644 forex-mtl/src/main/scala/forex/domain/OneFrameCurrencyInformation.scala create mode 100644 forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala diff --git a/forex-mtl/build.sbt b/forex-mtl/build.sbt index 8994026f..5acb9cf2 100644 --- a/forex-mtl/build.sbt +++ b/forex-mtl/build.sbt @@ -57,6 +57,10 @@ libraryDependencies ++= Seq( Libraries.http4sDsl, Libraries.http4sServer, Libraries.http4sCirce, + Libraries.sttpClientCore, + Libraries.sttpClientCirce, + Libraries.sttpClientBackend, + Libraries.scaffiene, Libraries.circeCore, Libraries.circeGeneric, Libraries.circeGenericExt, diff --git a/forex-mtl/project/Dependencies.scala b/forex-mtl/project/Dependencies.scala index 423210a1..8b567948 100644 --- a/forex-mtl/project/Dependencies.scala +++ b/forex-mtl/project/Dependencies.scala @@ -15,11 +15,14 @@ object Dependencies { val scalaCheck = "1.15.3" val scalaTest = "3.2.7" val catsScalaCheck = "0.3.2" + val sttp = "2.3.0" + val scaffeine = "0.28.0" } object Libraries { def circe(artifact: String): ModuleID = "io.circe" %% artifact % Versions.circe def http4s(artifact: String): ModuleID = "org.http4s" %% artifact % Versions.http4s + def sttp(artifact: String): ModuleID = "com.softwaremill.sttp.client" %% artifact % Versions.sttp lazy val cats = "org.typelevel" %% "cats-core" % Versions.cats lazy val catsEffect = "org.typelevel" %% "cats-effect" % Versions.catsEffect @@ -34,6 +37,14 @@ object Dependencies { lazy val circeParser = circe("circe-parser") lazy val pureConfig = "com.github.pureconfig" %% "pureconfig" % Versions.pureConfig + //Sttp client + lazy val sttpClientCore = sttp("core") + lazy val sttpClientCirce = sttp("circe") + lazy val sttpClientBackend = sttp("async-http-client-backend-cats") + + //Scala caffeine for caching + lazy val scaffiene = "com.github.cb372" %% "scalacache-caffeine" % Versions.scaffeine + // Compiler plugins lazy val kindProjector = "org.typelevel" %% "kind-projector" % Versions.kindProjector cross CrossVersion.full diff --git a/forex-mtl/src/main/resources/application.conf b/forex-mtl/src/main/resources/application.conf index b2af6efd..83806a9c 100644 --- a/forex-mtl/src/main/resources/application.conf +++ b/forex-mtl/src/main/resources/application.conf @@ -1,8 +1,12 @@ app { http { host = "0.0.0.0" - port = 8080 + port = 9000 timeout = 40 seconds } + one-frame{ + url = "localhost:8080" + token = "10dc303535874aeccc86a8251e6992f5" + cacheTTL = 4 #minutes + } } - diff --git a/forex-mtl/src/main/scala/forex/Main.scala b/forex-mtl/src/main/scala/forex/Main.scala index 6dda10a7..1633c6e2 100644 --- a/forex-mtl/src/main/scala/forex/Main.scala +++ b/forex-mtl/src/main/scala/forex/Main.scala @@ -20,9 +20,10 @@ class Application[F[_]: ConcurrentEffect: Timer] { config <- Config.stream("app") module = new Module[F](config) _ <- BlazeServerBuilder[F](ec) - .bindHttp(config.http.port, config.http.host) - .withHttpApp(module.httpApp) - .serve + .bindHttp(config.http.port, config.http.host) + .withHttpApp(module.httpApp) + .serve + .concurrently(module.ratesService.scheduleCacheRefresh()) } yield () } diff --git a/forex-mtl/src/main/scala/forex/Module.scala b/forex-mtl/src/main/scala/forex/Module.scala index 3bc47d58..52e509fd 100644 --- a/forex-mtl/src/main/scala/forex/Module.scala +++ b/forex-mtl/src/main/scala/forex/Module.scala @@ -1,17 +1,28 @@ package forex -import cats.effect.{ Concurrent, Timer } +import cats.effect.{Concurrent, Timer} +import forex.client.OneFrameClient import forex.config.ApplicationConfig +import forex.domain.Rate import forex.http.rates.RatesHttpRoutes -import forex.services._ import forex.programs._ +import forex.services._ import org.http4s._ import org.http4s.implicits._ -import org.http4s.server.middleware.{ AutoSlash, Timeout } +import org.http4s.server.middleware.{AutoSlash, Timeout} +import scalacache.Cache +import scalacache.caffeine.CaffeineCache +import sttp.client.{HttpURLConnectionBackend, Identity, NothingT, SttpBackend} class Module[F[_]: Concurrent: Timer](config: ApplicationConfig) { - private val ratesService: RatesService[F] = RatesServices.dummy[F] + private val rateCache: Cache[Rate] = CaffeineCache[Rate] + + private implicit val backend: SttpBackend[Identity, Nothing, NothingT] = HttpURLConnectionBackend() + + private lazy val oneFrameClient: OneFrameClient[F] = OneFrameClient.OneFrameHttpClient(config.oneFrame) + + val ratesService: RatesService[F] = RatesServices.live[F](oneFrameClient, rateCache) private val ratesProgram: RatesProgram[F] = RatesProgram[F](ratesService) @@ -33,5 +44,4 @@ class Module[F[_]: Concurrent: Timer](config: ApplicationConfig) { private val http: HttpRoutes[F] = ratesHttpRoutes val httpApp: HttpApp[F] = appMiddleware(routesMiddleware(http).orNotFound) - } diff --git a/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala new file mode 100644 index 00000000..39e9848e --- /dev/null +++ b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala @@ -0,0 +1,68 @@ +package forex.client + +import cats.effect.Async +import cats.implicits._ +import forex.config.OneFrameConfig +import forex.domain.OneFrameCurrencyInformation +import forex.domain.Rate.Pair +import io.circe.{Error => CError} +import sttp.client._ +import sttp.client.circe.asJson + +trait OneFrameClient[F[_]] { + def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CError], List[OneFrameCurrencyInformation]]]] +} + +class OneFrameHttpClient[F[_]: Async]( + oneFrameConfig: OneFrameConfig, + implicit val backend: SttpBackend[Identity, Nothing, NothingT] + ) extends OneFrameClient[F]{ + + def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CError], List[OneFrameCurrencyInformation]]]] = { + val param = pairs.map((pair: Pair) => "pair" -> s"${pair.from}${pair.to}") + val url = uri"http://${oneFrameConfig.url}/rates?$param" + println("herehere") + println(url) + + val request = basicRequest + .get(uri = url) + .contentType("application/json") + .header("token", "10dc303535874aeccc86a8251e6992f5") + .response(asJson[List[OneFrameCurrencyInformation]]) + .send().pure[F] + request + }.recoverWith { + case t => + println(s"t.getMessage = ${t.getMessage}") + Async[F].raiseError(new Exception("Failed to get rates", t)) + } + + +// override def getRate(pair: Pair): F[Response[Either[ResponseError[Error], OneFrameCurrencyInformation]]] = { +// val param = pair.to.show + pair.from.show +//// pairs.map((pair: Pair) => "pair" -> s"${pair.from}${pair.to}") +// val url = s"http://${oneFrameConfig.url}/rates?pair=$param" +// println(s"url = ${url}") +// Async[F].delay{ +// val request = basicRequest +// .get(uri = uri"$url") +// .contentType("application/json") +// .header("token", "10dc303535874aeccc86a8251e6992f5") +// .response(asJson[OneFrameCurrencyInformation]) +// println(s"requestHere = ${request.send()}") +// +// request.send() +// }.recoverWith { +// case t => Async[F].raiseError(new Exception("Failed to get rates", t)) +// } +// } +} + +object OneFrameClient { + def apply[F[_]: OneFrameClient]: OneFrameClient[F] = implicitly + + implicit def OneFrameHttpClient[F[_]: Async](config: OneFrameConfig)( + implicit backend: SttpBackend[Identity, Nothing, NothingT] + ): OneFrameClient[F] = + new OneFrameHttpClient[F](config, backend) +} diff --git a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala index 5c2a5ed8..e896b23f 100644 --- a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala +++ b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala @@ -4,6 +4,7 @@ import scala.concurrent.duration.FiniteDuration final case class ApplicationConfig( http: HttpConfig, + oneFrame: OneFrameConfig ) final case class HttpConfig( @@ -11,3 +12,9 @@ final case class HttpConfig( port: Int, timeout: FiniteDuration ) + +final case class OneFrameConfig( + url: String, + token: String + ) + diff --git a/forex-mtl/src/main/scala/forex/domain/Currency.scala b/forex-mtl/src/main/scala/forex/domain/Currency.scala index a6f2857d..939d2f11 100644 --- a/forex-mtl/src/main/scala/forex/domain/Currency.scala +++ b/forex-mtl/src/main/scala/forex/domain/Currency.scala @@ -39,4 +39,12 @@ object Currency { case "USD" => USD } + private val currencies: Vector[Currency] = Vector(AUD, CAD, CHF, EUR, GBP, NZD, JPY, SGD, USD) + val allPairs: Vector[(Currency, Currency)] = { + currencies + .combinations(2) + .flatMap(_.permutations) + .collect { case Seq(from: Currency, to: Currency) => (from, to) } + .toVector + } } diff --git a/forex-mtl/src/main/scala/forex/domain/OneFrameCurrencyInformation.scala b/forex-mtl/src/main/scala/forex/domain/OneFrameCurrencyInformation.scala new file mode 100644 index 00000000..483319aa --- /dev/null +++ b/forex-mtl/src/main/scala/forex/domain/OneFrameCurrencyInformation.scala @@ -0,0 +1,11 @@ +package forex.domain + +import io.circe.{Decoder, Encoder} +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} + +final case class OneFrameCurrencyInformation(from: String, to: String, bid: Double, ask: Double, price: Double, time_stamp: String) + +object OneFrameCurrencyInformation { + implicit val oneFrameCurrencyInformationDecoder: Decoder[OneFrameCurrencyInformation] = deriveDecoder + implicit val oneFrameCurrencyInformationEncoder: Encoder[OneFrameCurrencyInformation] = deriveEncoder +} diff --git a/forex-mtl/src/main/scala/forex/domain/Rate.scala b/forex-mtl/src/main/scala/forex/domain/Rate.scala index 238a5161..3f41532c 100644 --- a/forex-mtl/src/main/scala/forex/domain/Rate.scala +++ b/forex-mtl/src/main/scala/forex/domain/Rate.scala @@ -10,5 +10,7 @@ object Rate { final case class Pair( from: Currency, to: Currency - ) + ){ + val key = s"$from-$to" + } } diff --git a/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala b/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala index e523ffab..0f9ce038 100644 --- a/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala +++ b/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala @@ -1,8 +1,13 @@ package forex.services.rates import cats.Applicative +import cats.effect.Concurrent +import forex.client.OneFrameClient +import forex.domain.Rate import interpreters._ +import scalacache.Cache object Interpreters { def dummy[F[_]: Applicative]: Algebra[F] = new OneFrameDummy[F]() + def live[F[_]: Concurrent](oneFrameHttpClient: OneFrameClient[F], rateCache: Cache[Rate]): Algebra[F] = new OneFrameService[F](oneFrameHttpClient, rateCache) } diff --git a/forex-mtl/src/main/scala/forex/services/rates/algebra.scala b/forex-mtl/src/main/scala/forex/services/rates/algebra.scala index 8966dce5..7d6f1e51 100644 --- a/forex-mtl/src/main/scala/forex/services/rates/algebra.scala +++ b/forex-mtl/src/main/scala/forex/services/rates/algebra.scala @@ -1,8 +1,11 @@ package forex.services.rates +import cats.effect.Timer import forex.domain.Rate import errors._ +import fs2.Stream trait Algebra[F[_]] { def get(pair: Rate.Pair): F[Error Either Rate] + def scheduleCacheRefresh()(implicit timer: Timer[F]): Stream[F, Unit] } diff --git a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameDummy.scala b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameDummy.scala index 37a3f50c..51233851 100644 --- a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameDummy.scala +++ b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameDummy.scala @@ -2,14 +2,20 @@ package forex.services.rates.interpreters import forex.services.rates.Algebra import cats.Applicative +import cats.effect.Timer import cats.syntax.applicative._ import cats.syntax.either._ -import forex.domain.{ Price, Rate, Timestamp } +import forex.domain.{Price, Rate, Timestamp} import forex.services.rates.errors._ +import fs2.Stream + +import scala.concurrent.duration.DurationInt class OneFrameDummy[F[_]: Applicative] extends Algebra[F] { override def get(pair: Rate.Pair): F[Error Either Rate] = Rate(pair, Price(BigDecimal(100)), Timestamp.now).asRight[Error].pure[F] + override def scheduleCacheRefresh()(implicit timer: Timer[F]): Stream[F, Unit] = + Stream.awakeEvery[F](1.minute).evalMap(_ => Applicative[F].unit) } diff --git a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala new file mode 100644 index 00000000..3e42f99a --- /dev/null +++ b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala @@ -0,0 +1,49 @@ +package forex.services.rates.interpreters + +import cats.effect.{ Concurrent, Timer } +import cats.Applicative +import cats.implicits._ +import forex.domain._ +import forex.client.OneFrameClient +import forex.services.rates.Algebra +import forex.services.rates.errors.Error.OneFrameLookupFailed +import fs2.Stream +import forex.services.rates.errors._ +import scalacache.Cache +import scalacache.modes.sync.mode +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter +import scala.concurrent.duration.DurationInt + +class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateCache: Cache[Rate]) extends Algebra[F] { + + override def get(pair: Rate.Pair): F[Error Either Rate] = { + for{ + rate <- rateCache.get(pair.key) match { + case Some(rate) => rate.asRight[Error].pure[F] + case _ => OneFrameLookupFailed("Cache not updated. Please contact admin.").asLeft[Rate].pure[F] + } + } yield rate + } + + private val allPairs: Vector[Rate.Pair] = Currency.allPairs.map(Rate.Pair.tupled) + + private def populateCache(): F[Unit] = + for { + freshRates <- oneFrameClient.getRates(allPairs) + rates = freshRates.body match { + case Right(rates) => rates + case Left(_) => List.empty[OneFrameCurrencyInformation] + } + _ <- if (rates.nonEmpty) updateCache(rates) else Applicative[F].unit + } yield {} + + override def scheduleCacheRefresh()(implicit timer: Timer[F]): Stream[F, Unit] = + Stream.eval(populateCache()) >> Stream.awakeEvery[F](4.minutes) >> Stream.eval(populateCache()) + + private def updateCache(rates: List[OneFrameCurrencyInformation]): F[Unit] = + Applicative[F].pure(rates.map{rate => + val currentRate = Rate(Rate.Pair(Currency.fromString(rate.from), Currency.fromString(rate.to)), Price.apply(rate.price), Timestamp( + OffsetDateTime.parse(rate.time_stamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME))) + rateCache.put(currentRate.pair.key)(currentRate, 4.minutes.some)}).pure[F].void +} From a388508a66b86834fd34436ac089a89c97e75761 Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Fri, 29 Dec 2023 19:43:41 +0530 Subject: [PATCH 4/9] change config and clean up code --- forex-mtl/src/main/resources/application.conf | 4 +++- forex-mtl/src/main/scala/forex/Module.scala | 2 +- .../main/scala/forex/config/ApplicationConfig.scala | 8 +++++++- .../scala/forex/services/rates/Interpreters.scala | 3 ++- .../rates/interpreters/OneFrameService.scala | 12 +++++++----- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/forex-mtl/src/main/resources/application.conf b/forex-mtl/src/main/resources/application.conf index 83806a9c..0d18634c 100644 --- a/forex-mtl/src/main/resources/application.conf +++ b/forex-mtl/src/main/resources/application.conf @@ -7,6 +7,8 @@ app { one-frame{ url = "localhost:8080" token = "10dc303535874aeccc86a8251e6992f5" - cacheTTL = 4 #minutes + } + cache-config{ + one-frame-expiry = 4 #minutes } } diff --git a/forex-mtl/src/main/scala/forex/Module.scala b/forex-mtl/src/main/scala/forex/Module.scala index 52e509fd..b13460e1 100644 --- a/forex-mtl/src/main/scala/forex/Module.scala +++ b/forex-mtl/src/main/scala/forex/Module.scala @@ -22,7 +22,7 @@ class Module[F[_]: Concurrent: Timer](config: ApplicationConfig) { private lazy val oneFrameClient: OneFrameClient[F] = OneFrameClient.OneFrameHttpClient(config.oneFrame) - val ratesService: RatesService[F] = RatesServices.live[F](oneFrameClient, rateCache) + val ratesService: RatesService[F] = RatesServices.live[F](oneFrameClient, rateCache, config.cache) private val ratesProgram: RatesProgram[F] = RatesProgram[F](ratesService) diff --git a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala index e896b23f..b46224a4 100644 --- a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala +++ b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala @@ -4,7 +4,8 @@ import scala.concurrent.duration.FiniteDuration final case class ApplicationConfig( http: HttpConfig, - oneFrame: OneFrameConfig + oneFrame: OneFrameConfig, + cache: CacheConfig ) final case class HttpConfig( @@ -18,3 +19,8 @@ final case class OneFrameConfig( token: String ) +final case class CacheConfig( + oneFrameExpiry: Int + ) + + diff --git a/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala b/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala index 0f9ce038..55ffba79 100644 --- a/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala +++ b/forex-mtl/src/main/scala/forex/services/rates/Interpreters.scala @@ -3,11 +3,12 @@ package forex.services.rates import cats.Applicative import cats.effect.Concurrent import forex.client.OneFrameClient +import forex.config.CacheConfig import forex.domain.Rate import interpreters._ import scalacache.Cache object Interpreters { def dummy[F[_]: Applicative]: Algebra[F] = new OneFrameDummy[F]() - def live[F[_]: Concurrent](oneFrameHttpClient: OneFrameClient[F], rateCache: Cache[Rate]): Algebra[F] = new OneFrameService[F](oneFrameHttpClient, rateCache) + def live[F[_]: Concurrent](oneFrameHttpClient: OneFrameClient[F], rateCache: Cache[Rate], cacheConfig: CacheConfig): Algebra[F] = new OneFrameService[F](oneFrameHttpClient, rateCache, cacheConfig) } diff --git a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala index 3e42f99a..24637622 100644 --- a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala +++ b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala @@ -1,21 +1,25 @@ package forex.services.rates.interpreters -import cats.effect.{ Concurrent, Timer } +import cats.effect.{Concurrent, Timer} import cats.Applicative import cats.implicits._ import forex.domain._ import forex.client.OneFrameClient +import forex.config.CacheConfig import forex.services.rates.Algebra import forex.services.rates.errors.Error.OneFrameLookupFailed import fs2.Stream import forex.services.rates.errors._ import scalacache.Cache import scalacache.modes.sync.mode + import java.time.OffsetDateTime import java.time.format.DateTimeFormatter import scala.concurrent.duration.DurationInt -class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateCache: Cache[Rate]) extends Algebra[F] { +class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateCache: Cache[Rate], cacheConfig: CacheConfig) extends Algebra[F] { + + private val allPairs: Vector[Rate.Pair] = Currency.allPairs.map(Rate.Pair.tupled) override def get(pair: Rate.Pair): F[Error Either Rate] = { for{ @@ -26,8 +30,6 @@ class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateC } yield rate } - private val allPairs: Vector[Rate.Pair] = Currency.allPairs.map(Rate.Pair.tupled) - private def populateCache(): F[Unit] = for { freshRates <- oneFrameClient.getRates(allPairs) @@ -45,5 +47,5 @@ class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateC Applicative[F].pure(rates.map{rate => val currentRate = Rate(Rate.Pair(Currency.fromString(rate.from), Currency.fromString(rate.to)), Price.apply(rate.price), Timestamp( OffsetDateTime.parse(rate.time_stamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME))) - rateCache.put(currentRate.pair.key)(currentRate, 4.minutes.some)}).pure[F].void + rateCache.put(currentRate.pair.key)(currentRate, cacheConfig.oneFrameExpiry.minutes.some)}).void } From 0b78c4a5ad29d8b41e1a3b0b4e577946074fa241 Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Fri, 29 Dec 2023 23:38:53 +0530 Subject: [PATCH 5/9] add unit test --- .../forex/client/OneFrameHttpClient.scala | 28 +----- .../interpreters/OneFrameServiceSpec.scala | 86 +++++++++++++++++++ 2 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala diff --git a/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala index 39e9848e..9d4ed5a4 100644 --- a/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala +++ b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala @@ -5,12 +5,12 @@ import cats.implicits._ import forex.config.OneFrameConfig import forex.domain.OneFrameCurrencyInformation import forex.domain.Rate.Pair -import io.circe.{Error => CError} +import io.circe.{Error => CirceError} import sttp.client._ import sttp.client.circe.asJson trait OneFrameClient[F[_]] { - def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CError], List[OneFrameCurrencyInformation]]]] + def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] } class OneFrameHttpClient[F[_]: Async]( @@ -18,11 +18,9 @@ class OneFrameHttpClient[F[_]: Async]( implicit val backend: SttpBackend[Identity, Nothing, NothingT] ) extends OneFrameClient[F]{ - def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CError], List[OneFrameCurrencyInformation]]]] = { + def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] = { val param = pairs.map((pair: Pair) => "pair" -> s"${pair.from}${pair.to}") val url = uri"http://${oneFrameConfig.url}/rates?$param" - println("herehere") - println(url) val request = basicRequest .get(uri = url) @@ -36,26 +34,6 @@ class OneFrameHttpClient[F[_]: Async]( println(s"t.getMessage = ${t.getMessage}") Async[F].raiseError(new Exception("Failed to get rates", t)) } - - -// override def getRate(pair: Pair): F[Response[Either[ResponseError[Error], OneFrameCurrencyInformation]]] = { -// val param = pair.to.show + pair.from.show -//// pairs.map((pair: Pair) => "pair" -> s"${pair.from}${pair.to}") -// val url = s"http://${oneFrameConfig.url}/rates?pair=$param" -// println(s"url = ${url}") -// Async[F].delay{ -// val request = basicRequest -// .get(uri = uri"$url") -// .contentType("application/json") -// .header("token", "10dc303535874aeccc86a8251e6992f5") -// .response(asJson[OneFrameCurrencyInformation]) -// println(s"requestHere = ${request.send()}") -// -// request.send() -// }.recoverWith { -// case t => Async[F].raiseError(new Exception("Failed to get rates", t)) -// } -// } } object OneFrameClient { diff --git a/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala new file mode 100644 index 00000000..583fb5de --- /dev/null +++ b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala @@ -0,0 +1,86 @@ +package forex.services.rates.interpreters + +import cats.Applicative +import cats.effect._ +import cats.implicits._ +import forex.client.OneFrameClient +import forex.config.CacheConfig +import forex.domain._ +import io.circe.{DecodingFailure, Error => CirceError} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import scalacache.caffeine.CaffeineCache +import scalacache.modes.sync.mode +import sttp.client.{DeserializationError, Response, ResponseError} + +import java.time.OffsetDateTime +import scala.concurrent.duration.DurationInt + +class OneFrameServiceSpec extends AnyFunSuite with Matchers { + + implicit val contextShiftInstance: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) + implicit val timerInstance: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) + + // Test case + test("get should return cached rate if available") { + // Mock OneFrameClient for testing + class MockOneFrameClient[F[_]: Applicative] extends OneFrameClient[F] { + override def getRates( + pairs: Vector[Rate.Pair] + ): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] = { + val mockResponse = Right(List(OneFrameCurrencyInformation(from = "USD", + to= "JPY", + bid= 0.4332159351433372, + ask= 0.2056211718073755, + price= 0.31941855347535635, + time_stamp= "2023-12-29T17:22:46.691Z"))) + + Applicative[F].pure(Response.ok(mockResponse)) + } + } + val oneFrameClient = new MockOneFrameClient[IO] + val cache = CaffeineCache[Rate] + val cacheConfig = CacheConfig(5) + val service = new OneFrameService[IO](oneFrameClient, cache, cacheConfig) + + val pair = Rate.Pair(Currency.USD, Currency.EUR) + val rate = Rate(pair, Price(BigDecimal(1.2)), Timestamp(OffsetDateTime.now())) + + + // Put rate into the cache + cache.put(pair.key)(rate, 5.minutes.some) + + val result = service.get(pair).unsafeRunSync() + + result shouldBe Right(rate) + } + + // Negative test case + test("get should return OneFrameLookupFailed for an exception during the call") { + class MockOneFrameClientNegative[F[_]: Applicative] extends OneFrameClient[F] { + override def getRates( + pairs: Vector[Rate.Pair] + ): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] = { + val decodingFailure: CirceError = DecodingFailure("Simulated deserialization error", Nil) + val responseError: ResponseError[CirceError] = DeserializationError("decode error" ,decodingFailure) + val leftResponse: Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]] = + Left(responseError) + + Applicative[F].pure(Response.ok(leftResponse)) + } + } + + val oneFrameClientNegative = new MockOneFrameClientNegative[IO] + val cacheNegative = CaffeineCache[Rate] + val cacheConfigNegative = CacheConfig(5) + val service = new OneFrameService[IO](oneFrameClientNegative, cacheNegative, cacheConfigNegative) + + val pairNegative = Rate.Pair(Currency.USD, Currency.EUR) + + val resultNegative = service.get(pairNegative).unsafeRunSync() + + resultNegative shouldBe a[Left[_, _]] + resultNegative.left.map(_.toString) shouldBe Left("OneFrameLookupFailed(Cache not updated. Please contact admin.)") + } + +} From 3b61b0416daed238f1bf83428237e4b8ca6e4e9c Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Sat, 30 Dec 2023 01:51:59 +0530 Subject: [PATCH 6/9] add logging, clean up code and apply scala fmt --- forex-mtl/build.sbt | 1 + forex-mtl/project/Dependencies.scala | 2 + .../forex/client/OneFrameHttpClient.scala | 53 ++++++++++++------- .../forex/config/ApplicationConfig.scala | 11 ++-- .../rates/interpreters/OneFrameService.scala | 14 +++-- .../interpreters/OneFrameServiceSpec.scala | 23 ++++---- 6 files changed, 58 insertions(+), 46 deletions(-) diff --git a/forex-mtl/build.sbt b/forex-mtl/build.sbt index 5acb9cf2..8e362ade 100644 --- a/forex-mtl/build.sbt +++ b/forex-mtl/build.sbt @@ -66,6 +66,7 @@ libraryDependencies ++= Seq( Libraries.circeGenericExt, Libraries.circeParser, Libraries.pureConfig, + Libraries.scalaLogging, Libraries.logback, Libraries.scalaTest % Test, Libraries.scalaCheck % Test, diff --git a/forex-mtl/project/Dependencies.scala b/forex-mtl/project/Dependencies.scala index 8b567948..0625ed0b 100644 --- a/forex-mtl/project/Dependencies.scala +++ b/forex-mtl/project/Dependencies.scala @@ -12,6 +12,7 @@ object Dependencies { val kindProjector = "0.13.2" val logback = "1.2.3" + val scalaLogging = "3.9.2" val scalaCheck = "1.15.3" val scalaTest = "3.2.7" val catsScalaCheck = "0.3.2" @@ -49,6 +50,7 @@ object Dependencies { lazy val kindProjector = "org.typelevel" %% "kind-projector" % Versions.kindProjector cross CrossVersion.full // Runtime + lazy val scalaLogging = "com.typesafe.scala-logging" %% "scala-logging" % Versions.scalaLogging lazy val logback = "ch.qos.logback" % "logback-classic" % Versions.logback // Test diff --git a/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala index 9d4ed5a4..b4c5f631 100644 --- a/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala +++ b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala @@ -2,45 +2,58 @@ package forex.client import cats.effect.Async import cats.implicits._ +import com.typesafe.scalalogging.LazyLogging import forex.config.OneFrameConfig import forex.domain.OneFrameCurrencyInformation import forex.domain.Rate.Pair -import io.circe.{Error => CirceError} +import forex.services.rates.errors.Error.OneFrameLookupFailed +import forex.services.rates.errors._ import sttp.client._ import sttp.client.circe.asJson trait OneFrameClient[F[_]] { - def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] + def getRates(pairs: Vector[Pair]): F[Either[Error, List[OneFrameCurrencyInformation]]] } class OneFrameHttpClient[F[_]: Async]( - oneFrameConfig: OneFrameConfig, - implicit val backend: SttpBackend[Identity, Nothing, NothingT] - ) extends OneFrameClient[F]{ + oneFrameConfig: OneFrameConfig, + implicit val backend: SttpBackend[Identity, Nothing, NothingT] +) extends OneFrameClient[F] + with LazyLogging { - def getRates(pairs: Vector[Pair]): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] = { + def getRates(pairs: Vector[Pair]): F[Either[Error, List[OneFrameCurrencyInformation]]] = { val param = pairs.map((pair: Pair) => "pair" -> s"${pair.from}${pair.to}") - val url = uri"http://${oneFrameConfig.url}/rates?$param" + val url = uri"http://${oneFrameConfig.url}/rates?$param" - val request = basicRequest - .get(uri = url) - .contentType("application/json") - .header("token", "10dc303535874aeccc86a8251e6992f5") - .response(asJson[List[OneFrameCurrencyInformation]]) - .send().pure[F] - request - }.recoverWith { - case t => - println(s"t.getMessage = ${t.getMessage}") - Async[F].raiseError(new Exception("Failed to get rates", t)) + val request = basicRequest + .get(uri = url) + .contentType("application/json") + .header("token", oneFrameConfig.token) + .response(asJson[List[OneFrameCurrencyInformation]]) + .send() + .pure[F] + request.map { request => + request.body match { + case Right(rates) => rates.asRight[Error] + case Left(error) => + OneFrameLookupFailed(s" Parsing error - ${error.getMessage}").asLeft[List[OneFrameCurrencyInformation]] } + } + }.recoverWith { + case t => + logger.error(s"Failed to get rates with error: ${t.getMessage}") + Async[F].pure( + OneFrameLookupFailed(s"Failed to get rates with error: ${t.getMessage}") + .asLeft[List[OneFrameCurrencyInformation]] + ) + } } object OneFrameClient { def apply[F[_]: OneFrameClient]: OneFrameClient[F] = implicitly implicit def OneFrameHttpClient[F[_]: Async](config: OneFrameConfig)( - implicit backend: SttpBackend[Identity, Nothing, NothingT] - ): OneFrameClient[F] = + implicit backend: SttpBackend[Identity, Nothing, NothingT] + ): OneFrameClient[F] = new OneFrameHttpClient[F](config, backend) } diff --git a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala index b46224a4..a343e9aa 100644 --- a/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala +++ b/forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala @@ -14,13 +14,8 @@ final case class HttpConfig( timeout: FiniteDuration ) -final case class OneFrameConfig( - url: String, - token: String - ) +final case class OneFrameConfig(url: String, token: String) final case class CacheConfig( - oneFrameExpiry: Int - ) - - + oneFrameExpiry: Int +) diff --git a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala index 24637622..88cf3c1d 100644 --- a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala +++ b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala @@ -3,6 +3,7 @@ package forex.services.rates.interpreters import cats.effect.{Concurrent, Timer} import cats.Applicative import cats.implicits._ +import com.typesafe.scalalogging.LazyLogging import forex.domain._ import forex.client.OneFrameClient import forex.config.CacheConfig @@ -17,7 +18,7 @@ import java.time.OffsetDateTime import java.time.format.DateTimeFormatter import scala.concurrent.duration.DurationInt -class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateCache: Cache[Rate], cacheConfig: CacheConfig) extends Algebra[F] { +class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateCache: Cache[Rate], cacheConfig: CacheConfig) extends Algebra[F] with LazyLogging{ private val allPairs: Vector[Rate.Pair] = Currency.allPairs.map(Rate.Pair.tupled) @@ -25,7 +26,9 @@ class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateC for{ rate <- rateCache.get(pair.key) match { case Some(rate) => rate.asRight[Error].pure[F] - case _ => OneFrameLookupFailed("Cache not updated. Please contact admin.").asLeft[Rate].pure[F] + case _ => + logger.error("Unable to fetch rate from cache") + OneFrameLookupFailed("Cache not updated. Please contact admin.").asLeft[Rate].pure[F] } } yield rate } @@ -33,9 +36,11 @@ class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateC private def populateCache(): F[Unit] = for { freshRates <- oneFrameClient.getRates(allPairs) - rates = freshRates.body match { + rates = freshRates match { case Right(rates) => rates - case Left(_) => List.empty[OneFrameCurrencyInformation] + case Left(error) => + logger.error("API returned empty list when populating cache.", error) + List.empty[OneFrameCurrencyInformation] } _ <- if (rates.nonEmpty) updateCache(rates) else Applicative[F].unit } yield {} @@ -45,6 +50,7 @@ class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateC private def updateCache(rates: List[OneFrameCurrencyInformation]): F[Unit] = Applicative[F].pure(rates.map{rate => + logger.debug("Updating cache with latest value.") val currentRate = Rate(Rate.Pair(Currency.fromString(rate.from), Currency.fromString(rate.to)), Price.apply(rate.price), Timestamp( OffsetDateTime.parse(rate.time_stamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME))) rateCache.put(currentRate.pair.key)(currentRate, cacheConfig.oneFrameExpiry.minutes.some)}).void diff --git a/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala index 583fb5de..f1ab03c4 100644 --- a/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala +++ b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala @@ -6,12 +6,11 @@ import cats.implicits._ import forex.client.OneFrameClient import forex.config.CacheConfig import forex.domain._ -import io.circe.{DecodingFailure, Error => CirceError} +import forex.services.rates.errors.Error import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import scalacache.caffeine.CaffeineCache import scalacache.modes.sync.mode -import sttp.client.{DeserializationError, Response, ResponseError} import java.time.OffsetDateTime import scala.concurrent.duration.DurationInt @@ -27,15 +26,15 @@ class OneFrameServiceSpec extends AnyFunSuite with Matchers { class MockOneFrameClient[F[_]: Applicative] extends OneFrameClient[F] { override def getRates( pairs: Vector[Rate.Pair] - ): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] = { - val mockResponse = Right(List(OneFrameCurrencyInformation(from = "USD", + ): F[Either[Error, List[OneFrameCurrencyInformation]]] = { + val mockResponse = List(OneFrameCurrencyInformation(from = "USD", to= "JPY", bid= 0.4332159351433372, ask= 0.2056211718073755, price= 0.31941855347535635, - time_stamp= "2023-12-29T17:22:46.691Z"))) + time_stamp= "2023-12-29T17:22:46.691Z")) - Applicative[F].pure(Response.ok(mockResponse)) + Applicative[F].pure(mockResponse.asRight) } } val oneFrameClient = new MockOneFrameClient[IO] @@ -57,16 +56,13 @@ class OneFrameServiceSpec extends AnyFunSuite with Matchers { // Negative test case test("get should return OneFrameLookupFailed for an exception during the call") { + // Mock OneFrameClient for testing class MockOneFrameClientNegative[F[_]: Applicative] extends OneFrameClient[F] { override def getRates( pairs: Vector[Rate.Pair] - ): F[Response[Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]]]] = { - val decodingFailure: CirceError = DecodingFailure("Simulated deserialization error", Nil) - val responseError: ResponseError[CirceError] = DeserializationError("decode error" ,decodingFailure) - val leftResponse: Either[ResponseError[CirceError], List[OneFrameCurrencyInformation]] = - Left(responseError) - - Applicative[F].pure(Response.ok(leftResponse)) + ): F[Either[Error, List[OneFrameCurrencyInformation]]] = { + val leftResponse = Error.OneFrameLookupFailed("Something went wrong...") + Applicative[F].pure(leftResponse.asLeft) } } @@ -82,5 +78,4 @@ class OneFrameServiceSpec extends AnyFunSuite with Matchers { resultNegative shouldBe a[Left[_, _]] resultNegative.left.map(_.toString) shouldBe Left("OneFrameLookupFailed(Cache not updated. Please contact admin.)") } - } From cf079534794c3994143293d2bb4ce77c1a593334 Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Sat, 30 Dec 2023 02:08:45 +0530 Subject: [PATCH 7/9] add validation for currency --- forex-mtl/src/main/resources/logback.xml | 3 +- .../scala/forex/http/rates/QueryParams.scala | 16 +++++--- .../forex/http/rates/RatesHttpRoutes.scala | 15 +++++-- .../interpreters/OneFrameServiceSpec.scala | 41 ++++++++++--------- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/forex-mtl/src/main/resources/logback.xml b/forex-mtl/src/main/resources/logback.xml index 196c32fc..cbb6fad4 100644 --- a/forex-mtl/src/main/resources/logback.xml +++ b/forex-mtl/src/main/resources/logback.xml @@ -1,3 +1,4 @@ + @@ -10,5 +11,5 @@ - + diff --git a/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala b/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala index b19ed4ce..f08db9a5 100644 --- a/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala +++ b/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala @@ -1,15 +1,21 @@ package forex.http.rates +import cats.implicits.toBifunctorOps import forex.domain.Currency -import org.http4s.QueryParamDecoder -import org.http4s.dsl.impl.QueryParamDecoderMatcher +import org.http4s.{ ParseFailure, QueryParamDecoder } +import org.http4s.dsl.impl.ValidatingQueryParamDecoderMatcher + +import scala.util.Try object QueryParams { private[http] implicit val currencyQueryParam: QueryParamDecoder[Currency] = - QueryParamDecoder[String].map(Currency.fromString) + QueryParamDecoder[String].emap( + currency => + Try(Currency.fromString(currency)).toEither.leftMap(error => ParseFailure(error.getMessage, error.getMessage)) + ) - object FromQueryParam extends QueryParamDecoderMatcher[Currency]("from") - object ToQueryParam extends QueryParamDecoderMatcher[Currency]("to") + object FromQueryParam extends ValidatingQueryParamDecoderMatcher[Currency]("from") + object ToQueryParam extends ValidatingQueryParamDecoderMatcher[Currency]("to") } diff --git a/forex-mtl/src/main/scala/forex/http/rates/RatesHttpRoutes.scala b/forex-mtl/src/main/scala/forex/http/rates/RatesHttpRoutes.scala index d91dcffb..8b4a036e 100644 --- a/forex-mtl/src/main/scala/forex/http/rates/RatesHttpRoutes.scala +++ b/forex-mtl/src/main/scala/forex/http/rates/RatesHttpRoutes.scala @@ -1,11 +1,12 @@ package forex.http package rates +import cats.data.Validated.Valid import cats.effect.Sync import cats.syntax.flatMap._ import forex.programs.RatesProgram import forex.programs.rates.{ Protocol => RatesProgramProtocol } -import org.http4s.HttpRoutes +import org.http4s.{ HttpRoutes, Response, Status } import org.http4s.dsl.Http4sDsl import org.http4s.server.Router @@ -17,8 +18,16 @@ class RatesHttpRoutes[F[_]: Sync](rates: RatesProgram[F]) extends Http4sDsl[F] { private val httpRoutes: HttpRoutes[F] = HttpRoutes.of[F] { case GET -> Root :? FromQueryParam(from) +& ToQueryParam(to) => - rates.get(RatesProgramProtocol.GetRatesRequest(from, to)).flatMap(Sync[F].fromEither).flatMap { rate => - Ok(rate.asGetApiResponse) + (from, to) match { + case (Valid(from), Valid(to)) => + rates + .get(RatesProgramProtocol.GetRatesRequest(from, to)) + .flatMap { + case Left(err) => + Sync[F].pure(Response[F]().withStatus(Status.InternalServerError).withEntity(err.getMessage)) + case Right(rate) => Ok(rate.asGetApiResponse) + } + case (_, _) => BadRequest("Currency not supported.") } } diff --git a/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala index f1ab03c4..1b1fff6e 100644 --- a/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala +++ b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala @@ -18,36 +18,39 @@ import scala.concurrent.duration.DurationInt class OneFrameServiceSpec extends AnyFunSuite with Matchers { implicit val contextShiftInstance: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) - implicit val timerInstance: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) + implicit val timerInstance: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) // Test case test("get should return cached rate if available") { // Mock OneFrameClient for testing class MockOneFrameClient[F[_]: Applicative] extends OneFrameClient[F] { override def getRates( - pairs: Vector[Rate.Pair] - ): F[Either[Error, List[OneFrameCurrencyInformation]]] = { - val mockResponse = List(OneFrameCurrencyInformation(from = "USD", - to= "JPY", - bid= 0.4332159351433372, - ask= 0.2056211718073755, - price= 0.31941855347535635, - time_stamp= "2023-12-29T17:22:46.691Z")) + pairs: Vector[Rate.Pair] + ): F[Either[Error, List[OneFrameCurrencyInformation]]] = { + val mockResponse = List( + OneFrameCurrencyInformation( + from = "USD", + to = "JPY", + bid = 0.4332159351433372, + ask = 0.2056211718073755, + price = 0.31941855347535635, + time_stamp = "2023-12-29T17:22:46.691Z" + ) + ) Applicative[F].pure(mockResponse.asRight) } } val oneFrameClient = new MockOneFrameClient[IO] - val cache = CaffeineCache[Rate] - val cacheConfig = CacheConfig(5) - val service = new OneFrameService[IO](oneFrameClient, cache, cacheConfig) + val cache = CaffeineCache[Rate] + val cacheConfig = CacheConfig(5) + val service = new OneFrameService[IO](oneFrameClient, cache, cacheConfig) val pair = Rate.Pair(Currency.USD, Currency.EUR) val rate = Rate(pair, Price(BigDecimal(1.2)), Timestamp(OffsetDateTime.now())) - // Put rate into the cache - cache.put(pair.key)(rate, 5.minutes.some) + cache.put(pair.key)(rate, cacheConfig.oneFrameExpiry.minutes.some) val result = service.get(pair).unsafeRunSync() @@ -59,17 +62,17 @@ class OneFrameServiceSpec extends AnyFunSuite with Matchers { // Mock OneFrameClient for testing class MockOneFrameClientNegative[F[_]: Applicative] extends OneFrameClient[F] { override def getRates( - pairs: Vector[Rate.Pair] - ): F[Either[Error, List[OneFrameCurrencyInformation]]] = { + pairs: Vector[Rate.Pair] + ): F[Either[Error, List[OneFrameCurrencyInformation]]] = { val leftResponse = Error.OneFrameLookupFailed("Something went wrong...") Applicative[F].pure(leftResponse.asLeft) } } val oneFrameClientNegative = new MockOneFrameClientNegative[IO] - val cacheNegative = CaffeineCache[Rate] - val cacheConfigNegative = CacheConfig(5) - val service = new OneFrameService[IO](oneFrameClientNegative, cacheNegative, cacheConfigNegative) + val cacheNegative = CaffeineCache[Rate] + val cacheConfigNegative = CacheConfig(5) + val service = new OneFrameService[IO](oneFrameClientNegative, cacheNegative, cacheConfigNegative) val pairNegative = Rate.Pair(Currency.USD, Currency.EUR) From 7c2305eefc4846fc70459b6ab883dc00776753f8 Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Sun, 31 Dec 2023 15:13:12 +0530 Subject: [PATCH 8/9] fix config and test working of loggers --- forex-mtl/src/main/resources/application.conf | 4 +- .../forex/client/OneFrameHttpClient.scala | 2 +- .../scala/forex/http/rates/QueryParams.scala | 2 +- .../rates/interpreters/OneFrameService.scala | 42 ++++++++++++------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/forex-mtl/src/main/resources/application.conf b/forex-mtl/src/main/resources/application.conf index 0d18634c..16bd7968 100644 --- a/forex-mtl/src/main/resources/application.conf +++ b/forex-mtl/src/main/resources/application.conf @@ -5,10 +5,10 @@ app { timeout = 40 seconds } one-frame{ - url = "localhost:8080" + url = "http://localhost:8080" token = "10dc303535874aeccc86a8251e6992f5" } - cache-config{ + cache{ one-frame-expiry = 4 #minutes } } diff --git a/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala index b4c5f631..1bd9ce43 100644 --- a/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala +++ b/forex-mtl/src/main/scala/forex/client/OneFrameHttpClient.scala @@ -23,7 +23,7 @@ class OneFrameHttpClient[F[_]: Async]( def getRates(pairs: Vector[Pair]): F[Either[Error, List[OneFrameCurrencyInformation]]] = { val param = pairs.map((pair: Pair) => "pair" -> s"${pair.from}${pair.to}") - val url = uri"http://${oneFrameConfig.url}/rates?$param" + val url = uri"${oneFrameConfig.url}/rates?$param" val request = basicRequest .get(uri = url) diff --git a/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala b/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala index f08db9a5..f5428660 100644 --- a/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala +++ b/forex-mtl/src/main/scala/forex/http/rates/QueryParams.scala @@ -1,6 +1,6 @@ package forex.http.rates -import cats.implicits.toBifunctorOps +import cats.implicits._ import forex.domain.Currency import org.http4s.{ ParseFailure, QueryParamDecoder } import org.http4s.dsl.impl.ValidatingQueryParamDecoderMatcher diff --git a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala index 88cf3c1d..33744938 100644 --- a/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala +++ b/forex-mtl/src/main/scala/forex/services/rates/interpreters/OneFrameService.scala @@ -1,6 +1,6 @@ package forex.services.rates.interpreters -import cats.effect.{Concurrent, Timer} +import cats.effect.{ Concurrent, Timer } import cats.Applicative import cats.implicits._ import com.typesafe.scalalogging.LazyLogging @@ -18,20 +18,23 @@ import java.time.OffsetDateTime import java.time.format.DateTimeFormatter import scala.concurrent.duration.DurationInt -class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateCache: Cache[Rate], cacheConfig: CacheConfig) extends Algebra[F] with LazyLogging{ +class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], + rateCache: Cache[Rate], + cacheConfig: CacheConfig) + extends Algebra[F] + with LazyLogging { private val allPairs: Vector[Rate.Pair] = Currency.allPairs.map(Rate.Pair.tupled) - override def get(pair: Rate.Pair): F[Error Either Rate] = { - for{ + override def get(pair: Rate.Pair): F[Error Either Rate] = + for { rate <- rateCache.get(pair.key) match { - case Some(rate) => rate.asRight[Error].pure[F] - case _ => - logger.error("Unable to fetch rate from cache") - OneFrameLookupFailed("Cache not updated. Please contact admin.").asLeft[Rate].pure[F] - } + case Some(rate) => rate.asRight[Error].pure[F] + case _ => + logger.error("Unable to fetch rate from cache") + OneFrameLookupFailed("Cache not updated. Please contact admin.").asLeft[Rate].pure[F] + } } yield rate - } private def populateCache(): F[Unit] = for { @@ -48,10 +51,17 @@ class OneFrameService[F[_]: Concurrent](oneFrameClient: OneFrameClient[F], rateC override def scheduleCacheRefresh()(implicit timer: Timer[F]): Stream[F, Unit] = Stream.eval(populateCache()) >> Stream.awakeEvery[F](4.minutes) >> Stream.eval(populateCache()) - private def updateCache(rates: List[OneFrameCurrencyInformation]): F[Unit] = - Applicative[F].pure(rates.map{rate => - logger.debug("Updating cache with latest value.") - val currentRate = Rate(Rate.Pair(Currency.fromString(rate.from), Currency.fromString(rate.to)), Price.apply(rate.price), Timestamp( - OffsetDateTime.parse(rate.time_stamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME))) - rateCache.put(currentRate.pair.key)(currentRate, cacheConfig.oneFrameExpiry.minutes.some)}).void + private def updateCache(rates: List[OneFrameCurrencyInformation]): F[Unit] = { + logger.info("Updating cache with latest values.") + Applicative[F] + .pure(rates.map { rate => + val currentRate = Rate( + Rate.Pair(Currency.fromString(rate.from), Currency.fromString(rate.to)), + Price.apply(rate.price), + Timestamp(OffsetDateTime.parse(rate.time_stamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + ) + rateCache.put(currentRate.pair.key)(currentRate, cacheConfig.oneFrameExpiry.minutes.some) + }) + .void + } } From b7b7d4142fcc482eb359069f8d5e9e77006d0681 Mon Sep 17 00:00:00 2001 From: Sushant Adhikari Date: Sun, 31 Dec 2023 15:32:45 +0530 Subject: [PATCH 9/9] fix config and add assumptions and thought process in readme --- forex-mtl/Forex.md | 39 ++++++++++++++++++ forex-mtl/img.png | Bin 0 -> 101272 bytes forex-mtl/src/main/resources/application.conf | 3 ++ forex-mtl/src/main/scala/forex/Module.scala | 8 ++-- .../forex/config/ApplicationConfig.scala | 7 +++- .../forex/services/rates/Interpreters.scala | 8 +++- .../rates/interpreters/OneFrameService.scala | 9 ++-- .../interpreters/OneFrameServiceSpec.scala | 13 +++--- 8 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 forex-mtl/img.png diff --git a/forex-mtl/Forex.md b/forex-mtl/Forex.md index 5afcb9fb..2c1f75d6 100644 --- a/forex-mtl/Forex.md +++ b/forex-mtl/Forex.md @@ -84,3 +84,42 @@ $ curl -H "token: 10dc303535874aeccc86a8251e6992f5" 'localhost:8080/rates?pair=U ## F.A.Q. [Please click here for the F.A.Q.](./README.md) + + +### Assumption +1. Token will always remain the same. +2. Rate should not be older than 5 minutes. +3. Currently, the input is only for 2 valid currencies. +4. 1 API token = 10,000 successful requests per day + +### Limitation +1. The One-Frame service supports a maximum of 1000 requests per day for any given authentication token. + +### Suggested Solution +1. I initially though of making single calls for fetching exchange rate for a pair of currencies, quickly understood this would not work with our limitation. +2. So instead fetch all currencies every 4 minutes => 15 requests per hour => 360 requests per 24 hours (per day). +3. I have used a cache to hold the values with an expiry of 4 minutes, since we do not want to have any exchange rate older than 5 minutes, having a buffer of 1 minute extra will not cause any issues, because we are still managing the existing limitations. + +### Output +![img.png](img.png) + +### Running + +You need to download and install sbt for this application to run. + +#### Pre-requisite +Scala 2.13.12 +SBT 1.8.0 +Java 11 (I have developed and tested using Java 11) + +Once you have sbt installed, type/run the following command in the terminal: + +```bash +sbt run +or +Run via IDE +``` + +#### Extensions +1. Token is currently configured via configuration file, would be good to have a scheduler to fetch this and store in memory if needed. +2. I have worked with tag less final implementation via libraries but not directly in code, would be good to understand this better and have better error handling. diff --git a/forex-mtl/img.png b/forex-mtl/img.png new file mode 100644 index 0000000000000000000000000000000000000000..06deb02078da0599dd9a03b63e5d59eee36bc980 GIT binary patch literal 101272 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ঢ়$pR7uY1qh@+Mn^3YP1b(2fT&4~U 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%0D)Cn>n2TWz71G9 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

`@EI~4qruNF8u55kF1LaDt;2WE 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 Stream.awakeEvery[F](4.minutes) >> Stream.eval(populateCache()) + Stream.eval(populateCache()) >> Stream.awakeEvery[F](schedulerConfig.oneFrameRefresh.minutes) >> Stream.eval( + populateCache() + ) private def updateCache(rates: List[OneFrameCurrencyInformation]): F[Unit] = { logger.info("Updating cache with latest values.") diff --git a/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala index 1b1fff6e..fdf3ffc9 100644 --- a/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala +++ b/forex-mtl/src/test/scala/forex/services/rates/interpreters/OneFrameServiceSpec.scala @@ -4,7 +4,7 @@ import cats.Applicative import cats.effect._ import cats.implicits._ import forex.client.OneFrameClient -import forex.config.CacheConfig +import forex.config.{ CacheConfig, SchedulerConfig } import forex.domain._ import forex.services.rates.errors.Error import org.scalatest.funsuite.AnyFunSuite @@ -19,6 +19,9 @@ class OneFrameServiceSpec extends AnyFunSuite with Matchers { implicit val contextShiftInstance: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) implicit val timerInstance: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) + val cache = CaffeineCache[Rate] + val cacheConfig = CacheConfig(4) + val schedulerConfig = SchedulerConfig(4) // Test case test("get should return cached rate if available") { @@ -42,9 +45,7 @@ class OneFrameServiceSpec extends AnyFunSuite with Matchers { } } val oneFrameClient = new MockOneFrameClient[IO] - val cache = CaffeineCache[Rate] - val cacheConfig = CacheConfig(5) - val service = new OneFrameService[IO](oneFrameClient, cache, cacheConfig) + val service = new OneFrameService[IO](oneFrameClient, cache, cacheConfig, schedulerConfig) val pair = Rate.Pair(Currency.USD, Currency.EUR) val rate = Rate(pair, Price(BigDecimal(1.2)), Timestamp(OffsetDateTime.now())) @@ -70,9 +71,7 @@ class OneFrameServiceSpec extends AnyFunSuite with Matchers { } val oneFrameClientNegative = new MockOneFrameClientNegative[IO] - val cacheNegative = CaffeineCache[Rate] - val cacheConfigNegative = CacheConfig(5) - val service = new OneFrameService[IO](oneFrameClientNegative, cacheNegative, cacheConfigNegative) + val service = new OneFrameService[IO](oneFrameClientNegative, cache, cacheConfig, schedulerConfig) val pairNegative = Rate.Pair(Currency.USD, Currency.EUR)