diff --git a/build.sbt b/build.sbt index 1277317..3f2f57e 100644 --- a/build.sbt +++ b/build.sbt @@ -31,6 +31,7 @@ lazy val root = project.in(file(".")) Dependencies.Libraries.catsEffect, Dependencies.Libraries.circeParser, Dependencies.Libraries.lruMap, + Dependencies.Libraries.scalaj, Dependencies.Libraries.specs2Core, Dependencies.Libraries.specs2Mock ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index df6ae5b..a81457a 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -32,16 +32,17 @@ object Dependencies { object Libraries { // Java - val jodaMoney = "org.joda" % "joda-money" % V.jodaMoney - val jodaConvert = "org.joda" % "joda-convert" % V.jodaConvert + val jodaMoney = "org.joda" % "joda-money" % V.jodaMoney + val jodaConvert = "org.joda" % "joda-convert" % V.jodaConvert // Scala - val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEffect - val circeParser = "io.circe" %% "circe-parser" % V.circe - val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % V.lruMap + val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEffect + val circeParser = "io.circe" %% "circe-parser" % V.circe + val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % V.lruMap + val scalaj = "org.scalaj" %% "scalaj-http" % V.scalaj // Scala (test only) - val specs2Core = "org.specs2" %% "specs2-core" % V.specs2 % "test" - val specs2Mock = "org.specs2" %% "specs2-mock" % V.specs2 % "test" + val specs2Core = "org.specs2" %% "specs2-core" % V.specs2 % "test" + val specs2Mock = "org.specs2" %% "specs2-mock" % V.specs2 % "test" } } diff --git a/src/main/scala/com.snowplowanalytics/forex/Forex.scala b/src/main/scala/com.snowplowanalytics/forex/Forex.scala index d031607..1e96d99 100644 --- a/src/main/scala/com.snowplowanalytics/forex/Forex.scala +++ b/src/main/scala/com.snowplowanalytics/forex/Forex.scala @@ -23,7 +23,8 @@ import cats.implicits._ import cats.data.{EitherT, OptionT} import org.joda.money._ -import oerclient._ +import errors._ +import model._ /** Companion object to get Forex object */ object Forex { @@ -48,7 +49,7 @@ object Forex { } def getForex[F[_]: Sync](config: ForexConfig): F[Forex[F]] = - ForexClient + OerClient .getClient[F](config) .map(client => Forex(config, client)) } @@ -62,7 +63,7 @@ object Forex { * @param config A configurator for Forex object * @param client Passed down client that does actual work */ -case class Forex[F[_]](config: ForexConfig, client: ForexClient[F]) { +case class Forex[F[_]](config: ForexConfig, client: OerClient[F]) { def rate: ForexLookupTo[F] = ForexLookupTo(1, config.baseCurrency, config, client) @@ -110,7 +111,7 @@ case class ForexLookupTo[F[_]]( conversionAmount: Double, fromCurr: CurrencyUnit, config: ForexConfig, - client: ForexClient[F] + client: OerClient[F] ) { /** @@ -136,7 +137,7 @@ case class ForexLookupWhen[F[_]: Sync]( fromCurr: CurrencyUnit, toCurr: CurrencyUnit, config: ForexConfig, - client: ForexClient[F] + client: OerClient[F] ) { // convert `conversionAmt` into BigDecimal representation for its later usage in BigMoney val conversionAmt = new BigDecimal(conversionAmount) diff --git a/src/main/scala/com.snowplowanalytics/forex/ForexClient.scala b/src/main/scala/com.snowplowanalytics/forex/ForexClient.scala deleted file mode 100644 index 2f57896..0000000 --- a/src/main/scala/com.snowplowanalytics/forex/ForexClient.scala +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2013-2018 Snowplow Analytics Ltd. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package com.snowplowanalytics.forex - -import java.time.ZonedDateTime - -import cats.effect.Sync -import cats.syntax.apply._ -import cats.syntax.functor._ -import cats.syntax.option._ -import org.joda.money.CurrencyUnit - -import com.snowplowanalytics.lrumap.CreateLruMap -import oerclient._ - -/** - * Companion object for ForexClient class - * This class has one method for getting forex clients - * but for now there is only one client since we are only using OER - */ -object ForexClient { - - /** Creates a client with a cache and sensible default ForexConfig */ - def getClient[F[_]: Sync](appId: String, accountLevel: AccountType): F[ForexClient[F]] = - getClient[F](ForexConfig(appId = appId, accountLevel = accountLevel)) - - /** Getter for clients, creating the caches as defined in the config */ - def getClient[F[_]: Sync]( - config: ForexConfig - )( - implicit CLM1: CreateLruMap[F, NowishCacheKey, NowishCacheValue], - CLM2: CreateLruMap[F, EodCacheKey, EodCacheValue] - ): F[ForexClient[F]] = { - val nowishCacheF = - if (config.nowishCacheSize > 0) { - CLM1.create(config.nowishCacheSize).map(_.some) - } else { - Sync[F].pure(Option.empty[NowishCache[F]]) - } - - val eodCacheF = - if (config.eodCacheSize > 0) { - CLM2.create(config.eodCacheSize).map(_.some) - } else { - Sync[F].pure(Option.empty[EodCache[F]]) - } - - (nowishCacheF, eodCacheF).mapN { - case (nowish, eod) => - new OerClient[F](config, nowishCache = nowish, eodCache = eod) - } - } - - def getClient[F[_]: Sync]( - config: ForexConfig, - nowishCache: Option[NowishCache[F]], - eodCache: Option[EodCache[F]] - ): ForexClient[F] = new OerClient[F](config, nowishCache, eodCache) -} - -abstract class ForexClient[F[_]]( - val config: ForexConfig, - val nowishCache: Option[NowishCache[F]] = None, - val eodCache: Option[EodCache[F]] = None -) { - - /** - * Get the latest exchange rate from a given currency - * @param currency Desired currency - * @return result returned from API - */ - def getLiveCurrencyValue(currency: CurrencyUnit): F[ApiRequestResult] - - /** - * Get a historical exchange rate from a given currency and date - * @param currency Desired currency - * @param date Date of desired rate - * @return result returned from API - */ - def getHistoricalCurrencyValue(currency: CurrencyUnit, date: ZonedDateTime): F[ApiRequestResult] -} diff --git a/src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala b/src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala deleted file mode 100644 index b224b66..0000000 --- a/src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2013-2018 Snowplow Analytics Ltd. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package com.snowplowanalytics.forex - -import org.joda.money.CurrencyUnit - -/** User defined type for getNearestDay flag */ -sealed trait EodRounding - -/** Round to previous day*/ -object EodRoundDown extends EodRounding - -/** Round to next day*/ -object EodRoundUp extends EodRounding - -/** - * There are three types of accounts supported by OER API. - * For scala-forex library, the main difference between Unlimited/Enterprise - * and Developer users is that users with Unlimited/Enterprise accounts - * can use the base currency for API requests, but this library will provide - * automatic conversions between OER default base currencies(USD) - * and user-defined base currencies. However this will increase calls to the API - * and will slow down the performance. - */ -sealed trait AccountType -object DeveloperAccount extends AccountType -object EnterpriseAccount extends AccountType -object UnlimitedAccount extends AccountType - -/** - * Configure class for Forex object - * @param appId Key for the api - * @param accountLevel Type of the registered account - * @param nowishCacheSize Cache for nowish look up - * @param nowishSecs Time range for nowish look up - * @param eodCacheSize Cache for historical lookup - * @param getNearestDay Flag for deciding whether to get the exchange rate on closer day or previous day - * @param baseCurrency Base currency is set to be USD by default if configurableBase flag is false, otherwise it is user-defined - */ -case class ForexConfig( - /** - * Register an account on https://openexchangerates.org to obtain your unique key - */ - appId: String, - accountLevel: AccountType, - /** - * nowishCacheSize = (165 * 164 / 2) = 13530. - * There are 165 currencies in total, the combinations of a currency pair - * has 165 * (165 - 1) possibilities. (X,Y) is the same as (Y,X) hence 165 * 164 / 2 - */ - nowishCacheSize: Int = 13530, - /** 5 mins by default */ - nowishSecs: Int = 300, - /** 165 * 164 / 2 * 30 = 405900, assuming the cache stores data within a month */ - eodCacheSize: Int = 405900, - getNearestDay: EodRounding = EodRoundDown, - baseCurrency: CurrencyUnit = CurrencyUnit.USD -) diff --git a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClient.scala b/src/main/scala/com.snowplowanalytics/forex/OerClient.scala similarity index 77% rename from src/main/scala/com.snowplowanalytics/forex/oerclient/OerClient.scala rename to src/main/scala/com.snowplowanalytics/forex/OerClient.scala index 80a4669..948b114 100644 --- a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClient.scala +++ b/src/main/scala/com.snowplowanalytics/forex/OerClient.scala @@ -11,35 +11,19 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ package com.snowplowanalytics.forex -package oerclient -import java.net.URL -import java.net.HttpURLConnection import java.time.{LocalDateTime, ZoneId, ZonedDateTime} import java.math.{BigDecimal => JBigDecimal} -import scala.util.Try - import cats.effect.Sync import cats.implicits._ import cats.data.EitherT -import io.circe._ -import io.circe.parser.parse import org.joda.money.CurrencyUnit -object OerClient { - final case class OerResponse(rates: Map[CurrencyUnit, BigDecimal]) - - // Encoder ignores Currencies that are not parsable by CurrencyUnit - implicit val oerResponseDecoder = new Decoder[OerResponse] { - override def apply(c: HCursor): Decoder.Result[OerResponse] = - c.downField("rates").as[Map[String, BigDecimal]].map { map => - OerResponse( - map.toList.mapFilter { case (key, value) => Try(CurrencyUnit.of(key)).toOption.map(_ -> value) }.toMap) - } - } - -} +import com.snowplowanalytics.lrumap.CreateLruMap +import errors._ +import model._ +import responses._ /** * Implements Json for Open Exchange Rates(http://openexchangerates.org) @@ -47,16 +31,14 @@ object OerClient { * @param nowishCache - user defined nowishCache * @param eodCache - user defined eodCache */ -class OerClient[F[_]: Sync]( +case class OerClient[F[_]: Sync]( config: ForexConfig, - nowishCache: Option[NowishCache[F]] = None, - eodCache: Option[EodCache[F]] = None -) extends ForexClient[F](config, nowishCache, eodCache) { - - import OerClient._ + val nowishCache: Option[NowishCache[F]] = None, + val eodCache: Option[EodCache[F]] = None, + transport: Transport[F] +) { - /** Base URL to OER API */ - private val oerUrl = "http://openexchangerates.org/api/" + private val endpoint = "openexchangerates.org/api/" /** Sets the base currency in the url * according to the API, only Unlimited and Enterprise accounts @@ -88,7 +70,7 @@ class OerClient[F[_]: Sync]( */ def getLiveCurrencyValue(currency: CurrencyUnit): F[ApiRequestResult] = { val action = for { - response <- EitherT(getResponseFromApi(latest)) + response <- EitherT(transport.receive(endpoint, latest)) liveCurrency <- EitherT(extractLiveCurrency(response, currency)) } yield liveCurrency action.value @@ -167,7 +149,7 @@ class OerClient[F[_]: Sync]( } else { val historicalLink = buildHistoricalLink(date) val action = for { - response <- EitherT(getResponseFromApi(historicalLink)) + response <- EitherT(transport.receive(endpoint, historicalLink)) currency <- EitherT(extractHistoricalCurrency(response, currency, date)) } yield currency @@ -220,32 +202,50 @@ class OerClient[F[_]: Sync]( .map(_.bigDecimal) .toRight(OerResponseError(s"Currency not found in the API, invalid currency $currency", IllegalCurrency))) } +} - /** - * Helper method which returns the node containing - * a list of currency and rate pair. - * @param downloadPath - The URI link for the API request - * @return JSON node which contains currency information obtained from API - * or OerResponseError object which carries the error message returned by the API - */ - private def getResponseFromApi(downloadPath: String): F[Either[OerResponseError, OerResponse]] = - Sync[F].delay { - val url = new URL(oerUrl + downloadPath) - val conn = url.openConnection - conn match { - case httpUrlConn: HttpURLConnection => - if (httpUrlConn.getResponseCode >= 400) { - val errorString = scala.io.Source.fromInputStream(httpUrlConn.getErrorStream).mkString - parse(errorString) - .flatMap(_.hcursor.downField("message").as[String]) - .leftMap(e => OerResponseError(e.getMessage, OtherErrors)) - .flatMap(message => Left(OerResponseError(message, OtherErrors))) - } else { - parse(scala.io.Source.fromInputStream(httpUrlConn.getInputStream).mkString) - .flatMap(_.as[OerResponse]) - .leftMap(e => OerResponseError(e.getMessage, OtherErrors)) - } - case _ => throw new ClassCastException +/** + * Companion object for ForexClient class + * This class has one method for getting forex clients + * but for now there is only one client since we are only using OER + */ +object OerClient { + + /** Creates a client with a cache and sensible default ForexConfig */ + def getClient[F[_]: Sync](appId: String, accountLevel: AccountType): F[OerClient[F]] = + getClient[F](ForexConfig(appId = appId, accountLevel = accountLevel)) + + /** Getter for clients, creating the caches as defined in the config */ + def getClient[F[_]: Sync]( + config: ForexConfig + )( + implicit CLM1: CreateLruMap[F, NowishCacheKey, NowishCacheValue], + CLM2: CreateLruMap[F, EodCacheKey, EodCacheValue] + ): F[OerClient[F]] = { + val nowishCacheF = + if (config.nowishCacheSize > 0) { + CLM1.create(config.nowishCacheSize).map(_.some) + } else { + Sync[F].pure(Option.empty[NowishCache[F]]) } + + val eodCacheF = + if (config.eodCacheSize > 0) { + CLM2.create(config.eodCacheSize).map(_.some) + } else { + Sync[F].pure(Option.empty[EodCache[F]]) + } + + (nowishCacheF, eodCacheF).mapN { + case (nowish, eod) => + new OerClient[F](config, nowishCache = nowish, eodCache = eod, Transport.httpTransport[F]) } + } + + def getClient[F[_]: Sync]( + config: ForexConfig, + nowishCache: Option[NowishCache[F]], + eodCache: Option[EodCache[F]], + transport: Transport[F] + ): OerClient[F] = new OerClient[F](config, nowishCache, eodCache, transport) } diff --git a/src/main/scala/com.snowplowanalytics/forex/Transport.scala b/src/main/scala/com.snowplowanalytics/forex/Transport.scala new file mode 100644 index 0000000..8c47f1d --- /dev/null +++ b/src/main/scala/com.snowplowanalytics/forex/Transport.scala @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015-2019 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.forex + +import cats.Eval +import cats.effect.Sync +import cats.syntax.either._ +import io.circe.Decoder +import io.circe.parser._ +import scalaj.http._ + +import errors._ +import responses._ + +trait Transport[F[_]] { + + /** + * Main client logic for Request => Response function, + * where Response is wrapped in tparam `F` + * @param endpoint endpoint to interrogate + * @param path path to request + * @return extracted either error or exchanged rate wrapped in `F` + */ + def receive(endpoint: String, path: String): F[Either[OerResponseError, OerResponse]] + +} + +object Transport { + + /** + * Http Transport leveraging cats-effect's Sync. + * @return a Sync Transport + */ + implicit def httpTransport[F[_]: Sync]: Transport[F] = new Transport[F] { + def receive(endpoint: String, path: String): F[Either[OerResponseError, OerResponse]] = + Sync[F].delay(buildRequest(endpoint, path)) + } + + /** + * Eval http Transport to use in cases where you have to do side-effects (e.g. spark or beam). + * @return an Eval Transport + */ + implicit def evalHttpTransport: Transport[Eval] = new Transport[Eval] { + def receive(endpoint: String, path: String): Eval[Either[OerResponseError, OerResponse]] = + Eval.later { + buildRequest(endpoint, path) + } + } + + implicit def eitherDecoder: Decoder[Either[OerResponseError, OerResponse]] = + implicitly[Decoder[OerResponseError]].either(implicitly[Decoder[OerResponse]]) + + private def buildRequest( + endpoint: String, + path: String + ): Either[OerResponseError, OerResponse] = + for { + response <- Http("http://" + endpoint + path).asString.body.asRight + parsed <- parse(response) + .leftMap(e => OerResponseError(s"OER response is not JSON: ${e.getMessage}", OtherErrors)) + decoded <- parsed + .as[Either[OerResponseError, OerResponse]] + .leftMap(_ => OerResponseError(s"OER response couldn't be decoded", OtherErrors)) + res <- decoded + } yield res +} diff --git a/src/main/scala/com.snowplowanalytics/forex/errors.scala b/src/main/scala/com.snowplowanalytics/forex/errors.scala new file mode 100644 index 0000000..0280ace --- /dev/null +++ b/src/main/scala/com.snowplowanalytics/forex/errors.scala @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2013-2018 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.forex + +import io.circe.Decoder + +object errors { + + /** + * OER error states from the HTTP requests + * @param errorMessage - error message from OER API + * @param errorType - specific error type + */ + final case class OerResponseError(errorMessage: String, errorType: OerError) + + implicit val oerResponseErrorDecoder: Decoder[OerResponseError] = Decoder.decodeString + .prepare(_.downField("message")) + .map(OerResponseError(_, OtherErrors)) + + /** + * User defined error types + */ + sealed trait OerError + + /** + * Caused by invalid DateTime argument + * i.e. either earlier than the earliest date OER service is available or later than currenct time + */ + object ResourcesNotAvailable extends OerError + + /** + * Currency not supported by API or + * Joda Money or both + */ + object IllegalCurrency extends OerError + + /** + * Other possible error types e.g. + * access permissions + */ + object OtherErrors extends OerError +} diff --git a/src/main/scala/com.snowplowanalytics/forex/model.scala b/src/main/scala/com.snowplowanalytics/forex/model.scala new file mode 100644 index 0000000..9f688ae --- /dev/null +++ b/src/main/scala/com.snowplowanalytics/forex/model.scala @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2013-2018 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.forex + +import org.joda.money.CurrencyUnit + +object model { + + /** User defined type for getNearestDay flag */ + sealed trait EodRounding + + /** Round to previous day*/ + object EodRoundDown extends EodRounding + + /** Round to next day*/ + object EodRoundUp extends EodRounding + + /** + * There are three types of accounts supported by OER API. + * For scala-forex library, the main difference between Unlimited/Enterprise + * and Developer users is that users with Unlimited/Enterprise accounts + * can use the base currency for API requests, but this library will provide + * automatic conversions between OER default base currencies(USD) + * and user-defined base currencies. However this will increase calls to the API + * and will slow down the performance. + */ + sealed trait AccountType + object DeveloperAccount extends AccountType + object EnterpriseAccount extends AccountType + object UnlimitedAccount extends AccountType + + /** + * Configure class for Forex object + * @param appId Key for the api + * @param accountLevel Type of the registered account + * @param endpoint API endpoint + * @param nowishCacheSize Cache for nowish look up + * @param nowishSecs Time range for nowish look up + * @param eodCacheSize Cache for historical lookup + * @param getNearestDay Flag for deciding whether to get the exchange rate on closer day or previous day + * @param baseCurrency Base currency is set to be USD by default if configurableBase flag is false, otherwise it is user-defined + */ + case class ForexConfig( + // Register an account on https://openexchangerates.org to obtain your unique key + appId: String, + accountLevel: AccountType, + // nowishCacheSize = (165 * 164 / 2) = 13530. + // There are 165 currencies in total, the combinations of a currency pair + // has 165 * (165 - 1) possibilities. (X,Y) is the same as (Y,X) hence 165 * 164 / 2 + nowishCacheSize: Int = 13530, + //5 mins by default + nowishSecs: Int = 300, + // 165 * 164 / 2 * 30 = 405900, assuming the cache stores data within a month + eodCacheSize: Int = 405900, + getNearestDay: EodRounding = EodRoundDown, + baseCurrency: CurrencyUnit = CurrencyUnit.USD + ) +} diff --git a/src/main/scala/com.snowplowanalytics/package.scala b/src/main/scala/com.snowplowanalytics/forex/package.scala similarity index 97% rename from src/main/scala/com.snowplowanalytics/package.scala rename to src/main/scala/com.snowplowanalytics/forex/package.scala index bb7e703..0df3deb 100644 --- a/src/main/scala/com.snowplowanalytics/package.scala +++ b/src/main/scala/com.snowplowanalytics/forex/package.scala @@ -18,7 +18,7 @@ import java.math.BigDecimal import org.joda.money.CurrencyUnit import com.snowplowanalytics.lrumap.LruMap -import forex.oerclient.OerResponseError +import forex.errors._ package object forex { diff --git a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerResponseError.scala b/src/main/scala/com.snowplowanalytics/forex/responses.scala similarity index 51% rename from src/main/scala/com.snowplowanalytics/forex/oerclient/OerResponseError.scala rename to src/main/scala/com.snowplowanalytics/forex/responses.scala index e719182..d44c44e 100644 --- a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerResponseError.scala +++ b/src/main/scala/com.snowplowanalytics/forex/responses.scala @@ -11,34 +11,26 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ package com.snowplowanalytics.forex -package oerclient -/** - * OER error states from the HTTP requests - * @param errorMessage - error message from OER API - * @param errorType - specific error type - */ -case class OerResponseError(errorMessage: String, errorType: OerError) - -/** - * User defined error types - */ -sealed trait OerError +import scala.util.Try -/** - * Caused by invalid DateTime argument - * i.e. either earlier than the earliest date OER service is available or later than currenct time - */ -object ResourcesNotAvailable extends OerError +import cats.instances.list._ +import cats.syntax.functorFilter._ +import io.circe._ +import org.joda.money.CurrencyUnit -/** - * Currency not supported by API or - * Joda Money or both - */ -object IllegalCurrency extends OerError +object responses { + final case class OerResponse(rates: Map[CurrencyUnit, BigDecimal]) -/** - * Other possible error types e.g. - * access permissions - */ -object OtherErrors extends OerError + // Encoder ignores Currencies that are not parsable by CurrencyUnit + implicit val oerResponseDecoder: Decoder[OerResponse] = new Decoder[OerResponse] { + override def apply(c: HCursor): Decoder.Result[OerResponse] = + c.downField("rates").as[Map[String, BigDecimal]].map { map => + OerResponse( + map.toList.mapFilter { + case (key, value) => Try(CurrencyUnit.of(key)).toOption.map(_ -> value) + }.toMap + ) + } + } +} diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala index 098c240..260b556 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala @@ -18,6 +18,8 @@ import cats.effect.IO import org.joda.money.{CurrencyUnit, Money} import org.specs2.mutable.Specification +import model._ + /** * Testing method for getting the latest end-of-day rate * prior to the datetime or the day after according to the user's setting diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala index 878c0f4..7eb5e5f 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala @@ -20,6 +20,8 @@ import org.joda.money.{CurrencyUnit, Money} import org.specs2.mutable.Specification import org.specs2.matcher.DataTables +import model._ + /** * Testing method for getting the end-of-date exchange rate * since historical forex rate is fixed, the actual look up result should be diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala index ba1d7b4..f9f98f2 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala @@ -18,6 +18,8 @@ import cats.effect.IO import org.joda.money._ import org.specs2.mutable.Specification +import model._ + /** Testing method for getting the live exchange rate */ class ForexNowSpec extends Specification { args(skipAll = sys.env.get("OER_KEY").isEmpty) diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala index b8bdb1a..2e99f74 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala @@ -18,6 +18,8 @@ import cats.effect.IO import org.joda.money._ import org.specs2.mutable.Specification +import model._ + /** Testing method for getting the approximate exchange rate */ class ForexNowishSpec extends Specification { args(skipAll = sys.env.get("OER_KEY").isEmpty) diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala index d929de0..fd6cffe 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala @@ -15,6 +15,8 @@ package com.snowplowanalytics.forex import cats.effect.IO import org.specs2.mutable.Specification +import model._ + /** Testing that setting cache size to zero will disable the use of cache */ class ForexWithoutCachesSpec extends Specification { args(skipAll = sys.env.get("OER_KEY").isEmpty) diff --git a/src/test/scala/com.snowplowanalytics.forex/oerclient/OerClientSpec.scala b/src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala similarity index 99% rename from src/test/scala/com.snowplowanalytics.forex/oerclient/OerClientSpec.scala rename to src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala index dc7c41d..a665ced 100644 --- a/src/test/scala/com.snowplowanalytics.forex/oerclient/OerClientSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/OerClientSpec.scala @@ -11,7 +11,6 @@ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ package com.snowplowanalytics.forex -package oerclient import java.math.BigDecimal import java.time.ZonedDateTime @@ -20,6 +19,8 @@ import cats.effect.IO import org.joda.money.CurrencyUnit import org.specs2.mutable.Specification +import model._ + /** Testing methods for Open exchange rate client */ class OerClientSpec extends Specification { args(skipAll = sys.env.get("OER_KEY").isEmpty) diff --git a/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala b/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala index 655ada0..29961b4 100644 --- a/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala @@ -24,6 +24,7 @@ import org.specs2.mock.Mockito import org.specs2.mutable.Specification import com.snowplowanalytics.lrumap.CreateLruMap +import model._ /** Testing cache behaviours */ class SpiedCacheSpec extends Specification with Mockito { @@ -43,7 +44,12 @@ class SpiedCacheSpec extends Specification with Mockito { .create(config.eodCacheSize) .unsafeRunSync() ) - val client = ForexClient.getClient[IO](config, Some(spiedNowishCache), Some(spiedEodCache)) + val client = OerClient.getClient[IO]( + config, + Some(spiedNowishCache), + Some(spiedEodCache), + Transport.httpTransport[IO] + ) val spiedFx = Forex[IO](config, client) val spiedFxWith5NowishSecs = Forex[IO](fxConfigWith5NowishSecs, client) diff --git a/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala b/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala index 0377fec..6fca724 100644 --- a/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala @@ -18,7 +18,8 @@ import cats.effect.IO import org.joda.money.CurrencyUnit import org.specs2.mutable.Specification -import oerclient.{OerResponseError, ResourcesNotAvailable} +import errors._ +import model._ /** * Testing for exceptions caused by invalid dates