diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..07ce4bb --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,15 @@ +style = default +align = some +maxColumn = 120 +docstrings = JavaDoc +optIn.breakChainOnFirstMethodDot = true +spaces.afterKeywordBeforeParen = true +continuationIndent.defnSite = 2 +importSelectors = noBinPack +rewrite.rules = [ + AsciiSortImports, + RedundantBraces, + RedundantParens, + PreferCurlyFors + ] +align.tokens = ["|", "!", "!!", "||", "=>", "=", "->", "<-", "|@|", "//", "/", "+"] diff --git a/build.sbt b/build.sbt index 510dacc..9234223 100644 --- a/build.sbt +++ b/build.sbt @@ -21,6 +21,7 @@ lazy val root = project.in(file(".")) ) .settings(BuildSettings.buildSettings) .settings(BuildSettings.publishSettings) + .settings(BuildSettings.formattingSettings) .settings( libraryDependencies ++= Seq( Dependencies.Libraries.jodaTime, diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 85f38ae..4d09668 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -19,6 +19,9 @@ import Keys._ import bintray.BintrayPlugin._ import bintray.BintrayKeys._ +// Scalafmt +import org.scalafmt.sbt.ScalafmtPlugin.autoImport._ + object BuildSettings { // Basic settings for our app @@ -66,4 +69,8 @@ object BuildSettings { ) ) + + lazy val formattingSettings = Seq( + scalafmtOnCompile := true + ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index 945dd61..d69bbe8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,3 @@ addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") + +addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") diff --git a/src/main/scala/com.snowplowanalytics/forex/Forex.scala b/src/main/scala/com.snowplowanalytics/forex/Forex.scala index 06750d5..194f114 100644 --- a/src/main/scala/com.snowplowanalytics/forex/Forex.scala +++ b/src/main/scala/com.snowplowanalytics/forex/Forex.scala @@ -16,7 +16,7 @@ package com.snowplowanalytics.forex import java.math.BigDecimal import java.math.RoundingMode -// Joda +// Joda import org.joda.time._ import org.joda.money._ @@ -27,24 +27,24 @@ import com.snowplowanalytics.forex.oerclient._ * Companion object to get Forex object */ object Forex { + /** - * Fields for calculating currency rates conversions. - * Usually the currency rate has 6 decimal places - */ - val commonScale = 6 - + * Fields for calculating currency rates conversions. + * Usually the currency rate has 6 decimal places + */ + val commonScale = 6 + /** Helper method to get the ratio of from:to in BigDecimal type */ - def getForexRate(fromCurrIsBaseCurr: Boolean, baseOverFrom: BigDecimal, baseOverTo: BigDecimal): BigDecimal = { + def getForexRate(fromCurrIsBaseCurr: Boolean, baseOverFrom: BigDecimal, baseOverTo: BigDecimal): BigDecimal = if (!fromCurrIsBaseCurr) { val fromOverBase = new BigDecimal(1).divide(baseOverFrom, Forex.commonScale, RoundingMode.HALF_EVEN) fromOverBase.multiply(baseOverTo) } else { baseOverTo } - } - + /** - * Getter for Forex object + * Getter for Forex object */ def getForex(config: ForexConfig, clientConfig: ForexClientConfig): Forex = new Forex(config, clientConfig) @@ -52,10 +52,11 @@ object Forex { /** * Getter for Forex object with user defined caches */ - def getForex(config: ForexConfig, clientConfig: ForexClientConfig, - nowish: MaybeNowishCache, eod: MaybeEodCache): Forex = { + def getForex(config: ForexConfig, + clientConfig: ForexClientConfig, + nowish: MaybeNowishCache, + eod: MaybeEodCache): Forex = new Forex(config, clientConfig, nowishCache = nowish, eodCache = eod) - } } /** @@ -69,18 +70,16 @@ object Forex { * @param nowishCache - user defined nowishCache * @param eodCache - user defined eodCache */ -case class Forex( - config: ForexConfig, - clientConfig: ForexClientConfig, - nowishCache: MaybeNowishCache = None, - eodCache: MaybeEodCache = None) { +case class Forex(config: ForexConfig, + clientConfig: ForexClientConfig, + nowishCache: MaybeNowishCache = None, + eodCache: MaybeEodCache = None) { // For now, we are hard-wired to the OER Client library val client = ForexClient.getClient(config, clientConfig, nowish = nowishCache, eod = eodCache) - def rate: ForexLookupTo = { + def rate: ForexLookupTo = ForexLookupTo(1, config.baseCurrency, this) - } /** * Starts building a fluent interface, @@ -91,16 +90,14 @@ case class Forex( * @param currency(optional) - source currency * @return ForexLookupTo object which is the part of the fluent interface */ - def rate(currency: String): ForexLookupTo = { + def rate(currency: String): ForexLookupTo = ForexLookupTo(1, currency, this) - } /* * Wrapper for rate(currency: String) - */ - def rate(currency: CurrencyUnit): ForexLookupTo = { + */ + def rate(currency: CurrencyUnit): ForexLookupTo = rate(currency.getCode) - } /** * Starts building a currency conversion from @@ -109,138 +106,131 @@ case class Forex( * @param amount - The amount of currency to be converted * @param currency - The *source* currency(optional). * (The target currency will be supplied - * to the ForexLookupTo later). If not specified, + * to the ForexLookupTo later). If not specified, * it is set to be base currency by default * @return a ForexLookupTo, part of the * currency conversion fluent interface. */ - def convert(amount: Double): ForexLookupTo = { + def convert(amount: Double): ForexLookupTo = ForexLookupTo(amount, config.baseCurrency, this) - } - - def convert(amount: Double, currency: CurrencyUnit): ForexLookupTo = { + + def convert(amount: Double, currency: CurrencyUnit): ForexLookupTo = convert(amount, currency.getCode) - } // Wrapper method for convert(Int, CurrencyUnit) - def convert(amount: Double, currency: String): ForexLookupTo = { + def convert(amount: Double, currency: String): ForexLookupTo = ForexLookupTo(amount, currency, this) - } } - /** * ForexLookupTo is the second part of the fluent interface, * which passes parameters taken from Forex class to the next stage in the fluent interface * and sets the target currency for the lookup/conversion. - * Method in this class returns ForexLookupWhen object which will be passed to the next stage - * in the fluent interface. - * @param fx - Forex object which was configured ealier - * @param conversionAmount - the amount of money to be converted, + * Method in this class returns ForexLookupWhen object which will be passed to the next stage + * in the fluent interface. + * @param fx - Forex object which was configured ealier + * @param conversionAmount - the amount of money to be converted, * it is set to 1 unit for look up operation. - * @param fromCurr - the source currency + * @param fromCurr - the source currency */ case class ForexLookupTo(conversionAmount: Double, fromCurr: String, fx: Forex) { + /** * Continue building the target currency to the desired one * @param currency - Target currency * @return ForexLookupWhen object which is final part of the fluent interface - */ - def to(toCurr: String): ForexLookupWhen = { + */ + def to(toCurr: String): ForexLookupWhen = ForexLookupWhen(conversionAmount, fromCurr, toCurr, fx) - } // Wrapper for to(toCurr: String) - def to(toCurr: CurrencyUnit): ForexLookupWhen = { + def to(toCurr: CurrencyUnit): ForexLookupWhen = to(toCurr.getCode) - } } - + /** * ForexLookupWhen is the final part of the fluent interface, * methods in this class are the final stage of currency lookup/conversion * @param conversionAmount - The amount of money to be converted, it is set to 1 for lookup operation * @param fromCurr - The source currency * @param toCurr - The target currency - * @param fx - Forex object + * @param fx - Forex object */ case class ForexLookupWhen(conversionAmount: Double, fromCurr: String, toCurr: String, fx: Forex) { // convert `conversionAmt` into BigDecimal representation for its later usage in BigMoney - val conversionAmt = new BigDecimal(conversionAmount) - // convert `fromCurr` and `toCurr` in string representations to CurrencyUnit representations - val fromCurrencyUnit = convertToCurrencyUnit(fromCurr) + val conversionAmt = new BigDecimal(conversionAmount) + // convert `fromCurr` and `toCurr` in string representations to CurrencyUnit representations + val fromCurrencyUnit = convertToCurrencyUnit(fromCurr) val toCurrencyUnit = convertToCurrencyUnit(toCurr) /** * Performs live currency lookup/conversion, no caching available * @return Money representation in target currency or OerResponseError object if API request failed */ - def now: Either[OerResponseError, Money] = { + def now: Either[OerResponseError, Money] = { val timeStamp = DateTime.now - val from = fx.client.getLiveCurrencyValue(fromCurr) - val to = fx.client.getLiveCurrencyValue(toCurr) + val from = fx.client.getLiveCurrencyValue(fromCurr) + val to = fx.client.getLiveCurrencyValue(toCurr) if (from.isRight && to.isRight) { // API request succeeds - val baseOverFrom = from.right.get - val baseOverTo = to.right.get + val baseOverFrom = from.right.get + val baseOverTo = to.right.get val fromCurrIsBaseCurr = (fromCurr == fx.config.baseCurrency) - val rate = Forex.getForexRate(fromCurrIsBaseCurr, baseOverFrom, baseOverTo) - // Note that if `fromCurr` is not the same as the base currency, + val rate = Forex.getForexRate(fromCurrIsBaseCurr, baseOverFrom, baseOverTo) + // Note that if `fromCurr` is not the same as the base currency, // then we need to add the pair into the cache in particular, // because only were added earlier if (!fx.client.caches.nowish.isEmpty && fromCurr != fx.config.baseCurrency) { val Some(cache) = fx.client.caches.nowish - cache.put((fromCurr,toCurr), (timeStamp, rate)) + cache.put((fromCurr, toCurr), (timeStamp, rate)) } returnMoneyOrJodaError(rate) } else { - // API request fails + // API request fails returnApiError(from.left.get) } } - - + /** * Performs near-live currency lookup/conversion. - * A cached version of the live exchange rate is used - * if a cache exists and the timestamp of that exchange rate is less than or equal to "nowishSecs" old. + * A cached version of the live exchange rate is used + * if a cache exists and the timestamp of that exchange rate is less than or equal to "nowishSecs" old. * Otherwise a new lookup is performed. * @return Money representation in target currency or OerResponseError object if API request failed */ - def nowish: Either[OerResponseError, Money] = { + def nowish: Either[OerResponseError, Money] = fx.client.caches.nowish match { case Some(cache) => { - val nowishTime = DateTime.now.minusSeconds(fx.config.nowishSecs) - cache.get((fromCurr, toCurr)) match { - // from:to found in LRU cache - case Some(tpl) => { - val (timeStamp, exchangeRate) = tpl - if (nowishTime.isBefore(timeStamp) || nowishTime.equals(timeStamp)) { - // the timestamp in the cache is within the allowed range - returnMoneyOrJodaError(exchangeRate) - } else { - now - } + val nowishTime = DateTime.now.minusSeconds(fx.config.nowishSecs) + cache.get((fromCurr, toCurr)) match { + // from:to found in LRU cache + case Some(tpl) => { + val (timeStamp, exchangeRate) = tpl + if (nowishTime.isBefore(timeStamp) || nowishTime.equals(timeStamp)) { + // the timestamp in the cache is within the allowed range + returnMoneyOrJodaError(exchangeRate) + } else { + now } - // from:to not found in LRU - case None => { - cache.get((toCurr, fromCurr)) match { - // to:from found in LRU - case Some(tpl) => { - val (time, rate) = tpl - returnMoneyOrJodaError(new BigDecimal(1).divide(rate, Forex.commonScale, RoundingMode.HALF_EVEN)) - } - // Neither direction found in LRU - case None => { - now - } + } + // from:to not found in LRU + case None => { + cache.get((toCurr, fromCurr)) match { + // to:from found in LRU + case Some(tpl) => { + val (time, rate) = tpl + returnMoneyOrJodaError(new BigDecimal(1).divide(rate, Forex.commonScale, RoundingMode.HALF_EVEN)) + } + // Neither direction found in LRU + case None => { + now } } } } + } // If cache is disabled, nowish lookup will perform exactly the same as now() case None => now } - } /** * Gets the latest end-of-day rate prior to the event or post to the event @@ -252,55 +242,55 @@ case class ForexLookupWhen(conversionAmount: Double, fromCurr: String, toCurr: S } else { tradeDate.withTimeAtStartOfDay } - eod(latestEod) + eod(latestEod) } /** * Gets the end-of-day rate for the specified date * @return Money representation in target currency or OerResponseError object if API request failed */ - def eod(eodDate: DateTime): Either[OerResponseError, Money] = { + def eod(eodDate: DateTime): Either[OerResponseError, Money] = fx.client.caches.eod match { case Some(cache) => { cache.get((fromCurr, toCurr, eodDate)) match { - // from->to is found in the cache - case Some(rate) => + // from->to is found in the cache + case Some(rate) => returnMoneyOrJodaError(rate) // from->to not found in the cache - case None => - cache.get((toCurr, fromCurr, eodDate)) match { + case None => + cache.get((toCurr, fromCurr, eodDate)) match { // to->from found in the cache - case Some(exchangeRate) => - returnMoneyOrJodaError(new BigDecimal(1).divide(exchangeRate, Forex.commonScale, RoundingMode.HALF_EVEN)) + case Some(exchangeRate) => + returnMoneyOrJodaError( + new BigDecimal(1).divide(exchangeRate, Forex.commonScale, RoundingMode.HALF_EVEN)) // neither from->to nor to->from found in the cache case None => - getHistoricalRate(eodDate) + getHistoricalRate(eodDate) } } } - case None => getHistoricalRate(eodDate) + case None => getHistoricalRate(eodDate) } - } /** * Helper method to get the historical forex rate between two currencies on a given date, - * @return Money in target currency representation or error message if the date given is invalid + * @return Money in target currency representation or error message if the date given is invalid */ - private def getHistoricalRate(date: DateTime): Either[OerResponseError, Money] = { + private def getHistoricalRate(date: DateTime): Either[OerResponseError, Money] = { val from = fx.client.getHistoricalCurrencyValue(fromCurr, date) val to = fx.client.getHistoricalCurrencyValue(toCurr, date) if (from.isRight && to.isRight) { // API request succeeds - val baseOverFrom = from.right.get - val baseOverTo = to.right.get + val baseOverFrom = from.right.get + val baseOverTo = to.right.get val fromCurrIsBaseCurr = (fromCurr == fx.config.baseCurrency) - val rate = Forex.getForexRate(fromCurrIsBaseCurr, baseOverFrom, baseOverTo) - // Note that if `fromCurr` is not the same as the base currency, + val rate = Forex.getForexRate(fromCurrIsBaseCurr, baseOverFrom, baseOverTo) + // Note that if `fromCurr` is not the same as the base currency, // then we need to add the pair into the cache in particular, // because only were added earlier if (!fx.client.caches.eod.isEmpty && fromCurr != fx.config.baseCurrency) { val Some(cache) = fx.client.caches.eod - cache.put((fromCurr,toCurr, date), rate) + cache.put((fromCurr, toCurr, date), rate) } returnMoneyOrJodaError(rate) } else { @@ -309,30 +299,28 @@ case class ForexLookupWhen(conversionAmount: Double, fromCurr: String, toCurr: S } } - /** - * Helper method to convert a currency type in String representation to CurrencyUnit representation + * Helper method to convert a currency type in String representation to CurrencyUnit representation * @param currencyInStringRepresentation - the currency to be converted * @return CurrencyUnit representation of the currency, or OerResponseError if Joda money does not support the currency */ - private def convertToCurrencyUnit(currencyInStringRepresentation: String) : Either[OerResponseError, CurrencyUnit] = { + private def convertToCurrencyUnit(currencyInStringRepresentation: String): Either[OerResponseError, CurrencyUnit] = try { Right(CurrencyUnit.getInstance(currencyInStringRepresentation)) } catch { - case (e: IllegalCurrencyException) => + case (e: IllegalCurrencyException) => val errMessage = "Currency [" + fromCurr + "] is not supported by Joda money " Left(OerResponseError(errMessage, IllegalCurrency)) } - } /** * This method is called when the forex rate has been found in the API * @param rate - The forex rate between source and target currency * @return Money representation in target currency if both currencies are supported by Joda Money - * or OerResponseError with an error message containing the forex rate between the two currencies + * or OerResponseError with an error message containing the forex rate between the two currencies * if either of the currency is not supported by Joda Money */ - private def returnMoneyOrJodaError(rate: BigDecimal): Either[OerResponseError, Money] = { + private def returnMoneyOrJodaError(rate: BigDecimal): Either[OerResponseError, Money] = if (fromCurrencyUnit.isRight && toCurrencyUnit.isRight) { // Money in a given amount val moneyInSourceCurrency = BigMoney.of(fromCurrencyUnit.right.get, conversionAmt) @@ -343,8 +331,8 @@ case class ForexLookupWhen(conversionAmount: Double, fromCurr: String, toCurr: S Right(moneyInSourceCurrency.convertedTo(toCurrencyUnit.right.get, rate).toMoney(RoundingMode.HALF_EVEN)) } } else { - var errMessage = "The exchange rate of [" + fromCurr + "]:["+ toCurr + "] " + - "is " + rate + ". However, " + var errMessage = "The exchange rate of [" + fromCurr + "]:[" + toCurr + "] " + + "is " + rate + ". However, " if (fromCurrencyUnit.isLeft) { errMessage += fromCurrencyUnit.left.get.errorMessage } @@ -352,20 +340,19 @@ case class ForexLookupWhen(conversionAmount: Double, fromCurr: String, toCurr: S errMessage += toCurrencyUnit.left.get.errorMessage } Left(OerResponseError(errMessage, IllegalCurrency)) - } - } + } /** * This method is called if API requests fail * @param errObject - The OerResponseError object which contains the failure information returned from API - * @return OerResponseError object which states the failure information + * @return OerResponseError object which states the failure information * and also illegal currency info if there is any illegal currency */ - private def returnApiError(errObject : OerResponseError): Either[OerResponseError, Money] = { - var errMsg = "" + private def returnApiError(errObject: OerResponseError): Either[OerResponseError, Money] = { + var errMsg = "" if (fromCurrencyUnit.isLeft) { errMsg += fromCurrencyUnit.left.get.errorMessage - } + } if (toCurrencyUnit.isLeft) { errMsg += toCurrencyUnit.left.get.errorMessage } diff --git a/src/main/scala/com.snowplowanalytics/forex/ForexClient.scala b/src/main/scala/com.snowplowanalytics/forex/ForexClient.scala index 22ed14b..54550e2 100644 --- a/src/main/scala/com.snowplowanalytics/forex/ForexClient.scala +++ b/src/main/scala/com.snowplowanalytics/forex/ForexClient.scala @@ -23,32 +23,32 @@ import com.twitter.util.SynchronizedLruMap /** * 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 + * 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 { + /** * Getter for clients with specified caches(optional) */ - def getClient(config: ForexConfig, clientConfig: ForexClientConfig, - nowish: MaybeNowishCache= None, eod: MaybeEodCache = None): ForexClient = { + def getClient(config: ForexConfig, + clientConfig: ForexClientConfig, + nowish: MaybeNowishCache = None, + eod: MaybeEodCache = None): ForexClient = clientConfig match { - case oerClientConfig: OerClientConfig => new OerClient(config, oerClientConfig, nowishCache = nowish, eodCache = eod) + case oerClientConfig: OerClientConfig => + new OerClient(config, oerClientConfig, nowishCache = nowish, eodCache = eod) case _ => throw NoSuchClientException("This client is not supported by scala-forex currently") } - } } -abstract class ForexClient( - config: ForexConfig, - nowishCache: MaybeNowishCache = None, - eodCache: MaybeEodCache = None) { +abstract class ForexClient(config: ForexConfig, nowishCache: MaybeNowishCache = None, eodCache: MaybeEodCache = None) { // Assemble our caches object caches { // LRU cache for nowish request, with (source currency, target currency) as the key - // and (date time, exchange rate) as the value + // and (date time, exchange rate) as the value val nowish = if (nowishCache.isDefined) { nowishCache @@ -57,36 +57,36 @@ abstract class ForexClient( } else { None } - - // LRU cache for historical request, with (source currency, target currency, date time) as the key + + // LRU cache for historical request, with (source currency, target currency, date time) as the key // and exchange rate as the value - val eod = + val eod = if (eodCache.isDefined) { - eodCache + eodCache } else if (config.eodCacheSize > 0) { Some(new SynchronizedLruMap[EodCacheKey, EodCacheValue](config.eodCacheSize)) } else { None } } - + /** * Get the latest exchange rate from a given currency - * + * * @param currency * Desired currency * @return result returned from API */ - def getLiveCurrencyValue(currency: String): ApiRequestResult + def getLiveCurrencyValue(currency: String): 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: String, date: DateTime): ApiRequestResult + def getHistoricalCurrencyValue(currency: String, date: DateTime): ApiRequestResult } diff --git a/src/main/scala/com.snowplowanalytics/forex/ForexClientConfig.scala b/src/main/scala/com.snowplowanalytics/forex/ForexClientConfig.scala index b8bd5bc..8a769e2 100644 --- a/src/main/scala/com.snowplowanalytics/forex/ForexClientConfig.scala +++ b/src/main/scala/com.snowplowanalytics/forex/ForexClientConfig.scala @@ -14,6 +14,6 @@ package com.snowplowanalytics.forex /** * Configurator for Forex clients, - * every Forex client configurator is a sub-class of this class + * every Forex client configurator is a sub-class of this class */ trait ForexClientConfig diff --git a/src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala b/src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala index 25baf59..1dc0a9e 100644 --- a/src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala +++ b/src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala @@ -14,8 +14,10 @@ package com.snowplowanalytics.forex /** 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 @@ -23,24 +25,24 @@ object EodRoundUp extends EodRounding * Configure class for Forex object * * @param appId - Key for the api - * @param configurableBase - Flag for showing if the base currency is configurable + * @param configurableBase - Flag for showing if the base currency is configurable * @param nowishCacheSize - Cache for nowish look up * @param nowishSecs - Time range for nowish look up * @param historicalCacheSize - 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 + * @param baseCurrency - Base currency is set to be USD by default if configurableBase flag is false, otherwise it is user-defined */ case class ForexConfig( - /** + /** * 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, + nowishCacheSize: Int = 13530, /** 5 mins by default */ - nowishSecs: Int = 300, + nowishSecs: Int = 300, /** 165 * 164 / 2 * 30 = 405900, assuming the cache stores data within a month */ eodCacheSize: Int = 405900, getNearestDay: EodRounding = EodRoundDown, - baseCurrency: String = "USD" -) + baseCurrency: String = "USD" +) diff --git a/src/main/scala/com.snowplowanalytics/forex/NoSuchClientException.scala b/src/main/scala/com.snowplowanalytics/forex/NoSuchClientException.scala index 4b72298..3624249 100644 --- a/src/main/scala/com.snowplowanalytics/forex/NoSuchClientException.scala +++ b/src/main/scala/com.snowplowanalytics/forex/NoSuchClientException.scala @@ -16,7 +16,7 @@ package com.snowplowanalytics.forex import java.lang.Throwable /** - * NoSuchClientException is thrown when the forex client + * NoSuchClientException is thrown when the forex client * provided by the user is not available to scala-forex yet. */ -case class NoSuchClientException(errorMessage: String) extends Throwable \ No newline at end of file +case class NoSuchClientException(errorMessage: String) extends Throwable diff --git a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClient.scala b/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClient.scala index 270d8f4..aeb55b8 100644 --- a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClient.scala +++ b/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClient.scala @@ -34,8 +34,8 @@ class OerClient( config: ForexConfig, oerConfig: OerClientConfig, nowishCache: MaybeNowishCache = None, - eodCache: MaybeEodCache = None - ) extends ForexClient(config, nowishCache, eodCache) { + eodCache: MaybeEodCache = None +) extends ForexClient(config, nowishCache, eodCache) { /** Base URL to OER API */ private val oerUrl = "http://openexchangerates.org/api/" @@ -44,10 +44,10 @@ class OerClient( * according to the API, only Unlimited and Enterprise accounts * are allowed to set the base currency in the HTTP URL */ - private val base = oerConfig.accountLevel match { - case UnlimitedAccount => "&base=" + config.baseCurrency - case EnterpriseAccount => "&base=" + config.baseCurrency - case DeveloperAccount => "" + private val base = oerConfig.accountLevel match { + case UnlimitedAccount => "&base=" + config.baseCurrency + case EnterpriseAccount => "&base=" + config.baseCurrency + case DeveloperAccount => "" } /** @@ -66,7 +66,7 @@ class OerClient( private val mapper = new ObjectMapper() /** The earliest date OER service is availble */ - private val oerDataFrom = new DateTime(1999,1,1,0,0) + private val oerDataFrom = new DateTime(1999, 1, 1, 0, 0) /** * Gets live currency value for the desired currency, @@ -76,46 +76,43 @@ class OerClient( * @param currency - The desired currency we want to look up from the API * @return result returned from API */ - def getLiveCurrencyValue(currency: String): ApiRequestResult = { - + def getLiveCurrencyValue(currency: String): ApiRequestResult = getJsonNodeFromApi(latest) match { case Left(oerResponseError) => Left(oerResponseError) - case Right(node) => { + case Right(node) => { nowishCache match { case Some(cache) => { - val currencyNameIterator = node.getFieldNames - oerConfig.accountLevel match { - // If the user is using Developer account, - // then base currency returned from the API is USD. - // To store user-defined base currency into the cache, - // we need to convert the forex rate between target currency and USD - // to target currency and user-defined base currency - case DeveloperAccount - => { - val usdOverBase = node.findValue(config.baseCurrency).getDecimalValue - while (currencyNameIterator.hasNext) { - val currencyName = currencyNameIterator.next - val keyPair = (config.baseCurrency, currencyName) - val usdOverCurr = node.findValue(currencyName).getDecimalValue - // flag indicating if the base currency has been set to USD - val fromCurrIsBaseCurr = (config.baseCurrency == "USD") - val baseOverCurr = Forex.getForexRate(fromCurrIsBaseCurr, usdOverBase, usdOverCurr) - val valPair = (DateTime.now, baseOverCurr) - cache.put(keyPair, valPair) - } - } - // For Enterprise and Unlimited users, OER allows them to configure the base currencies. - // So the exchange rate returned from the API is between target currency and the base currency they defined. - case _ - => { - while (currencyNameIterator.hasNext) { - val currencyName = currencyNameIterator.next - val keyPair = (config.baseCurrency, currencyName) - val valPair = (DateTime.now, node.findValue(currencyName).getDecimalValue) - cache.put(keyPair, valPair) - } - } + val currencyNameIterator = node.getFieldNames + oerConfig.accountLevel match { + // If the user is using Developer account, + // then base currency returned from the API is USD. + // To store user-defined base currency into the cache, + // we need to convert the forex rate between target currency and USD + // to target currency and user-defined base currency + case DeveloperAccount => { + val usdOverBase = node.findValue(config.baseCurrency).getDecimalValue + while (currencyNameIterator.hasNext) { + val currencyName = currencyNameIterator.next + val keyPair = (config.baseCurrency, currencyName) + val usdOverCurr = node.findValue(currencyName).getDecimalValue + // flag indicating if the base currency has been set to USD + val fromCurrIsBaseCurr = (config.baseCurrency == "USD") + val baseOverCurr = Forex.getForexRate(fromCurrIsBaseCurr, usdOverBase, usdOverCurr) + val valPair = (DateTime.now, baseOverCurr) + cache.put(keyPair, valPair) } + } + // For Enterprise and Unlimited users, OER allows them to configure the base currencies. + // So the exchange rate returned from the API is between target currency and the base currency they defined. + case _ => { + while (currencyNameIterator.hasNext) { + val currencyName = currencyNameIterator.next + val keyPair = (config.baseCurrency, currencyName) + val valPair = (DateTime.now, node.findValue(currencyName).getDecimalValue) + cache.put(keyPair, valPair) + } + } + } } case None => // do nothing } @@ -127,7 +124,6 @@ class OerClient( } } } - } /** * Builds the historical link for the URI according to the date @@ -135,13 +131,14 @@ class OerClient( * which should be the same as date argument in the getHistoricalCurrencyValue method below * @return the link in string format */ - private def buildHistoricalLink(date: DateTime) : String = { + private def buildHistoricalLink(date: DateTime): String = { val dateCal = date.toGregorianCalendar val day = dateCal.get(Calendar.DAY_OF_MONTH) val month = dateCal.get(Calendar.MONTH) + 1 val year = dateCal.get(Calendar.YEAR) historical.format(year, month, day) } + /** * Gets historical forex rate for the given currency and date * return error message if the date is invalid @@ -152,51 +149,48 @@ class OerClient( * @param date - The specific date we want to look up on * @return result returned from API */ - def getHistoricalCurrencyValue(currency: String, date: DateTime): ApiRequestResult = { - - /** - * Return OerResponseError if the date given is not supported by OER - */ + def getHistoricalCurrencyValue(currency: String, date: DateTime): ApiRequestResult = + /** + * Return OerResponseError if the date given is not supported by OER + */ if (date.isBefore(oerDataFrom) || date.isAfter(DateTime.now)) { Left(OerResponseError("Exchange rate unavailable on the date [%s] ".format(date), ResourcesNotAvailable)) } else { val historicalLink = buildHistoricalLink(date) getJsonNodeFromApi(historicalLink) match { case Left(oerResponseError) => Left(oerResponseError) - case Right(node) => { + case Right(node) => { eodCache match { - case Some(cache) => { - val currencyNameIterator = node.getFieldNames - oerConfig.accountLevel match { - // If the user is using Developer account, - // then base currency returned from the API is USD. - // To store user-defined base currency into the cache, - // we need to convert the forex rate between target currency and USD - // to target currency and user-defined base currency - case DeveloperAccount - => { + case Some(cache) => { + val currencyNameIterator = node.getFieldNames + oerConfig.accountLevel match { + // If the user is using Developer account, + // then base currency returned from the API is USD. + // To store user-defined base currency into the cache, + // we need to convert the forex rate between target currency and USD + // to target currency and user-defined base currency + case DeveloperAccount => { val usdOverBase = node.findValue(config.baseCurrency).getDecimalValue while (currencyNameIterator.hasNext) { - val currencyName = currencyNameIterator.next - val keyPair = (config.baseCurrency, currencyName, date) - val usdOverCurr = node.findValue(currencyName).getDecimalValue + val currencyName = currencyNameIterator.next + val keyPair = (config.baseCurrency, currencyName, date) + val usdOverCurr = node.findValue(currencyName).getDecimalValue val fromCurrIsBaseCurr = (config.baseCurrency == "USD") - cache.put(keyPair, Forex.getForexRate(fromCurrIsBaseCurr, usdOverBase, usdOverCurr) ) + cache.put(keyPair, Forex.getForexRate(fromCurrIsBaseCurr, usdOverBase, usdOverCurr)) } } - // For Enterprise and Unlimited users, OER allows them to configure the base currencies. - // So the exchange rate returned from the API is between target currency and the base currency they defined. - case _ - => { + // For Enterprise and Unlimited users, OER allows them to configure the base currencies. + // So the exchange rate returned from the API is between target currency and the base currency they defined. + case _ => { while (currencyNameIterator.hasNext) { val currencyName = currencyNameIterator.next - val keyPair = (config.baseCurrency, currencyName, date) + val keyPair = (config.baseCurrency, currencyName, date) cache.put(keyPair, node.findValue(currencyName).getDecimalValue) } } + } } - } - case None => // do nothing + case None => // do nothing } val currencyNode = node.findValue(currency) if (currencyNode == null) { @@ -207,7 +201,6 @@ class OerClient( } } } - } /** * Helper method which returns the node containing @@ -223,16 +216,16 @@ class OerClient( case (httpUrlConn: HttpURLConnection) => { if (httpUrlConn.getResponseCode >= 400) { val errorStream = httpUrlConn.getErrorStream - val root = mapper.readTree(errorStream).getElements - var resNode = root.next + val root = mapper.readTree(errorStream).getElements + var resNode = root.next while (root.hasNext) { - resNode = root.next + resNode = root.next } Left(OerResponseError(resNode.getTextValue, OtherErrors)) - } else { + } else { val inputStream = httpUrlConn.getInputStream - val root = mapper.readTree(inputStream).getElements - var resNode = root.next + val root = mapper.readTree(inputStream).getElements + var resNode = root.next while (root.hasNext) { resNode = root.next } diff --git a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClientConfig.scala b/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClientConfig.scala index 5b14dd9..6b6e70d 100644 --- a/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClientConfig.scala +++ b/src/main/scala/com.snowplowanalytics/forex/oerclient/OerClientConfig.scala @@ -16,25 +16,22 @@ package oerclient /** OER-specific configuration */ case class OerClientConfig( /** - * Register an account on https://openexchangerates.org to obtain your unique key - */ - appId: String, - accountLevel: AccountType // Account type of the user + * Register an account on https://openexchangerates.org to obtain your unique key + */ + appId: String, + accountLevel: AccountType // Account type of the user ) extends ForexClientConfig /** * 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 + * 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 - - diff --git a/src/main/scala/com.snowplowanalytics/package.scala b/src/main/scala/com.snowplowanalytics/package.scala index e9c53cd..db2c5a6 100644 --- a/src/main/scala/com.snowplowanalytics/package.scala +++ b/src/main/scala/com.snowplowanalytics/package.scala @@ -14,7 +14,7 @@ package com.snowplowanalytics // Java import java.math.BigDecimal -// Joda +// Joda import org.joda.time._ // LRUCache import com.twitter.util.SynchronizedLruMap @@ -22,25 +22,26 @@ import com.twitter.util.SynchronizedLruMap import forex.oerclient.OerResponseError package object forex { + /** * The key and value for each cache entry */ - type NowishCacheKey = Tuple2[String, String] // source currency , target currency - type NowishCacheValue = Tuple2[DateTime, BigDecimal] // timestamp, exchange rate + type NowishCacheKey = Tuple2[String, String] // source currency , target currency + type NowishCacheValue = Tuple2[DateTime, BigDecimal] // timestamp, exchange rate + + type EodCacheKey = Tuple3[String, String, DateTime] // source currency, target currency, timestamp + type EodCacheValue = BigDecimal // exchange rate - type EodCacheKey = Tuple3[String, String, DateTime] // source currency, target currency, timestamp - type EodCacheValue = BigDecimal // exchange rate - // The API request either returns exchange rates in BigDecimal representation // or OerResponseError if the request failed - type ApiRequestResult = Either[OerResponseError, BigDecimal] - + type ApiRequestResult = Either[OerResponseError, BigDecimal] + /** * The two LRU caches we use */ - type NowishCache = SynchronizedLruMap[NowishCacheKey, NowishCacheValue] - type EodCache = SynchronizedLruMap[EodCacheKey, EodCacheValue] + type NowishCache = SynchronizedLruMap[NowishCacheKey, NowishCacheValue] + type EodCache = SynchronizedLruMap[EodCacheKey, EodCacheValue] - type MaybeNowishCache = Option[NowishCache] - type MaybeEodCache = Option[EodCache] + type MaybeNowishCache = Option[NowishCache] + type MaybeEodCache = Option[EodCache] } diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala index 7b01c6f..f6c19be 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexAtSpec.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -14,28 +14,29 @@ package com.snowplowanalytics.forex // Specs2 import org.specs2.mutable.Specification -// Joda +// Joda import org.joda.time._ // TestHelpers import TestHelpers._ /** - * Testing method for getting the latest end-of-day rate - * prior to the datetime or the day after according to the user's setting + * Testing method for getting the latest end-of-day rate + * prior to the datetime or the day after according to the user's setting */ -class ForexAtSpec extends Specification { +class ForexAtSpec extends Specification { + /** - * GBP->CAD with USD as baseCurrency + * GBP->CAD with USD as baseCurrency */ val tradeDate = new DateTime(2011, 3, 13, 11, 39, 27, 567, DateTimeZone.forID("America/New_York")) - + val gbpToCadWithBaseUsd = fx.rate("GBP").to("CAD").at(tradeDate) val cadMoney = gbpToCadWithBaseUsd.right.get "GBP to CAD with USD as base currency returning latest eod rate [%s]".format(cadMoney) should { "be > 0" in { - cadMoney.isPositive + cadMoney.isPositive } } @@ -43,12 +44,12 @@ class ForexAtSpec extends Specification { * GBP-> CAD with GBP as base currency */ val gbpToCadWithBaseGbp = fxWithBaseGBP.rate.to("CAD").at(tradeDate) - + val cadMoneyWithBaseGbp = gbpToCadWithBaseGbp.right.get "GBP to CAD with GBP as base currency returning latest eod rate [%s]".format(cadMoneyWithBaseGbp) should { "be > 0" in { - cadMoneyWithBaseGbp.isPositive + cadMoneyWithBaseGbp.isPositive } } @@ -64,5 +65,5 @@ class ForexAtSpec extends Specification { cnyTogbpmoney.isPositive } } - + } diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala index 3bd4ddd..3da2713 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexEodSpec.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -15,30 +15,29 @@ package com.snowplowanalytics.forex // Specs2 import org.specs2.mutable.Specification import org.specs2.matcher.DataTables -// Joda +// Joda import org.joda.time._ // TestHelpers import TestHelpers._ /** -* Testing method for getting the end-of-date exchange rate -* since historical forex rate is fixed, the actual look up result should be -* the same as the value in the table -*/ -class ForexEodSpec extends Specification with DataTables { + * Testing method for getting the end-of-date exchange rate + * since historical forex rate is fixed, the actual look up result should be + * the same as the value in the table + */ +class ForexEodSpec extends Specification with DataTables { - override def is = + override def is = "end-of-date lookup tests: forex rate between two currencies for a specific date is always the same" ! e1 - + // Table values obtained from OER API - def e1 = - "SOURCE CURRENCY" || "TARGET CURRENCY" | "DATE" | "EXPECTED OUTPUT" | - "USD" !! "GBP" ! "2011-03-13" ! "0.62" | - "USD" !! "AED" ! "2011-03-13" ! "3.67" | - "USD" !! "CAD" ! "2011-03-13" ! "0.98" | - "GBP" !! "USD" ! "2011-03-13" ! "1.60" | - "GBP" !! "SGD" ! "2008-03-13" ! "2.80" |> { - (fromCurr, toCurr, date, exp) => - fx.rate(fromCurr).to(toCurr).eod(DateTime.parse(date)).right.get.getAmount.toString must_== exp + def e1 = + "SOURCE CURRENCY" || "TARGET CURRENCY" | "DATE" | "EXPECTED OUTPUT" | + "USD" !! "GBP" ! "2011-03-13" ! "0.62" | + "USD" !! "AED" ! "2011-03-13" ! "3.67" | + "USD" !! "CAD" ! "2011-03-13" ! "0.98" | + "GBP" !! "USD" ! "2011-03-13" ! "1.60" | + "GBP" !! "SGD" ! "2008-03-13" ! "2.80" |> { (fromCurr, toCurr, date, exp) => + fx.rate(fromCurr).to(toCurr).eod(DateTime.parse(date)).right.get.getAmount.toString must_== exp } } diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala index f439fd3..9de5871 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexNowSpec.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2016 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -16,50 +16,51 @@ package com.snowplowanalytics.forex import java.math.RoundingMode // Specs2 import org.specs2.mutable.Specification -// Joda +// Joda import org.joda.money._ // TestHelpers import TestHelpers._ /** -* Testing method for getting the live exchange rate -*/ -class ForexNowSpec extends Specification { + * Testing method for getting the live exchange rate + */ +class ForexNowSpec extends Specification { + /** - * Trade 10000 USD to JPY at live exchange rate - */ + * Trade 10000 USD to JPY at live exchange rate + */ val tradeInYenNow = fx.convert(10000).to(CurrencyUnit.JPY).now val jpyMoneyWithBaseUsd = tradeInYenNow.right.get - + "convert 10000 USD dollars to Yen now = [%s]".format(jpyMoneyWithBaseUsd) should { - "be > 10000" in { + "be > 10000" in { jpyMoneyWithBaseUsd.isGreaterThan(Money.of(CurrencyUnit.JPY, 10000, RoundingMode.HALF_EVEN)) - } + } } /** - * GBP -> SGD with USD as base currency - */ + * GBP -> SGD with USD as base currency + */ val gbpToSgdWithBaseUsd = fx.rate(CurrencyUnit.GBP).to("SGD").now - val sgdMoneyWithBaseUsd = gbpToSgdWithBaseUsd.right.get - + val sgdMoneyWithBaseUsd = gbpToSgdWithBaseUsd.right.get + "GBP to SGD with base currency USD live exchange rate [%s]".format(sgdMoneyWithBaseUsd) should { - "be greater than 1 SGD" in { + "be greater than 1 SGD" in { sgdMoneyWithBaseUsd.isGreaterThan(Money.of(CurrencyUnit.getInstance("SGD"), 1)) } - } - + } + /** - * GBP -> SGD with GBP as base currency - */ + * GBP -> SGD with GBP as base currency + */ val gbpToSgdWithBaseGbp = fxWithBaseGBP.rate.to("SGD").now - - val sgdMoneyWithBaseGbp = gbpToSgdWithBaseUsd.right.get - + + val sgdMoneyWithBaseGbp = gbpToSgdWithBaseUsd.right.get + "GBP to SGD with base currency GBP live exchange rate [%s]".format(sgdMoneyWithBaseGbp) should { - "be greater than 1 SGD" in { + "be greater than 1 SGD" in { sgdMoneyWithBaseGbp.isGreaterThan(Money.of(CurrencyUnit.getInstance("SGD"), 1)) } } diff --git a/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala b/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala index 64a575c..b915e33 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexNowishSpec.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -16,51 +16,52 @@ package com.snowplowanalytics.forex import java.math.RoundingMode // Specs2 import org.specs2.mutable.Specification -// Joda +// Joda import org.joda.money._ // TestHelpers import TestHelpers._ /** -* Testing method for getting the approximate exchange rate -*/ -class ForexNowishSpec extends Specification { + * Testing method for getting the approximate exchange rate + */ +class ForexNowishSpec extends Specification { + /** - * CAD -> GBP with base currency USD - */ + * CAD -> GBP with base currency USD + */ val cadOverGbpNowish = fx.rate("CAD").to(CurrencyUnit.GBP).nowish - + val gbpmoney = cadOverGbpNowish.right.get - + "CAD to GBP with USD as base currency returning near-live rate [%s]".format(gbpmoney) should { - "be smaller than 1 pound" in { - gbpmoney.isLessThan(Money.of(CurrencyUnit.GBP, 1)) - } + "be smaller than 1 pound" in { + gbpmoney.isLessThan(Money.of(CurrencyUnit.GBP, 1)) + } } /** - * GBP -> JPY with base currency USD - */ + * GBP -> JPY with base currency USD + */ val gbpToJpyWithBaseUsd = fx.rate(CurrencyUnit.GBP).to(CurrencyUnit.getInstance("JPY")).nowish - val jpyMoneyWithBaseUsd = gbpToJpyWithBaseUsd.right.get - + val jpyMoneyWithBaseUsd = gbpToJpyWithBaseUsd.right.get + "GBP to JPY with USD as base currency returning near-live rate [%s]".format(jpyMoneyWithBaseUsd) should { - "be greater than 1 Yen" in { + "be greater than 1 Yen" in { jpyMoneyWithBaseUsd.isGreaterThan(BigMoney.of(CurrencyUnit.getInstance("JPY"), 1).toMoney(RoundingMode.HALF_EVEN)) } - } + } /** - * GBP -> JPY with base currency GBP - */ + * GBP -> JPY with base currency GBP + */ val gbpToJpyWithBaseGbp = fxWithBaseGBP.rate.to(CurrencyUnit.getInstance("JPY")).nowish - - val jpyMoneyWithBaseGbp = gbpToJpyWithBaseUsd.right.get - + + val jpyMoneyWithBaseGbp = gbpToJpyWithBaseUsd.right.get + "GBP to JPY with GBP as base currency returning near-live rate [%s]".format(jpyMoneyWithBaseGbp) should { - "be greater than 1 Yen" in { + "be greater than 1 Yen" in { jpyMoneyWithBaseGbp.isGreaterThan(BigMoney.of(CurrencyUnit.getInstance("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 eaa6778..edebba9 100644 --- a/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/ForexWithoutCachesSpec.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -18,16 +18,15 @@ import org.specs2.mutable.Specification import TestHelpers._ /** - * Testing that setting cache size to zero will disable the use of cache + * Testing that setting cache size to zero will disable the use of cache */ -class ForexWithoutCachesSpec extends Specification { +class ForexWithoutCachesSpec extends Specification { "Setting both cache sizes to zero" should { "disable the use of caches" in { - val fxWithoutCache = Forex.getForex(ForexConfig(nowishCacheSize = 0, eodCacheSize = 0), oerConfig) + val fxWithoutCache = Forex.getForex(ForexConfig(nowishCacheSize = 0, eodCacheSize = 0), oerConfig) fxWithoutCache.client.caches.eod.isEmpty fxWithoutCache.client.caches.nowish.isEmpty } } - -} \ No newline at end of file +} diff --git a/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala b/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala index 2a57287..d597c1c 100644 --- a/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/SpiedCacheSpec.scala @@ -16,7 +16,7 @@ package com.snowplowanalytics.forex import org.specs2.mutable.Specification // Mockito import org.specs2.mock.Mockito -// Joda +// Joda import org.joda.time._ // LRUCache import com.twitter.util.SynchronizedLruMap @@ -29,11 +29,12 @@ import TestHelpers._ /** * Testing cache behaviours */ -class SpiedCacheSpec extends Specification with Mockito{ - val spiedNowishCache = spy(new SynchronizedLruMap[NowishCacheKey, NowishCacheValue](config.nowishCacheSize)) +class SpiedCacheSpec extends Specification with Mockito { + val spiedNowishCache = spy(new SynchronizedLruMap[NowishCacheKey, NowishCacheValue](config.nowishCacheSize)) val spiedEodCache = spy(new SynchronizedLruMap[EodCacheKey, EodCacheValue](config.eodCacheSize)) - val spiedFx = Forex.getForex(config, oerConfig, Some(spiedNowishCache), Some(spiedEodCache)) - val spiedFxWith5NowishSecs = Forex.getForex(fxConfigWith5NowishSecs, oerConfig, Some(spiedNowishCache), Some(spiedEodCache)) + val spiedFx = Forex.getForex(config, oerConfig, Some(spiedNowishCache), Some(spiedEodCache)) + val spiedFxWith5NowishSecs = + Forex.getForex(fxConfigWith5NowishSecs, oerConfig, Some(spiedNowishCache), Some(spiedEodCache)) /** * nowish cache with 5-sec memory @@ -55,32 +56,29 @@ class SpiedCacheSpec extends Specification with Mockito{ // value will be different from previous value - // even if the monetary value is the same, the // timestamp will be different - "A second time lookup of CAD->GBP after the memory time" + + "A second time lookup of CAD->GBP after the memory time" + "should overwrite the value in the nowish cache" in { spiedNowishCache must haveValue(valueFromFirstHttpRequest).not } } - } - + } /** * CAD -> GBP with base currency USD on 13-03-2011 * The eod lookup will call get method on eod cache - * after the call, the key will be stored in the cache + * after the call, the key will be stored in the cache */ val date = new DateTime(2011, 3, 13, 0, 0) - + spiedFx.rate("CAD").to("GBP").eod(date) "Eod query on CAD->GBP" should { "call get method on eod cache" in { - there was one(spiedEodCache).get(("CAD", "GBP", date)) + there was one(spiedEodCache).get(("CAD", "GBP", date)) "Eod cache have (CAD, GBP) entry after the query" in { - spiedEodCache must haveKey(("CAD", "GBP", date)) + spiedEodCache must haveKey(("CAD", "GBP", date)) } } } - - } diff --git a/src/test/scala/com.snowplowanalytics.forex/TestHelpers.scala b/src/test/scala/com.snowplowanalytics.forex/TestHelpers.scala index d8a057a..99fa603 100644 --- a/src/test/scala/com.snowplowanalytics.forex/TestHelpers.scala +++ b/src/test/scala/com.snowplowanalytics.forex/TestHelpers.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -20,14 +20,14 @@ import com.twitter.util.LruMap /** * All tests can have access to the same Forex object */ -object TestHelpers{ - val key = sys.env("OER_KEY") // Warning: this will give nasty errors if env var not exported - val config = ForexConfig() // ForexConfig object with default values - val fxConfigWith5NowishSecs = ForexConfig(nowishSecs = 5) // ForexConfig object with 5 nowishSecs - val oerConfig = OerClientConfig(key, DeveloperAccount) // with default base currency USD - val fx = Forex.getForex(config, oerConfig) // Forex object with USD as base currency - val forexConfig = ForexConfig(nowishCacheSize = 0, eodCacheSize = 0) - val fxWithoutCache = Forex.getForex(forexConfig, oerConfig) // Forex object with caches disabled - val confWithBaseGBP = OerClientConfig(key, EnterpriseAccount) // set base currency to GBP - val fxWithBaseGBP = Forex.getForex(ForexConfig(baseCurrency = "GBP"), confWithBaseGBP) // Forex object with GBP as base currency +object TestHelpers { + val key = sys.env("OER_KEY") // Warning: this will give nasty errors if env var not exported + val config = ForexConfig() // ForexConfig object with default values + val fxConfigWith5NowishSecs = ForexConfig(nowishSecs = 5) // ForexConfig object with 5 nowishSecs + val oerConfig = OerClientConfig(key, DeveloperAccount) // with default base currency USD + val fx = Forex.getForex(config, oerConfig) // Forex object with USD as base currency + val forexConfig = ForexConfig(nowishCacheSize = 0, eodCacheSize = 0) + val fxWithoutCache = Forex.getForex(forexConfig, oerConfig) // Forex object with caches disabled + val confWithBaseGBP = OerClientConfig(key, EnterpriseAccount) // set base currency to GBP + val fxWithBaseGBP = Forex.getForex(ForexConfig(baseCurrency = "GBP"), confWithBaseGBP) // Forex object with GBP as base currency } diff --git a/src/test/scala/com.snowplowanalytics.forex/UnsupportedCurrencySpec.scala b/src/test/scala/com.snowplowanalytics.forex/UnsupportedCurrencySpec.scala index ae2cf13..f922e62 100644 --- a/src/test/scala/com.snowplowanalytics.forex/UnsupportedCurrencySpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/UnsupportedCurrencySpec.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -22,10 +22,10 @@ import oerclient.OerResponseError // Joda money import org.joda.money.Money -/** +/** * Testing for unsupported currencies in joda money, e.g. bitcoin(BTC) */ -class UnsupportedCurrencySpec extends Specification { +class UnsupportedCurrencySpec extends Specification { "Joda money" should { Seq("BTC", "RRU", "EEK") foreach { currency => (" not support currency: " + currency) >> { @@ -33,8 +33,8 @@ class UnsupportedCurrencySpec extends Specification { rate must beLike { case Left(OerResponseError(_, IllegalCurrency)) => ok } - } + } } } - + } diff --git a/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala b/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala index 0b91cf8..c79c639 100644 --- a/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/UnsupportedEodSpec.scala @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2013-2017 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, @@ -14,7 +14,7 @@ package com.snowplowanalytics.forex // Specs2 import org.specs2.mutable.Specification -// Joda +// Joda import org.joda.time._ // TestHelpers import TestHelpers._ @@ -25,14 +25,15 @@ import oerclient.ResourcesNotAvailable /** * Testing for exceptions caused by invalid dates */ -class UnsupportedEodSpec extends Specification { +class UnsupportedEodSpec extends Specification { "An end-of-date lookup in 1900" should { "throw an exception" in { + /** - * 1900 is earlier than 1990 which is the earliest available date for looking up exchange rates + * 1900 is earlier than 1990 which is the earliest available date for looking up exchange rates */ - val date1900 = new DateTime(1900, 3, 13, 0, 0) + val date1900 = new DateTime(1900, 3, 13, 0, 0) val rateIn1900 = fx.rate.to("GBP").eod(date1900) rateIn1900 must beLike { case Left(OerResponseError(_, ResourcesNotAvailable)) => ok @@ -42,10 +43,11 @@ class UnsupportedEodSpec extends Specification { "An end-of-date lookup in 2020" should { "throw an exception" in { + /** * 2020 is in the future so it won't be available either */ - val date2020 = new DateTime(2020, 3, 13, 0, 0) + val date2020 = new DateTime(2020, 3, 13, 0, 0) val rateIn2020 = fx.rate.to("GBP").eod(date2020) rateIn2020 must beLike { case Left(OerResponseError(_, ResourcesNotAvailable)) => ok diff --git a/src/test/scala/com.snowplowanalytics.forex/oerclient/OerClientSpec.scala b/src/test/scala/com.snowplowanalytics.forex/oerclient/OerClientSpec.scala index ce74aaf..6e7f0be 100644 --- a/src/test/scala/com.snowplowanalytics.forex/oerclient/OerClientSpec.scala +++ b/src/test/scala/com.snowplowanalytics.forex/oerclient/OerClientSpec.scala @@ -17,21 +17,21 @@ package oerclient import java.math.BigDecimal // Specs2 import org.specs2.mutable.Specification -// Joda +// Joda import org.joda.time._ // TestHelpers import TestHelpers._ /** Testing methods for Open exchange rate client */ -class OerClientSpec extends Specification { +class OerClientSpec extends Specification { - "live currency value for USD" should { + "live currency value for USD" should { "always equal to 1" in { fx.client.getLiveCurrencyValue("USD").right.get must_== (new BigDecimal(1)) } } - val gbpLiveRate = fx.client.getLiveCurrencyValue("GBP") + val gbpLiveRate = fx.client.getLiveCurrencyValue("GBP") "live currency value for GBP [%s]".format(gbpLiveRate.right.get) should { "be less than 1" in { gbpLiveRate.right.get must be < (new BigDecimal(1)) @@ -45,4 +45,3 @@ class OerClientSpec extends Specification { } } } -