From 815d2d9664db18f574e9c210a1f7001adf185bbe Mon Sep 17 00:00:00 2001 From: Luke Thomas Date: Wed, 26 Oct 2022 16:44:24 +0100 Subject: [PATCH] Change to http4s --- .scalafmt.conf | 1 + build.sbt | 10 ++-- src/main/scala/gsheets4s/GSheets4s.scala | 17 +++--- .../gsheets4s/http/GSheets4sDefaultUrls.scala | 8 +-- .../scala/gsheets4s/http/HttpClient.scala | 54 ++++++++----------- .../interpreters/RestSpreadsheetsValues.scala | 12 ++--- .../integration/SpreadsheetsValuesSpec.scala | 9 ++-- 7 files changed, 52 insertions(+), 59 deletions(-) create mode 100644 .scalafmt.conf diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..834f2d2 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1 @@ +version = 2.7.5 \ No newline at end of file diff --git a/build.sbt b/build.sbt index 9fe4a46..ba73acc 100644 --- a/build.sbt +++ b/build.sbt @@ -28,7 +28,7 @@ lazy val catsEffectVersion = "3.3.10" lazy val circeVersion = "0.14.1" lazy val refinedVersion = "0.9.28" lazy val attoVersion = "0.9.5" -lazy val hammockVersion = "0.11.3" +lazy val http4sVersion = "0.23.16" lazy val scalacheckVersion = "1.15.4" lazy val scalatestVersion = "3.2.11" lazy val scalaUriVersion = "4.0.1" @@ -51,10 +51,10 @@ lazy val gsheets4s = project.in(file(".")) "org.tpolecat" %% "atto-core", "org.tpolecat" %% "atto-refined" ).map(_ % attoVersion) ++ Seq( - "com.pepegar" %% "hammock-core", - "com.pepegar" %% "hammock-apache-http", - "com.pepegar" %% "hammock-circe" - ).map(_ % hammockVersion) ++ Seq( + "org.http4s" %% "http4s-dsl", + "org.http4s" %% "http4s-circe", + "org.http4s" %% "http4s-client" + ).map(_ % http4sVersion) ++ Seq( "org.scalatest" %% "scalatest" % scalatestVersion, "org.scalacheck" %% "scalacheck" % scalacheckVersion, "eu.timepit" %% "refined-scalacheck" % refinedVersion diff --git a/src/main/scala/gsheets4s/GSheets4s.scala b/src/main/scala/gsheets4s/GSheets4s.scala index bb3fe70..1af2a5d 100644 --- a/src/main/scala/gsheets4s/GSheets4s.scala +++ b/src/main/scala/gsheets4s/GSheets4s.scala @@ -1,20 +1,19 @@ package gsheets4s -import cats.effect.Sync - -import algebras._ -import http._ -import interpreters._ -import model._ -import cats.effect.Ref +import cats.effect.{Concurrent, Ref} +import gsheets4s.algebras._ +import gsheets4s.http._ +import gsheets4s.interpreters._ +import gsheets4s.model._ +import org.http4s.client.Client case class GSheets4s[F[_]]( spreadsheetsValues: SpreadsheetsValues[F] ) object GSheets4s { - def apply[F[_]: Sync](creds: Ref[F, Credentials]): GSheets4s[F] = { - val requester = new HammockRequester[F]() + def apply[F[_]: Concurrent](creds: Ref[F, Credentials], http4sClient: Client[F]): GSheets4s[F] = { + val requester = new Http4sRequester[F](http4sClient) val client = new HttpClient[F](creds, requester) val spreadsheetsValues = new RestSpreadsheetsValues(client) GSheets4s(spreadsheetsValues) diff --git a/src/main/scala/gsheets4s/http/GSheets4sDefaultUrls.scala b/src/main/scala/gsheets4s/http/GSheets4sDefaultUrls.scala index 5ef643c..b429a90 100644 --- a/src/main/scala/gsheets4s/http/GSheets4sDefaultUrls.scala +++ b/src/main/scala/gsheets4s/http/GSheets4sDefaultUrls.scala @@ -1,7 +1,7 @@ package gsheets4s.http -import cats.syntax.option._ -import hammock.Uri +import org.http4s.Uri +import org.http4s.implicits._ case class GSheets4sDefaultUrls( baseUrl: Uri, @@ -10,7 +10,7 @@ case class GSheets4sDefaultUrls( object GSheets4sDefaultUrls { implicit val defaultUrls: GSheets4sDefaultUrls = GSheets4sDefaultUrls( - Uri("https".some, none, "sheets.googleapis.com/v4/spreadsheets"), - Uri("https".some, none, "www.googleapis.com/oauth2/v4/token") + uri"https://sheets.googleapis.com/v4/spreadsheets", + uri"https://www.googleapis.com/oauth2/v4/token" ) } diff --git a/src/main/scala/gsheets4s/http/HttpClient.scala b/src/main/scala/gsheets4s/http/HttpClient.scala index a95b7db..a367d33 100644 --- a/src/main/scala/gsheets4s/http/HttpClient.scala +++ b/src/main/scala/gsheets4s/http/HttpClient.scala @@ -2,19 +2,14 @@ package gsheets4s package http import cats.Monad -import cats.data.NonEmptyList -import cats.effect.Sync +import cats.effect.{Concurrent, Ref} import cats.syntax.flatMap._ import cats.syntax.functor._ -import cats.syntax.show._ -import hammock._ -import hammock.apache.ApacheInterpreter -import hammock.circe._ -import io.circe.{Encoder, Decoder} -import io.lemonlabs.uri.Url - -import model.{Credentials, GsheetsError} -import cats.effect.Ref +import gsheets4s.model.{Credentials, GsheetsError} +import io.circe.{Decoder, Encoder} +import org.http4s.circe.CirceEntityCodec._ +import org.http4s.client.Client +import org.http4s.{Method, Request, Uri} trait HttpRequester[F[_]] { def request[O](uri: Uri, method: Method)(implicit d: Decoder[O]): F[O] @@ -22,33 +17,29 @@ trait HttpRequester[F[_]] { uri: Uri, body: I, method: Method)(implicit e: Encoder[I], d: Decoder[O]): F[O] } -class HammockRequester[F[_]: Sync] extends HttpRequester[F] { - implicit val interpreter = ApacheInterpreter.instance[F] +class Http4sRequester[F[_]: Concurrent](client: Client[F]) extends HttpRequester[F] { def request[O](uri: Uri, method: Method)(implicit d: Decoder[O]): F[O] = { - implicit val hammockDecoder = new HammockDecoderForCirce() - Hammock.request(method, uri, Map.empty).as[O].exec[F] + client.expect[O](Request[F](method, uri)) } def requestWithBody[I, O]( uri: Uri, body: I, method: Method)(implicit e: Encoder[I], d: Decoder[O]): F[O] = { - implicit val hammockEncoder = new HammockEncoderForCirce() - implicit val hammockDecoder = new HammockDecoderForCirce() - Hammock.request(method, uri, Map.empty, Some(body)).as[O].exec[F] + client.expect[O](Request[F](method, uri).withEntity(body)) } } class HttpClient[F[_]: Monad](creds: Ref[F, Credentials], requester: HttpRequester[F])( implicit urls: GSheets4sDefaultUrls) { def get[O]( - path: Url, + path: Uri, params: List[(String, String)] = List.empty)( implicit d: Decoder[O]): F[Either[GsheetsError, O]] = req(token => requester .request[Either[GsheetsError, O]](urlBuilder(token, path, params), Method.GET)) def put[I, O]( - path: Url, + path: Uri, body: I, params: List[(String, String)] = List.empty)( implicit e: Encoder[I], d: Decoder[O]): F[Either[GsheetsError, O]] = @@ -56,7 +47,7 @@ class HttpClient[F[_]: Monad](creds: Ref[F, Credentials], requester: HttpRequest urlBuilder(token, path, params), body, Method.PUT)) def post[I, O]( - path: Url, + path: Uri, body: I, params: List[(String, String)] = List.empty)( implicit e: Encoder[I], d: Decoder[O]): F[Either[GsheetsError, O]] = @@ -80,21 +71,20 @@ class HttpClient[F[_]: Monad](creds: Ref[F, Credentials], requester: HttpRequest } yield r private def refreshToken(c: Credentials)(implicit d: Decoder[String]): F[String] = { - val url = urls.refreshTokenUrl ? - NonEmptyList( - ("refresh_token" -> c.refreshToken), - List( - ("client_id" -> c.clientId), - ("client_secret" -> c.clientSecret), - ("grant_type" -> "refresh_token") - ) - ) + val url = urls.refreshTokenUrl.withQueryParam("refresh_token", c.refreshToken) + .withQueryParam("client_id", c.clientId) + .withQueryParam("client_secret", c.clientSecret) + .withQueryParam("grant_type", "refresh_token") + requester.request(url, Method.POST) } private def urlBuilder( accessToken: String, - path: Url, + path: Uri, params: List[(String, String)]): Uri = - (urls.baseUrl / path.show) ? NonEmptyList(("access_token" -> accessToken), params) + urls.baseUrl + .resolve(path) + .withQueryParam("access_token", accessToken) + .withQueryParams(params.toMap) } diff --git a/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala b/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala index a5daa3c..6ba11cf 100644 --- a/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala +++ b/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala @@ -2,15 +2,15 @@ package gsheets4s package interpreters import cats.syntax.show._ +import gsheets4s.algebras.SpreadsheetsValues +import gsheets4s.http._ +import gsheets4s.model._ import io.circe.generic.auto._ - -import algebras.SpreadsheetsValues -import http._ -import model._ +import org.http4s.Uri class RestSpreadsheetsValues[F[_]](client: HttpClient[F]) extends SpreadsheetsValues[F] { def get(spreadsheetID: String, range: A1Notation): F[Either[GsheetsError, ValueRange]] = - client.get(io.lemonlabs.uri.Url.parse(s"$spreadsheetID/values/${range.show}")) + client.get(Uri.unsafeFromString(s"$spreadsheetID/values/${range.show}")) def update( spreadsheetID: String, @@ -18,6 +18,6 @@ class RestSpreadsheetsValues[F[_]](client: HttpClient[F]) extends SpreadsheetsVa updates: ValueRange, valueInputOption: ValueInputOption ): F[Either[GsheetsError, UpdateValuesResponse]] = - client.put(io.lemonlabs.uri.Url.parse(s"$spreadsheetID/values/${range.show}"), updates, + client.put(Uri.unsafeFromString(s"$spreadsheetID/values/${range.show}"), updates, List(("valueInputOption", valueInputOption.value))) } diff --git a/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala b/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala index ef00f4e..28d7fb5 100644 --- a/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala +++ b/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala @@ -8,12 +8,15 @@ import model._ import org.scalatest.flatspec.AnyFlatSpec import cats.effect.Ref import cats.effect.unsafe.implicits.global +import org.http4s.client.JavaNetClientBuilder object Integration extends Tag ( if (sys.env.get("GSHEETS4S_ACCESS_TOKEN").isDefined) "" else classOf[Ignore].getName) class SpreadsheetsValuesSpec extends AnyFlatSpec { + val client = JavaNetClientBuilder[IO].create + val creds = for { accessToken <- sys.env.get("GSHEETS4S_ACCESS_TOKEN") refreshToken <- sys.env.get("GSHEETS4S_REFRESH_TOKEN") @@ -30,7 +33,7 @@ class SpreadsheetsValuesSpec extends AnyFlatSpec { "RestSpreadsheetsValues" should "update and get values" taggedAs Integration in { val res = (for { credsRef <- Ref.of[IO, Credentials](creds.get) - spreadsheetsValues = GSheets4s(credsRef).spreadsheetsValues + spreadsheetsValues = GSheets4s(credsRef, client).spreadsheetsValues prog <- new TestPrograms(spreadsheetsValues) .updateAndGet(spreadsheetID, vr, vio) } yield prog).unsafeRunSync() @@ -45,7 +48,7 @@ class SpreadsheetsValuesSpec extends AnyFlatSpec { it should "report an error if the spreadsheet it doesn't exist" taggedAs Integration in { val res = (for { credsRef <- Ref.of[IO, Credentials](creds.get) - spreadsheetsValues = GSheets4s(credsRef).spreadsheetsValues + spreadsheetsValues = GSheets4s(credsRef, client).spreadsheetsValues prog <- new TestPrograms(spreadsheetsValues) .updateAndGet("not-existing-spreadsheetid", vr, vio) } yield prog).unsafeRunSync() @@ -59,7 +62,7 @@ class SpreadsheetsValuesSpec extends AnyFlatSpec { it should "work with a faulty access token" taggedAs Integration in { val res = (for { credsRef <- Ref.of[IO, Credentials](creds.get.copy(accessToken = "faulty")) - spreadsheetsValues = GSheets4s(credsRef).spreadsheetsValues + spreadsheetsValues = GSheets4s(credsRef, client).spreadsheetsValues prog <- new TestPrograms(spreadsheetsValues) .updateAndGet(spreadsheetID, vr, vio) } yield prog).unsafeRunSync()