Skip to content

Commit

Permalink
Introduce scalaj (closes #162)
Browse files Browse the repository at this point in the history
  • Loading branch information
BenFradet authored and dilyand committed Nov 8, 2019
1 parent 3872c3e commit b6c1ce7
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 257 deletions.
1 change: 1 addition & 0 deletions build.sbt
Expand Up @@ -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
)
Expand Down
15 changes: 8 additions & 7 deletions project/Dependencies.scala
Expand Up @@ -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"
}
}
11 changes: 6 additions & 5 deletions src/main/scala/com.snowplowanalytics/forex/Forex.scala
Expand Up @@ -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 {
Expand All @@ -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))
}
Expand All @@ -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)

Expand Down Expand Up @@ -110,7 +111,7 @@ case class ForexLookupTo[F[_]](
conversionAmount: Double,
fromCurr: CurrencyUnit,
config: ForexConfig,
client: ForexClient[F]
client: OerClient[F]
) {

/**
Expand All @@ -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)
Expand Down
91 changes: 0 additions & 91 deletions src/main/scala/com.snowplowanalytics/forex/ForexClient.scala

This file was deleted.

68 changes: 0 additions & 68 deletions src/main/scala/com.snowplowanalytics/forex/ForexConfig.scala

This file was deleted.

Expand Up @@ -11,52 +11,34 @@
* 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)
* @param config - a configurator for Forex object
* @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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
}

0 comments on commit b6c1ce7

Please sign in to comment.