From 5764be482aa2f598bf2f10355ddacd41be4019ef Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 10 Apr 2019 15:04:22 +0200 Subject: [PATCH] Abstract over Forex creation (closes #161) --- .../com.snowplowanalytics/forex/Forex.scala | 50 ++++++++++++------- .../ForexAtSpec.scala | 11 ++-- .../ForexEodSpec.scala | 21 ++++---- .../ForexNowSpec.scala | 23 ++++----- .../ForexNowishSpec.scala | 30 +++++------ .../ForexWithoutCachesSpec.scala | 9 ++-- .../OerClientSpec.scala | 19 ++++--- .../UnsupportedEodSpec.scala | 7 +-- 8 files changed, 95 insertions(+), 75 deletions(-) diff --git a/src/main/scala/com.snowplowanalytics/forex/Forex.scala b/src/main/scala/com.snowplowanalytics/forex/Forex.scala index c669ab7..e53ee8a 100644 --- a/src/main/scala/com.snowplowanalytics/forex/Forex.scala +++ b/src/main/scala/com.snowplowanalytics/forex/Forex.scala @@ -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._ @@ -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. @@ -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 { @@ -182,12 +195,13 @@ final case class ForexLookupWhen[F[_]: Monad]( * @return Money representation in target currency or OerResponseError object if API request * failed */ - def nowish: F[Either[OerResponseError, Money]] = (for { - (time, rate) <- lookupNowishCache(fromCurr, toCurr) - nowishTime <- OptionT.liftF(C.now().map(_.minusSeconds(config.nowishSecs.toLong))) - if (nowishTime.isBefore(time) || nowishTime.equals(time)) - res = returnMoneyOrJodaError(rate) - } yield res).getOrElseF(now) + def nowish: F[Either[OerResponseError, Money]] = + (for { + (time, rate) <- lookupNowishCache(fromCurr, toCurr) + nowishTime <- OptionT.liftF(C.now().map(_.minusSeconds(config.nowishSecs.toLong))) + if nowishTime.isBefore(time) || nowishTime.equals(time) + res = returnMoneyOrJodaError(rate) + } yield res).getOrElseF(now) private def lookupNowishCache( fromCurr: CurrencyUnit, diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala index 955091f..ab95740 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala @@ -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 @@ -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")) diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala index c09df58..ee154b5 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala @@ -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 @@ -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) ^ @@ -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) } diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala index e1bd504..53b1f1b 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala @@ -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 @@ -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 { @@ -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))) } } @@ -65,8 +65,7 @@ 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))) } } @@ -74,11 +73,9 @@ class ForexNowSpec extends Specification { "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))) } } } diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala index d449dea..824eec9 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala @@ -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 @@ -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 { @@ -40,8 +41,7 @@ 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))) } } @@ -49,12 +49,12 @@ class ForexNowishSpec extends Specification { "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))) } } @@ -62,11 +62,11 @@ class ForexNowishSpec extends Specification { "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))) } } } diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala index 1aeff36..4445b87 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala @@ -12,6 +12,7 @@ */ package com.snowplowanalytics.forex +import cats.Eval import cats.effect.IO import org.specs2.mutable.Specification @@ -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 } diff --git a/src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala b/src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala index abc3e05..627d4ec 100644 --- a/src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala @@ -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 @@ -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)) } @@ -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)) } } } diff --git a/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala b/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala index 973e97c..bff4ddc 100644 --- a/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala @@ -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 @@ -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 {