Skip to content

Instantly share code, notes, and snippets.

@otobrglez
Created April 29, 2021 08:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save otobrglez/96cdde49a13664487324106cb8ca2304 to your computer and use it in GitHub Desktop.
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.
/*
* 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