diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cd858e8c4..058c94c10e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/build.sbt b/build.sbt index b94040c1b7..f76989dec0 100644 --- a/build.sbt +++ b/build.sbt @@ -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( @@ -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 @@ -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) @@ -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) @@ -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) @@ -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 ) @@ -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) @@ -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) @@ -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 { @@ -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, diff --git a/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/EndpointToHttp4sClient.scala b/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/EndpointToHttp4sClient.scala index ac5480df44..686fb6ca4b 100644 --- a/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/EndpointToHttp4sClient.scala +++ b/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/EndpointToHttp4sClient.scala @@ -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 @@ -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 => @@ -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 => @@ -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] @@ -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], @@ -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") @@ -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, @@ -169,7 +170,7 @@ 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, @@ -177,7 +178,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht ): 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) @@ -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 { @@ -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]) diff --git a/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/Http4sClientInterpreter.scala b/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/Http4sClientInterpreter.scala index 2c0dd6856a..06df041add 100644 --- a/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/Http4sClientInterpreter.scala +++ b/client/http4s-client/src/main/scala/sttp/tapir/client/http4s/Http4sClientInterpreter.scala @@ -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 @@ -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. @@ -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 } diff --git a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4ClientStreamingTests.scala b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4ClientStreamingTests.scala index 9a1bf85dc0..ba31b4a72f 100644 --- a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4ClientStreamingTests.scala +++ b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4ClientStreamingTests.scala @@ -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 diff --git a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientRequestTests.scala b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientRequestTests.scala index 4d17ca272a..9c8c552594 100644 --- a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientRequestTests.scala +++ b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientRequestTests.scala @@ -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")) diff --git a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala index 5541211625..d156a18886 100644 --- a/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala +++ b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala @@ -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 @@ -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) diff --git a/client/play-client/src/test/scala/sttp/tapir/client/play/PlayClientTests.scala b/client/play-client/src/test/scala/sttp/tapir/client/play/PlayClientTests.scala index eabb81444e..604b85d5dd 100644 --- a/client/play-client/src/test/scala/sttp/tapir/client/play/PlayClientTests.scala +++ b/client/play-client/src/test/scala/sttp/tapir/client/play/PlayClientTests.scala @@ -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 @@ -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() diff --git a/client/sttp-client/src/main/scalajvm-2/sttp/tapir/client/sttp/ws/akkahttp/TapirSttpClientAkkaHttpWebSockets.scala b/client/sttp-client/src/main/scalajvm-2/sttp/tapir/client/sttp/ws/akkahttp/TapirSttpClientAkkaHttpWebSockets.scala index 8d102cafc1..bac83ad4bf 100644 --- a/client/sttp-client/src/main/scalajvm-2/sttp/tapir/client/sttp/ws/akkahttp/TapirSttpClientAkkaHttpWebSockets.scala +++ b/client/sttp-client/src/main/scalajvm-2/sttp/tapir/client/sttp/ws/akkahttp/TapirSttpClientAkkaHttpWebSockets.scala @@ -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] = diff --git a/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/fs2/TapirSttpClientFs2WebSockets.scala b/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/fs2/TapirSttpClientFs2WebSockets.scala index c917e1f1de..4fdf5a659e 100644 --- a/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/fs2/TapirSttpClientFs2WebSockets.scala +++ b/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/fs2/TapirSttpClientFs2WebSockets.scala @@ -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 diff --git a/client/sttp-client/src/test/scalajs/sttp/tapir/client/sttp/SttpClientTests.scala b/client/sttp-client/src/test/scalajs/sttp/tapir/client/sttp/SttpClientTests.scala index ef97349afc..20abcf5dc5 100644 --- a/client/sttp-client/src/test/scalajs/sttp/tapir/client/sttp/SttpClientTests.scala +++ b/client/sttp-client/src/test/scalajs/sttp/tapir/client/sttp/SttpClientTests.scala @@ -1,6 +1,6 @@ package sttp.tapir.client.sttp -import cats.effect.{ContextShift, IO} +import cats.effect.IO import scala.concurrent.Future import sttp.tapir.{DecodeResult, Endpoint} @@ -8,7 +8,6 @@ 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] diff --git a/client/sttp-client/src/test/scalajvm-2/sttp.tapir.client.sttp/SttpAkkaClientTests.scala b/client/sttp-client/src/test/scalajvm-2/sttp.tapir.client.sttp/SttpAkkaClientTests.scala index aa322748d1..aaaf2e2784 100644 --- a/client/sttp-client/src/test/scalajvm-2/sttp.tapir.client.sttp/SttpAkkaClientTests.scala +++ b/client/sttp-client/src/test/scalajvm-2/sttp.tapir.client.sttp/SttpAkkaClientTests.scala @@ -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._ @@ -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] diff --git a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingTests.scala b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingTests.scala index 943f995e89..a1cf5cbc36 100644 --- a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingTests.scala +++ b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingTests.scala @@ -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 diff --git a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientTests.scala b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientTests.scala index 1b109608c3..267f74777a 100644 --- a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientTests.scala +++ b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientTests.scala @@ -1,6 +1,8 @@ package sttp.tapir.client.sttp -import cats.effect.{Blocker, ContextShift, IO} +import cats.effect.IO +import cats.effect.std.Dispatcher +import cats.effect.unsafe.implicits.global import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.client3._ @@ -8,12 +10,9 @@ import sttp.client3.httpclient.fs2.HttpClientFs2Backend import sttp.tapir.client.tests.ClientTests import sttp.tapir.{DecodeResult, Endpoint} -import scala.concurrent.ExecutionContext - abstract class SttpClientTests[R >: WebSockets with Fs2Streams[IO]] extends ClientTests[R] { - implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.Implicits.global) - val backend: SttpBackend[IO, R] = - HttpClientFs2Backend[IO](Blocker.liftExecutionContext(ExecutionContext.Implicits.global)).unsafeRunSync() + val (dispatcher, closeDispatcher) = Dispatcher[IO].allocated.unsafeRunSync() + val backend: SttpBackend[IO, R] = HttpClientFs2Backend[IO](dispatcher).unsafeRunSync() def wsToPipe: WebSocketToPipe[R] override def send[I, E, O](e: Endpoint[I, E, O, R], port: Port, args: I, scheme: String = "http"): IO[Either[E, O]] = { @@ -32,6 +31,7 @@ abstract class SttpClientTests[R >: WebSockets with Fs2Streams[IO]] extends Clie override protected def afterAll(): Unit = { backend.close().unsafeRunSync() + closeDispatcher.unsafeRunSync() super.afterAll() } } diff --git a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientBasicTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientBasicTests.scala index cb66342927..11fc239198 100644 --- a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientBasicTests.scala +++ b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientBasicTests.scala @@ -1,5 +1,7 @@ package sttp.tapir.client.tests +import cats.effect.unsafe.implicits.global + import sttp.model.{QueryParams, StatusCode} import sttp.tapir._ import sttp.tapir.model.UsernamePassword diff --git a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientMultipartTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientMultipartTests.scala index c2e94fb99b..235f130d50 100644 --- a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientMultipartTests.scala +++ b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientMultipartTests.scala @@ -1,5 +1,6 @@ package sttp.tapir.client.tests +import cats.effect.unsafe.implicits.global import sttp.tapir.tests._ trait ClientMultipartTests { this: ClientTests[Any] => diff --git a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientStreamingTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientStreamingTests.scala index 4783e6f44a..856e142924 100644 --- a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientStreamingTests.scala +++ b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientStreamingTests.scala @@ -1,8 +1,8 @@ package sttp.tapir.client.tests +import cats.effect.unsafe.implicits.global import sttp.capabilities.Streams -import sttp.tapir.DecodeResult -import sttp.tapir.tests.{in_stream_out_stream, not_existing_endpoint} +import sttp.tapir.tests.in_stream_out_stream trait ClientStreamingTests[S] { this: ClientTests[S] => val streams: Streams[S] @@ -12,13 +12,12 @@ trait ClientStreamingTests[S] { this: ClientTests[S] => def streamingTests(): Unit = { test(in_stream_out_stream(streams).showDetail) { - rmStream( - // TODO: remove explicit type parameters when https://github.com/lampepfl/dotty/issues/12803 fixed - send[streams.BinaryStream, Unit, streams.BinaryStream](in_stream_out_stream(streams), port, mkStream("mango cranberry")) - .unsafeRunSync() - .toOption - .get - ) shouldBe "mango cranberry" + // TODO: remove explicit type parameters when https://github.com/lampepfl/dotty/issues/12803 fixed + send[streams.BinaryStream, Unit, streams.BinaryStream](in_stream_out_stream(streams), port, mkStream("mango cranberry")) + .map(_.toOption.get) + .map(rmStream) + .map(_ shouldBe "mango cranberry") + .unsafeToFuture() } } } diff --git a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientTests.scala index b98bdbe6a7..617f562d18 100644 --- a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientTests.scala +++ b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientTests.scala @@ -3,6 +3,7 @@ package sttp.tapir.client.tests import java.io.InputStream import cats.effect._ +import cats.effect.unsafe.implicits.global import cats.implicits._ import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AsyncFunSuite diff --git a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientWebSocketTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientWebSocketTests.scala index b781e41390..de06990e72 100644 --- a/client/tests/src/main/scala/sttp/tapir/client/tests/ClientWebSocketTests.scala +++ b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientWebSocketTests.scala @@ -1,6 +1,7 @@ package sttp.tapir.client.tests import cats.effect.IO +import cats.effect.unsafe.implicits.global import sttp.capabilities.{Streams, WebSockets} import sttp.tapir._ import sttp.tapir.json.circe._ @@ -25,7 +26,8 @@ trait ClientWebSocketTests[S] { this: ClientTests[S with WebSockets] => .flatMap { r => sendAndReceiveLimited(r.toOption.get, 2, List("test1", "test2")) } - .unsafeRunSync() shouldBe List("echo: test1", "echo: test2") + .map(_ shouldBe List("echo: test1", "echo: test2")) + .unsafeToFuture() } test("web sockets, json client-terminated echo") { @@ -38,12 +40,15 @@ trait ClientWebSocketTests[S] { this: ClientTests[S with WebSockets] => .flatMap { r => sendAndReceiveLimited(r.toOption.get, 2, List(Fruit("apple"), Fruit("orange"))) } - .unsafeRunSync() shouldBe List(Fruit("echo: apple"), Fruit("echo: orange")) + .map(_ shouldBe List(Fruit("echo: apple"), Fruit("echo: orange"))) + .unsafeToFuture() } test("web sockets, client-terminated echo using fragmented frames") { send( - endpoint.get.in("ws" / "echo" / "fragmented").out(webSocketBody[String, CodecFormat.TextPlain, WebSocketFrame, CodecFormat.TextPlain].apply(streams)), + endpoint.get + .in("ws" / "echo" / "fragmented") + .out(webSocketBody[String, CodecFormat.TextPlain, WebSocketFrame, CodecFormat.TextPlain].apply(streams)), port, (), "ws" @@ -51,7 +56,8 @@ trait ClientWebSocketTests[S] { this: ClientTests[S with WebSockets] => .flatMap { r => sendAndReceiveLimited(r.toOption.get, 2, List("test")) } - .unsafeRunSync() shouldBe List(WebSocketFrame.Text("fragmented frame with echo: test", true, None)) + .map(_ shouldBe List(WebSocketFrame.Text("fragmented frame with echo: test", true, None))) + .unsafeToFuture() } // TODO: tests for ping/pong (control frames handling) diff --git a/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala b/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala index afced2ac55..38ac385e59 100644 --- a/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala +++ b/client/testserver/src/main/scala/sttp/tapir/client/tests/HttpServer.scala @@ -1,7 +1,10 @@ package sttp.tapir.client.tests import cats.effect._ +import cats.effect.std.Queue +import cats.effect.unsafe.implicits.global import cats.implicits._ +import fs2.{Pipe, Stream} import org.http4s.dsl.io._ import org.http4s.headers.{Accept, `Content-Type`} import org.http4s.server.Router @@ -30,10 +33,6 @@ class HttpServer(port: Port) { private val logger = org.log4s.getLogger - implicit private val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit private val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit private val timer: Timer[IO] = IO.timer(ec) - private var stopServer: IO[Unit] = _ // @@ -62,6 +61,7 @@ class HttpServer(port: Port) { case Some(c) => headers.filter(_.name == CIString("Cookie")) :+ Header.Raw(CIString("Set-Cookie"), c.value.reverse) case None => headers } + val filteredHeaders2: Header.ToRaw = filteredHeaders1.filterNot(_.name == CIString("Content-Length")) okOnlyHeaders(List(filteredHeaders2)) case r @ GET -> Root / "api" / "echo" / "param-to-header" => @@ -119,11 +119,11 @@ class HttpServer(port: Port) { } } - fs2.concurrent.Queue + Queue .unbounded[IO, WebSocketFrame] .flatMap { q => - val d = q.dequeue.through(echoReply) - val e = q.enqueue + val d = Stream.repeatEval(q.take).through(echoReply) + val e: Pipe[IO, WebSocketFrame, Unit] = s => s.evalMap(q.offer) WebSocketBuilder[IO].build(d, e) } @@ -139,11 +139,11 @@ class HttpServer(port: Port) { case f => throw new IllegalArgumentException(s"Unsupported frame: $f") } - fs2.concurrent.Queue + Queue .unbounded[IO, WebSocketFrame] .flatMap { q => - val d = q.dequeue.through(echoReply) - val e = q.enqueue + val d = Stream.repeatEval(q.take).through(echoReply) + val e: Pipe[IO, WebSocketFrame, Unit] = s => s.evalMap(q.offer) WebSocketBuilder[IO].build(d, e) } @@ -179,7 +179,7 @@ class HttpServer(port: Port) { // def start(): Unit = { - val (_, _stopServer) = BlazeServerBuilder[IO](ec) + val (_, _stopServer) = BlazeServerBuilder[IO](ExecutionContext.global) .bindHttp(port) .withHttpApp(app) .resource diff --git a/core/src/main/scala/sttp/tapir/server/interpreter/ServerInterpreter.scala b/core/src/main/scala/sttp/tapir/server/interpreter/ServerInterpreter.scala index 123005fd4c..4342fde8e4 100644 --- a/core/src/main/scala/sttp/tapir/server/interpreter/ServerInterpreter.scala +++ b/core/src/main/scala/sttp/tapir/server/interpreter/ServerInterpreter.scala @@ -19,7 +19,7 @@ class ServerInterpreter[R, F[_], B, S]( apply(request, List(se)) def apply(request: ServerRequest, ses: List[ServerEndpoint[_, _, _, R, F]]): F[Option[ServerResponse[B]]] = - callInterceptors(interceptors, Nil, responder(defaultSuccessStatusCode), ses).apply(request) + monad.suspend(callInterceptors(interceptors, Nil, responder(defaultSuccessStatusCode), ses).apply(request)) /** Accumulates endpoint interceptors and calls `next` with the potentially transformed request. */ private def callInterceptors( diff --git a/doc/endpoint/zio.md b/doc/endpoint/zio.md index d01320a7f0..2ef058a243 100644 --- a/doc/endpoint/zio.md +++ b/doc/endpoint/zio.md @@ -48,11 +48,11 @@ import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter This adds the following method on `ZEndpoint`: -* `def toRoutes[R](logic: I => ZIO[R, E, O]): HttpRoutes[ZIO[R with Clock, Throwable, *]]` +* `def toRoutes[R](logic: I => ZIO[R, E, O]): HttpRoutes[ZIO[R with Clock with Blocking, Throwable, *]]` And the following methods on `ZServerEndpoint` or `List[ZServerEndpoint]`: -* `def toRoutes[R]: HttpRoutes[ZIO[R with Clock, Throwable, *]]` +* `def toRoutes[R]: HttpRoutes[ZIO[R with Clock with Blocking, Throwable, *]]` Note that the resulting `HttpRoutes` always require a clock in their environment. @@ -64,6 +64,7 @@ import org.http4s.HttpRoutes import sttp.tapir.ztapir._ import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import zio.{Has, RIO, ZIO} +import zio.blocking.Blocking import zio.clock.Clock import zio.interop.catz._ @@ -76,7 +77,7 @@ val serverEndpoint1: ZServerEndpoint[Service1, Unit, Unit, Unit] = ??? val serverEndpoint2: ZServerEndpoint[Service2, Unit, Unit, Unit] = ??? type Env = Service1 with Service2 -val routes: HttpRoutes[RIO[Env with Clock, *]] = +val routes: HttpRoutes[RIO[Env with Clock with Blocking, *]] = ZHttp4sServerInterpreter().from(List( serverEndpoint1.widen[Env], serverEndpoint2.widen[Env] diff --git a/doc/server/aws.md b/doc/server/aws.md index 051c117c51..0c1ce97b3f 100644 --- a/doc/server/aws.md +++ b/doc/server/aws.md @@ -8,7 +8,7 @@ For an overview of how this works in more detail, see [this blog post](https://b ## Serverless interpreters -To implement the Lambda function, a server interpreter is available, which takes tapir endpoints with associated server logic, and returns an `AwsRequest => F[AwsResponse]` function. This is used in the `AwsLambdaRuntime` to implement the Lambda loop of reading the next request, computing and sending the response. +To implement the Lambda function, a server interpreter is available, which takes tapir endpoints with associated server logic, and returns an `AwsRequest => F[AwsResponse]` function. This is used in the `AwsLambdaIORuntime` to implement the Lambda loop of reading the next request, computing and sending the response. Currently, only an interpreter integrating with cats-effect is available (`AwsCatsEffectServerInterpreter`). To use, add the following dependency: diff --git a/doc/server/finatra.md b/doc/server/finatra.md index d2fc7b2e65..83ae82df31 100644 --- a/doc/server/finatra.md +++ b/doc/server/finatra.md @@ -62,6 +62,7 @@ or a cats-effect's example: ```scala mdoc:compile-only import cats.effect.IO +import cats.effect.std.Dispatcher import sttp.tapir._ import sttp.tapir.server.finatra.FinatraRoute import sttp.tapir.server.finatra.cats.FinatraCatsServerInterpreter @@ -72,7 +73,9 @@ def countCharacters(s: String): IO[Either[Unit, Int]] = val countCharactersEndpoint: Endpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) -val countCharactersRoute: FinatraRoute = FinatraCatsServerInterpreter().toRoute(countCharactersEndpoint)(countCharacters) +def dispatcher: Dispatcher[IO] = ??? + +val countCharactersRoute: FinatraRoute = FinatraCatsServerInterpreter(dispatcher).toRoute(countCharactersEndpoint)(countCharacters) ``` Note that the second argument to `toRoute` is a function with one argument, a tuple of type `I`. This means that diff --git a/doc/server/http4s.md b/doc/server/http4s.md index d87af91180..9d32682216 100644 --- a/doc/server/http4s.md +++ b/doc/server/http4s.md @@ -28,13 +28,6 @@ import sttp.tapir._ import sttp.tapir.server.http4s.Http4sServerInterpreter import cats.effect.IO import org.http4s.HttpRoutes -import cats.effect.{ContextShift, Timer} - -// will probably come from somewhere else -implicit val cs: ContextShift[IO] = - IO.contextShift(scala.concurrent.ExecutionContext.global) -implicit val t: Timer[IO] = - IO.timer(scala.concurrent.ExecutionContext.global) def countCharacters(s: String): IO[Either[Unit, Int]] = IO.pure(Right[Unit, Int](s.length)) @@ -53,13 +46,6 @@ import sttp.tapir._ import sttp.tapir.server.http4s.Http4sServerInterpreter import cats.effect.IO import org.http4s.HttpRoutes -import cats.effect.{ContextShift, Timer} - -// will probably come from somewhere else -implicit val cs: ContextShift[IO] = - IO.contextShift(scala.concurrent.ExecutionContext.global) -implicit val t: Timer[IO] = - IO.timer(scala.concurrent.ExecutionContext.global) def logic(s: String, i: Int): IO[Either[Unit, String]] = ??? val anEndpoint: Endpoint[(String, Int), Unit, String, Any] = ??? @@ -99,13 +85,8 @@ import sttp.model.sse.ServerSentEvent import sttp.tapir._ import sttp.tapir.server.http4s.{Http4sServerInterpreter, serverSentEventsBody} -import cats.effect.{ContextShift, Timer} - val sseEndpoint = endpoint.get.out(serverSentEventsBody[IO]) -implicit val cs: ContextShift[IO] = ??? -implicit val t: Timer[IO] = ??? - val routes = Http4sServerInterpreter[IO]().toRoutes(sseEndpoint)(_ => IO(Right(fs2.Stream(ServerSentEvent(Some("data"), None, None, None)))) ) @@ -129,9 +110,6 @@ import sttp.tapir.server.http4s.{Http4sServerInterpreter, Http4sServerOptions} import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler import sttp.tapir.server.interceptor.exception.DefaultExceptionHandler -implicit val cs: ContextShift[IO] = ??? -implicit val t: Timer[IO] = ??? - implicit val options: Http4sServerOptions[IO, IO] = Http4sServerOptions.customInterceptors[IO, IO]( exceptionHandler = Some(DefaultExceptionHandler), serverLog = Some(Http4sServerOptions.Log.defaultServerLog), diff --git a/doc/server/vertx.md b/doc/server/vertx.md index b0bb36ecaf..a89d19ddbe 100644 --- a/doc/server/vertx.md +++ b/doc/server/vertx.md @@ -99,7 +99,7 @@ This object contains the following methods: Here is simple example which starts HTTP server with one route: ```scala mdoc:compile-only import cats.effect._ -import cats.syntax.flatMap._ +import cats.effect.std.Dispatcher import io.vertx.core.Vertx import io.vertx.ext.web.Router import sttp.tapir._ @@ -107,8 +107,7 @@ import sttp.tapir.server.vertx.VertxCatsServerInterpreter import sttp.tapir.server.vertx.VertxCatsServerInterpreter._ object App extends IOApp { - - val responseEndpoint = + val responseEndpoint: Endpoint[String, Unit, String, Any] = endpoint .in("response") .in(query[String]("key")) @@ -117,38 +116,46 @@ object App extends IOApp { def handler(req: String): IO[Either[Unit, String]] = IO.pure(Right(req)) - val attach = VertxCatsServerInterpreter[IO]().route(responseEndpoint)(handler) - - override def run(args: List[String]): IO[ExitCode] = - Resource.make(IO.delay{ - val vertx = Vertx.vertx() - val server = vertx.createHttpServer() - val router = Router.router(vertx) - attach(router) - server.requestHandler(router).listen(8080) - } >>= (_.asF[IO]))({ server => - IO.delay(server.close) >>= (_.asF[IO].void) - }).use(_ => IO.never) + override def run(args: List[String]): IO[ExitCode] = { + Dispatcher[IO] + .flatMap { dispatcher => + Resource + .make( + IO.delay { + val vertx = Vertx.vertx() + val server = vertx.createHttpServer() + val router = Router.router(vertx) + val attach = VertxCatsServerInterpreter[IO](dispatcher).route(responseEndpoint)(handler) + attach(router) + server.requestHandler(router).listen(8080) + }.flatMap(_.asF[IO]) + )({ server => + IO.delay(server.close).flatMap(_.asF[IO].void) + }) + } + .use(_ => IO.never) + } } ``` This interpreter also supports streaming using FS2 streams: ```scala mdoc:compile-only import cats.effect._ +import cats.effect.std.Dispatcher import fs2._ import sttp.capabilities.fs2.Fs2Streams import sttp.tapir._ import sttp.tapir.server.vertx.VertxCatsServerInterpreter -implicit val effect: ConcurrentEffect[IO] = ??? - val streamedResponse = endpoint .in("stream") .in(query[Int]("key")) .out(streamTextBody(Fs2Streams[IO])(CodecFormat.TextPlain())) + +def dispatcher: Dispatcher[IO] = ??? -val attach = VertxCatsServerInterpreter().route(streamedResponse) { key => +val attach = VertxCatsServerInterpreter(dispatcher).route(streamedResponse) { key => IO.pure(Right(Stream.chunk(Chunk.array("Hello world!".getBytes)).repeatN(key))) } ``` diff --git a/docs/redoc-http4s/src/main/scala/sttp/tapir/redoc/http4s/RedocHttp4s.scala b/docs/redoc-http4s/src/main/scala/sttp/tapir/redoc/http4s/RedocHttp4s.scala index 067acdebf7..bbd650bb77 100644 --- a/docs/redoc-http4s/src/main/scala/sttp/tapir/redoc/http4s/RedocHttp4s.scala +++ b/docs/redoc-http4s/src/main/scala/sttp/tapir/redoc/http4s/RedocHttp4s.scala @@ -1,6 +1,6 @@ package sttp.tapir.redoc.http4s -import cats.effect.{ContextShift, Sync} +import cats.effect.Sync import org.http4s.dsl.Http4sDsl import org.http4s.headers._ import org.http4s.{Charset, HttpRoutes, MediaType} @@ -33,7 +33,7 @@ class RedocHttp4s( rawHtml.replace("{{docsPath}}", yamlName).replace("{{title}}", title).replace("{{redocVersion}}", redocVersion) } - def routes[F[_]: ContextShift: Sync]: HttpRoutes[F] = { + def routes[F[_]: Sync]: HttpRoutes[F] = { val dsl = Http4sDsl[F] import dsl._ diff --git a/docs/swagger-ui-http4s/src/main/scala-2.13/sttp/tapir/swagger/http4s/package.scala b/docs/swagger-ui-http4s/src/main/scala-2.13/sttp/tapir/swagger/http4s/package.scala deleted file mode 100644 index 8a344c1f7d..0000000000 --- a/docs/swagger-ui-http4s/src/main/scala-2.13/sttp/tapir/swagger/http4s/package.scala +++ /dev/null @@ -1,9 +0,0 @@ -package sttp.tapir.swagger - -import scala.concurrent.ExecutionContext -import cats.effect.Blocker - -package object http4s { - implicit def executionContextToBlocker(ec: ExecutionContext): Blocker = - Blocker.liftExecutionContext(ec) -} diff --git a/docs/swagger-ui-http4s/src/main/scala/sttp/tapir/swagger/http4s/SwaggerHttp4s.scala b/docs/swagger-ui-http4s/src/main/scala/sttp/tapir/swagger/http4s/SwaggerHttp4s.scala index c420d15c3d..e1dc5fb16d 100644 --- a/docs/swagger-ui-http4s/src/main/scala/sttp/tapir/swagger/http4s/SwaggerHttp4s.scala +++ b/docs/swagger-ui-http4s/src/main/scala/sttp/tapir/swagger/http4s/SwaggerHttp4s.scala @@ -2,13 +2,11 @@ package sttp.tapir.swagger.http4s import java.util.Properties -import cats.effect.{Blocker, ContextShift, Sync} +import cats.effect.Sync import org.http4s.{HttpRoutes, StaticFile, Uri} import org.http4s.dsl.Http4sDsl import org.http4s.headers.Location -import scala.concurrent.ExecutionContext - /** Usage: add `new SwaggerHttp4s(yaml).routes[F]` to your http4s router. For example: * `Router("/" -> new SwaggerHttp4s(yaml).routes[IO])` * or, in combination with other routes: @@ -36,7 +34,7 @@ class SwaggerHttp4s( p.getProperty("version") } - def routes[F[_]: ContextShift: Sync]: HttpRoutes[F] = { + def routes[F[_]: Sync]: HttpRoutes[F] = { val dsl = Http4sDsl[F] import dsl._ @@ -54,10 +52,7 @@ class SwaggerHttp4s( Ok(yaml) case GET -> `rootPath` / swaggerResource => StaticFile - .fromResource[F]( - s"/META-INF/resources/webjars/swagger-ui/$swaggerVersion/$swaggerResource", - Blocker.liftExecutionContext(ExecutionContext.global) - ) + .fromResource[F](s"/META-INF/resources/webjars/swagger-ui/$swaggerVersion/$swaggerResource") .getOrElseF(NotFound()) } } diff --git a/docs/swagger-ui-http4s/src/test/scala/sttp/tapir/swagger/http4s/SwaggerHttp4sTest.scala b/docs/swagger-ui-http4s/src/test/scala/sttp/tapir/swagger/http4s/SwaggerHttp4sTest.scala index 327488b0a0..dad9346ac8 100644 --- a/docs/swagger-ui-http4s/src/test/scala/sttp/tapir/swagger/http4s/SwaggerHttp4sTest.scala +++ b/docs/swagger-ui-http4s/src/test/scala/sttp/tapir/swagger/http4s/SwaggerHttp4sTest.scala @@ -1,6 +1,7 @@ package sttp.tapir.swagger.http4s -import cats.effect.{ContextShift, IO} +import cats.effect.IO +import cats.effect.unsafe.implicits.global import cats.instances.option._ import cats.syntax.apply._ import cats.syntax.flatMap._ @@ -13,11 +14,8 @@ import org.scalatest.flatspec.AnyFlatSpecLike import org.scalatest.matchers.should.Matchers import org.typelevel.ci.CIString -import scala.concurrent.ExecutionContext - class SwaggerHttp4sTest extends AnyFlatSpecLike with Matchers with OptionValues { - implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) val yaml: String = "I love chocolate" val contextPath = List("i", "love", "chocolate") @@ -34,7 +32,8 @@ class SwaggerHttp4sTest extends AnyFlatSpecLike with Matchers with OptionValues val uri = uri"/i/love/chocolate" val expectedLocationHeader = uri.addPath("index.html").withQueryParam("url", s"$uri/$yamlName") - val response = swaggerDocs.routes + val response = swaggerDocs + .routes[IO] .run(Request(GET, uri)) .value .unsafeRunSync() @@ -42,14 +41,13 @@ class SwaggerHttp4sTest extends AnyFlatSpecLike with Matchers with OptionValues response.status shouldBe Status.PermanentRedirect response.headers.headers.find(_.name == CIString("Location")).map(_.value) shouldBe Some(expectedLocationHeader.toString) - } it should "return the yaml" in { - val uri = uri"/i/love/chocolate".addPath(yamlName) - val (response, body) = swaggerDocs.routes + val (response, body) = swaggerDocs + .routes[IO] .run(Request(GET, uri)) .value .mproduct(_.traverse(_.as[String])) @@ -59,7 +57,5 @@ class SwaggerHttp4sTest extends AnyFlatSpecLike with Matchers with OptionValues response.status shouldBe Status.Ok body shouldBe yaml - } - } diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index 25a13e9944..39279ebccd 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -1,45 +1,42 @@ package sttp.tapir.examples import cats.effect._ -import sttp.client3._ +import cats.syntax.all._ import org.http4s.HttpRoutes import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ +import sttp.client3._ import sttp.tapir._ import sttp.tapir.server.http4s.Http4sServerInterpreter -import cats.syntax.all._ import scala.concurrent.ExecutionContext -object HelloWorldHttp4sServer extends App { +object HelloWorldHttp4sServer extends IOApp { // the endpoint: single fixed path input ("hello"), single query parameter // corresponds to: GET /hello?name=... val helloWorld: Endpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) - // mandatory implicits - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) - // converting an endpoint to a route (providing server-side logic); extension method comes from imported packages val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(helloWorld)(name => IO(s"Hello, $name!".asRight[Unit])) - // starting the server - - BlazeServerBuilder[IO](ec) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) - .resource - .use { _ => - IO { - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() - val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/hello?name=Frodo").send(backend).body - println("Got result: " + result) + implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - assert(result == "Hello, Frodo!") + override def run(args: List[String]): IO[ExitCode] = { + // starting the server + BlazeServerBuilder[IO](ec) + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) + .resource + .use { _ => + IO { + val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() + val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/hello?name=Frodo").send(backend).body + println("Got result: " + result) + assert(result == "Hello, Frodo!") + } } - } - .unsafeRunSync() + .as(ExitCode.Success) + } } diff --git a/examples/src/main/scala/sttp/tapir/examples/Http4sClientExample.scala b/examples/src/main/scala/sttp/tapir/examples/Http4sClientExample.scala index c486aa40fa..a8edd68f2e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/Http4sClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/Http4sClientExample.scala @@ -1,6 +1,6 @@ package sttp.tapir.examples -import cats.effect.{Blocker, ExitCode, IO, IOApp} +import cats.effect.{ExitCode, IO, IOApp} import com.typesafe.scalalogging.StrictLogging import io.circe.generic.auto._ import sttp.tapir._ @@ -9,8 +9,6 @@ import sttp.tapir.generic.auto._ import sttp.tapir.json.circe._ object Http4sClientExample extends IOApp with StrictLogging { - // The interpreter needs a Blocker instance in order to evaluate certain types of request/response bodies. - private implicit val blocker: Blocker = Blocker.liftExecutionContext(super.executionContext) case class User(id: Int, name: String) diff --git a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala index 17b6003488..f58ffc8a17 100644 --- a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala @@ -1,25 +1,25 @@ package sttp.tapir.examples -import java.util.concurrent.atomic.AtomicReference import cats.effect._ import cats.syntax.all._ import io.circe.generic.auto._ import org.http4s.HttpRoutes -import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router import org.http4s.syntax.kleisli._ import sttp.tapir._ import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter -import sttp.tapir.json.circe._ import sttp.tapir.generic.auto._ +import sttp.tapir.json.circe._ import sttp.tapir.openapi.OpenAPI import sttp.tapir.openapi.circe.yaml._ import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.swagger.http4s.SwaggerHttp4s +import java.util.concurrent.atomic.AtomicReference import scala.concurrent.ExecutionContext -object MultipleEndpointsDocumentationHttp4sServer extends App { +object MultipleEndpointsDocumentationHttp4sServer extends IOApp { // endpoint descriptions case class Author(name: String) case class Book(title: String, year: Int, author: Author) @@ -40,8 +40,6 @@ object MultipleEndpointsDocumentationHttp4sServer extends App { // server-side logic implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) val books = new AtomicReference( Vector( @@ -63,17 +61,19 @@ object MultipleEndpointsDocumentationHttp4sServer extends App { val openApiDocs: OpenAPI = OpenAPIDocsInterpreter().toOpenAPI(List(booksListing, addBook), "The tapir library", "1.0.0") val openApiYml: String = openApiDocs.toYaml - // starting the server - BlazeServerBuilder[IO](ec) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> (routes <+> new SwaggerHttp4s(openApiYml).routes[IO])).orNotFound) - .resource - .use { _ => - IO { - println("Go to: http://localhost:8080/docs") - println("Press any key to exit ...") - scala.io.StdIn.readLine() + override def run(args: List[String]): IO[ExitCode] = { + // starting the server + BlazeServerBuilder[IO](ec) + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> (routes <+> new SwaggerHttp4s(openApiYml).routes[IO])).orNotFound) + .resource + .use { _ => + IO { + println("Go to: http://localhost:8080/docs") + println("Press any key to exit ...") + scala.io.StdIn.readLine() + } } - } - .unsafeRunSync() + .as(ExitCode.Success) + } } diff --git a/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala index d72a9b598a..8159bf868b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala @@ -20,11 +20,10 @@ import java.time.Instant import scala.collection.immutable.ListMap import scala.concurrent.ExecutionContext -object OAuth2GithubHttp4sServer extends App { +object OAuth2GithubHttp4sServer extends IOApp { implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) + // github application details val clientId = "" val clientSecret = "" @@ -67,7 +66,8 @@ object OAuth2GithubHttp4sServer extends App { // converting endpoints to routes // simply redirect to github auth service - val loginRoute: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(login)(_ => IO(s"$authorizationUrl?client_id=$clientId".asRight[Unit])) + val loginRoute: HttpRoutes[IO] = + Http4sServerInterpreter[IO]().toRoutes(login)(_ => IO(s"$authorizationUrl?client_id=$clientId".asRight[Unit])) // after successful authorization github redirects you here def loginGithubRoute(backend: SttpBackend[IO, Any]): HttpRoutes[IO] = @@ -107,20 +107,22 @@ object OAuth2GithubHttp4sServer extends App { val httpClient = AsyncHttpClientCatsBackend.resource[IO]() - // starting the server - httpClient - .use(backend => - BlazeServerBuilder[IO](ec) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> (secretPlaceRoute <+> loginRoute <+> loginGithubRoute(backend))).orNotFound) - .resource - .use { _ => - IO { - println("Go to: http://localhost:8080") - println("Press any key to exit ...") - scala.io.StdIn.readLine() + override def run(args: List[String]): IO[ExitCode] = { + // starting the server + httpClient + .use(backend => + BlazeServerBuilder[IO](ec) + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> (secretPlaceRoute <+> loginRoute <+> loginGithubRoute(backend))).orNotFound) + .resource + .use { _ => + IO { + println("Go to: http://localhost:8080") + println("Press any key to exit ...") + scala.io.StdIn.readLine() + } } - } - ) - .unsafeRunSync() + ) + .as(ExitCode.Success) + } } diff --git a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala index 1ea0eb38f2..456ee1c197 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala @@ -1,24 +1,24 @@ package sttp.tapir.examples -import java.nio.charset.StandardCharsets import cats.effect._ import cats.syntax.all._ +import fs2._ import org.http4s.HttpRoutes import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ +import sttp.capabilities.fs2.Fs2Streams import sttp.client3._ +import sttp.model.HeaderNames import sttp.tapir._ import sttp.tapir.server.http4s.Http4sServerInterpreter -import fs2._ -import sttp.capabilities.fs2.Fs2Streams -import sttp.model.HeaderNames +import java.nio.charset.StandardCharsets import scala.concurrent.ExecutionContext import scala.concurrent.duration._ // https://github.com/softwaremill/tapir/issues/367 -object StreamingHttp4sFs2Server extends App { +object StreamingHttp4sFs2Server extends IOApp { // corresponds to: GET /receive?name=... // We need to provide both the schema of the value (for documentation), as well as the format (media type) of the // body. Here, the schema is a `string` and the media type is `text/plain`. @@ -29,8 +29,6 @@ object StreamingHttp4sFs2Server extends App { // mandatory implicits implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) // converting an endpoint to a route (providing server-side logic); extension method comes from imported packages val streamingRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(streamingEndpoint) { _ => @@ -47,19 +45,21 @@ object StreamingHttp4sFs2Server extends App { .map(s => Right((size, s))) } - // starting the server - BlazeServerBuilder[IO](ec) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> streamingRoutes).orNotFound) - .resource - .use { _ => - IO { - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() - val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/receive").send(backend).body - println("Got result: " + result) + override def run(args: List[String]): IO[ExitCode] = { + // starting the server + BlazeServerBuilder[IO](ec) + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> streamingRoutes).orNotFound) + .resource + .use { _ => + IO { + val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() + val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/receive").send(backend).body + println("Got result: " + result) - assert(result == "abcd" * 25) + assert(result == "abcd" * 25) + } } - } - .unsafeRunSync() + .as(ExitCode.Success) + } } diff --git a/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala index 23cb40ee0d..5a2659014a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala @@ -1,6 +1,6 @@ package sttp.tapir.examples -import cats.effect.{Blocker, ContextShift, IO, Timer} +import cats.effect.{ExitCode, IO, IOApp} import io.circe.generic.auto._ import fs2._ import org.http4s.HttpRoutes @@ -9,13 +9,13 @@ import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams -import sttp.tapir.generic.auto._ import sttp.client3._ import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend import sttp.tapir._ -import sttp.tapir.docs.asyncapi.AsyncAPIInterpreter import sttp.tapir.asyncapi.Server import sttp.tapir.asyncapi.circe.yaml._ +import sttp.tapir.docs.asyncapi.AsyncAPIInterpreter +import sttp.tapir.generic.auto._ import sttp.tapir.json.circe._ import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.ws.WebSocket @@ -23,14 +23,9 @@ import sttp.ws.WebSocket import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -object WebSocketHttp4sServer extends App { - // mandatory implicits - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) - val blocker: Blocker = Blocker.liftExecutionContext(ec) +object WebSocketHttp4sServer extends IOApp { - // + implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global case class CountResponse(received: Int) @@ -73,40 +68,42 @@ object WebSocketHttp4sServer extends App { val apiDocs = AsyncAPIInterpreter().toAsyncAPI(wsEndpoint, "Byte counter", "1.0", List("dev" -> Server("localhost:8080", "ws"))).toYaml println(s"Paste into https://playground.asyncapi.io/ to see the docs for this endpoint:\n$apiDocs") - // Starting the server - BlazeServerBuilder[IO](ec) - .bindHttp(8080, "localhost") - .withHttpApp(Router("/" -> wsRoutes).orNotFound) - .resource - .flatMap(_ => AsyncHttpClientFs2Backend.resource[IO](blocker)) - .use { backend => - // Client which interacts with the web socket - basicRequest - .response(asWebSocket { (ws: WebSocket[IO]) => - for { - _ <- ws.sendText("7 bytes") - _ <- ws.sendText("7 bytes") - r1 <- ws.receiveText() - _ = println(r1) - _ <- ws.sendText("10 bytes") - _ <- ws.sendText("12 bytes") - r2 <- ws.receiveText() - _ = println(r2) - _ <- IO.sleep(3.seconds) - _ <- ws.sendText("7 bytes") - r3 <- ws.receiveText() - r4 <- ws.receiveText() - r5 <- ws.receiveText() - r6 <- ws.receiveText() - _ = println(r3) - _ = println(r4) - _ = println(r5) - _ = println(r6) - } yield () - }) - .get(uri"ws://localhost:8080/count") - .send(backend) - .map(_ => println("Counting complete, bye!")) - } - .unsafeRunSync() + override def run(args: List[String]): IO[ExitCode] = { + // Starting the server + BlazeServerBuilder[IO](ec) + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> wsRoutes).orNotFound) + .resource + .flatMap(_ => AsyncHttpClientFs2Backend.resource[IO]()) + .use { backend => + // Client which interacts with the web socket + basicRequest + .response(asWebSocket { ws: WebSocket[IO] => + for { + _ <- ws.sendText("7 bytes") + _ <- ws.sendText("7 bytes") + r1 <- ws.receiveText() + _ = println(r1) + _ <- ws.sendText("10 bytes") + _ <- ws.sendText("12 bytes") + r2 <- ws.receiveText() + _ = println(r2) + _ <- IO.sleep(3.seconds) + _ <- ws.sendText("7 bytes") + r3 <- ws.receiveText() + r4 <- ws.receiveText() + r5 <- ws.receiveText() + r6 <- ws.receiveText() + _ = println(r3) + _ = println(r4) + _ = println(r5) + _ = println(r6) + } yield () + }) + .get(uri"ws://localhost:8080/count") + .send(backend) + .map(_ => println("Counting complete, bye!")) + } + .as(ExitCode.Success) + } } diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index 739984e02c..73200395e1 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -6,6 +6,7 @@ import org.http4s._ import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ +import zio.blocking.Blocking import sttp.tapir.generic.auto._ import sttp.tapir.json.circe._ import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter @@ -47,12 +48,13 @@ object ZioEnvExampleHttp4sServer extends App { val petEndpoint: ZEndpoint[Int, String, Pet] = endpoint.get.in("pet" / path[Int]("petId")).errorOut(stringBody).out(jsonBody[Pet]) - val petRoutes: HttpRoutes[RIO[PetService with Clock, *]] = + val petRoutes: HttpRoutes[RIO[PetService with Clock with Blocking, *]] = ZHttp4sServerInterpreter().from(petEndpoint)(petId => PetService.find(petId)).toRoutes // Same as above, but combining endpoint description with server logic: val petServerEndpoint: ZServerEndpoint[PetService, Int, String, Pet] = petEndpoint.zServerLogic(petId => PetService.find(petId)) - val petServerRoutes: HttpRoutes[RIO[PetService with Clock, *]] = ZHttp4sServerInterpreter().from(List(petServerEndpoint)).toRoutes + val petServerRoutes: HttpRoutes[RIO[PetService with Clock with Blocking, *]] = + ZHttp4sServerInterpreter().from(List(petServerEndpoint)).toRoutes // Documentation val yaml: String = { @@ -63,7 +65,7 @@ object ZioEnvExampleHttp4sServer extends App { // Starting the server val serve: ZIO[ZEnv with PetService, Throwable, Unit] = ZIO.runtime[ZEnv with PetService].flatMap { implicit runtime => - BlazeServerBuilder[RIO[PetService with Clock, *]](runtime.platform.executor.asEC) + BlazeServerBuilder[RIO[PetService with Clock with Blocking, *]](runtime.platform.executor.asEC) .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> (petRoutes <+> new SwaggerHttp4s(yaml).routes)).orNotFound) .serve diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index 2b3f6488d8..d05afe7200 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -12,6 +12,7 @@ import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import sttp.tapir.swagger.http4s.SwaggerHttp4s import sttp.tapir.ztapir._ import zio.clock.Clock +import zio.blocking.Blocking import zio.interop.catz._ import zio.{App, ExitCode, IO, RIO, UIO, URIO, ZEnv, ZIO} @@ -22,7 +23,7 @@ object ZioExampleHttp4sServer extends App { val petEndpoint: ZEndpoint[Int, String, Pet] = endpoint.get.in("pet" / path[Int]("petId")).errorOut(stringBody).out(jsonBody[Pet]) - val petRoutes: HttpRoutes[RIO[Clock, *]] = ZHttp4sServerInterpreter() + val petRoutes: HttpRoutes[RIO[Clock with Blocking, *]] = ZHttp4sServerInterpreter() .from(petEndpoint) { petId => if (petId == 35) { UIO(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) @@ -40,7 +41,7 @@ object ZioExampleHttp4sServer extends App { IO.fail("Unknown pet id") } } - val petServerRoutes: HttpRoutes[RIO[Clock, *]] = ZHttp4sServerInterpreter().from(petServerEndpoint).toRoutes + val petServerRoutes: HttpRoutes[RIO[Clock with Blocking, *]] = ZHttp4sServerInterpreter().from(petServerEndpoint).toRoutes // @@ -53,7 +54,7 @@ object ZioExampleHttp4sServer extends App { // Starting the server val serve: ZIO[ZEnv, Throwable, Unit] = ZIO.runtime[ZEnv].flatMap { implicit runtime => // This is needed to derive cats-effect instances for that are needed by http4s - BlazeServerBuilder[RIO[Clock, *]](runtime.platform.executor.asEC) + BlazeServerBuilder[RIO[Clock with Blocking, *]](runtime.platform.executor.asEC) .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> (petRoutes <+> new SwaggerHttp4s(yaml).routes)).orNotFound) .serve diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index aecd42cd22..3af3733847 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -10,6 +10,7 @@ import sttp.tapir.examples.UserAuthenticationLayer._ import sttp.tapir.server.http4s.ztapir._ import sttp.tapir.ztapir._ import zio._ +import zio.blocking.Blocking import zio.clock.Clock import zio.console._ import zio.interop.catz._ @@ -56,7 +57,7 @@ object ZioPartialServerLogicHttp4s extends App { // --- // interpreting as routes - val helloWorldRoutes: HttpRoutes[RIO[UserService with Console with Clock, *]] = + val helloWorldRoutes: HttpRoutes[RIO[UserService with Console with Clock with Blocking, *]] = ZHttp4sServerInterpreter().from(List(secureHelloWorld1WithLogic, secureHelloWorld2WithLogic)).toRoutes // testing @@ -89,8 +90,8 @@ object ZioPartialServerLogicHttp4s extends App { override def run(args: List[String]): URIO[ZEnv, ExitCode] = ZIO.runtime - .flatMap { implicit runtime: Runtime[ZEnv with UserService with Console] => - BlazeServerBuilder[RIO[UserService with Console with Clock, *]](runtime.platform.executor.asEC) + .flatMap { implicit runtime: Runtime[ZEnv & UserService & Console] => + BlazeServerBuilder[RIO[UserService & Console & Clock & Blocking, *]](runtime.platform.executor.asEC) .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) .resource diff --git a/integrations/cats/src/main/scala/sttp/tapir/integ/cats/CatsMonadError.scala b/integrations/cats/src/main/scala/sttp/tapir/integ/cats/CatsMonadError.scala index 7c642abbd5..513aaa2031 100644 --- a/integrations/cats/src/main/scala/sttp/tapir/integ/cats/CatsMonadError.scala +++ b/integrations/cats/src/main/scala/sttp/tapir/integ/cats/CatsMonadError.scala @@ -10,7 +10,7 @@ class CatsMonadError[F[_]](implicit F: Sync[F]) extends MonadError[F] { override def error[T](t: Throwable): F[T] = F.raiseError(t) override protected def handleWrappedError[T](rt: F[T])(h: PartialFunction[Throwable, F[T]]): F[T] = F.recoverWith(rt)(h) override def eval[T](t: => T): F[T] = F.delay(t) - override def suspend[T](t: => F[T]): F[T] = F.suspend(t) + override def suspend[T](t: => F[T]): F[T] = F.defer(t) override def flatten[T](ffa: F[F[T]]): F[T] = F.flatten(ffa) - override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guarantee(f)(e) -} \ No newline at end of file + override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guaranteeCase(f)(_ => e) +} diff --git a/project/Versions.scala b/project/Versions.scala index 236a7b5bfb..946f340de8 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,6 +1,6 @@ object Versions { - val http4s = "0.22.0-RC1" - val catsEffect = "2.5.1" + val http4s = "0.23.0-RC1" + val catsEffect = "3.1.1" val circe = "0.14.1" val circeYaml = "0.14.0" val sttp = "3.3.11" @@ -21,7 +21,7 @@ object Versions { val refined = "0.9.26" val enumeratum = "1.7.0" val zio = "1.0.9" - val zioInteropCats = "2.5.1.0" + val zioInteropCats = "3.1.1.0" val zioJson = "0.1.5" val playClient = "2.1.3" val playServer = "2.8.7" diff --git a/project/build.properties b/project/build.properties index 77df8ac33b..bb5389da21 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.5.4 \ No newline at end of file +sbt.version=1.5.5 \ No newline at end of file diff --git a/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala b/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala index 3e060a6b5e..a5ccd832de 100644 --- a/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala +++ b/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala @@ -5,6 +5,7 @@ import akka.http.scaladsl.server.Directives import akka.stream.scaladsl.{Flow, Source} import cats.data.NonEmptyList import cats.effect.{IO, Resource} +import cats.effect.unsafe.implicits.global import cats.implicits._ import org.scalatest.EitherValues import org.scalatest.matchers.should.Matchers._ @@ -17,7 +18,16 @@ import sttp.model.sse.ServerSentEvent import sttp.monad.FutureMonad import sttp.monad.syntax._ import sttp.tapir._ -import sttp.tapir.server.tests.{DefaultCreateServerTest, ServerAuthenticationTests, ServerBasicTests, ServerFileMultipartTests, ServerMetricsTest, ServerStreamingTests, ServerWebSocketTests, backendResource} +import sttp.tapir.server.tests.{ + DefaultCreateServerTest, + ServerAuthenticationTests, + ServerBasicTests, + ServerFileMultipartTests, + ServerMetricsTest, + ServerStreamingTests, + ServerWebSocketTests, + backendResource +} import sttp.tapir.tests.{Test, TestSuite} import java.util.UUID @@ -48,7 +58,7 @@ class AkkaHttpServerTest extends TestSuite with EitherValues { .use { port => basicRequest.get(uri"http://localhost:$port/api/test/directive").send(backend).map(_.body shouldBe Right("ok")) } - .unsafeRunSync() + .unsafeToFuture() }, Test("Send and receive SSE") { implicit val ec = actorSystem.dispatcher @@ -76,7 +86,7 @@ class AkkaHttpServerTest extends TestSuite with EitherValues { ) } } - .unsafeRunSync() + .unsafeToFuture() } ) diff --git a/server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerInterpreter.scala b/server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerInterpreter.scala index ef5ec5f60f..6a42e4ddb0 100644 --- a/server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerInterpreter.scala +++ b/server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerInterpreter.scala @@ -1,57 +1,91 @@ package sttp.tapir.server.finatra.cats -import cats.effect.Effect +import cats.effect.Async +import cats.effect.std.Dispatcher import com.twitter.inject.Logging -import io.catbird.util.Rerunnable -import io.catbird.util.effect._ +import com.twitter.util.{Future, Promise} import sttp.monad.MonadError import sttp.tapir.Endpoint import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.finatra.{FinatraRoute, FinatraServerInterpreter, FinatraServerOptions} +import scala.concurrent.{ExecutionContext, Future => ScalaFuture} import scala.reflect.ClassTag +import scala.util.{Failure, Success} -trait FinatraCatsServerInterpreter extends Logging { +trait FinatraCatsServerInterpreter[F[_]] extends Logging { - def finatraServerOptions: FinatraServerOptions = FinatraServerOptions.default + implicit def fa: Async[F] - def toRoute[I, E, O, F[_]]( + def finatraCatsServerOptions: FinatraCatsServerOptions[F] + + def toRoute[I, E, O]( e: Endpoint[I, E, O, Any] - )(logic: I => F[Either[E, O]])(implicit eff: Effect[F]): FinatraRoute = { + )(logic: I => F[Either[E, O]]): FinatraRoute = { toRoute(e.serverLogic(logic)) } - def toRouteRecoverErrors[I, E, O, F[_]](e: Endpoint[I, E, O, Any])(logic: I => F[O])(implicit + def toRouteRecoverErrors[I, E, O](e: Endpoint[I, E, O, Any])(logic: I => F[O])(implicit eIsThrowable: E <:< Throwable, - eClassTag: ClassTag[E], - eff: Effect[F] + eClassTag: ClassTag[E] ): FinatraRoute = { toRoute(e.serverLogicRecoverErrors(logic)) } - def toRoute[I, E, O, F[_]]( + def toRoute[I, E, O]( e: ServerEndpoint[I, E, O, Any, F] - )(implicit eff: Effect[F]): FinatraRoute = { - FinatraServerInterpreter(finatraServerOptions).toRoute(e.endpoint.serverLogic(i => eff.toIO(e.logic(new CatsMonadError)(i)).to[Rerunnable].run)) + ): FinatraRoute = { + FinatraServerInterpreter( + FinatraServerOptions(finatraCatsServerOptions.createFile, finatraCatsServerOptions.deleteFile, finatraCatsServerOptions.interceptors) + ).toRoute( + e.endpoint.serverLogic { i => + val scalaFutureResult = finatraCatsServerOptions.dispatcher.unsafeToFuture(e.logic(CatsMonadError)(i)) + scalaFutureResult.asTwitter(cats.effect.unsafe.implicits.global.compute) + } + ) + } + + private object CatsMonadError extends MonadError[F] { + override def unit[T](t: T): F[T] = Async[F].pure(t) + override def map[T, T2](ft: F[T])(f: T => T2): F[T2] = Async[F].map(ft)(f) + override def flatMap[T, T2](ft: F[T])(f: T => F[T2]): F[T2] = Async[F].flatMap(ft)(f) + override def error[T](t: Throwable): F[T] = Async[F].raiseError(t) + override protected def handleWrappedError[T](rt: F[T])(h: PartialFunction[Throwable, F[T]]): F[T] = Async[F].recoverWith(rt)(h) + override def eval[T](t: => T): F[T] = Async[F].delay(t) + override def suspend[T](t: => F[T]): F[T] = Async[F].defer(t) + override def flatten[T](ffa: F[F[T]]): F[T] = Async[F].flatten(ffa) + override def ensure[T](f: F[T], e: => F[Unit]): F[T] = Async[F].guaranteeCase(f)(_ => e) } - private class CatsMonadError[F[_]](implicit F: Effect[F]) extends MonadError[F] { - override def unit[T](t: T): F[T] = F.pure(t) - override def map[T, T2](fa: F[T])(f: T => T2): F[T2] = F.map(fa)(f) - override def flatMap[T, T2](fa: F[T])(f: T => F[T2]): F[T2] = F.flatMap(fa)(f) - override def error[T](t: Throwable): F[T] = F.raiseError(t) - override protected def handleWrappedError[T](rt: F[T])(h: PartialFunction[Throwable, F[T]]): F[T] = F.recoverWith(rt)(h) - override def eval[T](t: => T): F[T] = F.delay(t) - override def suspend[T](t: => F[T]): F[T] = F.defer(t) - override def flatten[T](ffa: F[F[T]]): F[T] = F.flatten(ffa) - override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guarantee(f)(e) + /** Convert from a Scala Future to a Twitter Future + * Source: https://twitter.github.io/util/guide/util-cookbook/futures.html + */ + private implicit class RichScalaFuture[A](val sf: ScalaFuture[A]) { + def asTwitter(implicit e: ExecutionContext): Future[A] = { + val promise: Promise[A] = new Promise[A]() + sf.onComplete { + case Success(value) => promise.setValue(value) + case Failure(exception) => promise.setException(exception) + } + promise + } } } object FinatraCatsServerInterpreter { - def apply(serverOptions: FinatraServerOptions = FinatraServerOptions.default): FinatraCatsServerInterpreter = { - new FinatraCatsServerInterpreter { - override def finatraServerOptions: FinatraServerOptions = serverOptions + def apply[F[_]]( + dispatcher: Dispatcher[F] + )(implicit _fa: Async[F]): FinatraCatsServerInterpreter[F] = { + new FinatraCatsServerInterpreter[F] { + override implicit def fa: Async[F] = _fa + override def finatraCatsServerOptions: FinatraCatsServerOptions[F] = FinatraCatsServerOptions.default(dispatcher) + } + } + + def apply[F[_]](serverOptions: FinatraCatsServerOptions[F])(implicit _fa: Async[F]): FinatraCatsServerInterpreter[F] = { + new FinatraCatsServerInterpreter[F] { + override implicit def fa: Async[F] = _fa + override def finatraCatsServerOptions: FinatraCatsServerOptions[F] = serverOptions } } } diff --git a/server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerOptions.scala b/server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerOptions.scala new file mode 100644 index 0000000000..f0c00a2fa7 --- /dev/null +++ b/server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerOptions.scala @@ -0,0 +1,65 @@ +package sttp.tapir.server.finatra.cats + +import cats.effect.std.Dispatcher +import com.twitter.util.Future +import com.twitter.util.logging.Logging +import sttp.tapir.TapirFile +import sttp.tapir.server.finatra.{FinatraContent, FinatraServerOptions} +import sttp.tapir.server.interceptor.Interceptor +import sttp.tapir.server.interceptor.content.UnsupportedMediaTypeInterceptor +import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DecodeFailureInterceptor, DefaultDecodeFailureHandler} +import sttp.tapir.server.interceptor.exception.{DefaultExceptionHandler, ExceptionHandler, ExceptionInterceptor} +import sttp.tapir.server.interceptor.log.{ServerLog, ServerLogInterceptor} +import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor + +case class FinatraCatsServerOptions[F[_]]( + dispatcher: Dispatcher[F], + createFile: Array[Byte] => Future[TapirFile], + deleteFile: TapirFile => Future[Unit], + interceptors: List[Interceptor[Future, FinatraContent]] +) + +object FinatraCatsServerOptions extends Logging { + + /** Creates default [[FinatraCatsServerOptions]] with custom interceptors, sitting between two interceptor groups: + * 1. the optional exception interceptor and the optional logging interceptor (which should typically be first + * when processing the request, and last when processing the response)), + * 2. the optional unsupported media type interceptor and the decode failure handling interceptor (which should + * typically be last when processing the request). + * + * The options can be then further customised using copy constructors or the methods to append/prepend + * interceptors. + * + * @param serverLog The server log using which an interceptor will be created, if any. + * @param additionalInterceptors Additional interceptors, e.g. handling decode failures, or providing alternate + * responses. + * @param unsupportedMediaTypeInterceptor Whether to return 415 (unsupported media type) if there's no body in the + * endpoint's outputs, which can satisfy the constraints from the `Accept` + * header. + * @param decodeFailureHandler The decode failure handler, from which an interceptor will be created. + */ + def customInterceptors[F[_]]( + dispatcher: Dispatcher[F], + metricsInterceptor: Option[MetricsRequestInterceptor[Future, FinatraContent]] = None, + exceptionHandler: Option[ExceptionHandler] = Some(DefaultExceptionHandler), + serverLog: Option[ServerLog[Unit]] = Some(FinatraServerOptions.defaultServerLog), + additionalInterceptors: List[Interceptor[Future, FinatraContent]] = Nil, + unsupportedMediaTypeInterceptor: Option[UnsupportedMediaTypeInterceptor[Future, FinatraContent]] = Some( + new UnsupportedMediaTypeInterceptor() + ), + decodeFailureHandler: DecodeFailureHandler = DefaultDecodeFailureHandler.handler + ): FinatraCatsServerOptions[F] = + FinatraCatsServerOptions( + dispatcher, + FinatraServerOptions.defaultCreateFile(FinatraServerOptions.futurePool), + FinatraServerOptions.defaultDeleteFile(FinatraServerOptions.futurePool), + metricsInterceptor.toList ++ + exceptionHandler.map(new ExceptionInterceptor[Future, FinatraContent](_)).toList ++ + serverLog.map(sl => new ServerLogInterceptor[Unit, Future, FinatraContent](sl, (_: Unit, _) => Future.Done)).toList ++ + additionalInterceptors ++ + unsupportedMediaTypeInterceptor.toList ++ + List(new DecodeFailureInterceptor[Future, FinatraContent](decodeFailureHandler)) + ) + + def default[F[_]](dispatcher: Dispatcher[F]): FinatraCatsServerOptions[F] = customInterceptors(dispatcher) +} diff --git a/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraCatsTestServerInterpreter.scala b/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraCatsTestServerInterpreter.scala index 1d50dc728e..dbbd0b97a5 100644 --- a/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraCatsTestServerInterpreter.scala +++ b/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraCatsTestServerInterpreter.scala @@ -1,10 +1,11 @@ package sttp.tapir.server.finatra.cats import cats.data.NonEmptyList -import cats.effect.{ContextShift, IO, Resource, Timer} +import cats.effect.std.Dispatcher +import cats.effect.{IO, Resource} import sttp.tapir.Endpoint import sttp.tapir.server.ServerEndpoint -import sttp.tapir.server.finatra.{FinatraContent, FinatraRoute, FinatraServerOptions, FinatraTestServerInterpreter} +import sttp.tapir.server.finatra.{FinatraContent, FinatraRoute, FinatraTestServerInterpreter} import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.server.tests.TestServerInterpreter @@ -13,24 +14,25 @@ import sttp.tapir.tests.Port import scala.concurrent.ExecutionContext import scala.reflect.ClassTag -class FinatraCatsTestServerInterpreter extends TestServerInterpreter[IO, Any, FinatraRoute, FinatraContent] { +class FinatraCatsTestServerInterpreter(dispatcher: Dispatcher[IO]) extends TestServerInterpreter[IO, Any, FinatraRoute, FinatraContent] { implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) override def route[I, E, O]( e: ServerEndpoint[I, E, O, Any, IO], decodeFailureHandler: Option[DecodeFailureHandler] = None, metricsInterceptor: Option[MetricsRequestInterceptor[IO, FinatraContent]] = None ): FinatraRoute = { - val serverOptions: FinatraServerOptions = - FinatraServerOptions.customInterceptors(decodeFailureHandler = decodeFailureHandler.getOrElse(DefaultDecodeFailureHandler.handler)) - FinatraCatsServerInterpreter(serverOptions).toRoute(e) + val serverOptions: FinatraCatsServerOptions[IO] = + FinatraCatsServerOptions.customInterceptors( + dispatcher, + decodeFailureHandler = decodeFailureHandler.getOrElse(DefaultDecodeFailureHandler.handler) + ) + FinatraCatsServerInterpreter[IO](serverOptions).toRoute(e) } override def routeRecoverErrors[I, E <: Throwable, O](e: Endpoint[I, E, O, Any], fn: I => IO[O])(implicit eClassTag: ClassTag[E] - ): FinatraRoute = FinatraCatsServerInterpreter().toRouteRecoverErrors(e)(fn) + ): FinatraRoute = FinatraCatsServerInterpreter[IO](dispatcher).toRouteRecoverErrors(e)(fn) override def server(routes: NonEmptyList[FinatraRoute]): Resource[IO, Port] = FinatraTestServerInterpreter.server(routes) } diff --git a/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraServerCatsTests.scala b/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraServerCatsTests.scala index c0f2a755d6..5d0ed6181d 100644 --- a/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraServerCatsTests.scala +++ b/server/finatra-server/finatra-server-cats/src/test/scala/sttp/tapir/server/finatra/cats/FinatraServerCatsTests.scala @@ -2,14 +2,20 @@ package sttp.tapir.server.finatra.cats import cats.effect.{IO, Resource} import sttp.client3.impl.cats.CatsMonadAsyncError -import sttp.tapir.server.tests.{DefaultCreateServerTest, ServerAuthenticationTests, ServerBasicTests, ServerFileMultipartTests, backendResource} +import sttp.tapir.server.tests.{ + DefaultCreateServerTest, + ServerAuthenticationTests, + ServerBasicTests, + ServerFileMultipartTests, + backendResource +} import sttp.tapir.tests.{Test, TestSuite} class FinatraServerCatsTests extends TestSuite { override def tests: Resource[IO, List[Test]] = backendResource.map { backend => implicit val m: CatsMonadAsyncError[IO] = new CatsMonadAsyncError[IO]() - val interpreter = new FinatraCatsTestServerInterpreter() + val interpreter = new FinatraCatsTestServerInterpreter(dispatcher) val createTestServer = new DefaultCreateServerTest(backend, interpreter) new ServerBasicTests(createTestServer, interpreter).tests() ++ diff --git a/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerOptions.scala b/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerOptions.scala index a8cf4aa474..c7bd641240 100644 --- a/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerOptions.scala +++ b/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerOptions.scala @@ -73,7 +73,7 @@ object FinatraServerOptions extends Logging { def defaultDeleteFile(futurePool: FuturePool): TapirFile => Future[Unit] = file => { futurePool { Defaults.deleteFile()(file) } } - private lazy val futurePool = FuturePool.unboundedPool + private[finatra] lazy val futurePool = FuturePool.unboundedPool lazy val defaultServerLog: ServerLog[Unit] = DefaultServerLog( doLogWhenHandled = debugLog, diff --git a/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala b/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala index ef980bd9ad..24dc261440 100644 --- a/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala +++ b/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala @@ -1,7 +1,7 @@ package sttp.tapir.server.finatra import cats.data.NonEmptyList -import cats.effect.{ContextShift, IO, Resource, Timer} +import cats.effect.{IO, Resource} import com.twitter.finatra.http.routing.HttpRouter import com.twitter.finatra.http.{Controller, EmbeddedHttpServer, HttpServer} import com.twitter.util.Future @@ -10,6 +10,7 @@ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.server.tests.TestServerInterpreter +import sttp.tapir.server.ServerEndpoint import sttp.tapir.tests.Port import scala.concurrent.ExecutionContext @@ -17,10 +18,6 @@ import scala.concurrent.duration.DurationInt import scala.reflect.ClassTag class FinatraTestServerInterpreter extends TestServerInterpreter[Future, Any, FinatraRoute, FinatraContent] { - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) - override def route[I, E, O]( e: ServerEndpoint[I, E, O, Any, Future], decodeFailureHandler: Option[DecodeFailureHandler] = None, @@ -44,7 +41,7 @@ class FinatraTestServerInterpreter extends TestServerInterpreter[Future, Any, Fi } object FinatraTestServerInterpreter { - def server(routes: NonEmptyList[FinatraRoute])(implicit ioTimer: Timer[IO]): Resource[IO, Port] = { + def server(routes: NonEmptyList[FinatraRoute]): Resource[IO, Port] = { def waitUntilHealthy(s: EmbeddedHttpServer, count: Int): IO[EmbeddedHttpServer] = if (s.isHealthy) IO.pure(s) else if (count > 1000) IO.raiseError(new IllegalStateException("Server unhealthy")) diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sBodyListener.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sBodyListener.scala index b7689bb050..d177ab8ac2 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sBodyListener.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sBodyListener.scala @@ -1,6 +1,6 @@ package sttp.tapir.server.http4s -import cats.effect.ExitCase +import cats.effect.kernel.Resource.ExitCase._ import cats.{Applicative, ~>} import sttp.monad.MonadError import sttp.monad.syntax._ @@ -15,8 +15,8 @@ class Http4sBodyListener[F[_], G[_]](gToF: G ~> F)(implicit m: MonadError[G], a: case ws @ Left(_) => cb(Success(())).map(_ => ws) case Right(entity) => m.unit(Right(entity.onFinalizeCase { - case ExitCase.Completed | ExitCase.Canceled => gToF(cb(Success(()))) - case ExitCase.Error(ex) => gToF(cb(Failure(ex))) + case Succeeded | Canceled => gToF(cb(Success(()))) + case Errored(ex) => gToF(cb(Failure(ex))) })) } } diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala index b82501ad79..464393e045 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala @@ -1,9 +1,10 @@ package sttp.tapir.server.http4s -import cats.effect.{Blocker, ContextShift, Sync} +import cats.effect.{Async, Sync} import cats.syntax.all._ -import cats.~> +import cats.{Monad, ~>} import fs2.Chunk +import fs2.io.file.Files import org.http4s.headers.{`Content-Disposition`, `Content-Type`} import org.http4s.{Charset, EntityDecoder, Request, multipart} import sttp.capabilities.fs2.Fs2Streams @@ -14,7 +15,7 @@ import sttp.tapir.{RawBodyType, RawPart, TapirFile} import java.io.ByteArrayInputStream -private[http4s] class Http4sRequestBody[F[_]: Sync: ContextShift, G[_]: Sync]( // TODO: constraints? +private[http4s] class Http4sRequestBody[F[_]: Async, G[_]: Monad]( request: Request[F], serverRequest: ServerRequest, serverOptions: Http4sServerOptions[F, G], @@ -26,7 +27,7 @@ private[http4s] class Http4sRequestBody[F[_]: Sync: ContextShift, G[_]: Sync]( / private def toRawFromStream[R](body: fs2.Stream[F, Byte], bodyType: RawBodyType[R], charset: Option[Charset]): G[RawValue[R]] = { def asChunk: G[Chunk[Byte]] = t(body.compile.to(Chunk)) - def asByteArray: G[Array[Byte]] = t(body.compile.to(Chunk).map(_.toByteBuffer.array())) + def asByteArray: G[Array[Byte]] = t(body.compile.to(Chunk).map(_.toArray[Byte])) bodyType match { case RawBodyType.StringBody(defaultCharset) => @@ -36,7 +37,7 @@ private[http4s] class Http4sRequestBody[F[_]: Sync: ContextShift, G[_]: Sync]( / case RawBodyType.InputStreamBody => asByteArray.map(b => RawValue(new ByteArrayInputStream(b))) case RawBodyType.FileBody => serverOptions.createFile(serverRequest).flatMap { file => - val fileSink = fs2.io.file.writeAll[F](file.toPath, Blocker.liftExecutionContext(serverOptions.blockingExecutionContext)) + val fileSink = Files[F].writeAll(file.toPath) t(body.through(fileSink).compile.drain.map(_ => RawValue(file, Seq(file)))) } case m: RawBodyType.MultipartBody => diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala index ab2453b578..493c289709 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala @@ -1,7 +1,7 @@ package sttp.tapir.server.http4s import cats.arrow.FunctionK -import cats.effect.{Concurrent, ContextShift, Sync, Timer} +import cats.effect.{Async, Sync} import cats.~> import org.http4s._ import sttp.capabilities.WebSockets @@ -12,7 +12,6 @@ import sttp.tapir.server.ServerEndpoint import scala.reflect.ClassTag trait Http4sServerInterpreter[F[_]] extends Http4sServerToHttpInterpreter[F, F] { - def toRoutes[I, E, O](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])( logic: I => F[Either[E, O]] ): HttpRoutes[F] = toRoutes( @@ -32,13 +31,10 @@ trait Http4sServerInterpreter[F[_]] extends Http4sServerToHttpInterpreter[F, F] } object Http4sServerInterpreter { - def apply[F[_]]()(implicit _fs: Concurrent[F], _fcs: ContextShift[F], _timer: Timer[F]): Http4sServerInterpreter[F] = { + def apply[F[_]]()(implicit _fa: Async[F]): Http4sServerInterpreter[F] = { new Http4sServerInterpreter[F] { - override implicit def gs: Sync[F] = _fs - override implicit def gcs: ContextShift[F] = _fcs - override implicit def fs: Concurrent[F] = _fs - override implicit def fcs: ContextShift[F] = _fcs - override implicit def timer: Timer[F] = _timer + override implicit def gs: Sync[F] = _fa + override implicit def fa: Async[F] = _fa override def fToG: F ~> F = FunctionK.id[F] override def gToF: F ~> F = FunctionK.id[F] } @@ -46,13 +42,10 @@ object Http4sServerInterpreter { def apply[F[_]]( serverOptions: Http4sServerOptions[F, F] - )(implicit _fs: Concurrent[F], _fcs: ContextShift[F], _timer: Timer[F]): Http4sServerInterpreter[F] = { + )(implicit _fa: Async[F]): Http4sServerInterpreter[F] = { new Http4sServerInterpreter[F] { - override implicit def gs: Sync[F] = _fs - override implicit def gcs: ContextShift[F] = _fcs - override implicit def fs: Concurrent[F] = _fs - override implicit def fcs: ContextShift[F] = _fcs - override implicit def timer: Timer[F] = _timer + override implicit def gs: Sync[F] = _fa + override implicit def fa: Async[F] = _fa override def fToG: F ~> F = FunctionK.id[F] override def gToF: F ~> F = FunctionK.id[F] override def http4sServerOptions: Http4sServerOptions[F, F] = serverOptions diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala index 76c968288e..d608a284c2 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala @@ -1,7 +1,7 @@ package sttp.tapir.server.http4s import cats.Applicative -import cats.effect.{ContextShift, Sync} +import cats.effect.Sync import cats.implicits.catsSyntaxOptionId import sttp.tapir.model.ServerRequest import sttp.tapir.server.interceptor.Interceptor @@ -12,7 +12,7 @@ import sttp.tapir.server.interceptor.log.{DefaultServerLog, ServerLog, ServerLog import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.{Defaults, TapirFile} -import scala.concurrent.ExecutionContext +import java.io.File /** @tparam F The effect type used for response body streams. Usually the same as `G`. * @tparam G The effect type used for representing arbitrary side-effects, such as creating files or logging. @@ -21,7 +21,6 @@ import scala.concurrent.ExecutionContext case class Http4sServerOptions[F[_], G[_]]( createFile: ServerRequest => G[TapirFile], deleteFile: TapirFile => G[Unit], - blockingExecutionContext: ExecutionContext, ioChunkSize: Int, interceptors: List[Interceptor[G, Http4sResponseBody[F]]] ) { @@ -52,20 +51,18 @@ object Http4sServerOptions { * header * @param decodeFailureHandler The decode failure handler, from which an interceptor will be created. */ - def customInterceptors[F[_], G[_]: Sync: ContextShift]( + def customInterceptors[F[_], G[_]: Sync]( exceptionHandler: Option[ExceptionHandler], serverLog: Option[ServerLog[G[Unit]]], metricsInterceptor: Option[MetricsRequestInterceptor[G, Http4sResponseBody[F]]] = None, additionalInterceptors: List[Interceptor[G, Http4sResponseBody[F]]] = Nil, unsupportedMediaTypeInterceptor: Option[UnsupportedMediaTypeInterceptor[G, Http4sResponseBody[F]]] = new UnsupportedMediaTypeInterceptor[G, Http4sResponseBody[F]]().some, - decodeFailureHandler: DecodeFailureHandler = DefaultDecodeFailureHandler.handler, - blockingExecutionContext: ExecutionContext = ExecutionContext.Implicits.global + decodeFailureHandler: DecodeFailureHandler = DefaultDecodeFailureHandler.handler ): Http4sServerOptions[F, G] = Http4sServerOptions( - defaultCreateFile[G].apply(blockingExecutionContext), - defaultDeleteFile[G].apply(blockingExecutionContext), - blockingExecutionContext, + defaultCreateFile[G], + defaultDeleteFile[G], 8192, metricsInterceptor.toList ++ exceptionHandler.map(new ExceptionInterceptor[G, Http4sResponseBody[F]](_)).toList ++ @@ -75,11 +72,9 @@ object Http4sServerOptions { List(new DecodeFailureInterceptor[G, Http4sResponseBody[F]](decodeFailureHandler)) ) - def defaultCreateFile[F[_]](implicit sync: Sync[F], cs: ContextShift[F]): ExecutionContext => ServerRequest => F[TapirFile] = - ec => _ => cs.evalOn(ec)(sync.delay(Defaults.createTempFile())) + def defaultCreateFile[F[_]](implicit sync: Sync[F]): ServerRequest => F[File] = _ => sync.blocking(Defaults.createTempFile()) - def defaultDeleteFile[F[_]](implicit sync: Sync[F], cs: ContextShift[F]): ExecutionContext => TapirFile => F[Unit] = - ec => file => cs.evalOn(ec)(sync.delay(Defaults.deleteFile()(file))) + def defaultDeleteFile[F[_]](implicit sync: Sync[F]): TapirFile => F[Unit] = file => sync.blocking(Defaults.deleteFile()(file)) object Log { def defaultServerLog[F[_]: Sync]: DefaultServerLog[F[Unit]] = @@ -100,6 +95,6 @@ object Http4sServerOptions { } } - def default[F[_], G[_]: Sync: ContextShift]: Http4sServerOptions[F, G] = + def default[F[_], G[_]: Sync]: Http4sServerOptions[F, G] = customInterceptors(Some(DefaultExceptionHandler), Some(Log.defaultServerLog[G])) } diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerToHttpInterpreter.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerToHttpInterpreter.scala index da7d28909d..6ab9b1aa54 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerToHttpInterpreter.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerToHttpInterpreter.scala @@ -1,11 +1,11 @@ package sttp.tapir.server.http4s import cats.data.{Kleisli, OptionT} -import cats.effect.{Concurrent, ContextShift, Sync, Timer} +import cats.effect.std.Queue +import cats.effect.{Async, Sync} import cats.implicits._ import cats.~> -import fs2.Pipe -import fs2.concurrent.Queue +import fs2.{Pipe, Stream} import org.http4s._ import org.http4s.server.websocket.WebSocketBuilder import org.http4s.websocket.WebSocketFrame @@ -23,11 +23,8 @@ import scala.reflect.ClassTag trait Http4sServerToHttpInterpreter[F[_], G[_]] { + implicit def fa: Async[F] implicit def gs: Sync[G] - implicit def gcs: ContextShift[G] - implicit def fs: Concurrent[F] - implicit def fcs: ContextShift[F] - implicit def timer: Timer[F] def fToG: F ~> G def gToF: G ~> F @@ -81,8 +78,9 @@ trait Http4sServerToHttpInterpreter[F[_], G[_]] { case Some(Left(pipeF)) => Queue.bounded[F, WebSocketFrame](32).flatMap { queue => pipeF.flatMap { pipe => - val receive: Pipe[F, WebSocketFrame, Unit] = pipe.andThen(s => s.evalMap(f => queue.enqueue1(f))) - WebSocketBuilder[F].copy(headers = headers, filterPingPongs = false).build(queue.dequeue, receive) + val send: Stream[F, WebSocketFrame] = Stream.repeatEval(queue.take) + val receive: Pipe[F, WebSocketFrame, Unit] = pipe.andThen(s => s.evalMap(f => queue.offer(f))) + WebSocketBuilder[F].copy(headers = headers, filterPingPongs = false).build(send, receive) } } case Some(Right(entity)) => @@ -101,36 +99,24 @@ object Http4sServerToHttpInterpreter { private[http4s] val log: Logger = getLogger def apply[F[_], G[_]]()(_fToG: F ~> G)(_gToF: G ~> F)(implicit - _gs: Sync[G], - _gcs: ContextShift[G], - _fs: Concurrent[F], - _fcs: ContextShift[F], - _timer: Timer[F] + _fa: Async[F], + _gs: Sync[G] ): Http4sServerToHttpInterpreter[F, G] = { new Http4sServerToHttpInterpreter[F, G] { override implicit def gs: Sync[G] = _gs - override implicit def gcs: ContextShift[G] = _gcs - override implicit def fs: Concurrent[F] = _fs - override implicit def fcs: ContextShift[F] = _fcs + override implicit def fa: Async[F] = _fa override def fToG: F ~> G = _fToG override def gToF: G ~> F = _gToF - override implicit def timer: Timer[F] = _timer } } def apply[F[_], G[_]](serverOptions: Http4sServerOptions[F, G])(_fToG: F ~> G)(_gToF: G ~> F)(implicit - _gs: Sync[G], - _gcs: ContextShift[G], - _fs: Concurrent[F], - _fcs: ContextShift[F], - _timer: Timer[F] + _fa: Async[F], + _gs: Sync[G] ): Http4sServerToHttpInterpreter[F, G] = { new Http4sServerToHttpInterpreter[F, G] { override implicit def gs: Sync[G] = _gs - override implicit def gcs: ContextShift[G] = _gcs - override implicit def fs: Concurrent[F] = _fs - override implicit def fcs: ContextShift[F] = _fcs - override implicit def timer: Timer[F] = _timer + override implicit def fa: Async[F] = _fa override def fToG: F ~> G = _fToG override def gToF: G ~> F = _gToF override def http4sServerOptions: Http4sServerOptions[F, G] = serverOptions diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala index d20090f1ee..7b2ab67380 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala @@ -1,7 +1,8 @@ package sttp.tapir.server.http4s -import cats.effect.{Blocker, Concurrent, ContextShift, Timer} +import cats.effect.Async import cats.syntax.all._ +import fs2.io.file.Files import fs2.{Chunk, Stream} import org.http4s import org.http4s._ @@ -15,7 +16,7 @@ import sttp.tapir.{CodecFormat, RawBodyType, RawPart, WebSocketBodyOutput} import java.nio.charset.Charset -private[http4s] class Http4sToResponseBody[F[_]: Concurrent: Timer: ContextShift, G[_]]( +private[http4s] class Http4sToResponseBody[F[_]: Async, G[_]]( serverOptions: Http4sServerOptions[F, G] ) extends ToResponseBody[Http4sResponseBody[F], Fs2Streams[F]] { override val streams: Fs2Streams[F] = Fs2Streams[F] @@ -40,17 +41,16 @@ private[http4s] class Http4sToResponseBody[F[_]: Concurrent: Timer: ContextShift bodyType match { case RawBodyType.StringBody(charset) => val bytes = r.toString.getBytes(charset) - fs2.Stream.chunk(Chunk.bytes(bytes)) - case RawBodyType.ByteArrayBody => fs2.Stream.chunk(Chunk.bytes(r)) + fs2.Stream.chunk(Chunk.array(bytes)) + case RawBodyType.ByteArrayBody => fs2.Stream.chunk(Chunk.array(r)) case RawBodyType.ByteBufferBody => fs2.Stream.chunk(Chunk.byteBuffer(r)) case RawBodyType.InputStreamBody => fs2.io.readInputStream( r.pure[F], - serverOptions.ioChunkSize, - Blocker.liftExecutionContext(serverOptions.blockingExecutionContext) + serverOptions.ioChunkSize ) case RawBodyType.FileBody => - fs2.io.file.readAll[F](r.toPath, Blocker.liftExecutionContext(serverOptions.blockingExecutionContext), serverOptions.ioChunkSize) + Files[F].readAll(r.toPath, serverOptions.ioChunkSize) case m: RawBodyType.MultipartBody => val parts = (r: Seq[RawPart]).flatMap(rawPartToBodyPart(m, _)) val body = implicitly[EntityEncoder[F, multipart.Multipart[F]]].toEntity(multipart.Multipart(parts.toVector)).body diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala index 064412846f..dc170c2780 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala @@ -1,18 +1,18 @@ package sttp.tapir.server.http4s import cats.Monad -import cats.effect.{Concurrent, Timer} +import cats.effect.Temporal +import cats.effect.std.Queue +import cats.syntax.all._ import fs2._ -import fs2.concurrent.Queue import org.http4s.websocket.{WebSocketFrame => Http4sWebSocketFrame} import scodec.bits.ByteVector import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.{DecodeResult, WebSocketBodyOutput, WebSocketFrameDecodeFailure} import sttp.ws.WebSocketFrame -import cats.syntax.all._ private[http4s] object Http4sWebSockets { - def pipeToBody[F[_]: Concurrent: Timer, REQ, RESP]( + def pipeToBody[F[_]: Temporal, REQ, RESP]( pipe: Pipe[F, REQ, RESP], o: WebSocketBodyOutput[Pipe[F, REQ, RESP], REQ, RESP, _, Fs2Streams[F]] ): F[Pipe[F, Http4sWebSocketFrame, Http4sWebSocketFrame]] = { @@ -38,7 +38,7 @@ private[http4s] object Http4sWebSockets { .unNoneTerminate .through(pipe) .map(o.responses.encode) - .mergeHaltL(pongs.dequeue) + .mergeHaltL(Stream.repeatEval(pongs.take)) .mergeHaltL(autoPings) .map(frameToHttp4sFrame) } @@ -99,7 +99,7 @@ private[http4s] object Http4sWebSockets { ): Stream[F, WebSocketFrame] = { if (doAuto) { s.evalMap { - case WebSocketFrame.Ping(payload) => pongs.enqueue1(WebSocketFrame.Pong(payload)).map(_ => none[WebSocketFrame]) + case WebSocketFrame.Ping(payload) => pongs.offer(WebSocketFrame.Pong(payload)).map(_ => none[WebSocketFrame]) case f => f.some.pure[F] }.collect { case Some(f) => f diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala index 8e3a5c5c8b..3d9cc8a181 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala @@ -1,41 +1,46 @@ package sttp.tapir.server.http4s import cats.effect.IO -import org.scalatest.funsuite.AnyFunSuite +import cats.effect.unsafe.implicits.global +import org.scalatest.funsuite.{AnyFunSuite, AsyncFunSuite} import org.scalatest.matchers.should.Matchers import sttp.capabilities.fs2.Fs2Streams import sttp.model.sse.ServerSentEvent import java.nio.charset.Charset -class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { +class Http4sServerSentEventsTest extends AsyncFunSuite with Matchers { test("serialiseSSEToBytes should successfully serialise simple Server Sent Event to ByteString") { val sse: fs2.Stream[IO, ServerSentEvent] = fs2.Stream(ServerSentEvent(Some("data"), Some("event"), Some("id1"), Some(10))) val serialised = Http4sServerSentEvents.serialiseSSEToBytes(Fs2Streams[IO])(sse) val futureEventsBytes = serialised.compile.toList - futureEventsBytes.map(sseEvents => { - sseEvents shouldBe - s"""data: data + futureEventsBytes + .map(sseEvents => { + sseEvents shouldBe + s"""data: data |event: event |id: id1 |retry: 10 | |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList - }).unsafeRunSync() + }) + .unsafeToFuture() } test("serialiseSSEToBytes should omit fields that are not set") { val sse = fs2.Stream(ServerSentEvent(Some("data"), None, Some("id1"), None)) val serialised = Http4sServerSentEvents.serialiseSSEToBytes(Fs2Streams[IO])(sse) val futureEvents = serialised.compile.toList - futureEvents.map(sseEvents => { - sseEvents shouldBe + futureEvents + .map(sseEvents => { + sseEvents shouldBe s"""data: data |id: id1 | |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList - }).unsafeRunSync() + }) + .unsafeToFuture() } test("serialiseSSEToBytes should successfully serialise multiline data event") { @@ -51,19 +56,21 @@ class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { ) val serialised = Http4sServerSentEvents.serialiseSSEToBytes(Fs2Streams[IO])(sse) val futureEvents = serialised.compile.toList - futureEvents.map(sseEvents => { - sseEvents shouldBe + futureEvents + .map(sseEvents => { + sseEvents shouldBe s"""data: some data info 1 |data: some data info 2 |data: some data info 3 | |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList - }).unsafeRunSync() + }) + .unsafeToFuture() } test("parseBytesToSSE should successfully parse SSE bytes to SSE structure") { val sseBytes = fs2.Stream.iterable( - """data: event1 data + """data: event1 data |event: event1 |id: id1 |retry: 5 @@ -78,19 +85,21 @@ class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { ) val parsed = Http4sServerSentEvents.parseBytesToSSE(Fs2Streams[IO])(sseBytes) val futureEvents = parsed.compile.toList - futureEvents.map(events => - events shouldBe List( - ServerSentEvent(Some("event1 data"), Some("event1"), Some("id1"), Some(5)), - ServerSentEvent( - Some("""event2 data1 + futureEvents + .map(events => + events shouldBe List( + ServerSentEvent(Some("event1 data"), Some("event1"), Some("id1"), Some(5)), + ServerSentEvent( + Some("""event2 data1 |event2 data2 |event2 data3""".stripMargin), - None, - Some("id2"), - None + None, + Some("id2"), + None + ) ) ) - ).unsafeRunSync() + .unsafeToFuture() } } diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala index a3074b1dc0..b7f5ca3bcf 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala @@ -2,6 +2,7 @@ package sttp.tapir.server.http4s import cats.effect._ import cats.syntax.all._ +import cats.effect.unsafe.implicits.global import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ @@ -42,8 +43,6 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi val sse1 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200))) val sse2 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200))) - import interpreter.timer - def additionalTests(): List[Test] = List( Test("should work with a router and routes in a context") { val e = endpoint.get.in("test" / "router").out(stringBody).serverLogic(_ => IO.pure("ok".asRight[Unit])) diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala index 43aaf3e9a1..adcfb2a08e 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala @@ -1,7 +1,7 @@ package sttp.tapir.server.http4s import cats.data.{Kleisli, NonEmptyList} -import cats.effect.{ContextShift, IO, Resource, Timer} +import cats.effect.{IO, Resource} import cats.syntax.all._ import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ @@ -22,8 +22,6 @@ import scala.reflect.ClassTag class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[IO] with WebSockets, HttpRoutes[IO], Http4sResponseBody[IO]] { implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) - implicit val timer: Timer[IO] = IO.timer(ec) override def route[I, E, O]( e: ServerEndpoint[I, E, O, Fs2Streams[IO] with WebSockets, IO], diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala index 996be462b2..601ac017a5 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala @@ -5,7 +5,6 @@ import cats.effect.{IO, Resource} import cats.implicits._ import com.typesafe.scalalogging.StrictLogging import org.scalatest.Assertion -import org.slf4j.{Logger, LoggerFactory} import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.client3._ @@ -15,6 +14,7 @@ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.interceptor.decodefailure.DecodeFailureHandler import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.tests._ +import cats.effect.unsafe.implicits.global trait CreateServerTest[F[_], +R, ROUTE, B] { def testServer[I, E, O]( @@ -77,7 +77,7 @@ class DefaultCreateServerTest[F[_], +R, ROUTE, B]( .use { port => runTest(backend, uri"http://localhost:$port").guarantee(IO(logger.info(s"Tests completed on port $port"))) } - .unsafeRunSync() + .unsafeToFuture() ) } } diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala index fd4247182b..f2820f1589 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala @@ -248,7 +248,7 @@ class ServerBasicTests[F[_], ROUTE, B]( .send(backend) .map { r => if (multipleValueHeaderSupport) { - r.headers.filter(_.is("hh")).map(_.value).toList shouldBe List("v3", "v2", "v1", "v0") + r.headers.filter(_.is("hh")).map(_.value).toSet shouldBe Set("v3", "v2", "v1", "v0") } else { r.headers.filter(_.is("hh")).map(_.value).headOption should contain("v3, v2, v1, v0") } @@ -543,25 +543,25 @@ class ServerBasicTests[F[_], ROUTE, B]( .header("A", "1") .header("X", "3") .send(backend) - .map(_.headers.map(h => h.name -> h.value).toSet should contain allOf ("Y" -> "3", "B" -> "2")) + .map(_.headers.map(h => h.name.toLowerCase -> h.value).toSet should contain allOf ("y" -> "3", "b" -> "2")) }, testServer(in_4query_out_4header_extended)(in => pureResult(in.asRight[Unit])) { (backend, baseUri) => basicRequest .get(uri"$baseUri?a=1&b=2&x=3&y=4") .send(backend) - .map(_.headers.map(h => h.name -> h.value).toSet should contain allOf ("A" -> "1", "B" -> "2", "X" -> "3", "Y" -> "4")) + .map(_.headers.map(h => h.name.toLowerCase -> h.value).toSet should contain allOf ("a" -> "1", "b" -> "2", "x" -> "3", "y" -> "4")) }, testServer(in_3query_out_3header_mapped_to_tuple)(in => pureResult(in.asRight[Unit])) { (backend, baseUri) => basicRequest .get(uri"$baseUri?p1=1&p2=2&p3=3") .send(backend) - .map(_.headers.map(h => h.name -> h.value).toSet should contain allOf ("P1" -> "1", "P2" -> "2", "P3" -> "3")) + .map(_.headers.map(h => h.name.toLowerCase -> h.value).toSet should contain allOf ("p1" -> "1", "p2" -> "2", "p3" -> "3")) }, testServer(in_2query_out_2query_mapped_to_unit)(in => pureResult(in.asRight[Unit])) { (backend, baseUri) => basicRequest .get(uri"$baseUri?p1=1&p2=2") .send(backend) - .map(_.headers.map(h => h.name -> h.value).toSet should contain allOf ("P1" -> "DEFAULT_HEADER", "P2" -> "2")) + .map(_.headers.map(h => h.name.toLowerCase -> h.value).toSet should contain allOf ("p1" -> "DEFAULT_HEADER", "p2" -> "2")) }, testServer(in_query_with_default_out_string)(in => pureResult(in.asRight[Unit])) { (backend, baseUri) => basicRequest.get(uri"$baseUri?p1=x").send(backend).map(_.body shouldBe Right("x")) >> diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala index 9bac56b6c3..befec6b87e 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala @@ -1,7 +1,7 @@ package sttp.tapir.server.tests import cats.data.NonEmptyList -import cats.effect.{ContextShift, IO, Resource} +import cats.effect.{IO, Resource} import sttp.tapir.Endpoint import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.interceptor.decodefailure.DecodeFailureHandler @@ -11,7 +11,6 @@ import sttp.tapir.tests.Port import scala.reflect.ClassTag trait TestServerInterpreter[F[_], +R, ROUTE, B] { - implicit lazy val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) def route[I, E, O]( e: ServerEndpoint[I, E, O, R, F], decodeFailureHandler: Option[DecodeFailureHandler] = None, diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/package.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/package.scala index 8141ec015b..ec50ffbde6 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/package.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/package.scala @@ -1,13 +1,11 @@ package sttp.tapir.server -import cats.effect.{Blocker, ContextShift, IO, Resource} +import cats.effect.{IO, Resource} import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.client3.SttpBackend -import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend +import sttp.client3.httpclient.fs2.HttpClientFs2Backend package object tests { - private implicit lazy val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) - val backendResource: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = - AsyncHttpClientFs2Backend.resource[IO](Blocker.liftExecutionContext(scala.concurrent.ExecutionContext.global)) + val backendResource: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = HttpClientFs2Backend.resource() } diff --git a/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerInterpreter.scala b/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerInterpreter.scala index d5507c239d..33adffd437 100644 --- a/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerInterpreter.scala +++ b/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerInterpreter.scala @@ -1,6 +1,7 @@ package sttp.tapir.server.vertx -import cats.effect.{Async, CancelToken, ConcurrentEffect, Effect, IO, Sync} +import cats.effect.std.Dispatcher +import cats.effect.{Async, Sync} import cats.syntax.all._ import io.vertx.core.{Future, Handler} import io.vertx.ext.web.{Route, Router, RoutingContext} @@ -23,17 +24,15 @@ import scala.reflect.ClassTag trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter { - implicit def fs: Sync[F] + implicit def fa: Async[F] - def vertxCatsServerOptions: VertxCatsServerOptions[F] = VertxCatsServerOptions.default[F] + def vertxCatsServerOptions: VertxCatsServerOptions[F] /** Given a Router, creates and mounts a Route matching this endpoint, with default error handling * @param logic the logic to associate with the endpoint * @return A function, that given a router, will attach this endpoint to it */ - def route[I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])(logic: I => F[Either[E, O]])(implicit - effect: ConcurrentEffect[F] - ): Router => Route = + def route[I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])(logic: I => F[Either[E, O]]): Router => Route = route(e.serverLogic(logic)) /** Given a Router, creates and mounts a Route matching this endpoint, with custom error handling @@ -43,7 +42,6 @@ trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter { def routeRecoverErrors[I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])( logic: I => F[O] )(implicit - effect: ConcurrentEffect[F], eIsThrowable: E <:< Throwable, eClassTag: ClassTag[E] ): Router => Route = @@ -54,16 +52,15 @@ trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter { */ def route[I, E, O]( e: ServerEndpoint[I, E, O, Fs2Streams[F], F] - )(implicit - effect: ConcurrentEffect[F] ): Router => Route = { router => val readStreamCompatible = fs2ReadStreamCompatible(vertxCatsServerOptions) mountWithDefaultHandlers(e)(router, extractRouteDefinition(e.endpoint)).handler(endpointHandler(e, readStreamCompatible)) } private def endpointHandler[I, E, O, A, S <: Streams[S]]( - e: ServerEndpoint[I, E, O, Fs2Streams[F], F], readStreamCompatible: ReadStreamCompatible[S] - )(implicit effect: Effect[F]): Handler[RoutingContext] = { rc => + e: ServerEndpoint[I, E, O, Fs2Streams[F], F], + readStreamCompatible: ReadStreamCompatible[S] + ): Handler[RoutingContext] = { rc => implicit val monad: MonadError[F] = monadError[F] implicit val bodyListener: BodyListener[F, RoutingContext => Unit] = new VertxBodyListener[F] val fFromVFuture = new CatsFFromVFuture[F] @@ -85,18 +82,19 @@ trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter { // we obtain the cancel token only after the effect is run, so we need to pass it to the exception handler // via a mutable ref; however, before this is done, it's possible an exception has already been reported; // if so, we need to use this fact to cancel the operation nonetheless - val cancelRef = new AtomicReference[Option[Either[Throwable, CancelToken[IO]]]](None) + type CancelToken = () => scala.concurrent.Future[Unit] + val cancelRef = new AtomicReference[Option[Either[Throwable, CancelToken]]](None) rc.response.exceptionHandler { (t: Throwable) => cancelRef.getAndSet(Some(Left(t))).collect { case Right(t) => - t.unsafeRunSync() + t() } () } - val cancelToken = effect.toIO(result).unsafeRunCancelable { _ => () } + val cancelToken = vertxCatsServerOptions.dispatcher.unsafeRunCancelable(result) cancelRef.getAndSet(Some(Right(cancelToken))).collect { case Left(_) => - cancelToken.unsafeRunSync() + cancelToken() } () @@ -104,31 +102,30 @@ trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter { } object VertxCatsServerInterpreter { - def apply[F[_]]()(implicit _fs: Sync[F]): VertxCatsServerInterpreter[F] = { + def apply[F[_]](dispatcher: Dispatcher[F])(implicit _fa: Async[F]): VertxCatsServerInterpreter[F] = { new VertxCatsServerInterpreter[F] { - override implicit def fs: Sync[F] = _fs + override implicit def fa: Async[F] = _fa + override def vertxCatsServerOptions: VertxCatsServerOptions[F] = VertxCatsServerOptions.default[F](dispatcher)(fa) } } - def apply[F[_]](serverOptions: VertxCatsServerOptions[F])(implicit _fs: Sync[F]): VertxCatsServerInterpreter[F] = { + def apply[F[_]](serverOptions: VertxCatsServerOptions[F])(implicit _fa: Async[F]): VertxCatsServerInterpreter[F] = { new VertxCatsServerInterpreter[F] { - override implicit def fs: Sync[F] = _fs - + override implicit def fa: Async[F] = _fa override def vertxCatsServerOptions: VertxCatsServerOptions[F] = serverOptions } } - private[vertx] def monadError[F[_]](implicit F: Effect[F]): MonadError[F] = new MonadError[F] { + private[vertx] def monadError[F[_]](implicit F: Sync[F]): MonadError[F] = new MonadError[F] { override def unit[T](t: T): F[T] = F.pure(t) override def map[T, T2](fa: F[T])(f: T => T2): F[T2] = F.map(fa)(f) override def flatMap[T, T2](fa: F[T])(f: T => F[T2]): F[T2] = F.flatMap(fa)(f) override def error[T](t: Throwable): F[T] = F.raiseError(t) - override protected def handleWrappedError[T](rt: F[T])(h: PartialFunction[Throwable, F[T]]): F[T] = - F.recoverWith(rt)(h) + override protected def handleWrappedError[T](rt: F[T])(h: PartialFunction[Throwable, F[T]]): F[T] = F.recoverWith(rt)(h) override def eval[T](t: => T): F[T] = F.delay(t) override def suspend[T](t: => F[T]): F[T] = F.defer(t) override def flatten[T](ffa: F[F[T]]): F[T] = F.flatten(ffa) - override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guarantee(f)(e) + override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guaranteeCase(f)(_ => e) } private[vertx] class CatsFFromVFuture[F[_]: Async] extends FromVFuture[F] { @@ -137,7 +134,7 @@ object VertxCatsServerInterpreter { implicit class VertxFutureToCatsF[A](f: => Future[A]) { def asF[F[_]: Async]: F[A] = { - Async[F].async { cb => + Async[F].async_ { cb => f.onComplete({ handler => if (handler.succeeded()) { cb(Right(handler.result())) @@ -149,4 +146,4 @@ object VertxCatsServerInterpreter { } } } -} \ No newline at end of file +} diff --git a/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerOptions.scala b/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerOptions.scala index cbfa48d084..9dc47aac70 100644 --- a/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerOptions.scala +++ b/server/vertx/src/main/scala/sttp/tapir/server/vertx/VertxCatsServerOptions.scala @@ -2,6 +2,8 @@ package sttp.tapir.server.vertx import cats.Applicative import cats.effect.Sync +import cats.effect.kernel.Async +import cats.effect.std.Dispatcher import cats.implicits.catsSyntaxOptionId import io.vertx.core.logging.LoggerFactory import io.vertx.ext.web.RoutingContext @@ -16,6 +18,7 @@ import sttp.tapir.{Defaults, TapirFile} import java.io.File final case class VertxCatsServerOptions[F[_]]( + dispatcher: Dispatcher[F], uploadDirectory: TapirFile, deleteFile: TapirFile => F[Unit], maxQueueSizeForReadStream: Int, @@ -48,7 +51,8 @@ object VertxCatsServerOptions { * header. * @param decodeFailureHandler The decode failure handler, from which an interceptor will be created. */ - def customInterceptors[F[_]: Sync]( + def customInterceptors[F[_]: Async]( + dispatcher: Dispatcher[F], metricsInterceptor: Option[MetricsRequestInterceptor[F, RoutingContext => Unit]] = None, exceptionHandler: Option[ExceptionHandler] = Some(DefaultExceptionHandler), serverLog: Option[ServerLog[Unit]] = Some(VertxServerOptions.defaultServerLog(LoggerFactory.getLogger("tapir-vertx"))), @@ -58,6 +62,7 @@ object VertxCatsServerOptions { decodeFailureHandler: DecodeFailureHandler = DefaultDecodeFailureHandler.handler ): VertxCatsServerOptions[F] = { VertxCatsServerOptions( + dispatcher, File.createTempFile("tapir", null).getParentFile.getAbsoluteFile: TapirFile, file => Sync[F].delay(Defaults.deleteFile()(file)), maxQueueSizeForReadStream = 16, @@ -70,5 +75,5 @@ object VertxCatsServerOptions { ) } - def default[F[_]: Sync]: VertxCatsServerOptions[F] = customInterceptors() + def default[F[_]: Async](dispatcher: Dispatcher[F]): VertxCatsServerOptions[F] = customInterceptors(dispatcher) } diff --git a/server/vertx/src/main/scala/sttp/tapir/server/vertx/streams/fs2.scala b/server/vertx/src/main/scala/sttp/tapir/server/vertx/streams/fs2.scala index 6d9c4d6260..6c2aef0fcc 100644 --- a/server/vertx/src/main/scala/sttp/tapir/server/vertx/streams/fs2.scala +++ b/server/vertx/src/main/scala/sttp/tapir/server/vertx/streams/fs2.scala @@ -1,173 +1,166 @@ package sttp.tapir.server.vertx.streams -import cats.effect.concurrent.Deferred -import cats.effect.concurrent.Ref -import cats.effect.syntax.concurrent._ -import cats.effect.ConcurrentEffect -import cats.effect.ExitCase -import cats.syntax.applicative._ -import cats.syntax.flatMap._ -import cats.syntax.foldable._ -import cats.syntax.functor._ -import cats.syntax.traverse._ -import _root_.fs2.Chunk -import _root_.fs2.Stream +import _root_.fs2.{Chunk, Stream} +import cats.effect.kernel.Async +import cats.effect.kernel.Resource.ExitCase._ +import cats.effect.{Deferred, Ref} +import cats.syntax.all._ +import io.vertx.core.Handler import io.vertx.core.buffer.Buffer import io.vertx.core.streams.ReadStream -import io.vertx.core.Handler import sttp.capabilities.fs2.Fs2Streams -import sttp.tapir.server.vertx.streams.ReadStreamState._ import sttp.tapir.server.vertx.VertxCatsServerOptions +import sttp.tapir.server.vertx.streams.ReadStreamState._ import scala.collection.immutable.{Queue => SQueue} object fs2 { - implicit class DeferredOps[F[_], A](dfd: Deferred[F, A]) extends DeferredLike[F, A] { + implicit class DeferredOps[F[_]: Async, A](dfd: Deferred[F, A]) extends DeferredLike[F, A] { override def complete(a: A): F[Unit] = - dfd.complete(a) + dfd.complete(a).void override def get: F[A] = dfd.get } - def fs2ReadStreamCompatible[F[_]](opts: VertxCatsServerOptions[F])(implicit - F: ConcurrentEffect[F] - ): ReadStreamCompatible[Fs2Streams[F]] = + implicit def fs2ReadStreamCompatible[F[_]](opts: VertxCatsServerOptions[F])(implicit F: Async[F]): ReadStreamCompatible[Fs2Streams[F]] = { new ReadStreamCompatible[Fs2Streams[F]] { override val streams: Fs2Streams[F] = Fs2Streams[F] - override def asReadStream(stream: Stream[F, Byte]): ReadStream[Buffer] = - F.toIO(for { - promise <- Deferred[F, Unit] - state <- Ref.of(StreamState.empty[F](promise)) - _ <- stream.chunks - .evalMap({ chunk => - val buffer = Buffer.buffer(chunk.toArray) - state.get.flatMap { - case StreamState(None, handler, _, _) => - F.delay(handler.handle(buffer)) - case StreamState(Some(promise), _, _, _) => - for { - _ <- promise.get - // Handler in state may be updated since the moment when we wait - // promise so let's get more recent version. - updatedState <- state.get - } yield updatedState.handler.handle(buffer) - } - }) - .onFinalizeCase({ - case ExitCase.Completed => - state.get.flatMap { state => - F.delay(state.endHandler.handle(null)) - } - case ExitCase.Canceled => - state.get.flatMap { state => - F.delay(state.errorHandler.handle(new Exception("Cancelled!"))) - } - case ExitCase.Error(cause) => - state.get.flatMap { state => - F.delay(state.errorHandler.handle(cause)) - } - }) - .compile - .drain - .start - } yield new ReadStream[Buffer] { self => - override def handler(handler: Handler[Buffer]): ReadStream[Buffer] = - F.toIO(state.update(_.copy(handler = handler)).as(self)) - .unsafeRunSync() - - override def endHandler(handler: Handler[Void]): ReadStream[Buffer] = - F.toIO(state.update(_.copy(endHandler = handler)).as(self)) - .unsafeRunSync() + override def asReadStream(stream: Stream[F, Byte]): ReadStream[Buffer] = { + opts.dispatcher.unsafeRunSync { + for { + promise <- Deferred[F, Unit] + state <- Ref.of(StreamState.empty[F](promise)) + _ <- F.start( + stream.chunks + .evalMap({ chunk => + val buffer = Buffer.buffer(chunk.toArray) + state.get.flatMap { + case StreamState(None, handler, _, _) => + F.delay(handler.handle(buffer)) + case StreamState(Some(promise), _, _, _) => + for { + _ <- promise.get + // Handler in state may be updated since the moment when we wait + // promise so let's get more recent version. + updatedState <- state.get + } yield updatedState.handler.handle(buffer) + } + }) + .onFinalizeCase({ + case Succeeded => + state.get.flatMap { state => + F.delay(state.endHandler.handle(null)) + } + case Canceled => + state.get.flatMap { state => + F.delay(state.errorHandler.handle(new Exception("Cancelled!"))) + } + case Errored(cause) => + state.get.flatMap { state => + F.delay(state.errorHandler.handle(cause)) + } + }) + .compile + .drain + ) + } yield new ReadStream[Buffer] { + self => + override def handler(handler: Handler[Buffer]): ReadStream[Buffer] = + opts.dispatcher.unsafeRunSync(state.update(_.copy(handler = handler)).as(self)) - override def exceptionHandler(handler: Handler[Throwable]): ReadStream[Buffer] = - F.toIO(state.update(_.copy(errorHandler = handler)).as(self)) - .unsafeRunSync() + override def endHandler(handler: Handler[Void]): ReadStream[Buffer] = + opts.dispatcher.unsafeRunSync(state.update(_.copy(endHandler = handler)).as(self)) - override def pause(): ReadStream[Buffer] = - F.toIO(for { - deferred <- Deferred[F, Unit] - _ <- state.update { - case cur @ StreamState(Some(_), _, _, _) => - cur - case cur @ StreamState(None, _, _, _) => - cur.copy(paused = Some(deferred)) - } - } yield self) - .unsafeRunSync() + override def exceptionHandler(handler: Handler[Throwable]): ReadStream[Buffer] = + opts.dispatcher.unsafeRunSync(state.update(_.copy(errorHandler = handler)).as(self)) - override def resume(): ReadStream[Buffer] = - F.toIO(for { - oldState <- state.getAndUpdate(_.copy(paused = None)) - _ <- oldState.paused.fold(F.unit)(_.complete(())) - } yield self) - .unsafeRunSync() + override def pause(): ReadStream[Buffer] = + opts.dispatcher.unsafeRunSync(for { + deferred <- Deferred[F, Unit] + _ <- state.update { + case cur @ StreamState(Some(_), _, _, _) => + cur + case cur @ StreamState(None, _, _, _) => + cur.copy(paused = Some(deferred)) + } + } yield self) - override def fetch(n: Long): ReadStream[Buffer] = - self - }).unsafeRunSync() + override def resume(): ReadStream[Buffer] = + opts.dispatcher.unsafeRunSync(for { + oldState <- state.getAndUpdate(_.copy(paused = None)) + _ <- oldState.paused.fold(Async[F].unit)(_.complete(())) + } yield self) - override def fromReadStream(readStream: ReadStream[Buffer]): Stream[F, Byte] = - F.toIO(for { - stateRef <- Ref.of(ReadStreamState[F, Chunk[Byte]](Queued(SQueue.empty), Queued(SQueue.empty))) - stream = Stream.unfoldChunkEval[F, Unit, Byte](()) { _ => - for { - dfd <- Deferred[F, WrappedBuffer[Chunk[Byte]]] - tuple <- stateRef.modify(_.dequeueBuffer(dfd)) - (mbBuffer, mbAction) = tuple - _ <- mbAction.traverse(identity) - wrappedBuffer <- mbBuffer match { - case Left(deferred) => - deferred.get - case Right(buffer) => - buffer.pure[F] - } - result <- wrappedBuffer match { - case Right(buffer) => Some((buffer, ())).pure[F] - case Left(None) => None.pure[F] - case Left(Some(cause)) => ConcurrentEffect[F].raiseError(cause) - } - } yield result + override def fetch(n: Long): ReadStream[Buffer] = + self } + } + } - _ <- Stream - .unfoldEval[F, Unit, ActivationEvent](())({ _ => + override def fromReadStream(readStream: ReadStream[Buffer]): Stream[F, Byte] = + opts.dispatcher.unsafeRunSync { + for { + stateRef <- Ref.of(ReadStreamState[F, Chunk[Byte]](Queued(SQueue.empty), Queued(SQueue.empty))) + stream = Stream.unfoldChunkEval[F, Unit, Byte](()) { _ => for { - dfd <- Deferred[F, WrappedEvent] - mbEvent <- stateRef.modify(_.dequeueActivationEvent(dfd)) - result <- mbEvent match { + dfd <- Deferred[F, WrappedBuffer[Chunk[Byte]]] + tuple <- stateRef.modify(_.dequeueBuffer(dfd)) + (mbBuffer, mbAction) = tuple + _ <- mbAction.traverse(identity) + wrappedBuffer <- mbBuffer match { case Left(deferred) => deferred.get - case Right(event) => - event.pure[F] + case Right(buffer) => + buffer.pure[F] } - } yield result.map((_, ())) - }) - .evalMap({ - case Pause => - ConcurrentEffect[F].delay(readStream.pause()) - case Resume => - ConcurrentEffect[F].delay(readStream.resume()) - }) - .compile - .drain - .start - } yield { - readStream.endHandler { _ => - F.toIO(stateRef.modify(_.halt(None)).flatMap(_.sequence_)).unsafeRunSync() - } - readStream.exceptionHandler { cause => - F.toIO(stateRef.modify(_.halt(Some(cause))).flatMap(_.sequence_)).unsafeRunSync() - } - readStream.handler { buffer => - val chunk = Chunk.array(buffer.getBytes) - val maxSize = opts.maxQueueSizeForReadStream - F.toIO(stateRef.modify(_.enqueue(chunk, maxSize)).flatMap(_.sequence_)).unsafeRunSync() - } + result <- wrappedBuffer match { + case Right(buffer) => Some((buffer, ())).pure[F] + case Left(None) => None.pure[F] + case Left(Some(cause)) => Async[F].raiseError(cause) + } + } yield result + } + + _ <- F.start( + Stream + .unfoldEval[F, Unit, ActivationEvent](())({ _ => + for { + dfd <- Deferred[F, WrappedEvent] + mbEvent <- stateRef.modify(_.dequeueActivationEvent(dfd)) + result <- mbEvent match { + case Left(deferred) => + deferred.get + case Right(event) => + event.pure[F] + } + } yield result.map((_, ())) + }) + .evalMap({ + case Pause => F.delay(readStream.pause()) + case Resume => F.delay(readStream.resume()) + }) + .compile + .drain + ) + } yield { + readStream.endHandler { _ => + opts.dispatcher.unsafeRunSync(stateRef.modify(_.halt(None)).flatMap(_.sequence_)) + } + readStream.exceptionHandler { cause => + opts.dispatcher.unsafeRunSync(stateRef.modify(_.halt(Some(cause))).flatMap(_.sequence_)) + } + readStream.handler { buffer => + val chunk = Chunk.array(buffer.getBytes) + val maxSize = opts.maxQueueSizeForReadStream + opts.dispatcher.unsafeRunSync(stateRef.modify(_.enqueue(chunk, maxSize)).flatMap(_.sequence_)) + } - stream - }).unsafeRunSync() + stream + } + } } + } } diff --git a/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxServerTest.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxServerTest.scala index d711b15e82..8ffe704849 100644 --- a/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxServerTest.scala +++ b/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxServerTest.scala @@ -4,7 +4,14 @@ import cats.effect.{IO, Resource} import io.vertx.core.Vertx import sttp.capabilities.fs2.Fs2Streams import sttp.monad.MonadError -import sttp.tapir.server.tests.{DefaultCreateServerTest, ServerAuthenticationTests, ServerBasicTests, ServerFileMultipartTests, ServerStreamingTests, backendResource} +import sttp.tapir.server.tests.{ + DefaultCreateServerTest, + ServerAuthenticationTests, + ServerBasicTests, + ServerFileMultipartTests, + ServerStreamingTests, + backendResource +} import sttp.tapir.server.vertx.VertxCatsServerInterpreter.CatsFFromVFuture import sttp.tapir.tests.{Test, TestSuite} @@ -16,7 +23,7 @@ class CatsVertxServerTest extends TestSuite { override def tests: Resource[IO, List[Test]] = backendResource.flatMap { backend => vertxResource.map { implicit vertx => implicit val m: MonadError[IO] = VertxCatsServerInterpreter.monadError[IO] - val interpreter = new CatsVertxTestServerInterpreter(vertx) + val interpreter = new CatsVertxTestServerInterpreter(vertx, dispatcher) val createServerTest = new DefaultCreateServerTest(backend, interpreter) new ServerBasicTests(createServerTest, interpreter).tests() ++ diff --git a/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxTestServerInterpreter.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxTestServerInterpreter.scala index 98bb8f8d87..41652f4d4b 100644 --- a/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxTestServerInterpreter.scala +++ b/server/vertx/src/test/scala/sttp/tapir/server/vertx/CatsVertxTestServerInterpreter.scala @@ -1,7 +1,8 @@ package sttp.tapir.server.vertx import cats.data.NonEmptyList -import cats.effect.{IO, Resource, Sync} +import cats.effect.std.Dispatcher +import cats.effect.{IO, Resource} import io.vertx.core.Vertx import io.vertx.core.http.HttpServerOptions import io.vertx.ext.web.{Route, Router, RoutingContext} @@ -16,7 +17,7 @@ import sttp.tapir.tests.Port import scala.reflect.ClassTag -class CatsVertxTestServerInterpreter(vertx: Vertx) +class CatsVertxTestServerInterpreter(vertx: Vertx, dispatcher: Dispatcher[IO]) extends TestServerInterpreter[IO, Fs2Streams[IO], Router => Route, RoutingContext => Unit] { private val ioFromVFuture = new CatsFFromVFuture[IO] @@ -28,6 +29,7 @@ class CatsVertxTestServerInterpreter(vertx: Vertx) ): Router => Route = { val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.customInterceptors( + dispatcher, metricsInterceptor = metricsInterceptor, decodeFailureHandler = decodeFailureHandler.getOrElse(DefaultDecodeFailureHandler.handler) ) @@ -37,14 +39,15 @@ class CatsVertxTestServerInterpreter(vertx: Vertx) override def routeRecoverErrors[I, E <: Throwable, O](e: Endpoint[I, E, O, Fs2Streams[IO]], fn: I => IO[O])(implicit eClassTag: ClassTag[E] ): Router => Route = { - VertxCatsServerInterpreter[IO]().routeRecoverErrors(e)(fn) + val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.default(dispatcher) + VertxCatsServerInterpreter[IO](options).routeRecoverErrors(e)(fn) } override def server(routes: NonEmptyList[Router => Route]): Resource[IO, Port] = { val router = Router.router(vertx) + routes.toList.foreach(_.apply(router)) val server = vertx.createHttpServer(new HttpServerOptions().setPort(0)).requestHandler(router) val listenIO = ioFromVFuture(server.listen(0)) - routes.toList.foreach(_.apply(router)) Resource.make(listenIO)(s => ioFromVFuture(s.close).void).map(_.actualPort()) } } diff --git a/server/vertx/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala index 15c4c41144..2913d7db0d 100644 --- a/server/vertx/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala +++ b/server/vertx/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala @@ -46,7 +46,7 @@ class VertxTestServerInterpreter(vertx: Vertx) extends TestServerInterpreter[Fut object VertxTestServerInterpreter { def vertxFutureToIo[A](future: => VFuture[A]): IO[A] = - IO.async[A] { cb => + IO.async_[A] { cb => future .onFailure { cause => cb(Left(cause)) } .onSuccess { result => cb(Right(result)) } diff --git a/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxServerTest.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxServerTest.scala index 12481244ec..72d80f2631 100644 --- a/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxServerTest.scala +++ b/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxServerTest.scala @@ -5,17 +5,20 @@ import io.vertx.core.Vertx import io.vertx.ext.web.{Route, Router, RoutingContext} import sttp.capabilities.zio.ZioStreams import sttp.monad.MonadError -import sttp.tapir.server.tests.{DefaultCreateServerTest, ServerAuthenticationTests, ServerBasicTests, ServerFileMultipartTests, ServerStreamingTests, backendResource} -import sttp.tapir.server.vertx.VertxZioServerInterpreter.RioFromVFuture +import sttp.tapir.server.tests.{ + DefaultCreateServerTest, + ServerAuthenticationTests, + ServerBasicTests, + ServerFileMultipartTests, + ServerStreamingTests, + backendResource +} import sttp.tapir.tests.{Test, TestSuite} import zio.Task -import zio.interop.catz._ class ZioVertxServerTest extends TestSuite { - import ZioVertxTestServerInterpreter._ - def vertxResource: Resource[IO, Vertx] = - Resource.make(Task.effect(Vertx.vertx()))(vertx => new RioFromVFuture[Any].apply(vertx.close).unit).mapK(zioToIo) + Resource.make(IO.delay(Vertx.vertx()))(vertx => IO.delay(vertx.close()).void) override def tests: Resource[IO, List[Test]] = backendResource.flatMap { backend => vertxResource.map { implicit vertx => diff --git a/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxTestServerInterpreter.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxTestServerInterpreter.scala index 109f01acbb..50dae69ce1 100644 --- a/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxTestServerInterpreter.scala +++ b/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxTestServerInterpreter.scala @@ -1,8 +1,7 @@ package sttp.tapir.server.vertx -import cats.arrow.FunctionK import cats.data.NonEmptyList -import cats.effect.{ConcurrentEffect, IO, Resource} +import cats.effect.{IO, Resource} import io.vertx.core.Vertx import io.vertx.core.http.HttpServerOptions import io.vertx.ext.web.{Route, Router, RoutingContext} @@ -12,9 +11,7 @@ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.server.tests.TestServerInterpreter -import sttp.tapir.server.vertx.VertxZioServerInterpreter.RioFromVFuture import sttp.tapir.tests.Port -import zio.interop.catz._ import zio.{Runtime, Task} import scala.reflect.ClassTag @@ -22,8 +19,6 @@ import scala.reflect.ClassTag class ZioVertxTestServerInterpreter(vertx: Vertx) extends TestServerInterpreter[Task, ZioStreams, Router => Route, RoutingContext => Unit] { import ZioVertxTestServerInterpreter._ - private val taskFromVFuture = new RioFromVFuture[Any] - override def route[I, E, O]( e: ServerEndpoint[I, E, O, ZioStreams, Task], decodeFailureHandler: Option[DecodeFailureHandler], @@ -45,17 +40,11 @@ class ZioVertxTestServerInterpreter(vertx: Vertx) extends TestServerInterpreter[ override def server(routes: NonEmptyList[Router => Route]): Resource[IO, Port] = { val router = Router.router(vertx) val server = vertx.createHttpServer(new HttpServerOptions().setPort(0)).requestHandler(router) - val listenIO = taskFromVFuture(server.listen(0)) routes.toList.foreach(_.apply(router)) - Resource.make(listenIO)(s => taskFromVFuture(s.close).unit).map(_.actualPort()).mapK(zioToIo) + Resource.eval(VertxTestServerInterpreter.vertxFutureToIo(server.listen(0)).map(_.actualPort())) } } object ZioVertxTestServerInterpreter { implicit val runtime: Runtime[zio.ZEnv] = Runtime.default - - val zioToIo: FunctionK[Task, IO] = new FunctionK[Task, IO] { - override def apply[A](fa: Task[A]): IO[A] = - ConcurrentEffect[Task].toIO(fa) - } } diff --git a/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/Fs2StreamTest.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/Fs2StreamTest.scala index ecb4779505..d58f937448 100644 --- a/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/Fs2StreamTest.scala +++ b/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/Fs2StreamTest.scala @@ -1,25 +1,32 @@ package sttp.tapir.server.vertx.streams -import java.nio.ByteBuffer -import cats.effect.{ContextShift, IO, Timer} -import cats.effect.concurrent.{Deferred, Ref} +import _root_.fs2.{Chunk, Stream} +import cats.effect.std.Dispatcher +import cats.effect.unsafe.implicits.global +import cats.effect.{Deferred, IO, Outcome, Ref, Temporal} import cats.syntax.flatMap._ import cats.syntax.option._ -import _root_.fs2.Stream -import _root_.fs2.Chunk import io.vertx.core.buffer.Buffer +import org.scalatest.BeforeAndAfterAll import org.scalatest.Retries -import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.flatspec.{AnyFlatSpec, AsyncFlatSpec} import org.scalatest.matchers.should.Matchers import org.scalatest.tagobjects.Retryable import sttp.tapir.server.vertx.VertxCatsServerOptions +import java.nio.ByteBuffer import scala.concurrent.duration._ -class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { - implicit val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) - implicit val timer: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) - val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.default[IO].copy(maxQueueSizeForReadStream = 4) +class Fs2StreamTest extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { + + private val (dispatcher, shutdownDispatcher) = Dispatcher[IO].allocated.unsafeRunSync() + + override protected def afterAll(): Unit = { + shutdownDispatcher.unsafeRunSync() + super.afterAll() + } + + val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.default(dispatcher).copy(maxQueueSizeForReadStream = 4) def intAsBuffer(int: Int): Chunk[Byte] = { val buffer = ByteBuffer.allocate(4) @@ -55,7 +62,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { io.flatTap(a => IO.delay(cond(a))) .handleErrorWith { case e => if (attempts < maxAttempts) { - timer.sleep(frequency) *> internal(attempts + 1) + Temporal[IO].sleep(frequency) *> internal(attempts + 1) } else { IO.raiseError(e) } @@ -96,7 +103,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { _ = shouldIncreaseMonotonously(snapshot3) _ <- dfd.complete(Right(())) _ <- eventually(completed.get)({ case true => () }) - } yield ()).unsafeRunSync() + } yield succeed).unsafeToFuture() } it should "interrupt read stream after zio stream interruption" in { @@ -104,7 +111,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { if (num > 20) { IO.raiseError(new Exception("!")) } else { - timer.sleep(100.millis).as(((intAsBuffer(num), num + 1)).some) + Temporal[IO].sleep(100.millis).as(((intAsBuffer(num), num + 1)).some) } }) //.interruptAfter(2.seconds) val readStream = fs2.fs2ReadStreamCompatible[IO](options).asReadStream(stream) @@ -135,7 +142,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { interrupted <- interruptedRef.get } yield (completed, interrupted))({ case (false, Some(_)) => }) - } yield ()).unsafeRunSync() + } yield succeed).unsafeToFuture() } it should "drain read stream without pauses if buffer has enough space" in { @@ -156,13 +163,13 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { } readStream.end() } - result <- resultFiber.join + result <- resultFiber.joinWith(IO.pure(Nil)) } yield { shouldIncreaseMonotonously(result) result should have size count.toLong readStream.pauseCount shouldBe 0 // readStream.resumeCount should be <= 1 - }).unsafeRunSync() + }).unsafeToFuture() } it should "drain read stream with small buffer" in { @@ -186,16 +193,17 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { readStream.end() }) .start - result <- resultFiber.join + result <- resultFiber.joinWith(IO.pure(Nil)) } yield { shouldIncreaseMonotonously(result) result should have size count.toLong readStream.pauseCount should be > 0 readStream.resumeCount should be > 0 - }).unsafeRunSync() + }).unsafeToFuture() } it should "drain failed read stream" in { + val ex = new Exception("!") val count = 50 val readStream = new FakeStream() val stream = fs2.fs2ReadStreamCompatible[IO](options).fromReadStream(readStream) @@ -213,12 +221,12 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with Retries { Thread.sleep(25) readStream.handle(intAsVertxBuffer(i)) } - readStream.fail(new Exception("!")) + readStream.fail(ex) }) .start result <- resultFiber.join.attempt } yield { - result.isLeft shouldBe true - }).unsafeRunSync() + result shouldBe Right(Outcome.errored(ex)) + }).unsafeToFuture() } } diff --git a/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/ZStreamTest.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/ZStreamTest.scala index 949944eff4..722a04c3f0 100644 --- a/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/ZStreamTest.scala +++ b/server/vertx/src/test/scala/sttp/tapir/server/vertx/streams/ZStreamTest.scala @@ -2,7 +2,7 @@ package sttp.tapir.server.vertx.streams import java.nio.ByteBuffer import io.vertx.core.buffer.Buffer -import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.flatspec.{AnyFlatSpec, AsyncFlatSpec} import org.scalatest.matchers.should.Matchers import org.scalatest.EitherValues._ import _root_.zio._ @@ -14,7 +14,7 @@ import _root_.zio.internal.tracing.TracingConfig import sttp.capabilities.zio.ZioStreams import sttp.tapir.server.vertx.VertxZioServerOptions -class ZStreamTest extends AnyFlatSpec with Matchers { +class ZStreamTest extends AsyncFlatSpec with Matchers { val runtime = Runtime.default.mapPlatform(_.withTracingConfig(TracingConfig.disabled)) @@ -60,7 +60,7 @@ class ZStreamTest extends AnyFlatSpec with Matchers { .provideLayer(clock.Clock.live) val readStream = zio.zioReadStreamCompatible(options)(runtime).asReadStream(stream) runtime - .unsafeRunSync(for { + .unsafeRunToFuture(for { ref <- ZRef.make[List[Int]](Nil) completed <- ZRef.make[Boolean](false) _ <- Task.effect { @@ -84,9 +84,7 @@ class ZStreamTest extends AnyFlatSpec with Matchers { snapshot3 <- eventually(ref.get)({ case list => list.length should be > snapshot2.length }) _ = shouldIncreaseMonotonously(snapshot3) _ <- eventually(completed.get)({ case true => () }) - } yield ()) - .toEither - .value + } yield succeed) } it should "interrupt read stream after zio stream interruption" in { @@ -99,7 +97,7 @@ class ZStreamTest extends AnyFlatSpec with Matchers { .provideLayer(clock.Clock.live) ++ ZStream.fail(new Exception("!")) val readStream = zio.zioReadStreamCompatible(options)(runtime).asReadStream(stream) runtime - .unsafeRunSync(for { + .unsafeRunToFuture(for { ref <- ZRef.make[List[Int]](Nil) completedRef <- ZRef.make[Boolean](false) interruptedRef <- ZRef.make[Option[Throwable]](None) @@ -126,9 +124,7 @@ class ZStreamTest extends AnyFlatSpec with Matchers { _ = shouldIncreaseMonotonously(snapshot) _ <- eventually(completedRef.get &&& interruptedRef.get)({ case (false, Some(_)) => }) - } yield ()) - .toEither - .value + } yield succeed) } it should "drain read stream without pauses if buffer has enough space" in { @@ -137,7 +133,7 @@ class ZStreamTest extends AnyFlatSpec with Matchers { val readStream = new FakeStream() val stream = zio.zioReadStreamCompatible(opts)(runtime).fromReadStream(readStream) runtime - .unsafeRunSync(for { + .unsafeRunToFuture(for { resultFiber <- stream .mapChunks((chunkAsInt _).andThen(Chunk.single)) .toIterator @@ -158,8 +154,6 @@ class ZStreamTest extends AnyFlatSpec with Matchers { readStream.pauseCount shouldBe 0 // readStream.resumeCount shouldBe 0 }) - .toEither - .value } it should "drain read stream with small buffer" in { @@ -168,7 +162,7 @@ class ZStreamTest extends AnyFlatSpec with Matchers { val readStream = new FakeStream() val stream = zio.zioReadStreamCompatible(opts)(runtime).fromReadStream(readStream) runtime - .unsafeRunSync(for { + .unsafeRunToFuture(for { resultFiber <- stream .mapChunks((chunkAsInt _).andThen(Chunk.single)) .mapM(i => ZIO.sleep(50.millis).as(i)) @@ -193,8 +187,6 @@ class ZStreamTest extends AnyFlatSpec with Matchers { readStream.pauseCount should be > 0 readStream.resumeCount should be > 0 }) - .toEither - .value } it should "drain failed read stream" in { @@ -203,7 +195,7 @@ class ZStreamTest extends AnyFlatSpec with Matchers { val readStream = new FakeStream() val stream = zio.zioReadStreamCompatible(opts)(runtime).fromReadStream(readStream) runtime - .unsafeRunSync(for { + .unsafeRunToFuture(for { resultFiber <- stream .mapChunks((chunkAsInt _).andThen(Chunk.single)) .mapM(i => ZIO.sleep(50.millis).as(i)) @@ -229,7 +221,5 @@ class ZStreamTest extends AnyFlatSpec with Matchers { readStream.resumeCount should be > 0 result.lastOption.collect { case Left(e) => e } should not be empty }) - .toEither - .value } } diff --git a/server/zio-http/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala b/server/zio-http/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala index 5e19e796d4..00953dbb7b 100644 --- a/server/zio-http/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala +++ b/server/zio-http/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala @@ -16,7 +16,7 @@ class ZioHttpRequestBody[R](request: Request, serverRequest: ServerRequest, serv extends RequestBody[RIO[R, *], ZioStreams] { override val streams: capabilities.Streams[ZioStreams] = ZioStreams - def asByteArray: Task[Array[Byte]] = request.content match { + private def asByteArray: Task[Array[Byte]] = request.content match { case HttpData.Empty => Task.succeed(Array.emptyByteArray) case HttpData.CompleteData(data) => Task.succeed(data.toArray) case HttpData.StreamData(data) => data.runCollect.map(_.toArray) @@ -31,7 +31,7 @@ class ZioHttpRequestBody[R](request: Request, serverRequest: ServerRequest, serv case RawBodyType.MultipartBody(_, _) => Task.never } - val stream: Stream[Throwable, Byte] = request.content match { + private def stream: Stream[Throwable, Byte] = request.content match { case HttpData.Empty => ZStream.empty case HttpData.CompleteData(data) => ZStream.fromChunk(data) case HttpData.StreamData(stream) => stream diff --git a/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala b/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala index 4705eebf2c..e82fe942c9 100644 --- a/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala +++ b/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala @@ -13,26 +13,34 @@ import sttp.tapir.server.tests.{ } import sttp.tapir.server.ziohttp.ZioHttpInterpreter.zioMonadError import sttp.tapir.tests.{Test, TestSuite} -import zio.Task +import zhttp.service.{EventLoopGroup, ServerChannelFactory} +import zhttp.service.server.ServerChannelFactory +import zio.interop.catz._ +import zio.{Runtime, Task} class ZioHttpServerTest extends TestSuite { - override def tests: Resource[IO, List[Test]] = backendResource.map { backend => - val interpreter = new ZioHttpTestServerInterpreter() - val createServerTest = new DefaultCreateServerTest(backend, interpreter) + override def tests: Resource[IO, List[Test]] = backendResource.flatMap { backend => + implicit val r: Runtime[Any] = Runtime.default + // creating the netty dependencies once, to speed up tests + (EventLoopGroup.auto(0) ++ ServerChannelFactory.auto).build.toResource[IO].map { + (nettyDeps: EventLoopGroup with ServerChannelFactory) => + val interpreter = new ZioHttpTestServerInterpreter(nettyDeps) + val createServerTest = new DefaultCreateServerTest(backend, interpreter) - implicit val m: MonadError[Task] = zioMonadError + implicit val m: MonadError[Task] = zioMonadError - new ServerBasicTests( - createServerTest, - interpreter, - multipleValueHeaderSupport = false, - inputStreamSupport = true, - supportsUrlEncodedPathSegments = false, - supportsMultipleSetCookieHeaders = false - ).tests() ++ - new ServerStreamingTests(createServerTest, ZioStreams).tests() ++ - new ServerAuthenticationTests(createServerTest).tests() ++ - new ServerMetricsTest(createServerTest).tests() + new ServerBasicTests( + createServerTest, + interpreter, + multipleValueHeaderSupport = false, + inputStreamSupport = true, + supportsUrlEncodedPathSegments = false, + supportsMultipleSetCookieHeaders = false + ).tests() ++ + new ServerStreamingTests(createServerTest, ZioStreams).tests() ++ + new ServerAuthenticationTests(createServerTest).tests() ++ + new ServerMetricsTest(createServerTest).tests() + } } } diff --git a/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala b/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala index 23341f18d3..83ea7bfbae 100644 --- a/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala +++ b/server/zio-http/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala @@ -10,8 +10,7 @@ import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.server.tests.TestServerInterpreter import sttp.tapir.tests.Port import zhttp.http._ -import zhttp.service.server.ServerChannelFactory -import zhttp.service.{EventLoopGroup, Server} +import zhttp.service.{EventLoopGroup, Server, ServerChannelFactory} import zio._ import zio.interop.catz._ import zio.stream.Stream @@ -19,7 +18,7 @@ import zio.stream.Stream import java.util.concurrent.atomic.AtomicInteger import scala.reflect.ClassTag -class ZioHttpTestServerInterpreter +class ZioHttpTestServerInterpreter(nettyDeps: EventLoopGroup with ServerChannelFactory) extends TestServerInterpreter[Task, ZioStreams, Http[Any, Throwable, Request, Response[Any, Throwable]], Stream[Throwable, Byte]] { override def route[I, E, O]( @@ -49,7 +48,7 @@ class ZioHttpTestServerInterpreter .flatMap(p => Server .make(server ++ Server.port(p)) - .provideLayer(EventLoopGroup.auto(0) ++ ServerChannelFactory.auto) + .provide(nettyDeps) .map(_ => p) ) .toResource[IO] diff --git a/server/zio-http4s-server/src/main/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerInterpreter.scala b/server/zio-http4s-server/src/main/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerInterpreter.scala index f0f027fa8b..75e7266932 100644 --- a/server/zio-http4s-server/src/main/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerInterpreter.scala +++ b/server/zio-http4s-server/src/main/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerInterpreter.scala @@ -1,15 +1,17 @@ package sttp.tapir.server.http4s.ztapir import org.http4s.HttpRoutes -import sttp.tapir.server.http4s.{Http4sServerOptions, Http4sServerInterpreter} +import sttp.tapir.server.http4s.{Http4sServerInterpreter, Http4sServerOptions} import sttp.tapir.ztapir._ -import zio.{RIO, ZIO} +import zio.blocking.Blocking import zio.clock.Clock import zio.interop.catz._ +import zio.{RIO, ZIO} trait ZHttp4sServerInterpreter[R] { - def zHttp4sServerOptions: Http4sServerOptions[RIO[R with Clock, *], RIO[R with Clock, *]] = Http4sServerOptions.default + def zHttp4sServerOptions: Http4sServerOptions[RIO[R with Clock with Blocking, *], RIO[R with Clock with Blocking, *]] = + Http4sServerOptions.default def from[I, E, O](e: ZEndpoint[I, E, O])(logic: I => ZIO[R, E, O]): ServerEndpointsToRoutes = from[I, E, O](e.zServerLogic(logic)) @@ -27,8 +29,8 @@ trait ZHttp4sServerInterpreter[R] { class ServerEndpointsToRoutes( serverEndpoints: List[ZServerEndpoint[R, _, _, _]] ) { - def toRoutes: HttpRoutes[RIO[R with Clock, *]] = { - Http4sServerInterpreter(zHttp4sServerOptions).toRoutes(serverEndpoints.map(_.widen[R with Clock])) + def toRoutes: HttpRoutes[RIO[R with Clock with Blocking, *]] = { + Http4sServerInterpreter(zHttp4sServerOptions).toRoutes(serverEndpoints.map(_.widen[R with Clock with Blocking])) } } } @@ -38,9 +40,12 @@ object ZHttp4sServerInterpreter { new ZHttp4sServerInterpreter[R] {} } - def apply[R](serverOptions: Http4sServerOptions[RIO[R with Clock, *], RIO[R with Clock, *]]): ZHttp4sServerInterpreter[R] = { + def apply[R]( + serverOptions: Http4sServerOptions[RIO[R with Clock with Blocking, *], RIO[R with Clock with Blocking, *]] + ): ZHttp4sServerInterpreter[R] = { new ZHttp4sServerInterpreter[R] { - override def zHttp4sServerOptions: Http4sServerOptions[RIO[R with Clock, *], RIO[R with Clock, *]] = serverOptions + override def zHttp4sServerOptions: Http4sServerOptions[RIO[R with Clock with Blocking, *], RIO[R with Clock with Blocking, *]] = + serverOptions } } } diff --git a/server/zio-http4s-server/src/test/scala/sttp/tapir/server/http4s/ztapir/ZEndpointTest.scala b/server/zio-http4s-server/src/test/scala/sttp/tapir/server/http4s/ztapir/ZEndpointTest.scala index 1810a11276..22848d9534 100644 --- a/server/zio-http4s-server/src/test/scala/sttp/tapir/server/http4s/ztapir/ZEndpointTest.scala +++ b/server/zio-http4s-server/src/test/scala/sttp/tapir/server/http4s/ztapir/ZEndpointTest.scala @@ -5,8 +5,8 @@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import zio.{Has, RIO, ZIO} import sttp.tapir.ztapir._ +import zio.blocking.Blocking import zio.clock.Clock -import zio.interop.catz._ class ZEndpointTest extends AnyFlatSpec with Matchers { it should "compile with widened endpoints" in { @@ -21,7 +21,7 @@ class ZEndpointTest extends AnyFlatSpec with Matchers { endpoint.serverLogic(_ => ZIO.succeed(Right(())): ZIO[Service2, Nothing, Either[Unit, Unit]]) type Env = Service1 with Service2 - val routes: HttpRoutes[RIO[Env with Clock, *]] = + val routes: HttpRoutes[RIO[Env with Clock with Blocking, *]] = ZHttp4sServerInterpreter().from(List(serverEndpoint1.widen[Env], serverEndpoint2.widen[Env])).toRoutes } } diff --git a/serverless/aws/examples/src/main/scala/sttp/tapir/serverless/aws/examples/LambdaApiExample.scala b/serverless/aws/examples/src/main/scala/sttp/tapir/serverless/aws/examples/LambdaApiExample.scala index 9f16d7d4dc..e0f7c1226a 100644 --- a/serverless/aws/examples/src/main/scala/sttp/tapir/serverless/aws/examples/LambdaApiExample.scala +++ b/serverless/aws/examples/src/main/scala/sttp/tapir/serverless/aws/examples/LambdaApiExample.scala @@ -1,6 +1,7 @@ package sttp.tapir.serverless.aws.examples import cats.effect.IO +import cats.effect.unsafe.implicits.global import cats.syntax.all._ import com.amazonaws.services.lambda.runtime.{Context, RequestStreamHandler} import io.circe.Printer diff --git a/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaHandler.scala b/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaHandler.scala index e2f76b658e..144f18eb37 100644 --- a/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaHandler.scala +++ b/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaHandler.scala @@ -1,6 +1,7 @@ package sttp.tapir.serverless.aws.lambda.tests import cats.effect.IO +import cats.effect.unsafe.implicits.global import com.amazonaws.services.lambda.runtime.{Context, RequestStreamHandler} import io.circe.Printer import io.circe.generic.auto._ diff --git a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala b/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala index adde9a6cf5..f423b72818 100644 --- a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala +++ b/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala @@ -1,6 +1,7 @@ package sttp.tapir.serverless.aws.lambda.tests import cats.effect.IO +import cats.effect.unsafe.implicits.global import org.scalatest.Assertions import org.scalatest.compatible.Assertion import org.scalatest.funsuite.AnyFunSuite @@ -61,7 +62,7 @@ class AwsLambdaSamLocalHttpTest extends AnyFunSuite { basicRequest .get(uri"$baseUri/echo/query?a=1&b=2&x=3&y=4") .send(backend) - .map(_.headers.map(h => h.name -> h.value).toSet should contain allOf ("A" -> "1", "B" -> "2", "X" -> "3", "Y" -> "4")) + .map(_.headers.map(h => h.name.toLowerCase -> h.value).toSet should contain allOf ("a" -> "1", "b" -> "2", "x" -> "3", "y" -> "4")) } private def testServer(t: ServerEndpoint[_, _, _, Any, IO], suffix: String = "")( diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala b/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala index a54cdab865..74e256b24c 100644 --- a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala +++ b/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala @@ -1,22 +1,24 @@ package sttp.tapir.serverless.aws.lambda.runtime -import cats.effect.{Blocker, ConcurrentEffect, ContextShift} +import cats.effect.unsafe.implicits.global +import cats.effect.{Async, IO} import cats.syntax.all._ -import com.typesafe.scalalogging.StrictLogging import sttp.client3.httpclient.fs2.HttpClientFs2Backend import sttp.tapir.server.ServerEndpoint import sttp.tapir.serverless.aws.lambda._ -import scala.concurrent.ExecutionContext - -abstract class AwsLambdaRuntime[F[_]: ContextShift: ConcurrentEffect] extends StrictLogging { - def endpoints: Iterable[ServerEndpoint[_, _, _, Any, F]] - implicit def executionContext: ExecutionContext = ExecutionContext.global - def serverOptions: AwsServerOptions[F] = AwsServerOptions.customInterceptors() - - def main(args: Array[String]): Unit = { - val backend = HttpClientFs2Backend.resource(Blocker.liftExecutionContext(scala.concurrent.ExecutionContext.global)) +object AwsLambdaRuntime { + def apply[F[_]: Async](endpoints: Iterable[ServerEndpoint[_, _, _, Any, F]], serverOptions: AwsServerOptions[F]): F[Unit] = { + val backend = HttpClientFs2Backend.resource() val route: Route[F] = AwsCatsEffectServerInterpreter(serverOptions).toRoute(endpoints.toList) - ConcurrentEffect[F].toIO(AwsLambdaRuntimeLogic(route, sys.env("AWS_LAMBDA_RUNTIME_API"), backend)).foreverM.unsafeRunSync() + AwsLambdaRuntimeInvocation.handleNext(route, sys.env("AWS_LAMBDA_RUNTIME_API"), backend).foreverM } } + +/** A runtime which uses the [[IO]] effect */ +abstract class AwsLambdaIORuntime { + def endpoints: Iterable[ServerEndpoint[_, _, _, Any, IO]] + def serverOptions: AwsServerOptions[IO] = AwsServerOptions.customInterceptors() + + def main(args: Array[String]): Unit = AwsLambdaRuntime(endpoints, serverOptions).unsafeRunSync() +} diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeLogic.scala b/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala similarity index 91% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeLogic.scala rename to serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala index 8ecc0bb648..915ed0d534 100644 --- a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeLogic.scala +++ b/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala @@ -1,6 +1,6 @@ package sttp.tapir.serverless.aws.lambda.runtime -import cats.effect.{ConcurrentEffect, ContextShift, Resource} +import cats.effect.{Resource, Sync} import cats.syntax.either._ import com.typesafe.scalalogging.StrictLogging import io.circe.Printer @@ -16,16 +16,17 @@ import sttp.tapir.serverless.aws.lambda.{AwsRequest, AwsResponse, Route} import scala.concurrent.duration.DurationInt // loosely based on https://github.com/carpe/scalambda/blob/master/native/src/main/scala/io/carpe/scalambda/native/ScalambdaIO.scala -object AwsLambdaRuntimeLogic extends StrictLogging { +object AwsLambdaRuntimeInvocation extends StrictLogging { - def apply[F[_]: ContextShift: ConcurrentEffect]( + /** Handles the next, single lambda invocation, read from api at `awsRuntimeApiHost` using `backend`, with the given `route`. */ + def handleNext[F[_]: Sync]( route: Route[F], - awsRuntimeApi: String, + awsRuntimeApiHost: String, backend: Resource[F, SttpBackend[F, Any]] ): F[Either[Throwable, Unit]] = { implicit val monad: MonadError[F] = new CatsMonadError[F] - val runtimeApiInvocationUri = uri"http://$awsRuntimeApi/2018-06-01/runtime/invocation" + val runtimeApiInvocationUri = uri"http://$awsRuntimeApiHost/2018-06-01/runtime/invocation" /** Make request (without a timeout as prescribed by the AWS Custom Lambda Runtime documentation). * This is due to the possibility of the runtime being frozen between lambda function invocations. diff --git a/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/AwsLambdaCreateServerStubTest.scala b/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/AwsLambdaCreateServerStubTest.scala index 97e9d8dfe5..1b91d722db 100644 --- a/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/AwsLambdaCreateServerStubTest.scala +++ b/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/AwsLambdaCreateServerStubTest.scala @@ -2,6 +2,7 @@ package sttp.tapir.serverless.aws.lambda import cats.data.NonEmptyList import cats.effect.IO +import cats.effect.unsafe.implicits.global import org.scalatest.Assertion import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams @@ -34,7 +35,7 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[IO, Any, Route[IO], val se: ServerEndpoint[I, E, O, Any, IO] = e.serverLogic(fn) val route: Route[IO] = AwsCatsEffectServerInterpreter(serverOptions).toRoute(se) val name = e.showDetail + (if (testNameSuffix == "") "" else " " + testNameSuffix) - Test(name)(runTest(stubBackend(route), uri"http://localhost:3000").unsafeRunSync()) + Test(name)(runTest(stubBackend(route), uri"http://localhost:3000").unsafeToFuture()) } override def testServerLogic[I, E, O](e: ServerEndpoint[I, E, O, Any, IO], testNameSuffix: String)( @@ -43,7 +44,7 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[IO, Any, Route[IO], val serverOptions: AwsServerOptions[IO] = AwsServerOptions.customInterceptors(encodeResponseBody = false) val route: Route[IO] = AwsCatsEffectServerInterpreter(serverOptions).toRoute(e) val name = e.showDetail + (if (testNameSuffix == "") "" else " " + testNameSuffix) - Test(name)(runTest(stubBackend(route), uri"http://localhost:3000").unsafeRunSync()) + Test(name)(runTest(stubBackend(route), uri"http://localhost:3000").unsafeToFuture()) } override def testServer(name: String, rs: => NonEmptyList[Route[IO]])( @@ -56,7 +57,7 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[IO, Any, Route[IO], } IO.pure(responses.find(_.code != StatusCode.NotFound).getOrElse(Response("", StatusCode.NotFound))) } - Test(name)(runTest(backend, uri"http://localhost:3000").unsafeRunSync()) + Test(name)(runTest(backend, uri"http://localhost:3000").unsafeToFuture()) } private def stubBackend(route: Route[IO]): SttpBackend[IO, Fs2Streams[IO] with WebSockets] = diff --git a/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeLogicTest.scala b/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala similarity index 82% rename from serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeLogicTest.scala rename to serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala index e1d90c405c..18306d8032 100644 --- a/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeLogicTest.scala +++ b/serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala @@ -1,7 +1,8 @@ package sttp.tapir.serverless.aws.lambda.runtime -import cats.effect.{ContextShift, IO, Resource} +import cats.effect.{IO, Resource} import cats.syntax.all._ +import cats.effect.unsafe.implicits.global import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import sttp.client3._ @@ -9,14 +10,12 @@ import sttp.client3.testing.SttpBackendStub import sttp.model.{Header, StatusCode} import sttp.tapir._ import sttp.tapir.integ.cats.CatsMonadError -import sttp.tapir.serverless.aws.lambda.runtime.AwsLambdaRuntimeLogicTest._ +import sttp.tapir.serverless.aws.lambda.runtime.AwsLambdaRuntimeInvocationTest._ import sttp.tapir.serverless.aws.lambda.{AwsCatsEffectServerInterpreter, AwsServerOptions} -import scala.concurrent.ExecutionContext.Implicits.global - import scala.collection.immutable.Seq -class AwsLambdaRuntimeLogicTest extends AnyFunSuite with Matchers { +class AwsLambdaRuntimeInvocationTest extends AnyFunSuite with Matchers { val nextInvocationUri = uri"http://aws/2018-06-01/runtime/invocation/next" @@ -36,7 +35,7 @@ class AwsLambdaRuntimeLogicTest extends AnyFunSuite with Matchers { .thenRespondOk() // when - val result = AwsLambdaRuntimeLogic(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() + val result = AwsLambdaRuntimeInvocation.handleNext(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() // then hello shouldBe "hello" @@ -52,7 +51,7 @@ class AwsLambdaRuntimeLogicTest extends AnyFunSuite with Matchers { .thenRespondF(_ => throw new RuntimeException) // when - val result = AwsLambdaRuntimeLogic(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() + val result = AwsLambdaRuntimeInvocation.handleNext(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() // then result.isLeft shouldBe true @@ -69,7 +68,7 @@ class AwsLambdaRuntimeLogicTest extends AnyFunSuite with Matchers { .thenRespondOk() // when - val result = AwsLambdaRuntimeLogic(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() + val result = AwsLambdaRuntimeInvocation.handleNext(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() // then result.isLeft shouldBe true @@ -84,7 +83,7 @@ class AwsLambdaRuntimeLogicTest extends AnyFunSuite with Matchers { .thenRespond(Response(awsRequest, StatusCode.Ok)) // when - val result = AwsLambdaRuntimeLogic(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() + val result = AwsLambdaRuntimeInvocation.handleNext(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() // then result.isLeft shouldBe true @@ -101,7 +100,7 @@ class AwsLambdaRuntimeLogicTest extends AnyFunSuite with Matchers { .thenRespondOk() // when - val result = AwsLambdaRuntimeLogic(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() + val result = AwsLambdaRuntimeInvocation.handleNext(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() // then result shouldBe Right(()) @@ -118,15 +117,14 @@ class AwsLambdaRuntimeLogicTest extends AnyFunSuite with Matchers { .thenRespondF(_ => throw new RuntimeException) // when - val result = AwsLambdaRuntimeLogic(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() + val result = AwsLambdaRuntimeInvocation.handleNext(route, "aws", Resource.eval(IO.pure(backend))).unsafeRunSync() // then result.isLeft shouldBe true } } -object AwsLambdaRuntimeLogicTest { - implicit val contextShift: ContextShift[IO] = IO.contextShift(global) +object AwsLambdaRuntimeInvocationTest { val options: AwsServerOptions[IO] = AwsServerOptions.customInterceptors() val awsRequest: String = diff --git a/tests/src/main/scala/sttp/tapir/tests/Test.scala b/tests/src/main/scala/sttp/tapir/tests/Test.scala index 290d23fe08..bac881bf23 100644 --- a/tests/src/main/scala/sttp/tapir/tests/Test.scala +++ b/tests/src/main/scala/sttp/tapir/tests/Test.scala @@ -1,8 +1,11 @@ package sttp.tapir.tests import org.scalactic.source.Position +import org.scalatest.Assertion -class Test(val name: String, val f: () => Unit, val pos: Position) +import scala.concurrent.Future + +class Test(val name: String, val f: () => Future[Assertion], val pos: Position) object Test { - def apply(name: String)(f: => Unit)(implicit pos: Position): Test = new Test(name, () => f, pos) + def apply(name: String)(f: => Future[Assertion])(implicit pos: Position): Test = new Test(name, () => f, pos) } diff --git a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala similarity index 69% rename from tests/src/main/scala/sttp/tapir/tests/TestSuite.scala rename to tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala index 63221c9710..a005d9bb08 100644 --- a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala +++ b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala @@ -1,19 +1,21 @@ package sttp.tapir.tests -import cats.effect.{ContextShift, IO, Resource} +import cats.effect.std.Dispatcher +import cats.effect.unsafe.implicits.global +import cats.effect.{IO, Resource} import org.scalactic.source.Position import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuite - -trait TestSuite extends AnyFunSuite with BeforeAndAfterAll { - - implicit lazy val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) +import org.scalatest.funsuite.AsyncFunSuite +trait TestSuite extends AsyncFunSuite with BeforeAndAfterAll { def tests: Resource[IO, List[Test]] def testNameFilter: Option[String] = None // define to run a single test (temporarily for debugging) + protected val (dispatcher, shutdownDispatcher) = Dispatcher[IO].allocated.unsafeRunSync() + // we need to register the tests when the class is constructed, as otherwise scalatest skips it val (allTests, doRelease) = tests.allocated.unsafeRunSync() + allTests.foreach { t => if (testNameFilter.forall(filter => t.name.contains(filter))) { implicit val pos: Position = t.pos @@ -25,6 +27,7 @@ trait TestSuite extends AnyFunSuite with BeforeAndAfterAll { override protected def afterAll(): Unit = { // the resources can only be released after all of the tests are run release.unsafeRunSync() + shutdownDispatcher.unsafeRunSync() super.afterAll() } }