Created
April 29, 2021 08:18
-
-
Save otobrglez/96cdde49a13664487324106cb8ca2304 to your computer and use it in GitHub Desktop.
HttpClient that uses Scala Cats, Circe and some caching for Akka and Akka Http.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* HttpClient that uses Scala Cats, Circe and some caching for Akka and Akka Http. | |
* Author: Oto Brglez | |
*/ | |
import akka.actor.ActorSystem | |
import akka.http.scaladsl.Http | |
import akka.http.scaladsl.model._ | |
import akka.http.scaladsl.model.headers.RawHeader | |
import akka.http.scaladsl.unmarshalling.Unmarshal | |
import better.files.Dsl._ | |
import better.files._ | |
import cats.data._ | |
import cats.implicits._ | |
import com.typesafe.scalalogging.LazyLogging | |
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport | |
import io.circe.Json | |
import scala.concurrent.{ExecutionContext, Future} | |
object HttpClient extends HttpClient | |
trait HttpClient extends LazyLogging { | |
type AccessToken = String | |
import FailFastCirceSupport._ | |
protected def fetch(uri: Uri, | |
accessToken: Option[AccessToken] = None, | |
httpMethod: HttpMethod = HttpMethods.GET, | |
json: Option[Json] = None) | |
(implicit actorSystem: ActorSystem, | |
executionContext: ExecutionContext): EitherT[Future, Throwable, Json] = { | |
val cacheKey: String = s"${httpMethod.value} ${uri.toString()}" | |
.replaceAll("[^\\w\\s-]", "") | |
.replace('-', ' ').trim.replaceAll("\\s+", "-").toLowerCase | |
val cacheFile: File = pwd / "cache" / s"$cacheKey.json" | |
def buildRequest: HttpRequest = | |
json.foldLeft(HttpRequest(httpMethod).withUri(uri) | |
.withHeaders(accessToken.map[Seq[RawHeader]](t => Seq(RawHeader("Authorization", s"Bearer $t"))).getOrElse(Seq.empty)))( | |
(request, json) => request.withEntity(HttpEntity(MediaTypes.`application/json`, json.noSpaces))) | |
def execute: EitherT[Future, Throwable, HttpResponse] = EitherT.liftF(Http().singleRequest(buildRequest)) | |
val process: HttpResponse => EitherT[Future, Throwable, Json] = { response => | |
if (!response.status.isSuccess()) return Future.failed[Json](new Exception("Request failed")).attemptT | |
EitherT(Unmarshal(response).to[Json].map(_.hcursor.asRight[Throwable].map(_.value))) | |
} | |
def loadCache: EitherT[Future, Throwable, Option[HttpResponse]] = | |
EitherT.right[Throwable](Future.successful[Option[HttpResponse]]( | |
Option.when(httpMethod == HttpMethods.GET && cacheFile.exists)( | |
HttpResponse(StatusCodes.OK, entity = HttpEntity(MediaTypes.`application/json`, cacheFile.contentAsString))))) | |
val storeCache: Json => EitherT[Future, Throwable, Json] = json => | |
EitherT.right[Throwable](Future.successful[Json] { | |
if (httpMethod == HttpMethods.GET) cacheFile.write(json.noSpaces) | |
json | |
}) | |
loadCache.flatMap { | |
case Some(response) => process(response) | |
case None => execute >>= process >>= storeCache | |
} | |
} | |
def get(uri: Uri, | |
accessToken: Option[AccessToken] = None) | |
(implicit actorSystem: ActorSystem, | |
executionContext: ExecutionContext): EitherT[Future, Throwable, Json] = | |
fetch(uri, accessToken) | |
def post(uri: Uri, | |
json: Json, | |
accessToken: Option[AccessToken] = None) | |
(implicit actorSystem: ActorSystem, | |
executionContext: ExecutionContext): EitherT[Future, Throwable, Json] = | |
fetch(uri, accessToken, HttpMethods.POST, json.some) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment