Skip to content

Commit

Permalink
Abstract over Forex creation (closes #161)
Browse files Browse the repository at this point in the history
  • Loading branch information
BenFradet authored and dilyand committed Nov 8, 2019
1 parent 9b1a325 commit d294179
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 69 deletions.
37 changes: 25 additions & 12 deletions src/main/scala/com.snowplowanalytics/forex/Forex.scala
Expand Up @@ -18,7 +18,7 @@ import java.math.{BigDecimal, RoundingMode}

import scala.util.{Failure, Success, Try}

import cats.{Monad, Eval}
import cats.{Eval, Monad}
import cats.effect.Sync
import cats.data.{EitherT, OptionT}
import cats.implicits._
Expand All @@ -27,18 +27,31 @@ import org.joda.money._
import errors._
import model._

/** Companion object to get Forex object */
object Forex {
trait CreateForex[F[_]] {
def create(config: ForexConfig): F[Forex[F]]
}

object CreateForex {
def apply[F[_]](implicit ev: CreateForex[F]) = ev

implicit def syncCreateForex[F[_]: Sync: ZonedClock]: CreateForex[F] = new CreateForex[F] {
def create(config: ForexConfig): F[Forex[F]] =
OerClient
.getClient[F](config)
.map(client => Forex(config, client))
}

def getForex[F[_]: Sync: ZonedClock](config: ForexConfig): F[Forex[F]] =
OerClient
.getClient[F](config)
.map(client => Forex(config, client))
implicit def evalCreateForex(implicit C: ZonedClock[Eval]): CreateForex[Eval] =
new CreateForex[Eval] {
def create(config: ForexConfig): Eval[Forex[Eval]] =
OerClient
.getClient[Eval](config)
.map(client => Forex(config, client))
}
}

def unsafeGetForex(config: ForexConfig)(implicit C: ZonedClock[Eval]): Eval[Forex[Eval]] =
OerClient
.getClient[Eval](config)
.map(client => Forex(config, client))
/** Companion object to get Forex object */
object Forex {

/**
* Fields for calculating currency rates conversions.
Expand Down Expand Up @@ -156,7 +169,7 @@ final case class ForexLookupWhen[F[_]: Monad](
def now: F[Either[OerResponseError, Money]] = {
val product = for {
fromRate <- EitherT(client.getLiveCurrencyValue(fromCurr))
toRate <- EitherT(client.getLiveCurrencyValue(toCurr))
toRate <- EitherT(client.getLiveCurrencyValue(toCurr))
} yield (fromRate, toRate)

(product.flatMapF {
Expand Down
11 changes: 6 additions & 5 deletions src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala
Expand Up @@ -14,6 +14,7 @@ package com.snowplowanalytics.forex

import java.time.{ZoneId, ZonedDateTime}

import cats.Eval
import cats.effect.IO
import org.joda.money.{CurrencyUnit, Money}
import org.specs2.mutable.Specification
Expand All @@ -27,13 +28,13 @@ import model._
class ForexAtSpec extends Specification {
args(skipAll = sys.env.get("OER_KEY").isEmpty)

val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = Forex.getForex[IO](ForexConfig(key, DeveloperAccount))
val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = CreateForex[IO].create(ForexConfig(key, DeveloperAccount))
val ioFxWithBaseGBP =
Forex.getForex[IO](ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
val evalFx = Forex.unsafeGetForex(ForexConfig(key, DeveloperAccount))
CreateForex[IO].create(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
val evalFx = CreateForex[Eval].create(ForexConfig(key, DeveloperAccount))
val evalFxWithBaseGBP =
Forex.unsafeGetForex(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
CreateForex[Eval].create(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))

val tradeDate =
ZonedDateTime.of(2011, 3, 13, 11, 39, 27, 567, ZoneId.of("America/New_York"))
Expand Down
21 changes: 12 additions & 9 deletions src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala
Expand Up @@ -15,6 +15,7 @@ package com.snowplowanalytics.forex
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter

import cats.Eval
import cats.effect.IO
import org.joda.money.{CurrencyUnit, Money}
import org.specs2.mutable.Specification
Expand All @@ -29,9 +30,9 @@ import model._
*/
class ForexEodSpec extends Specification with DataTables {

val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = Forex.getForex[IO](ForexConfig(key, DeveloperAccount))
val evalFx = Forex.unsafeGetForex(ForexConfig(key, DeveloperAccount))
val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = CreateForex[IO].create(ForexConfig(key, DeveloperAccount))
val evalFx = CreateForex[Eval].create(ForexConfig(key, DeveloperAccount))

override def is =
skipAllIf(sys.env.get("OER_KEY").isEmpty) ^
Expand All @@ -47,14 +48,16 @@ class ForexEodSpec extends Specification with DataTables {
CurrencyUnit.GBP !! CurrencyUnit.of("SGD") ! "2008-03-13T00:01:01+00:00" ! "2.80" |> {
(fromCurr, toCurr, date, exp) =>
ioFx
.flatMap(_.rate(fromCurr)
.to(toCurr)
.eod(ZonedDateTime.parse(date, DateTimeFormatter.ISO_OFFSET_DATE_TIME)))
.flatMap(
_.rate(fromCurr)
.to(toCurr)
.eod(ZonedDateTime.parse(date, DateTimeFormatter.ISO_OFFSET_DATE_TIME)))
.unsafeRunSync() must beRight((m: Money) => m.getAmount.toString mustEqual exp)
evalFx
.flatMap(_.rate(fromCurr)
.to(toCurr)
.eod(ZonedDateTime.parse(date, DateTimeFormatter.ISO_OFFSET_DATE_TIME)))
.flatMap(
_.rate(fromCurr)
.to(toCurr)
.eod(ZonedDateTime.parse(date, DateTimeFormatter.ISO_OFFSET_DATE_TIME)))
.value must beRight((m: Money) => m.getAmount.toString mustEqual exp)
}

Expand Down
23 changes: 10 additions & 13 deletions src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala
Expand Up @@ -14,6 +14,7 @@ package com.snowplowanalytics.forex

import java.math.RoundingMode

import cats.Eval
import cats.effect.IO
import org.joda.money._
import org.specs2.mutable.Specification
Expand All @@ -24,13 +25,13 @@ import model._
class ForexNowSpec extends Specification {
args(skipAll = sys.env.get("OER_KEY").isEmpty)

val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = Forex.getForex[IO](ForexConfig(key, DeveloperAccount))
val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = CreateForex[IO].create(ForexConfig(key, DeveloperAccount))
val ioFxWithBaseGBP =
Forex.getForex[IO](ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
val evalFx = Forex.unsafeGetForex(ForexConfig(key, DeveloperAccount))
CreateForex[IO].create(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
val evalFx = CreateForex[Eval].create(ForexConfig(key, DeveloperAccount))
val evalFxWithBaseGBP =
Forex.unsafeGetForex(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
CreateForex[Eval].create(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))

/** Trade 10000 USD to JPY at live exchange rate */
"convert 10000 USD dollars to Yen now" should {
Expand All @@ -53,8 +54,7 @@ class ForexNowSpec extends Specification {
(m: Money) => m.isGreaterThan(Money.of(CurrencyUnit.of("SGD"), 1)))
val evalGbpToSgdWithBaseUsd =
evalFx.flatMap(_.rate(CurrencyUnit.GBP).to(CurrencyUnit.of("SGD")).now)
evalGbpToSgdWithBaseUsd.value must beRight(
(m: Money) => m.isGreaterThan(Money.of(CurrencyUnit.of("SGD"), 1)))
evalGbpToSgdWithBaseUsd.value must beRight((m: Money) => m.isGreaterThan(Money.of(CurrencyUnit.of("SGD"), 1)))
}
}

Expand All @@ -65,20 +65,17 @@ class ForexNowSpec extends Specification {
ioGbpToSgdWithBaseGbp.unsafeRunSync() must beRight(
(m: Money) => m.isGreaterThan(Money.of(CurrencyUnit.of("SGD"), 1)))
val evalGbpToSgdWithBaseGbp = evalFxWithBaseGBP.flatMap(_.rate.to(CurrencyUnit.of("SGD")).now)
evalGbpToSgdWithBaseGbp.value must beRight(
(m: Money) => m.isGreaterThan(Money.of(CurrencyUnit.of("SGD"), 1)))
evalGbpToSgdWithBaseGbp.value must beRight((m: Money) => m.isGreaterThan(Money.of(CurrencyUnit.of("SGD"), 1)))
}
}

/** GBP with GBP as base currency */
"Do not throw JodaTime exception on converting identical currencies" should {
"be equal 1 GBP" in {
val ioGbpToGbpWithBaseGbp = ioFxWithBaseGBP.flatMap(_.rate.to(CurrencyUnit.GBP).now)
ioGbpToGbpWithBaseGbp.unsafeRunSync() must beRight(
(m: Money) => m.isEqual(Money.of(CurrencyUnit.of("GBP"), 1)))
ioGbpToGbpWithBaseGbp.unsafeRunSync() must beRight((m: Money) => m.isEqual(Money.of(CurrencyUnit.of("GBP"), 1)))
val evalGbpToGbpWithBaseGbp = evalFxWithBaseGBP.flatMap(_.rate.to(CurrencyUnit.GBP).now)
evalGbpToGbpWithBaseGbp.value must beRight(
(m: Money) => m.isEqual(Money.of(CurrencyUnit.of("GBP"), 1)))
evalGbpToGbpWithBaseGbp.value must beRight((m: Money) => m.isEqual(Money.of(CurrencyUnit.of("GBP"), 1)))
}
}
}
30 changes: 15 additions & 15 deletions src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala
Expand Up @@ -14,6 +14,7 @@ package com.snowplowanalytics.forex

import java.math.RoundingMode

import cats.Eval
import cats.effect.IO
import org.joda.money._
import org.specs2.mutable.Specification
Expand All @@ -24,13 +25,13 @@ import model._
class ForexNowishSpec extends Specification {
args(skipAll = sys.env.get("OER_KEY").isEmpty)

val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = Forex.getForex[IO](ForexConfig(key, DeveloperAccount))
val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = CreateForex[IO].create(ForexConfig(key, DeveloperAccount))
val ioFxWithBaseGBP =
Forex.getForex[IO](ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
val evalFx = Forex.unsafeGetForex(ForexConfig(key, DeveloperAccount))
CreateForex[IO].create(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
val evalFx = CreateForex[Eval].create(ForexConfig(key, DeveloperAccount))
val evalFxWithBaseGBP =
Forex.unsafeGetForex(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))
CreateForex[Eval].create(ForexConfig(key, EnterpriseAccount, baseCurrency = CurrencyUnit.GBP))

/** CAD -> GBP with base currency USD */
"CAD to GBP with USD as base currency returning near-live rate" should {
Expand All @@ -40,33 +41,32 @@ class ForexNowishSpec extends Specification {
.unsafeRunSync() must beRight((m: Money) => m.isLessThan(Money.of(CurrencyUnit.GBP, 1)))
val evalCadOverGbpNowish =
evalFx.flatMap(_.rate(CurrencyUnit.CAD).to(CurrencyUnit.GBP).nowish)
evalCadOverGbpNowish
.value must beRight((m: Money) => m.isLessThan(Money.of(CurrencyUnit.GBP, 1)))
evalCadOverGbpNowish.value must beRight((m: Money) => m.isLessThan(Money.of(CurrencyUnit.GBP, 1)))
}
}

/** GBP -> JPY with base currency USD */
"GBP to JPY with USD as base currency returning near-live rate" should {
"be greater than 1 Yen" in {
val ioGbpToJpyWithBaseUsd = ioFx.flatMap(_.rate(CurrencyUnit.GBP).to(CurrencyUnit.JPY).nowish)
ioGbpToJpyWithBaseUsd.unsafeRunSync() must beRight((m: Money) =>
m.isGreaterThan(BigMoney.of(CurrencyUnit.JPY, 1).toMoney(RoundingMode.HALF_EVEN)))
ioGbpToJpyWithBaseUsd.unsafeRunSync() must beRight(
(m: Money) => m.isGreaterThan(BigMoney.of(CurrencyUnit.JPY, 1).toMoney(RoundingMode.HALF_EVEN)))
val evalGbpToJpyWithBaseUsd =
evalFx.flatMap(_.rate(CurrencyUnit.GBP).to(CurrencyUnit.JPY).nowish)
evalGbpToJpyWithBaseUsd.value must beRight((m: Money) =>
m.isGreaterThan(BigMoney.of(CurrencyUnit.JPY, 1).toMoney(RoundingMode.HALF_EVEN)))
evalGbpToJpyWithBaseUsd.value must beRight(
(m: Money) => m.isGreaterThan(BigMoney.of(CurrencyUnit.JPY, 1).toMoney(RoundingMode.HALF_EVEN)))
}
}

/** GBP -> JPY with base currency GBP */
"GBP to JPY with GBP as base currency returning near-live rate" should {
"be greater than 1 Yen" in {
val ioGbpToJpyWithBaseGbp = ioFxWithBaseGBP.flatMap(_.rate.to(CurrencyUnit.JPY).nowish)
ioGbpToJpyWithBaseGbp.unsafeRunSync() must beRight((m: Money) =>
m.isGreaterThan(BigMoney.of(CurrencyUnit.of("JPY"), 1).toMoney(RoundingMode.HALF_EVEN)))
ioGbpToJpyWithBaseGbp.unsafeRunSync() must beRight(
(m: Money) => m.isGreaterThan(BigMoney.of(CurrencyUnit.of("JPY"), 1).toMoney(RoundingMode.HALF_EVEN)))
val evalGbpToJpyWithBaseGbp = evalFxWithBaseGBP.flatMap(_.rate.to(CurrencyUnit.JPY).nowish)
evalGbpToJpyWithBaseGbp.value must beRight((m: Money) =>
m.isGreaterThan(BigMoney.of(CurrencyUnit.of("JPY"), 1).toMoney(RoundingMode.HALF_EVEN)))
evalGbpToJpyWithBaseGbp.value must beRight(
(m: Money) => m.isGreaterThan(BigMoney.of(CurrencyUnit.of("JPY"), 1).toMoney(RoundingMode.HALF_EVEN)))
}
}
}
Expand Up @@ -12,6 +12,7 @@
*/
package com.snowplowanalytics.forex

import cats.Eval
import cats.effect.IO
import org.specs2.mutable.Specification

Expand All @@ -25,12 +26,12 @@ class ForexWithoutCachesSpec extends Specification {

"Setting both cache sizes to zero" should {
"disable the use of caches" in {
val ioFxWithoutCache = Forex.getForex[IO](
ForexConfig(key, DeveloperAccount, nowishCacheSize = 0, eodCacheSize = 0))
val ioFxWithoutCache =
CreateForex[IO].create(ForexConfig(key, DeveloperAccount, nowishCacheSize = 0, eodCacheSize = 0))
ioFxWithoutCache.unsafeRunSync().client.eodCache.isEmpty
ioFxWithoutCache.unsafeRunSync().client.nowishCache.isEmpty
val evalFxWithoutCache = Forex.unsafeGetForex(
ForexConfig(key, DeveloperAccount, nowishCacheSize = 0, eodCacheSize = 0))
val evalFxWithoutCache =
CreateForex[Eval].create(ForexConfig(key, DeveloperAccount, nowishCacheSize = 0, eodCacheSize = 0))
evalFxWithoutCache.value.client.eodCache.isEmpty
evalFxWithoutCache.value.client.nowishCache.isEmpty
}
Expand Down
19 changes: 11 additions & 8 deletions src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala
Expand Up @@ -15,6 +15,7 @@ package com.snowplowanalytics.forex
import java.math.BigDecimal
import java.time.ZonedDateTime

import cats.Eval
import cats.effect.IO
import org.joda.money.CurrencyUnit
import org.specs2.mutable.Specification
Expand All @@ -25,16 +26,18 @@ import model._
class OerClientSpec extends Specification {
args(skipAll = sys.env.get("OER_KEY").isEmpty)

val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = Forex.getForex[IO](ForexConfig(key, DeveloperAccount))
val evalFx = Forex.unsafeGetForex(ForexConfig(key, DeveloperAccount))
val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = CreateForex[IO].create(ForexConfig(key, DeveloperAccount))
val evalFx = CreateForex[Eval].create(ForexConfig(key, DeveloperAccount))

"live currency value for USD" should {
"always equal to 1" in {
ioFx.map(_.client)
ioFx
.map(_.client)
.flatMap(_.getLiveCurrencyValue(CurrencyUnit.USD))
.unsafeRunSync() must beRight(new BigDecimal(1))
evalFx.map(_.client)
evalFx
.map(_.client)
.flatMap(_.getLiveCurrencyValue(CurrencyUnit.USD))
.value must beRight(new BigDecimal(1))
}
Expand All @@ -52,10 +55,10 @@ class OerClientSpec extends Specification {
"historical currency value for USD on 01/01/2008" should {
"always equal to 1 as well" in {
val date = ZonedDateTime.parse("2008-01-01T01:01:01.123+09:00")
ioFx.flatMap(_.client.getHistoricalCurrencyValue(CurrencyUnit.USD, date))
ioFx
.flatMap(_.client.getHistoricalCurrencyValue(CurrencyUnit.USD, date))
.unsafeRunSync() must beRight(new BigDecimal(1))
evalFx.flatMap(_.client.getHistoricalCurrencyValue(CurrencyUnit.USD, date))
.value must beRight(new BigDecimal(1))
evalFx.flatMap(_.client.getHistoricalCurrencyValue(CurrencyUnit.USD, date)).value must beRight(new BigDecimal(1))
}
}
}
Expand Up @@ -14,6 +14,7 @@ package com.snowplowanalytics.forex

import java.time.{ZoneId, ZonedDateTime}

import cats.Eval
import cats.effect.IO
import org.joda.money.CurrencyUnit
import org.specs2.mutable.Specification
Expand All @@ -27,9 +28,9 @@ import model._
class UnsupportedEodSpec extends Specification {
args(skipAll = sys.env.get("OER_KEY").isEmpty)

val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = Forex.getForex[IO](ForexConfig(key, DeveloperAccount))
val evalFx = Forex.unsafeGetForex(ForexConfig(key, DeveloperAccount))
val key = sys.env.getOrElse("OER_KEY", "")
val ioFx = CreateForex[IO].create(ForexConfig(key, DeveloperAccount))
val evalFx = CreateForex[Eval].create(ForexConfig(key, DeveloperAccount))

"An end-of-date lookup in 1900" should {
"throw an exception" in {
Expand Down

0 comments on commit d294179

Please sign in to comment.