Skip to content

Commit

Permalink
Merge pull request #1154 from softwaremill/cats-effect-3
Browse files Browse the repository at this point in the history
Migrate to cats-effect 3 & http4s 0.23
  • Loading branch information
adamw authored Jul 15, 2021
2 parents 37a66e1 + 0410fbb commit e61458c
Show file tree
Hide file tree
Showing 87 changed files with 857 additions and 779 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
matrix:
target-platform: [ "JVM", "JS" ]
env:
JAVA_OPTS: -Xmx5G
JAVA_OPTS: "-Xmx3500M -Xlog:gc -XX:+PrintGCDetails -Xlog:gc*::time -Dsbt.task.timings=true"
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down Expand Up @@ -61,7 +61,7 @@ jobs:
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
runs-on: ubuntu-20.04
env:
JAVA_OPTS: -Xmx5G
JAVA_OPTS: -Xmx3500M
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down
32 changes: 16 additions & 16 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq(
mimaPreviousArtifacts := Set.empty, // we only use MiMa for `core` for now, using versioningSchemeSettings
ideSkipProject := (scalaVersion.value == scala2_12) || (scalaVersion.value == scala3) || thisProjectRef.value.project.contains("JS"),
// slow down for CI
Test / parallelExecution := false
Test / parallelExecution := false,
// remove false alarms about unused implicit definitions in macros
scalacOptions += "-Ywarn-macros:after",
evictionErrorLevel := Level.Info
)

val versioningSchemeSettings = Seq(
Expand All @@ -68,7 +71,8 @@ val versioningSchemeSettings = Seq(

val commonJvmSettings: Seq[Def.Setting[_]] = commonSettings ++ Seq(
Compile / unmanagedSourceDirectories ++= versionedScalaJvmSourceDirectories((Compile / sourceDirectory).value, scalaVersion.value),
Test / unmanagedSourceDirectories ++= versionedScalaJvmSourceDirectories((Test / sourceDirectory).value, scalaVersion.value)
Test / unmanagedSourceDirectories ++= versionedScalaJvmSourceDirectories((Test / sourceDirectory).value, scalaVersion.value),
Test / testOptions += Tests.Argument("-oD") // js has other options which conflict with timings
)

// run JS tests inside Gecko, due to jsdom not supporting fetch and to avoid having to install node
Expand Down Expand Up @@ -792,7 +796,7 @@ lazy val serverTests: ProjectMatrix = (projectMatrix in file("server/tests"))
.settings(
name := "tapir-server-tests",
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "async-http-client-backend-fs2-ce2" % Versions.sttp
"com.softwaremill.sttp.client3" %% "httpclient-backend-fs2" % Versions.sttp
)
)
.dependsOn(tests)
Expand All @@ -818,7 +822,7 @@ lazy val http4sServer: ProjectMatrix = (projectMatrix in file("server/http4s-ser
name := "tapir-http4s-server",
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-blaze-server" % Versions.http4s,
"com.softwaremill.sttp.shared" %% "fs2-ce2" % Versions.sttpShared
"com.softwaremill.sttp.shared" %% "fs2" % Versions.sttpShared
)
)
.jvmPlatform(scalaVersions = scala2And3Versions)
Expand Down Expand Up @@ -876,11 +880,7 @@ lazy val finatraServerCats: ProjectMatrix =
.settings(commonJvmSettings)
.settings(
name := "tapir-finatra-server-cats",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % Versions.catsEffect,
"io.catbird" %% "catbird-finagle" % Versions.catbird,
"io.catbird" %% "catbird-effect" % Versions.catbird
)
libraryDependencies ++= Seq("org.typelevel" %% "cats-effect" % Versions.catsEffect)
)
.jvmPlatform(scalaVersions = scala2Versions)
.dependsOn(finatraServer % "compile->compile;test->test", serverTests % Test)
Expand All @@ -904,7 +904,7 @@ lazy val vertxServer: ProjectMatrix = (projectMatrix in file("server/vertx"))
name := "tapir-vertx-server",
libraryDependencies ++= Seq(
"io.vertx" % "vertx-web" % Versions.vertx,
"com.softwaremill.sttp.shared" %% "fs2-ce2" % Versions.sttpShared % Optional,
"com.softwaremill.sttp.shared" %% "fs2" % Versions.sttpShared % Optional,
"com.softwaremill.sttp.shared" %% "zio" % Versions.sttpShared % Optional,
"dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats % Test
)
Expand Down Expand Up @@ -938,7 +938,7 @@ lazy val awsLambda: ProjectMatrix = (projectMatrix in file("serverless/aws/lambd
name := "tapir-aws-lambda",
libraryDependencies ++= loggerDependencies,
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "httpclient-backend-fs2-ce2" % Versions.sttp
"com.softwaremill.sttp.client3" %% "httpclient-backend-fs2" % Versions.sttp
)
)
.jvmPlatform(scalaVersions = scala2Versions)
Expand Down Expand Up @@ -1069,7 +1069,7 @@ lazy val http4sClient: ProjectMatrix = (projectMatrix in file("client/http4s-cli
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-core" % Versions.http4s,
"org.http4s" %% "http4s-blaze-client" % Versions.http4s % Test,
"com.softwaremill.sttp.shared" %% "fs2-ce2" % Versions.sttpShared % Optional
"com.softwaremill.sttp.shared" %% "fs2" % Versions.sttpShared % Optional
)
)
.jvmPlatform(scalaVersions = scala2And3Versions)
Expand All @@ -1087,8 +1087,8 @@ lazy val sttpClient: ProjectMatrix = (projectMatrix in file("client/sttp-client"
scalaVersions = scala2And3Versions,
settings = commonJvmSettings ++ Seq(
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "httpclient-backend-fs2-ce2" % Versions.sttp % Test,
"com.softwaremill.sttp.shared" %% "fs2-ce2" % Versions.sttpShared % Optional
"com.softwaremill.sttp.client3" %% "httpclient-backend-fs2" % Versions.sttp % Test,
"com.softwaremill.sttp.shared" %% "fs2" % Versions.sttpShared % Optional
),
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
Expand Down Expand Up @@ -1167,9 +1167,9 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
"org.http4s" %% "http4s-dsl" % Versions.http4s,
"org.http4s" %% "http4s-circe" % Versions.http4s,
"com.softwaremill.sttp.client3" %% "akka-http-backend" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-fs2-ce2" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-fs2" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-zio" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-cats-ce2" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.sttp,
"com.pauldijou" %% "jwt-circe" % Versions.jwtScala
),
libraryDependencies ++= loggerDependencies,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package sttp.tapir.client.http4s

import cats.Applicative
import cats.effect.{Blocker, ContextShift, Effect, Sync}
import cats.effect.Async
import cats.implicits._
import fs2.Chunk
import fs2.io.file.Files
import org.http4s._
import org.http4s.headers.`Content-Type`
import org.typelevel.ci.CIString
Expand All @@ -30,9 +31,9 @@ import sttp.tapir.{
import java.io.{ByteArrayInputStream, File, InputStream}
import java.nio.ByteBuffer

private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Http4sClientOptions) {
private[http4s] class EndpointToHttp4sClient(clientOptions: Http4sClientOptions) {

def toHttp4sRequest[I, E, O, R, F[_]: ContextShift: Effect](
def toHttp4sRequest[I, E, O, R, F[_]: Async](
e: Endpoint[I, E, O, R],
baseUriStr: Option[String]
): I => (Request[F], Response[F] => F[DecodeResult[Either[E, O]]]) = { params =>
Expand All @@ -47,7 +48,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
(request, responseParser)
}

def toHttp4sRequestUnsafe[I, E, O, R, F[_]: ContextShift: Effect](
def toHttp4sRequestUnsafe[I, E, O, R, F[_]: Async](
e: Endpoint[I, E, O, R],
baseUriStr: Option[String]
): I => (Request[F], Response[F] => F[Either[E, O]]) = { params =>
Expand All @@ -64,7 +65,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
}

@scala.annotation.tailrec
private def setInputParams[I, F[_]: ContextShift: Effect](
private def setInputParams[I, F[_]: Async](
input: EndpointInput[I],
params: Params,
req: Request[F]
Expand Down Expand Up @@ -115,7 +116,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
}
}

private def setBody[R, T, CF <: CodecFormat, F[_]: ContextShift: Effect](
private def setBody[R, T, CF <: CodecFormat, F[_]: Async](
value: T,
bodyType: RawBodyType[R],
codec: Codec[R, T, CF],
Expand All @@ -133,10 +134,10 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
val entityEncoder = EntityEncoder.chunkEncoder[F].contramap(Chunk.byteBuffer)
req.withEntity(encoded.asInstanceOf[ByteBuffer])(entityEncoder)
case RawBodyType.InputStreamBody =>
val entityEncoder = EntityEncoder.inputStreamEncoder[F, InputStream](blocker)
val entityEncoder = EntityEncoder.inputStreamEncoder[F, InputStream]
req.withEntity(Applicative[F].pure(encoded.asInstanceOf[InputStream]))(entityEncoder)
case RawBodyType.FileBody =>
val entityEncoder = EntityEncoder.fileEncoder[F](blocker)
val entityEncoder = EntityEncoder.fileEncoder[F]
req.withEntity(encoded.asInstanceOf[File])(entityEncoder)
case _: RawBodyType.MultipartBody =>
throw new IllegalArgumentException("Multipart body isn't supported yet")
Expand All @@ -156,7 +157,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
throw new IllegalArgumentException("Only Fs2Streams streaming is supported")
}

private def handleInputPair[I, F[_]: ContextShift: Effect](
private def handleInputPair[I, F[_]: Async](
left: EndpointInput[_],
right: EndpointInput[_],
params: Params,
Expand All @@ -169,15 +170,15 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
setInputParams(right.asInstanceOf[EndpointInput[Any]], rightParams, req2)
}

private def handleMapped[II, T, F[_]: ContextShift: Effect](
private def handleMapped[II, T, F[_]: Async](
tuple: EndpointInput[II],
codec: Mapping[T, II],
params: Params,
req: Request[F]
): Request[F] =
setInputParams(tuple.asInstanceOf[EndpointInput[Any]], ParamsAsAny(codec.encode(params.asAny.asInstanceOf[II])), req)

private def parseHttp4sResponse[I, E, O, R, F[_]: Sync: ContextShift](
private def parseHttp4sResponse[I, E, O, R, F[_]: Async](
e: Endpoint[I, E, O, R]
): Response[F] => F[DecodeResult[Either[E, O]]] = { response =>
val code = sttp.model.StatusCode(response.status.code)
Expand All @@ -194,7 +195,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
}
}

private def responseFromOutput[F[_]: Sync: ContextShift](out: EndpointOutput[_]): Response[F] => F[Any] = { response =>
private def responseFromOutput[F[_]: Async](out: EndpointOutput[_]): Response[F] => F[Any] = { response =>
bodyIsStream(out) match {
case Some(streams) =>
streams match {
Expand All @@ -216,7 +217,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
response.body.compile.toVector.map(_.toArray).map(new ByteArrayInputStream(_)).map(_.asInstanceOf[Any])
case RawBodyType.FileBody =>
val file = clientOptions.createFile()
response.body.through(fs2.io.file.writeAll(file.toPath, blocker)).compile.drain.map(_ => file.asInstanceOf[Any])
response.body.through(Files[F].writeAll(file.toPath)).compile.drain.map(_ => file.asInstanceOf[Any])
case RawBodyType.MultipartBody(_, _) => throw new IllegalArgumentException("Multipart bodies aren't supported in responses")
}
.getOrElse[F[Any]](((): Any).pure[F])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package sttp.tapir.client.http4s

import cats.effect.{Blocker, ContextShift, Effect}
import cats.effect.Async
import org.http4s.{Request, Response}
import sttp.tapir.{DecodeResult, Endpoint}

abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] {
abstract class Http4sClientInterpreter[F[_]: Async] {

def http4sClientOptions: Http4sClientOptions = Http4sClientOptions.default

Expand All @@ -16,10 +16,11 @@ abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] {
* - an `org.http4s.Request[F]`, which can be sent using an http4s client, or run against `org.http4s.HttpRoutes[F]`;
* - a response parser that extracts the expected entity from the received `org.http4s.Response[F]`.
*/
def toRequest[I, E, O, R](e: Endpoint[I, E, O, R], baseUri: Option[String])(implicit
blocker: Blocker,
def toRequest[I, E, O, R](
e: Endpoint[I, E, O, R],
baseUri: Option[String]
): I => (Request[F], Response[F] => F[DecodeResult[Either[E, O]]]) =
new EndpointToHttp4sClient(blocker, http4sClientOptions).toHttp4sRequest[I, E, O, R, F](e, baseUri)
new EndpointToHttp4sClient(http4sClientOptions).toHttp4sRequest[I, E, O, R, F](e, baseUri)

/** Interprets the endpoint as a client call, using the given `baseUri` as the starting point to create the target
* uri. If `baseUri` is not provided, the request will be a relative one.
Expand All @@ -29,14 +30,12 @@ abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] {
* - an `org.http4s.Request[F]`, which can be sent using an http4s client, or run against `org.http4s.HttpRoutes[F]`;
* - a response parser that extracts the expected entity from the received `org.http4s.Response[F]`.
*/
def toRequestUnsafe[I, E, O, R](e: Endpoint[I, E, O, R], baseUri: Option[String])(implicit
blocker: Blocker
): I => (Request[F], Response[F] => F[Either[E, O]]) =
new EndpointToHttp4sClient(blocker, http4sClientOptions).toHttp4sRequestUnsafe[I, E, O, R, F](e, baseUri)
def toRequestUnsafe[I, E, O, R](e: Endpoint[I, E, O, R], baseUri: Option[String]): I => (Request[F], Response[F] => F[Either[E, O]]) =
new EndpointToHttp4sClient(http4sClientOptions).toHttp4sRequestUnsafe[I, E, O, R, F](e, baseUri)
}

object Http4sClientInterpreter {
def apply[F[_]: ContextShift: Effect](clientOptions: Http4sClientOptions = Http4sClientOptions.default): Http4sClientInterpreter[F] =
def apply[F[_]: Async](clientOptions: Http4sClientOptions = Http4sClientOptions.default): Http4sClientInterpreter[F] =
new Http4sClientInterpreter[F] {
override def http4sClientOptions: Http4sClientOptions = clientOptions
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sttp.tapir.client.http4s

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import fs2.text
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir.client.tests.ClientStreamingTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import sttp.tapir._

import scala.concurrent.ExecutionContext.global

class Http4sClientRequestTests extends AnyFunSuite with Matchers {
private implicit val cs: ContextShift[IO] = IO.contextShift(global)
private implicit val blocker: Blocker = Blocker.liftExecutionContext(global)

test("should exclude optional query parameter when its value is None") {
// given
val testEndpoint = endpoint.get.in(query[Option[String]]("param"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package sttp.tapir.client.http4s

import cats.effect.{Blocker, ContextShift, IO, Timer}
import cats.effect.IO
import org.http4s.blaze.client.BlazeClientBuilder
import org.http4s.{Request, Response}
import sttp.tapir.client.tests.ClientTests
Expand All @@ -9,10 +9,6 @@ import sttp.tapir.{DecodeResult, Endpoint}
import scala.concurrent.ExecutionContext.global

abstract class Http4sClientTests[R] extends ClientTests[R] {
implicit val cs: ContextShift[IO] = IO.contextShift(global)
implicit val timer: Timer[IO] = IO.timer(global)
implicit val blocker: Blocker = Blocker.liftExecutionContext(global)

override def send[I, E, O](e: Endpoint[I, E, O, R], port: Port, args: I, scheme: String = "http"): IO[Either[E, O]] = {
val (request, parseResponse) = Http4sClientInterpreter[IO]().toRequestUnsafe(e, Some(s"http://localhost:$port")).apply(args)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package sttp.tapir.client.play

import akka.actor.ActorSystem
import akka.stream.Materializer
import cats.effect.{ContextShift, IO}
import cats.effect.IO
import play.api.libs.ws.StandaloneWSClient
import play.api.libs.ws.ahc.StandaloneAhcWSClient
import sttp.tapir.client.tests.ClientTests
Expand All @@ -12,7 +12,6 @@ import scala.concurrent.{ExecutionContext, Future}

abstract class PlayClientTests[R] extends ClientTests[R] {

implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.Implicits.global)
implicit val materializer: Materializer = Materializer(ActorSystem("tests"))

implicit val wsClient: StandaloneWSClient = StandaloneAhcWSClient()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package sttp.tapir.client.sttp.ws.akkahttp

import sttp.capabilities.WebSockets
import sttp.capabilities.akka.AkkaStreams
import sttp.capabilities.{Effect, WebSockets}
import sttp.tapir.client.sttp.WebSocketToPipe

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.ExecutionContext

trait TapirSttpClientAkkaHttpWebSockets {
implicit def webSocketsSupportedForAkkaStreams(implicit ec: ExecutionContext): WebSocketToPipe[AkkaStreams with WebSockets] =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sttp.tapir.client.sttp.ws.fs2

import cats.effect.Concurrent
import sttp.capabilities.{Effect, WebSockets}
import sttp.capabilities.WebSockets
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir.client.sttp.WebSocketToPipe

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package sttp.tapir.client.sttp

import cats.effect.{ContextShift, IO}
import cats.effect.IO

import scala.concurrent.Future
import sttp.tapir.{DecodeResult, Endpoint}
import sttp.tapir.client.tests.ClientTests
import sttp.client3._

abstract class SttpClientTests[R >: Any] extends ClientTests[R] {
implicit val cs: ContextShift[IO] = IO.contextShift(executionContext)
val backend: SttpBackend[Future, R] = FetchBackend()
def wsToPipe: WebSocketToPipe[R]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sttp.tapir.client.sttp

import akka.actor.ActorSystem
import cats.effect.{ContextShift, IO}
import cats.effect.IO
import sttp.capabilities.WebSockets
import sttp.capabilities.akka.AkkaStreams
import sttp.client3._
Expand All @@ -12,7 +12,6 @@ import sttp.tapir.{DecodeResult, Endpoint}
import scala.concurrent.ExecutionContext

abstract class SttpAkkaClientTests[R >: WebSockets with AkkaStreams] extends ClientTests[R] {
implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.Implicits.global)
implicit val actorSystem = ActorSystem("tests")
val backend = AkkaHttpBackend.usingActorSystem(actorSystem)
def wsToPipe: WebSocketToPipe[R]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sttp.tapir.client.sttp

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cats.implicits._
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir.client.tests.ClientStreamingTests
Expand Down
Loading

0 comments on commit e61458c

Please sign in to comment.