Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions forex-mtl/Forex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Implemented the Proxy Live Interpreter which accepts currency and returns the response calling the Rates API.

1. Created Unit test cases which is successful scenario which returns response successfully.
2. Created test case to fail when we are passing some other currency which is not valid input to the Rates API. For that I have added UNK means (unknown language) so that Rates API gives with fail response.
3. If the Rates API is down somehow, handled it properly instead of failing with some exceptions.
4. Made the Rates API and token configurable in the application.conf and used them internally so if the endpoint changes, we need not make any code changes.

Tested in the intellij Setup and ran the test cases.
3 changes: 2 additions & 1 deletion forex-mtl/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ libraryDependencies ++= Seq(
Libraries.logback,
Libraries.scalaTest % Test,
Libraries.scalaCheck % Test,
Libraries.catsScalaCheck % Test
Libraries.catsScalaCheck % Test,
"org.scalaj" %% "scalaj-http" % "2.4.2"
)
4 changes: 4 additions & 0 deletions forex-mtl/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ app {
port = 8080
timeout = 40 seconds
}
url{
endPointUrl = "http://localhost:8080"
token = "10dc303535874aeccc86a8251e6992f5"
}
}

2 changes: 1 addition & 1 deletion forex-mtl/src/main/scala/forex/Module.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.http4s.server.middleware.{ AutoSlash, Timeout }

class Module[F[_]: Concurrent: Timer](config: ApplicationConfig) {

private val ratesService: RatesService[F] = RatesServices.dummy[F]
private val ratesService: RatesService[F] = RatesServices.live[F](config)

private val ratesProgram: RatesProgram[F] = RatesProgram[F](ratesService)

Expand Down
6 changes: 6 additions & 0 deletions forex-mtl/src/main/scala/forex/config/ApplicationConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import scala.concurrent.duration.FiniteDuration

case class ApplicationConfig(
http: HttpConfig,
url: UrlConfig
)

case class HttpConfig(
host: String,
port: Int,
timeout: FiniteDuration
)

case class UrlConfig(
endPointUrl: String,
token: String
)
4 changes: 4 additions & 0 deletions forex-mtl/src/main/scala/forex/domain/Currency.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ object Currency {
case object SGD extends Currency
case object USD extends Currency

case object UNK extends Currency

implicit val show: Show[Currency] = Show.show {
case AUD => "AUD"
case CAD => "CAD"
Expand All @@ -25,6 +27,7 @@ object Currency {
case JPY => "JPY"
case SGD => "SGD"
case USD => "USD"
case UNK => "UNK"
}

def fromString(s: String): Currency = s.toUpperCase match {
Expand All @@ -37,6 +40,7 @@ object Currency {
case "JPY" => JPY
case "SGD" => SGD
case "USD" => USD
case "UNK" => UNK
}

}
9 changes: 7 additions & 2 deletions forex-mtl/src/main/scala/forex/domain/Rate.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package forex.domain

import cats.implicits.toShow
case class Rate(
pair: Rate.Pair,
price: Price,
timestamp: Timestamp
)
){
override def toString: String = pair.toString + price.value
}

object Rate {
final case class Pair(
from: Currency,
to: Currency
)
){
def combine = from.show + to.show
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package forex.services.rates

import cats.Applicative
import forex.config.ApplicationConfig
import interpreters._

object Interpreters {
def dummy[F[_]: Applicative]: Algebra[F] = new OneFrameDummy[F]()

def live[F[_]: Applicative](config: ApplicationConfig): Algebra[F] = new OneFrameLive[F](config)
}
1 change: 1 addition & 0 deletions forex-mtl/src/main/scala/forex/services/rates/errors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ object errors {
sealed trait Error
object Error {
final case class OneFrameLookupFailed(msg: String) extends Error
final case class ServiceUnavailableError(msg: String) extends Error
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package forex.services.rates.interpreters

import cats.Applicative
import cats.data.EitherT
import forex.config.ApplicationConfig
import forex.domain.{Currency, Price, Rate, Timestamp}
import forex.services.rates.{Algebra, errors}
import scalaj.http.Http
import io.circe.parser.decode
import io.circe.generic.auto._

import java.time.{Instant, OffsetDateTime, ZoneId}

class OneFrameLive[F[_] : Applicative](config: ApplicationConfig) extends Algebra[F] {
private case class OneFrameResponse(from: String, to: String, bid: Double, ask: Double, price: Double, time_stamp: String) {
def toRate: Rate = {
val parsedTime = OffsetDateTime.ofInstant(Instant.parse(time_stamp), ZoneId.systemDefault())
Rate(Rate.Pair(Currency.fromString(from), Currency.fromString(to)), Price(price), Timestamp(parsedTime))
}
}

override def get(pair: Rate.Pair): F[Either[errors.Error, Rate]] = {
val response = executeGetRequest(config.url.endPointUrl + "/rates", Seq(("pair", pair.combine)))
EitherT.fromEither[F](response).value
}

private def executeGetRequest(uri: String, parameters: Seq[(String, String)]): Either[errors.Error, Rate] = {
val response = try {
Some(Http(uri).header("token", config.url.token).params(parameters).asString)
} catch {
case _: Exception => None
}
if (response.isDefined) {
decode[List[OneFrameResponse]](response.get.body) match {
case Left(res) =>
Left(errors.Error.OneFrameLookupFailed(res.getMessage): errors.Error)
case Right(res) => Right(res.head.toRate)
}
} else Left(errors.Error.ServiceUnavailableError("Service Rates API is down. Please try after sometime."): errors.Error)
}
}
56 changes: 56 additions & 0 deletions forex-mtl/src/test/scala/forex/testing/OneFrameLiveTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package forex.testing

import cats.Applicative
import forex.config.{ApplicationConfig, HttpConfig, UrlConfig}
//import cats.effect.IO
import cats.implicits._

import scala.concurrent.Future
//import cats.effect.testing.scalatest.AsyncIOSpec
import forex.domain._
//import forex.programs.rates.errors
import forex.services.rates.Interpreters
//import forex.services.rates.interpreters.OneFrameResponse
import org.scalatest.{EitherValues, OptionValues}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpecLike
import scala.concurrent.duration._

//import java.time.Instant

class OneFrameLiveTest
extends AsyncWordSpecLike
with Matchers
with OptionValues
with EitherValues {

"OneFrameTestingLiveModule" should {
"return an price on valid request" in {
implicit val futureApplicative: Applicative[Future] = Applicative[Future]

val httpConfig = HttpConfig("0.0.0.0", 8080, 40.seconds)
val urlConfig = UrlConfig("http://localhost:8080", "10dc303535874aeccc86a8251e6992f5")
val config = ApplicationConfig(httpConfig, urlConfig)
val service = Interpreters.live(config)(futureApplicative)
val result = service.get(Rate.Pair(Currency.GBP, Currency.USD))

result.map { result =>
result shouldBe Symbol("Right")
}
}
"return an error on invalid currency request" in {
implicit val futureApplicative: Applicative[Future] = Applicative[Future]
val httpConfig = HttpConfig("0.0.0.0", 8080, 40.seconds)
val urlConfig = UrlConfig("http://localhost:8080", "10dc303535874aeccc86a8251e6992f5")
val config = ApplicationConfig(httpConfig, urlConfig)

val service = Interpreters.live(config)(futureApplicative)
val result = service.get(Rate.Pair(Currency.GBP, Currency.UNK))

result.map { result =>
result shouldBe Symbol("Left")
}
}
}

}