From d6566372e16e20462329371f2f3315cadc14e84a Mon Sep 17 00:00:00 2001 From: adamw Date: Fri, 9 Apr 2021 15:30:44 +0200 Subject: [PATCH 01/41] #1050: http4s server and client migrated to cats-effect 3 / http4s 1.x, some other modules partially as well --- build.sbt | 49 +++--- .../http4s/EndpointToHttp4sClient.scala | 36 ++--- .../http4s/Http4sClientInterpreter.scala | 12 +- .../http4s/Http4ClientStreamingTests.scala | 1 + .../client/http4s/Http4sClientTests.scala | 6 +- .../tapir/client/tests/ClientBasicTests.scala | 2 + .../client/tests/ClientMultipartTests.scala | 1 + .../client/tests/ClientStreamingTests.scala | 1 + .../sttp/tapir/client/tests/ClientTests.scala | 1 + .../client/tests/ClientWebSocketTests.scala | 5 +- project/Versions.scala | 8 +- project/build.properties | 2 +- .../server/akkahttp/AkkaHttpServerTest.scala | 1 + .../tapir/server/http4s/CatsMonadError.scala | 16 ++ .../server/http4s/Http4sRequestBody.scala | 69 +++++++++ .../http4s/Http4sServerInterpreter.scala | 139 ++++++++++++++++++ .../server/http4s/Http4sServerOptions.scala | 98 ++++++++++++ .../server/http4s/Http4sServerRequest.scala | 26 ++++ .../http4s/Http4sServerSentEvents.scala | 27 ++++ .../server/http4s/Http4sToResponseBody.scala | 89 +++++++++++ .../server/http4s/Http4sWebSockets.scala | 109 ++++++++++++++ .../sttp/tapir/server/http4s/package.scala | 20 +++ .../http4s/Http4sServerSentEventsTest.scala | 96 ++++++++++++ .../server/http4s/Http4sServerTest.scala | 119 +++++++++++++++ .../http4s/Http4sTestServerInterpreter.scala | 54 +++++++ .../tapir/server/http4s/CatsMonadError.scala | 4 +- .../server/http4s/Http4sRequestBody.scala | 19 +-- .../http4s/Http4sServerInterpreter.scala | 77 ++++------ .../server/http4s/Http4sServerOptions.scala | 17 +-- .../server/http4s/Http4sServerRequest.scala | 7 +- .../server/http4s/Http4sToResponseBody.scala | 33 +++-- .../server/http4s/Http4sWebSockets.scala | 12 +- .../http4s/Http4sServerSentEventsTest.scala | 49 +++--- .../server/http4s/Http4sServerTest.scala | 3 +- .../http4s/Http4sTestServerInterpreter.scala | 4 +- .../tapir/server/tests/CreateServerTest.scala | 1 + .../server/tests/TestServerInterpreter.scala | 3 +- .../sttp/tapir/server/tests/package.scala | 5 +- .../VertxCatsServerInterpreter.scala | 17 +-- .../scala/sttp/tapir/tests/TestSuite.scala | 6 +- 40 files changed, 1047 insertions(+), 197 deletions(-) create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala create mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala create mode 100644 server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala create mode 100644 server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala create mode 100644 server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala diff --git a/build.sbt b/build.sbt index 35f36a0f9f..a6c14611ac 100644 --- a/build.sbt +++ b/build.sbt @@ -34,7 +34,8 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( // slow down for CI Test / parallelExecution := false, // remove false alarms about unused implicit definitions in macros - scalacOptions += "-Ywarn-macros:after" + scalacOptions += "-Ywarn-macros:after", + evictionErrorLevel := Level.Info ) val commonJvmSettings: Seq[Def.Setting[_]] = commonSettings @@ -137,12 +138,12 @@ lazy val rootProject = (project in file(".")) .settings( publishArtifact := false, name := "tapir", - testJVM := (test in Test).all(filterProject(p => !p.contains("JS"))).value, - testJS := (test in Test).all(filterProject(_.contains("JS"))).value, - testDocs := (test in Test).all(filterProject(p => p.contains("Docs") || p.contains("openapi") || p.contains("asyncapi"))).value, - testServers := (test in Test).all(filterProject(p => p.contains("Server"))).value, - testClients := (test in Test).all(filterProject(p => p.contains("Client"))).value, - testOther := (test in Test) + testJVM := (Test / test).all(filterProject(p => !p.contains("JS"))).value, + testJS := (Test / test).all(filterProject(_.contains("JS"))).value, + testDocs := (Test / test).all(filterProject(p => p.contains("Docs") || p.contains("openapi") || p.contains("asyncapi"))).value, + testServers := (Test / test).all(filterProject(p => p.contains("Server"))).value, + testClients := (Test / test).all(filterProject(p => p.contains("Client"))).value, + testOther := (Test / test) .all( filterProject(p => !p.contains("Server") && !p.contains("Client") && !p.contains("Docs") && !p.contains("openapi") && !p.contains("asyncapi") @@ -155,14 +156,14 @@ lazy val rootProject = (project in file(".")) // start a test server before running tests of a client interpreter; this is required both for JS tests run inside a // nodejs/browser environment, as well as for JVM tests where akka-http isn't available (e.g. dotty). val clientTestServerSettings = Seq( - test in Test := (test in Test) - .dependsOn(startClientTestServer in clientTestServer2_13) + Test / test := (Test / test) + .dependsOn(clientTestServer2_13 / startClientTestServer) .value, - testOnly in Test := (testOnly in Test) - .dependsOn(startClientTestServer in clientTestServer2_13) + Test / testOnly := (Test / testOnly) + .dependsOn(clientTestServer2_13 / startClientTestServer) .evaluated, - testOptions in Test += Tests.Setup(() => { - val port = (clientTestServerPort in clientTestServer2_13).value + Test / testOptions += Tests.Setup(() => { + val port = (clientTestServer2_13 / clientTestServerPort).value PollingUtils.waitUntilServerAvailable(new URL(s"http://localhost:$port")) }) ) @@ -171,16 +172,16 @@ lazy val clientTestServer = (projectMatrix in file("client/testserver")) .settings(commonJvmSettings) .settings( name := "testing-server", - skip in publish := true, + publish / skip := true, libraryDependencies ++= loggerDependencies ++ Seq( "org.http4s" %% "http4s-dsl" % Versions.http4s, "org.http4s" %% "http4s-blaze-server" % Versions.http4s, "org.http4s" %% "http4s-circe" % Versions.http4s ), // the test server needs to be started before running any client tests - mainClass in reStart := Some("sttp.tapir.client.tests.HttpServer"), - reStartArgs in reStart := Seq(s"${(clientTestServerPort in Test).value}"), - fullClasspath in reStart := (fullClasspath in Test).value, + reStart / mainClass := Some("sttp.tapir.client.tests.HttpServer"), + reStart / reStartArgs := Seq(s"${(Test / clientTestServerPort).value}"), + reStart / fullClasspath := (reStart / fullClasspath).value, clientTestServerPort := 51823, startClientTestServer := reStart.toTask("").value ) @@ -205,8 +206,8 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core")) scalaTestPlusScalaCheck.value % Test, "com.47deg" %%% "scalacheck-toolbox-datetime" % "0.5.0" % Test ), - unmanagedSourceDirectories in Compile += { - val sourceDir = (sourceDirectory in Compile).value + Compile / unmanagedSourceDirectories += { + val sourceDir = (Compile / sourceDirectory).value CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, n)) if n >= 13 => sourceDir / "scala-2.13+" case _ => sourceDir / "scala-2.13-" @@ -214,8 +215,8 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core")) }, // Until https://youtrack.jetbrains.com/issue/SCL-18636 is fixed and IntelliJ properly imports projects with // generated sources, they are explicitly added to git. See also below: commented out plugin. - unmanagedSourceDirectories in Compile += { - (sourceDirectory in Compile).value / "boilerplate-gen" + Compile / unmanagedSourceDirectories += { + (Compile / sourceDirectory).value / "boilerplate-gen" } ) .jvmPlatform( @@ -748,11 +749,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 = scala2_12Versions) .dependsOn(finatraServer % "compile->compile;test->test", serverTests % Test) 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 30f4bff868..fd137f6aae 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,11 +1,13 @@ 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 import sttp.capabilities.Streams import sttp.capabilities.fs2.Fs2Streams import sttp.model.ResponseMetadata @@ -29,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 => @@ -46,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 => @@ -63,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] @@ -95,12 +97,12 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht case EndpointIO.StreamBodyWrapper(StreamBodyIO(streams, _, _, _)) => setStreamingBody(streams)(value.asInstanceOf[streams.BinaryStream], req) case EndpointIO.Header(name, codec, _) => - val headers = codec.encode(value).map(value => Header(name, value)) + val headers = codec.encode(value).map(value => Header.Raw(CIString(name), value): Header.ToRaw) req.putHeaders(headers: _*) case EndpointIO.Headers(codec, _) => - val headers = codec.encode(value).map(h => Header(h.name, h.value)) + val headers = codec.encode(value).map(h => Header.Raw(CIString(h.name), h.value): Header.ToRaw) req.putHeaders(headers: _*) - case EndpointIO.FixedHeader(h, _, _) => req.putHeaders(Header(h.name, h.value)) + case EndpointIO.FixedHeader(h, _, _) => req.putHeaders(Header.Raw(CIString(h.name), h.value)) case EndpointInput.ExtractFromRequest(_, _) => req // ignoring case a: EndpointInput.Auth[_] => setInputParams(a.input, params, req) case EndpointInput.Pair(left, right, _, split) => handleInputPair(left, right, params, split, req) @@ -110,7 +112,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], @@ -128,10 +130,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") @@ -151,7 +153,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, @@ -164,7 +166,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, @@ -172,7 +174,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) @@ -181,7 +183,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht val output = if (code.isSuccess) e.output else e.errorOutput // headers with cookies - val headers = response.headers.toList.map(h => sttp.model.Header(h.name.toString(), h.value)).toVector + val headers = response.headers.headers.map(h => sttp.model.Header(h.name.toString, h.value)).toVector parser(response).map { responseBody => val params = clientOutputParams(output, responseBody, ResponseMetadata(code, response.status.reason, headers)) @@ -189,7 +191,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 { @@ -211,7 +213,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 fb96e71617..134a047bc1 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] { /** 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. @@ -15,10 +15,9 @@ abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] { * - 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, clientOptions: Http4sClientOptions ): I => (Request[F], Response[F] => F[DecodeResult[Either[E, O]]]) = - new EndpointToHttp4sClient(blocker, clientOptions).toHttp4sRequest[I, E, O, R, F](e, baseUri) + new EndpointToHttp4sClient(clientOptions).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,12 +28,11 @@ abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] { * - 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, clientOptions: Http4sClientOptions ): I => (Request[F], Response[F] => F[Either[E, O]]) = - new EndpointToHttp4sClient(blocker, clientOptions).toHttp4sRequestUnsafe[I, E, O, R, F](e, baseUri) + new EndpointToHttp4sClient(clientOptions).toHttp4sRequestUnsafe[I, E, O, R, F](e, baseUri) } object Http4sClientInterpreter { - def apply[F[_]: ContextShift: Effect]: Http4sClientInterpreter[F] = new Http4sClientInterpreter[F] {} + def apply[F[_]: Async]: Http4sClientInterpreter[F] = new Http4sClientInterpreter[F] {} } 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/Http4sClientTests.scala b/client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala index 5cafee14b9..facfc7b14e 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.client.blaze.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, FN[_]](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/tests/src/main/scala/sttp/tapir/client/tests/ClientBasicTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientBasicTests.scala index 23ca8cc48e..5fe1c86afb 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 1ada67dca6..23ed71f5c6 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,5 +1,6 @@ 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} 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 86198216e0..9d601c238d 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..d6075e7670 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._ @@ -43,7 +44,9 @@ trait ClientWebSocketTests[S] { this: ClientTests[S with WebSockets] => 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" diff --git a/project/Versions.scala b/project/Versions.scala index 14a79c0fbe..ad46773a4a 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,11 +1,11 @@ object Versions { - val http4s = "0.21.22" - val catsEffect = "2.4.1" + val http4s = "1.0.0-M20" + val catsEffect = "3.0.1" val circe = "0.13.0" val circeYaml = "0.13.1" - val sttp = "3.2.3" + val sttp = "3.3.0-RC1" val sttpModel = "1.4.1" - val sttpShared = "1.1.1" + val sttpShared = "1.2.0" val akkaHttp = "10.2.4" val akkaStreams = "2.6.14" val swaggerUi = "3.46.0" diff --git a/project/build.properties b/project/build.properties index 862afa5f2f..5b6d073587 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.4.7 \ No newline at end of file +sbt.version=1.5.0 \ 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 3a725fd25f..47f1e42ad0 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._ diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala new file mode 100644 index 0000000000..9793cea610 --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala @@ -0,0 +1,16 @@ +package sttp.tapir.server.http4s + +import cats.effect.Sync +import sttp.monad.MonadError + +private[http4s] class CatsMonadError[F[_]](implicit F: Sync[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.suspend(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) +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala new file mode 100644 index 0000000000..fd5420bf92 --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala @@ -0,0 +1,69 @@ +package sttp.tapir.server.http4s + +import java.io.ByteArrayInputStream +import cats.effect.{Blocker, ContextShift, Sync} +import cats.syntax.all._ +import cats.~> +import fs2.Chunk +import org.http4s.headers.{`Content-Disposition`, `Content-Type`} +import org.http4s.{Charset, EntityDecoder, Request, multipart} +import sttp.capabilities.fs2.Fs2Streams +import sttp.model.{Header, Part} +import sttp.tapir.model.ServerRequest +import sttp.tapir.server.interpreter.RequestBody +import sttp.tapir.{RawBodyType, RawPart} + +private[http4s] class Http4sRequestBody[F[_]: Sync: ContextShift, G[_]: Sync]( // TODO: constraints? + request: Request[F], + serverRequest: ServerRequest, + serverOptions: Http4sServerOptions[F, G], + t: F ~> G +) extends RequestBody[G, Fs2Streams[F]] { + override val streams: Fs2Streams[F] = Fs2Streams[F] + override def toRaw[R](bodyType: RawBodyType[R]): G[R] = toRawFromStream(request.body, bodyType, request.charset) + override def toStream(): streams.BinaryStream = request.body + + private def toRawFromStream[R](body: fs2.Stream[F, Byte], bodyType: RawBodyType[R], charset: Option[Charset]): G[R] = { + def asChunk: G[Chunk[Byte]] = t(body.compile.to(Chunk)) + def asByteArray: G[Array[Byte]] = t(body.compile.to(Chunk).map(_.toByteBuffer.array())) + + bodyType match { + case RawBodyType.StringBody(defaultCharset) => asByteArray.map(new String(_, charset.map(_.nioCharset).getOrElse(defaultCharset))) + case RawBodyType.ByteArrayBody => asByteArray + case RawBodyType.ByteBufferBody => asChunk.map(_.toByteBuffer) + case RawBodyType.InputStreamBody => asByteArray.map(new ByteArrayInputStream(_)) + case RawBodyType.FileBody => + serverOptions.createFile(serverRequest).flatMap { file => + val fileSink = fs2.io.file.writeAll[F](file.toPath, Blocker.liftExecutionContext(serverOptions.blockingExecutionContext)) + t(body.through(fileSink).compile.drain.map(_ => file)) + } + case m: RawBodyType.MultipartBody => + // TODO: use MultipartDecoder.mixedMultipart once available? + t(implicitly[EntityDecoder[F, multipart.Multipart[F]]].decode(request, strict = false).value.flatMap { + case Left(failure) => Sync[F].raiseError(failure) + case Right(mp) => + val rawPartsF: Vector[F[RawPart]] = mp.parts + .flatMap(part => part.name.flatMap(name => m.partType(name)).map((part, _)).toList) + .map { case (part, codecMeta) => toRawPart(part, codecMeta).asInstanceOf[F[RawPart]] } + + val rawParts: F[Vector[RawPart]] = rawPartsF.sequence + + rawParts.asInstanceOf[F[R]] // R is Seq[RawPart] + }) + } + } + + private def toRawPart[R](part: multipart.Part[F], partType: RawBodyType[R]): G[Part[R]] = { + val dispositionParams = part.headers.get(`Content-Disposition`).map(_.parameters).getOrElse(Map.empty) + val charset = part.headers.get(`Content-Type`).flatMap(_.charset) + toRawFromStream(part.body, partType, charset) + .map(r => + Part( + part.name.getOrElse(""), + r, + otherDispositionParams = dispositionParams - Part.NameDispositionParam, + headers = part.headers.toList.map(h => Header(h.name.value, h.value)) + ) + ) + } +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala new file mode 100644 index 0000000000..42dd49587d --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala @@ -0,0 +1,139 @@ +package sttp.tapir.server.http4s + +import cats.arrow.FunctionK +import cats.data.{Kleisli, OptionT} +import cats.effect.{Concurrent, ContextShift, Sync, Timer} +import cats.syntax.all._ +import cats.~> +import fs2.Pipe +import fs2.concurrent.Queue +import org.http4s.server.websocket.WebSocketBuilder +import org.http4s.util.CaseInsensitiveString +import org.http4s.websocket.WebSocketFrame +import org.http4s._ +import org.log4s.{Logger, getLogger} +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams +import sttp.model.{Header => SttpHeader} +import sttp.tapir.Endpoint +import sttp.tapir.model.ServerResponse +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.interpreter.ServerInterpreter + +import scala.reflect.ClassTag + +trait Http4sServerInterpreter { + def toHttp[I, E, O, F[_], G[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(t: F ~> G)(logic: I => G[Either[E, O]])(implicit + serverOptions: Http4sServerOptions[F, G], + gs: Sync[G], + fs: Concurrent[F], + fcs: ContextShift[F], + timer: Timer[F] + ): Http[OptionT[G, *], F] = toHttp(e.serverLogic(logic))(t) + + def toHttpRecoverErrors[I, E, O, F[_], G[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(t: F ~> G)(logic: I => G[O])(implicit + serverOptions: Http4sServerOptions[F, G], + gs: Sync[G], + fs: Concurrent[F], + fcs: ContextShift[F], + eIsThrowable: E <:< Throwable, + eClassTag: ClassTag[E], + timer: Timer[F] + ): Http[OptionT[G, *], F] = toHttp(e.serverLogicRecoverErrors(logic))(t) + + def toRoutes[I, E, O, F[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])( + logic: I => F[Either[E, O]] + )(implicit serverOptions: Http4sServerOptions[F, F], fs: Concurrent[F], fcs: ContextShift[F], timer: Timer[F]): HttpRoutes[F] = toRoutes( + e.serverLogic(logic) + ) + + def toRouteRecoverErrors[I, E, O, F[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(logic: I => F[O])(implicit + serverOptions: Http4sServerOptions[F, F], + fs: Concurrent[F], + fcs: ContextShift[F], + eIsThrowable: E <:< Throwable, + eClassTag: ClassTag[E], + timer: Timer[F] + ): HttpRoutes[F] = toRoutes(e.serverLogicRecoverErrors(logic)) + + // + + def toHttp[I, E, O, F[_], G[_]](se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, G])( + t: F ~> G + )(implicit + serverOptions: Http4sServerOptions[F, G], + gs: Sync[G], + fs: Concurrent[F], + fcs: ContextShift[F], + timer: Timer[F] + ): Http[OptionT[G, *], F] = toHttp(List(se))(t) + + def toRoutes[I, E, O, F[_]]( + se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, F] + )(implicit serverOptions: Http4sServerOptions[F, F], fs: Concurrent[F], fcs: ContextShift[F], timer: Timer[F]): HttpRoutes[F] = toRoutes( + List(se) + ) + + // + + def toRoutes[F[_]](serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, F]])(implicit + serverOptions: Http4sServerOptions[F, F], + fs: Concurrent[F], + fcs: ContextShift[F], + timer: Timer[F] + ): HttpRoutes[F] = toHttp(serverEndpoints)(FunctionK.id[F]) + + // + + def toHttp[F[_], G[_]](serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, G]])(t: F ~> G)(implicit + serverOptions: Http4sServerOptions[F, G], + gs: Sync[G], + fs: Concurrent[F], + fcs: ContextShift[F], + timer: Timer[F] + ): Http[OptionT[G, *], F] = { + implicit val monad: CatsMonadError[G] = new CatsMonadError[G] + + Kleisli { (req: Request[F]) => + val serverRequest = new Http4sServerRequest(req) + val interpreter = new ServerInterpreter[Fs2Streams[F] with WebSockets, G, Http4sResponseBody[F], Fs2Streams[F]]( + new Http4sRequestBody[F, G](req, serverRequest, serverOptions, t), + new Http4sToResponseBody[F, G](serverOptions), + serverOptions.interceptors + ) + + OptionT(interpreter(serverRequest, serverEndpoints).flatMap { + case None => none.pure[G] + case Some(response) => t(serverResponseToHttp4s[F](response)).map(_.some) + }) + } + } + + private def serverResponseToHttp4s[F[_]: Concurrent]( + response: ServerResponse[Http4sResponseBody[F]] + ): F[Response[F]] = { + val statusCode = statusCodeToHttp4sStatus(response.code) + val headers = Headers(response.headers.map { case SttpHeader(k, v) => Header.Raw(CaseInsensitiveString(k), v) }.toList) + + response.body match { + 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].build(queue.dequeue, receive, headers = headers, filterPingPongs = false) + } + } + case Some(Right(entity)) => + Response(status = statusCode, headers = headers, body = entity).pure[F] + + case None => Response[F](status = statusCode, headers = headers).pure[F] + } + } + + private def statusCodeToHttp4sStatus(code: sttp.model.StatusCode): Status = + Status.fromInt(code.code).getOrElse(throw new IllegalArgumentException(s"Invalid status code: $code")) +} + +object Http4sServerInterpreter extends Http4sServerInterpreter { + private[http4s] val log: Logger = getLogger +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala new file mode 100644 index 0000000000..d2f6b284ed --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala @@ -0,0 +1,98 @@ +package sttp.tapir.server.http4s + +import cats.Applicative +import cats.effect.{ContextShift, Sync} +import cats.implicits.catsSyntaxOptionId +import sttp.tapir.Defaults +import sttp.tapir.model.ServerRequest +import sttp.tapir.server.interceptor.log.{DefaultServerLog, ServerLog, ServerLogInterceptor} +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 java.io.File +import scala.concurrent.ExecutionContext + +/** @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. + * Usually the same as `F`. + */ +case class Http4sServerOptions[F[_], G[_]]( + createFile: ServerRequest => G[File], + blockingExecutionContext: ExecutionContext, + ioChunkSize: Int, + interceptors: List[Interceptor[G, Http4sResponseBody[F]]] +) { + def prependInterceptor(i: Interceptor[G, Http4sResponseBody[F]]): Http4sServerOptions[F, G] = + copy(interceptors = i :: interceptors) + def appendInterceptor(i: Interceptor[G, Http4sResponseBody[F]]): Http4sServerOptions[F, G] = + copy(interceptors = interceptors :+ i) +} + +object Http4sServerOptions { + + /** Creates default [[Http4sServerOptions]] 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 exceptionHandler Whether to respond to exceptions, or propagate them to http4s. + * @param serverLog The server log using which an interceptor will be created, if any. To keep the default, use + * `Http4sServerOptions.Log.defaultServerLog` + * @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[_], G[_]: Sync: ContextShift]( + exceptionHandler: Option[ExceptionHandler], + serverLog: Option[ServerLog[G[Unit]]], + 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 + ): Http4sServerOptions[F, G] = + Http4sServerOptions( + defaultCreateFile[G].apply(blockingExecutionContext), + blockingExecutionContext, + 8192, + exceptionHandler.map(new ExceptionInterceptor[G, Http4sResponseBody[F]](_)).toList ++ + serverLog.map(Log.serverLogInterceptor[F, G]).toList ++ + additionalInterceptors ++ + unsupportedMediaTypeInterceptor.toList ++ + List(new DecodeFailureInterceptor[G, Http4sResponseBody[F]](decodeFailureHandler)) + ) + + def defaultCreateFile[F[_]](implicit sync: Sync[F], cs: ContextShift[F]): ExecutionContext => ServerRequest => F[File] = + ec => _ => cs.evalOn(ec)(sync.delay(Defaults.createTempFile())) + + object Log { + def defaultServerLog[F[_]: Sync]: DefaultServerLog[F[Unit]] = + DefaultServerLog[F[Unit]]( + doLogWhenHandled = debugLog[F], + doLogAllDecodeFailures = debugLog[F], + doLogExceptions = (msg: String, ex: Throwable) => Sync[F].delay(Http4sServerInterpreter.log.error(ex)(msg)), + noLog = Applicative[F].unit + ) + + def serverLogInterceptor[F[_], G[_]](serverLog: ServerLog[G[Unit]]): ServerLogInterceptor[G[Unit], G, Http4sResponseBody[F]] = + new ServerLogInterceptor[G[Unit], G, Http4sResponseBody[F]](serverLog, (f, _) => f) + + private def debugLog[F[_]: Sync](msg: String, exOpt: Option[Throwable]): F[Unit] = + exOpt match { + case None => Sync[F].delay(Http4sServerInterpreter.log.debug(msg)) + case Some(ex) => Sync[F].delay(Http4sServerInterpreter.log.debug(ex)(msg)) + } + } + + implicit def default[F[_], G[_]: Sync: ContextShift]: Http4sServerOptions[F, G] = + customInterceptors(Some(DefaultExceptionHandler), Some(Log.defaultServerLog[G])) +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala new file mode 100644 index 0000000000..c1e1537076 --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala @@ -0,0 +1,26 @@ +package sttp.tapir.server.http4s + +import org.http4s.Request +import sttp.model.{Header, Method, QueryParams, Uri} +import sttp.tapir.model.{ConnectionInfo, ServerRequest} + +import scala.collection.immutable.Seq + +private[http4s] class Http4sServerRequest[F[_]](req: Request[F]) extends ServerRequest { + override def protocol: String = req.httpVersion.toString() + override lazy val connectionInfo: ConnectionInfo = ConnectionInfo(req.server, req.remote, req.isSecure) + override def underlying: Any = req + + /** Can differ from `uri.path`, if the endpoint is deployed in a context */ + override lazy val pathSegments: List[String] = { + // if the routes are mounted within a context (e.g. using a router), we have to match against what comes + // after the context. This information is stored in the the PathInfoCaret attribute + req.pathInfo.dropWhile(_ == '/').split("/").toList.map(org.http4s.Uri.decode(_)) + } + + override lazy val queryParameters: QueryParams = QueryParams.fromMultiMap(req.multiParams) + + override def method: Method = Method(req.method.name.toUpperCase) + override def uri: Uri = Uri.unsafeParse(req.uri.toString()) + override lazy val headers: Seq[Header] = req.headers.toList.map(h => Header(h.name.value, h.value)) +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala new file mode 100644 index 0000000000..e43ddba0aa --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala @@ -0,0 +1,27 @@ +package sttp.tapir.server.http4s + +import fs2.{Stream, text} +import sttp.capabilities.fs2.Fs2Streams +import sttp.model.sse.ServerSentEvent + +object Http4sServerSentEvents { + + def serialiseSSEToBytes[F[_]](streams: Fs2Streams[F]): Stream[F, ServerSentEvent] => streams.BinaryStream = sseStream => { + sseStream + .map(sse => { + s"${sse.toString()}\n\n" + }) + .through(text.utf8Encode) + } + + def parseBytesToSSE[F[_]](streams: Fs2Streams[F]): streams.BinaryStream => Stream[F, ServerSentEvent] = stream => { + stream + .through(text.utf8Decode[F]) + .through(text.lines[F]) + .split(_.isEmpty) + .filter(_.nonEmpty) + .map(_.toList) + .map(ServerSentEvent.parse) + } + +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala new file mode 100644 index 0000000000..85d5815ba9 --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala @@ -0,0 +1,89 @@ +package sttp.tapir.server.http4s + +import cats.effect.{Blocker, Concurrent, ContextShift, Timer} +import cats.syntax.all._ +import fs2.{Chunk, Stream} +import org.http4s +import org.http4s.headers.{`Content-Disposition`, `Content-Type`} +import org.http4s.{EntityBody, EntityEncoder, Header, Headers, multipart} +import org.http4s.util.CaseInsensitiveString +import sttp.capabilities.fs2.Fs2Streams +import sttp.model.{HasHeaders, Part, Header => SttpHeader} +import sttp.tapir.{CodecFormat, RawBodyType, RawPart, WebSocketBodyOutput} +import sttp.tapir.server.interpreter.ToResponseBody + +import java.nio.charset.Charset + +private[http4s] class Http4sToResponseBody[F[_]: Concurrent: Timer: ContextShift, G[_]]( + serverOptions: Http4sServerOptions[F, G] +) extends ToResponseBody[Http4sResponseBody[F], Fs2Streams[F]] { + override val streams: Fs2Streams[F] = Fs2Streams[F] + + override def fromRawValue[R](v: R, headers: HasHeaders, format: CodecFormat, bodyType: RawBodyType[R]): Http4sResponseBody[F] = + Right(rawValueToEntity(bodyType, v)) + + override def fromStreamValue( + v: Stream[F, Byte], + headers: HasHeaders, + format: CodecFormat, + charset: Option[Charset] + ): Http4sResponseBody[F] = + Right(v) + + override def fromWebSocketPipe[REQ, RESP]( + pipe: streams.Pipe[REQ, RESP], + o: WebSocketBodyOutput[streams.Pipe[REQ, RESP], REQ, RESP, _, Fs2Streams[F]] + ): Http4sResponseBody[F] = Left(Http4sWebSockets.pipeToBody(pipe, o)) + + private def rawValueToEntity[CF <: CodecFormat, R](bodyType: RawBodyType[R], r: R): EntityBody[F] = { + 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)) + case RawBodyType.ByteBufferBody => fs2.Stream.chunk(Chunk.byteBuffer(r)) + case RawBodyType.InputStreamBody => + fs2.io.readInputStream( + r.pure[F], + serverOptions.ioChunkSize, + Blocker.liftExecutionContext(serverOptions.blockingExecutionContext) + ) + case RawBodyType.FileBody => + fs2.io.file.readAll[F](r.toPath, Blocker.liftExecutionContext(serverOptions.blockingExecutionContext), 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 + body + } + } + + private def rawPartToBodyPart[T](m: RawBodyType.MultipartBody, part: Part[T]): Option[multipart.Part[F]] = { + m.partType(part.name).map { partType => + val headers = part.headers.map { case SttpHeader(hk, hv) => + Header.Raw(CaseInsensitiveString(hk), hv) + }.toList + + val partContentType = part.contentType.map(parseContentType).getOrElse(`Content-Type`(http4s.MediaType.application.`octet-stream`)) + val entity = rawValueToEntity(partType.asInstanceOf[RawBodyType[Any]], part.body) + + val dispositionParams = part.otherDispositionParams + (Part.NameDispositionParam -> part.name) + val contentDispositionHeader = `Content-Disposition`("form-data", dispositionParams) + + val shouldAddCtHeader = headers.exists(_.name == `Content-Type`.name) + val allHeaders = if (shouldAddCtHeader) { + Headers(partContentType :: contentDispositionHeader :: headers) + } else { + Headers(contentDispositionHeader :: headers) + } + + multipart.Part(allHeaders, entity) + } + } + + private def parseContentType(ct: String): `Content-Type` = + `Content-Type`( + http4s.MediaType + .parse(ct) + .getOrElse(throw new IllegalArgumentException(s"Cannot parse content type: $ct")) + ) +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala new file mode 100644 index 0000000000..064412846f --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala @@ -0,0 +1,109 @@ +package sttp.tapir.server.http4s + +import cats.Monad +import cats.effect.{Concurrent, Timer} +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]( + pipe: Pipe[F, REQ, RESP], + o: WebSocketBodyOutput[Pipe[F, REQ, RESP], REQ, RESP, _, Fs2Streams[F]] + ): F[Pipe[F, Http4sWebSocketFrame, Http4sWebSocketFrame]] = { + Queue.bounded[F, WebSocketFrame](1).map { pongs => (in: Stream[F, Http4sWebSocketFrame]) => + val sttpFrames = in.map(http4sFrameToFrame) + val concatenated = optionallyConcatenateFrames(sttpFrames, o.concatenateFragmentedFrames) + val ignorePongs = optionallyIgnorePong(concatenated, o.ignorePong) + val autoPongs = optionallyAutoPong(ignorePongs, pongs, o.autoPongOnPing) + val autoPings = o.autoPing match { + case Some((interval, frame)) => Stream.awakeEvery[F](interval).map(_ => frame) + case None => Stream.empty + } + + autoPongs + .map { + case _: WebSocketFrame.Close if !o.decodeCloseRequests => None + case f => + o.requests.decode(f) match { + case failure: DecodeResult.Failure => throw new WebSocketFrameDecodeFailure(f, failure) + case DecodeResult.Value(v) => Some(v) + } + } + .unNoneTerminate + .through(pipe) + .map(o.responses.encode) + .mergeHaltL(pongs.dequeue) + .mergeHaltL(autoPings) + .map(frameToHttp4sFrame) + } + } + + private def http4sFrameToFrame(f: Http4sWebSocketFrame): WebSocketFrame = + f match { + case t: Http4sWebSocketFrame.Text => WebSocketFrame.Text(t.str, t.last, None) + case Http4sWebSocketFrame.Ping(data) => WebSocketFrame.Ping(data.toArray) + case Http4sWebSocketFrame.Pong(data) => WebSocketFrame.Pong(data.toArray) + case c: Http4sWebSocketFrame.Close => WebSocketFrame.Close(c.closeCode, "") + case _ => WebSocketFrame.Binary(f.data.toArray, f.last, None) + } + + private def frameToHttp4sFrame(w: WebSocketFrame): Http4sWebSocketFrame = { + w match { + case WebSocketFrame.Text(p, finalFragment, _) => Http4sWebSocketFrame.Text(p, finalFragment) + case WebSocketFrame.Binary(p, finalFragment, _) => Http4sWebSocketFrame.Binary(ByteVector(p), finalFragment) + case WebSocketFrame.Ping(p) => Http4sWebSocketFrame.Ping(ByteVector(p)) + case WebSocketFrame.Pong(p) => Http4sWebSocketFrame.Pong(ByteVector(p)) + case WebSocketFrame.Close(code, reason) => Http4sWebSocketFrame.Close(code, reason).fold(throw _, identity) + } + } + + private def optionallyConcatenateFrames[F[_]](s: Stream[F, WebSocketFrame], doConcatenate: Boolean): Stream[F, WebSocketFrame] = { + if (doConcatenate) { + type Accumulator = Option[Either[Array[Byte], String]] + + s.mapAccumulate(None: Accumulator) { + case (None, f: WebSocketFrame.Ping) => (None, Some(f)) + case (None, f: WebSocketFrame.Pong) => (None, Some(f)) + case (None, f: WebSocketFrame.Close) => (None, Some(f)) + case (None, f: WebSocketFrame.Data[_]) if f.finalFragment => (None, Some(f)) + case (Some(Left(acc)), f: WebSocketFrame.Binary) if f.finalFragment => (None, Some(f.copy(payload = acc ++ f.payload))) + case (Some(Left(acc)), f: WebSocketFrame.Binary) if !f.finalFragment => (Some(Left(acc ++ f.payload)), None) + case (Some(Right(acc)), f: WebSocketFrame.Text) if f.finalFragment => (None, Some(f.copy(payload = acc + f.payload))) + case (Some(Right(acc)), f: WebSocketFrame.Text) if !f.finalFragment => (Some(Right(acc + f.payload)), None) + case (acc, f) => throw new IllegalStateException(s"Cannot accumulate web socket frames. Accumulator: $acc, frame: $f.") + }.collect { case (_, Some(f)) => f } + } else { + s + } + } + + private def optionallyIgnorePong[F[_]](s: Stream[F, WebSocketFrame], doIgnore: Boolean): Stream[F, WebSocketFrame] = { + if (doIgnore) { + s.filter { + case WebSocketFrame.Pong(_) => false + case _ => true + } + } else s + } + + private def optionallyAutoPong[F[_]: Monad]( + s: Stream[F, WebSocketFrame], + pongs: Queue[F, WebSocketFrame], + doAuto: Boolean + ): Stream[F, WebSocketFrame] = { + if (doAuto) { + s.evalMap { + case WebSocketFrame.Ping(payload) => pongs.enqueue1(WebSocketFrame.Pong(payload)).map(_ => none[WebSocketFrame]) + case f => f.some.pure[F] + }.collect { case Some(f) => + f + } + } else s + } +} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala new file mode 100644 index 0000000000..c202cc00c9 --- /dev/null +++ b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala @@ -0,0 +1,20 @@ +package sttp.tapir.server + +import fs2.Pipe +import org.http4s.EntityBody +import org.http4s.websocket.WebSocketFrame +import sttp.capabilities.fs2.Fs2Streams +import sttp.model.sse.ServerSentEvent +import sttp.tapir.{CodecFormat, StreamBodyIO, streamTextBody} + +import java.nio.charset.Charset + +package object http4s { + private[http4s] type Http4sResponseBody[F[_]] = Either[F[Pipe[F, WebSocketFrame, WebSocketFrame]], EntityBody[F]] + + def serverSentEventsBody[F[_]]: StreamBodyIO[fs2.Stream[F, Byte], fs2.Stream[F, ServerSentEvent], Fs2Streams[F]] = { + val fs2Streams = Fs2Streams[F] + streamTextBody(fs2Streams)(CodecFormat.TextEventStream(), Some(Charset.forName("UTF-8"))) + .map(Http4sServerSentEvents.parseBytesToSSE(fs2Streams))(Http4sServerSentEvents.serialiseSSEToBytes(fs2Streams)) + } +} diff --git a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala new file mode 100644 index 0000000000..8e3a5c5c8b --- /dev/null +++ b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala @@ -0,0 +1,96 @@ +package sttp.tapir.server.http4s + +import cats.effect.IO +import org.scalatest.funsuite.AnyFunSuite +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 { + + 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 + |event: event + |id: id1 + |retry: 10 + | + |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList + }).unsafeRunSync() + } + + 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 + s"""data: data + |id: id1 + | + |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList + }).unsafeRunSync() + } + + test("serialiseSSEToBytes should successfully serialise multiline data event") { + val sse = fs2.Stream( + ServerSentEvent( + Some("""some data info 1 + |some data info 2 + |some data info 3""".stripMargin), + None, + None, + None + ) + ) + val serialised = Http4sServerSentEvents.serialiseSSEToBytes(Fs2Streams[IO])(sse) + val futureEvents = serialised.compile.toList + 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() + } + + test("parseBytesToSSE should successfully parse SSE bytes to SSE structure") { + val sseBytes = fs2.Stream.iterable( + """data: event1 data + |event: event1 + |id: id1 + |retry: 5 + | + | + |data: event2 data1 + |data: event2 data2 + |data: event2 data3 + |id: id2 + | + |""".stripMargin.getBytes(Charset.forName("UTF-8")) + ) + 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 + |event2 data2 + |event2 data3""".stripMargin), + None, + Some("id2"), + None + ) + ) + ).unsafeRunSync() + } + +} diff --git a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala new file mode 100644 index 0000000000..0d7a840e0f --- /dev/null +++ b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala @@ -0,0 +1,119 @@ +package sttp.tapir.server.http4s + +import cats.effect._ +import cats.syntax.all._ +import org.http4s.server.Router +import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.syntax.kleisli._ +import org.scalatest.{EitherValues, OptionValues} +import org.scalatest.matchers.should.Matchers._ +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams +import sttp.client3._ +import sttp.model.sse.ServerSentEvent +import sttp.tapir._ +import sttp.tapir.server.tests.{ + CreateServerTest, + ServerAuthenticationTests, + ServerBasicTests, + ServerStreamingTests, + ServerWebSocketTests, + backendResource +} +import sttp.tapir.tests.{Test, TestSuite} +import sttp.ws.{WebSocket, WebSocketFrame} + +import java.util.UUID +import scala.concurrent.ExecutionContext +import scala.concurrent.duration.DurationInt +import scala.util.Random + +class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite with EitherValues with OptionValues { + + override def tests: Resource[IO, List[Test]] = backendResource.map { backend => + implicit val m: CatsMonadError[IO] = new CatsMonadError[IO] + val interpreter = new Http4sTestServerInterpreter() + val createServerTest = new CreateServerTest(interpreter) + def randomUUID = Some(UUID.randomUUID().toString) + 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])) + val routes = Http4sServerInterpreter.toRoutes(e) + + BlazeServerBuilder[IO](ExecutionContext.global) + .bindHttp(0, "localhost") + .withHttpApp(Router("/api" -> routes).orNotFound) + .resource + .use { server => + val port = server.address.getPort + basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right("ok")) + } + .unsafeRunSync() + }, + createServerTest.testServer( + endpoint.out( + webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain] + .apply(Fs2Streams[IO]) + .autoPing(Some((1.second, WebSocketFrame.ping))) + ), + "automatic pings" + )((_: Unit) => IO(Right((in: fs2.Stream[IO, String]) => in))) { baseUri => + basicRequest + .response(asWebSocket { ws: WebSocket[IO] => + List(ws.receive().timeout(60.seconds), ws.receive().timeout(60.seconds)).sequence + }) + .get(baseUri.scheme("ws")) + .send(backend) + .map(_.body should matchPattern { case Right(List(WebSocketFrame.Ping(_), WebSocketFrame.Ping(_))) => }) + }, + createServerTest.testServer( + endpoint.out(streamBinaryBody(Fs2Streams[IO])), + "streaming should send data according to producer stream rate" + )((_: Unit) => + IO(Right(fs2.Stream.awakeEvery[IO](1.second).map(_.toString()).through(fs2.text.utf8Encode).interruptAfter(5.seconds))) + ) { baseUri => + basicRequest + .response( + asStream(Fs2Streams[IO])(bs => { + bs.through(fs2.text.utf8Decode).mapAccumulate(0)((pings, currentTime) => (pings + 1, currentTime)).compile.last + }) + ) + .get(baseUri) + .send(backend) + .map(_.body match { + case Right(Some((pings, _))) => pings should be >= 2 + case wrongResponse => fail(s"expected to get count of received data chunks, instead got $wrongResponse") + }) + }, + createServerTest.testServer( + endpoint.out(serverSentEventsBody[IO]), + "Send and receive SSE" + )((_: Unit) => IO(Right(fs2.Stream(sse1, sse2)))) { baseUri => + basicRequest + .response(asStream[IO, List[ServerSentEvent], Fs2Streams[IO]](Fs2Streams[IO]) { stream => + Http4sServerSentEvents + .parseBytesToSSE(Fs2Streams[IO]) + .apply(stream) + .compile + .toList + }) + .get(baseUri) + .send(backend) + .map(_.body.value shouldBe List(sse1, sse2)) + } + ) + + new ServerBasicTests(backend, createServerTest, interpreter).tests() ++ + new ServerStreamingTests(backend, createServerTest, Fs2Streams[IO]).tests() ++ + new ServerWebSocketTests(backend, createServerTest, Fs2Streams[IO]) { + override def functionToPipe[A, B](f: A => B): streams.Pipe[A, B] = in => in.map(f) + }.tests() ++ + new ServerAuthenticationTests(backend, createServerTest).tests() ++ + additionalTests() + } +} diff --git a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala new file mode 100644 index 0000000000..605a0d65e3 --- /dev/null +++ b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala @@ -0,0 +1,54 @@ +package sttp.tapir.server.http4s + +import cats.data.{Kleisli, NonEmptyList} +import cats.effect.{ContextShift, IO, Resource, Timer} +import cats.syntax.all._ +import org.http4s.syntax.kleisli._ +import org.http4s.{HttpRoutes, Request, Response} +import org.http4s.server.blaze.BlazeServerBuilder +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams +import sttp.tapir.Endpoint +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} +import sttp.tapir.server.interceptor.exception.DefaultExceptionHandler +import sttp.tapir.server.tests.TestServerInterpreter +import sttp.tapir.tests.Port + +import scala.concurrent.ExecutionContext +import scala.reflect.ClassTag + +class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[IO] with WebSockets, HttpRoutes[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], + decodeFailureHandler: Option[DecodeFailureHandler] = None + ): HttpRoutes[IO] = { + implicit val serverOptions: Http4sServerOptions[IO, IO] = Http4sServerOptions + .customInterceptors( + exceptionHandler = Some(DefaultExceptionHandler), + serverLog = Some(Http4sServerOptions.Log.defaultServerLog), + decodeFailureHandler = decodeFailureHandler.getOrElse(DefaultDecodeFailureHandler.handler) + ) + Http4sServerInterpreter.toRoutes(e) + } + + override def routeRecoverErrors[I, E <: Throwable, O](e: Endpoint[I, E, O, Fs2Streams[IO] with WebSockets], fn: I => IO[O])(implicit + eClassTag: ClassTag[E] + ): HttpRoutes[IO] = { + Http4sServerInterpreter.toRouteRecoverErrors(e)(fn) + } + + override def server(routes: NonEmptyList[HttpRoutes[IO]]): Resource[IO, Port] = { + val service: Kleisli[IO, Request[IO], Response[IO]] = routes.reduceK.orNotFound + + BlazeServerBuilder[IO](ExecutionContext.global) + .bindHttp(0, "localhost") + .withHttpApp(service) + .resource + .map(_.address.getPort) + } +} diff --git a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala index 9793cea610..a3aec62ef4 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala @@ -10,7 +10,7 @@ private[http4s] class CatsMonadError[F[_]](implicit F: Sync[F]) extends MonadErr 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) + override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guaranteeCase(f)(_ => e) } 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 fd5420bf92..3feced979d 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,10 +1,11 @@ package sttp.tapir.server.http4s import java.io.ByteArrayInputStream -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 @@ -13,7 +14,7 @@ import sttp.tapir.model.ServerRequest import sttp.tapir.server.interpreter.RequestBody import sttp.tapir.{RawBodyType, RawPart} -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], @@ -25,7 +26,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[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) => asByteArray.map(new String(_, charset.map(_.nioCharset).getOrElse(defaultCharset))) @@ -34,7 +35,7 @@ private[http4s] class Http4sRequestBody[F[_]: Sync: ContextShift, G[_]: Sync]( / case RawBodyType.InputStreamBody => asByteArray.map(new ByteArrayInputStream(_)) 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(_ => file)) } case m: RawBodyType.MultipartBody => @@ -54,15 +55,15 @@ private[http4s] class Http4sRequestBody[F[_]: Sync: ContextShift, G[_]: Sync]( / } private def toRawPart[R](part: multipart.Part[F], partType: RawBodyType[R]): G[Part[R]] = { - val dispositionParams = part.headers.get(`Content-Disposition`).map(_.parameters).getOrElse(Map.empty) - val charset = part.headers.get(`Content-Type`).flatMap(_.charset) + val dispositionParams = part.headers.get[`Content-Disposition`].map(_.parameters).getOrElse(Map.empty) + val charset = part.headers.get[`Content-Type`].flatMap(_.charset) toRawFromStream(part.body, partType, charset) .map(r => Part( part.name.getOrElse(""), r, - otherDispositionParams = dispositionParams - Part.NameDispositionParam, - headers = part.headers.toList.map(h => Header(h.name.value, h.value)) + otherDispositionParams = dispositionParams.map { case (k, v) => k.toString -> v } - Part.NameDispositionParam, + headers = part.headers.headers.map(h => Header(h.name.toString, h.value)) ) ) } 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 42dd49587d..820b6222d4 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 @@ -2,16 +2,16 @@ package sttp.tapir.server.http4s import cats.arrow.FunctionK import cats.data.{Kleisli, OptionT} -import cats.effect.{Concurrent, ContextShift, Sync, Timer} +import cats.effect.std.Queue +import cats.effect.{Async, Sync} import cats.syntax.all._ import cats.~> -import fs2.Pipe -import fs2.concurrent.Queue +import fs2.{Pipe, Stream} import org.http4s.server.websocket.WebSocketBuilder -import org.http4s.util.CaseInsensitiveString import org.http4s.websocket.WebSocketFrame import org.http4s._ import org.log4s.{Logger, getLogger} +import org.typelevel.ci.CIString import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.model.{Header => SttpHeader} @@ -23,74 +23,58 @@ import sttp.tapir.server.interpreter.ServerInterpreter import scala.reflect.ClassTag trait Http4sServerInterpreter { - def toHttp[I, E, O, F[_], G[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(t: F ~> G)(logic: I => G[Either[E, O]])(implicit - serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] + def toHttp[I, E, O, F[_]: Async, G[_]: Sync]( + e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets] + )(t: F ~> G)(logic: I => G[Either[E, O]])(implicit + serverOptions: Http4sServerOptions[F, G] ): Http[OptionT[G, *], F] = toHttp(e.serverLogic(logic))(t) - def toHttpRecoverErrors[I, E, O, F[_], G[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(t: F ~> G)(logic: I => G[O])(implicit + def toHttpRecoverErrors[I, E, O, F[_]: Async, G[_]: Sync]( + e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets] + )(t: F ~> G)(logic: I => G[O])(implicit serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], eIsThrowable: E <:< Throwable, - eClassTag: ClassTag[E], - timer: Timer[F] + eClassTag: ClassTag[E] ): Http[OptionT[G, *], F] = toHttp(e.serverLogicRecoverErrors(logic))(t) - def toRoutes[I, E, O, F[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])( + def toRoutes[I, E, O, F[_]: Async](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])( logic: I => F[Either[E, O]] - )(implicit serverOptions: Http4sServerOptions[F, F], fs: Concurrent[F], fcs: ContextShift[F], timer: Timer[F]): HttpRoutes[F] = toRoutes( + )(implicit serverOptions: Http4sServerOptions[F, F]): HttpRoutes[F] = toRoutes( e.serverLogic(logic) ) - def toRouteRecoverErrors[I, E, O, F[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(logic: I => F[O])(implicit + def toRouteRecoverErrors[I, E, O, F[_]: Async](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(logic: I => F[O])(implicit serverOptions: Http4sServerOptions[F, F], - fs: Concurrent[F], - fcs: ContextShift[F], eIsThrowable: E <:< Throwable, - eClassTag: ClassTag[E], - timer: Timer[F] + eClassTag: ClassTag[E] ): HttpRoutes[F] = toRoutes(e.serverLogicRecoverErrors(logic)) // - def toHttp[I, E, O, F[_], G[_]](se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, G])( + def toHttp[I, E, O, F[_]: Async, G[_]: Sync](se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, G])( t: F ~> G )(implicit - serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] + serverOptions: Http4sServerOptions[F, G] ): Http[OptionT[G, *], F] = toHttp(List(se))(t) - def toRoutes[I, E, O, F[_]]( + def toRoutes[I, E, O, F[_]: Async]( se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, F] - )(implicit serverOptions: Http4sServerOptions[F, F], fs: Concurrent[F], fcs: ContextShift[F], timer: Timer[F]): HttpRoutes[F] = toRoutes( + )(implicit serverOptions: Http4sServerOptions[F, F]): HttpRoutes[F] = toRoutes( List(se) ) // - def toRoutes[F[_]](serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, F]])(implicit - serverOptions: Http4sServerOptions[F, F], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] + def toRoutes[F[_]: Async](serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, F]])(implicit + serverOptions: Http4sServerOptions[F, F] ): HttpRoutes[F] = toHttp(serverEndpoints)(FunctionK.id[F]) // - def toHttp[F[_], G[_]](serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, G]])(t: F ~> G)(implicit - serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] + def toHttp[F[_]: Async, G[_]: Sync]( + serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, G]] + )(t: F ~> G)(implicit + serverOptions: Http4sServerOptions[F, G] ): Http[OptionT[G, *], F] = { implicit val monad: CatsMonadError[G] = new CatsMonadError[G] @@ -109,18 +93,19 @@ trait Http4sServerInterpreter { } } - private def serverResponseToHttp4s[F[_]: Concurrent]( + private def serverResponseToHttp4s[F[_]: Async]( response: ServerResponse[Http4sResponseBody[F]] ): F[Response[F]] = { val statusCode = statusCodeToHttp4sStatus(response.code) - val headers = Headers(response.headers.map { case SttpHeader(k, v) => Header.Raw(CaseInsensitiveString(k), v) }.toList) + val headers = Headers(response.headers.map { case SttpHeader(k, v) => Header.Raw(CIString(k), v) }.toList) response.body match { 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].build(queue.dequeue, receive, headers = headers, filterPingPongs = false) + 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)) => 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 d2f6b284ed..993ef583f2 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.Defaults import sttp.tapir.model.ServerRequest @@ -12,7 +12,6 @@ import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, Decode import sttp.tapir.server.interceptor.exception.{DefaultExceptionHandler, ExceptionHandler, ExceptionInterceptor} import java.io.File -import scala.concurrent.ExecutionContext /** @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. @@ -20,7 +19,6 @@ import scala.concurrent.ExecutionContext */ case class Http4sServerOptions[F[_], G[_]]( createFile: ServerRequest => G[File], - blockingExecutionContext: ExecutionContext, ioChunkSize: Int, interceptors: List[Interceptor[G, Http4sResponseBody[F]]] ) { @@ -51,18 +49,16 @@ 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]]], 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), - blockingExecutionContext, + defaultCreateFile[G], 8192, exceptionHandler.map(new ExceptionInterceptor[G, Http4sResponseBody[F]](_)).toList ++ serverLog.map(Log.serverLogInterceptor[F, G]).toList ++ @@ -71,8 +67,7 @@ object Http4sServerOptions { List(new DecodeFailureInterceptor[G, Http4sResponseBody[F]](decodeFailureHandler)) ) - def defaultCreateFile[F[_]](implicit sync: Sync[F], cs: ContextShift[F]): ExecutionContext => ServerRequest => F[File] = - ec => _ => cs.evalOn(ec)(sync.delay(Defaults.createTempFile())) + def defaultCreateFile[F[_]](implicit sync: Sync[F]): ServerRequest => F[File] = _ => sync.blocking(Defaults.createTempFile()) object Log { def defaultServerLog[F[_]: Sync]: DefaultServerLog[F[Unit]] = @@ -93,6 +88,6 @@ object Http4sServerOptions { } } - implicit def default[F[_], G[_]: Sync: ContextShift]: Http4sServerOptions[F, G] = + implicit 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/Http4sServerRequest.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala index c1e1537076..2b8e4ade2c 100644 --- a/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala +++ b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala @@ -8,19 +8,20 @@ import scala.collection.immutable.Seq private[http4s] class Http4sServerRequest[F[_]](req: Request[F]) extends ServerRequest { override def protocol: String = req.httpVersion.toString() - override lazy val connectionInfo: ConnectionInfo = ConnectionInfo(req.server, req.remote, req.isSecure) + override lazy val connectionInfo: ConnectionInfo = + ConnectionInfo(req.server.map(_.toInetSocketAddress), req.remote.map(_.toInetSocketAddress), req.isSecure) override def underlying: Any = req /** Can differ from `uri.path`, if the endpoint is deployed in a context */ override lazy val pathSegments: List[String] = { // if the routes are mounted within a context (e.g. using a router), we have to match against what comes // after the context. This information is stored in the the PathInfoCaret attribute - req.pathInfo.dropWhile(_ == '/').split("/").toList.map(org.http4s.Uri.decode(_)) + req.pathInfo.renderString.dropWhile(_ == '/').split("/").toList.map(org.http4s.Uri.decode(_)) } override lazy val queryParameters: QueryParams = QueryParams.fromMultiMap(req.multiParams) override def method: Method = Method(req.method.name.toUpperCase) override def uri: Uri = Uri.unsafeParse(req.uri.toString()) - override lazy val headers: Seq[Header] = req.headers.toList.map(h => Header(h.name.value, h.value)) + override lazy val headers: Seq[Header] = req.headers.headers.map(h => Header(h.name.toString, h.value)) } 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 85d5815ba9..636d7b0832 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,20 +1,21 @@ 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.headers.{`Content-Disposition`, `Content-Type`} import org.http4s.{EntityBody, EntityEncoder, Header, Headers, multipart} -import org.http4s.util.CaseInsensitiveString +import org.typelevel.ci.CIString import sttp.capabilities.fs2.Fs2Streams -import sttp.model.{HasHeaders, Part, Header => SttpHeader} +import sttp.model.{HasHeaders, HeaderNames, Part, Header => SttpHeader} import sttp.tapir.{CodecFormat, RawBodyType, RawPart, WebSocketBodyOutput} import sttp.tapir.server.interpreter.ToResponseBody 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] @@ -39,17 +40,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 @@ -60,18 +60,21 @@ private[http4s] class Http4sToResponseBody[F[_]: Concurrent: Timer: ContextShift private def rawPartToBodyPart[T](m: RawBodyType.MultipartBody, part: Part[T]): Option[multipart.Part[F]] = { m.partType(part.name).map { partType => val headers = part.headers.map { case SttpHeader(hk, hv) => - Header.Raw(CaseInsensitiveString(hk), hv) + Header.Raw(CIString(hk), hv): Header.ToRaw }.toList - val partContentType = part.contentType.map(parseContentType).getOrElse(`Content-Type`(http4s.MediaType.application.`octet-stream`)) + val partContentType: `Content-Type` = + part.contentType.map(parseContentType).getOrElse(`Content-Type`(http4s.MediaType.application.`octet-stream`)) val entity = rawValueToEntity(partType.asInstanceOf[RawBodyType[Any]], part.body) - val dispositionParams = part.otherDispositionParams + (Part.NameDispositionParam -> part.name) - val contentDispositionHeader = `Content-Disposition`("form-data", dispositionParams) + val dispositionParams = (part.otherDispositionParams + (Part.NameDispositionParam -> part.name)).map { case (k, v) => + CIString(k) -> v + } + val contentDispositionHeader: Header.ToRaw = `Content-Disposition`("form-data", dispositionParams) - val shouldAddCtHeader = headers.exists(_.name == `Content-Type`.name) + val shouldAddCtHeader = part.headers.exists(_.is(HeaderNames.ContentType)) val allHeaders = if (shouldAddCtHeader) { - Headers(partContentType :: contentDispositionHeader :: headers) + Headers((partContentType: Header.ToRaw) :: contentDispositionHeader :: headers) } else { Headers(contentDispositionHeader :: headers) } 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..3c6f25206c 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,6 +1,7 @@ package sttp.tapir.server.http4s import cats.effect.IO +import cats.effect.unsafe.implicits.global import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import sttp.capabilities.fs2.Fs2Streams @@ -14,28 +15,32 @@ class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { 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() + }) + .unsafeRunSync() } 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() + }) + .unsafeRunSync() } 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() + }) + .unsafeRunSync() } 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() + .unsafeRunSync() } } 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 0d7a840e0f..a9111a5ee4 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.server.blaze.BlazeServerBuilder import org.http4s.syntax.kleisli._ @@ -38,8 +39,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 605a0d65e3..f63d0ce7b7 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.syntax.kleisli._ import org.http4s.{HttpRoutes, Request, Response} @@ -20,8 +20,6 @@ import scala.reflect.ClassTag class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[IO] with WebSockets, HttpRoutes[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 0de6170d67..58deb7bea4 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 @@ -11,6 +11,7 @@ import sttp.tapir._ import sttp.tapir.server.interceptor.decodefailure.DecodeFailureHandler import sttp.tapir.server.ServerEndpoint import sttp.tapir.tests._ +import cats.effect.unsafe.implicits.global class CreateServerTest[F[_], +R, ROUTE](interpreter: TestServerInterpreter[F, R, ROUTE]) extends StrictLogging { def testServer[I, E, O]( 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 52ab979dba..0cd4602590 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.interceptor.decodefailure.DecodeFailureHandler import sttp.tapir.server.ServerEndpoint @@ -10,7 +10,6 @@ import sttp.tapir.tests.Port import scala.reflect.ClassTag trait TestServerInterpreter[F[_], +R, ROUTE] { - 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): ROUTE def routeRecoverErrors[I, E <: Throwable, O](e: Endpoint[I, E, O, R], fn: I => F[O])(implicit eClassTag: ClassTag[E]): ROUTE def server(routes: NonEmptyList[ROUTE]): Resource[IO, Port] 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..b633b780ec 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,12 @@ 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 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)) + AsyncHttpClientFs2Backend.resource[IO]() } diff --git a/server/vertx/src/main/scala/sttp/tapir/server/vertx/interpreters/VertxCatsServerInterpreter.scala b/server/vertx/src/main/scala/sttp/tapir/server/vertx/interpreters/VertxCatsServerInterpreter.scala index 0cebf51157..d1ec9b37ff 100644 --- a/server/vertx/src/main/scala/sttp/tapir/server/vertx/interpreters/VertxCatsServerInterpreter.scala +++ b/server/vertx/src/main/scala/sttp/tapir/server/vertx/interpreters/VertxCatsServerInterpreter.scala @@ -1,6 +1,6 @@ package sttp.tapir.server.vertx.interpreters -import cats.effect.{Async, ConcurrentEffect, Effect} +import cats.effect.{Async, IO, LiftIO, Sync} import cats.syntax.all._ import io.vertx.core.{Future, Handler} import io.vertx.ext.web.{Route, Router, RoutingContext} @@ -25,8 +25,7 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { * @return A function, that given a router, will attach this endpoint to it */ def route[F[_], I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])(logic: I => F[Either[E, O]])(implicit - endpointOptions: VertxCatsServerOptions[F], - effect: ConcurrentEffect[F] + endpointOptions: VertxCatsServerOptions[F] ): Router => Route = route(e.serverLogic(logic)) @@ -39,7 +38,6 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { logic: I => F[O] )(implicit endpointOptions: VertxCatsServerOptions[F], - effect: ConcurrentEffect[F], eIsThrowable: E <:< Throwable, eClassTag: ClassTag[E] ): Router => Route = @@ -52,16 +50,15 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { def route[F[_], I, E, O]( e: ServerEndpoint[I, E, O, Fs2Streams[F], F] )(implicit - endpointOptions: VertxCatsServerOptions[F], - effect: ConcurrentEffect[F] + endpointOptions: VertxCatsServerOptions[F] ): Router => Route = { router => import sttp.tapir.server.vertx.streams.fs2._ mountWithDefaultHandlers(e)(router, extractRouteDefinition(e.endpoint)).handler(endpointHandler(e)) } - private def endpointHandler[F[_], I, E, O, A, S: ReadStreamCompatible]( + private def endpointHandler[F[_]: Async: LiftIO, I, E, O, A, S: ReadStreamCompatible]( e: ServerEndpoint[I, E, O, _, F] - )(implicit serverOptions: VertxCatsServerOptions[F], effect: Effect[F]): Handler[RoutingContext] = { rc => + )(implicit serverOptions: VertxCatsServerOptions[F]): Handler[RoutingContext] = { rc => implicit val monad: MonadError[F] = monadError[F] val fFromVFuture = new CatsFFromVFuture[F] val interpreter = new ServerInterpreter( @@ -85,7 +82,7 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { () } - 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) @@ -95,7 +92,7 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { 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] { diff --git a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala b/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala index 997feac74a..2ed49bfd7b 100644 --- a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala +++ b/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala @@ -1,13 +1,11 @@ package sttp.tapir.tests -import cats.effect.{ContextShift, IO, Resource} +import cats.effect.{IO, Resource} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite +import cats.effect.unsafe.implicits.global trait TestSuite extends AnyFunSuite with BeforeAndAfterAll { - - implicit lazy val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) - def tests: Resource[IO, List[Test]] def testNameFilter: Option[String] = None // define to run a single test (temporarily for debugging) From 41be44b5a65beed1a7bc23ea5d2cbb91330ca17a Mon Sep 17 00:00:00 2001 From: adamw Date: Fri, 9 Apr 2021 20:14:33 +0200 Subject: [PATCH 02/41] Migrate some more http4s-related code --- .../tapir/client/play/PlayClientTests.scala | 3 +- .../client/sttp/SttpAkkaClientTests.scala | 3 +- .../sttp/SttpClientStreamingTests.scala | 1 + .../tapir/client/sttp/SttpClientTests.scala | 6 +- .../sttp/tapir/client/tests/HttpServer.scala | 63 ++++---- .../sttp/tapir/redoc/http4s/RedocHttp4s.scala | 8 +- .../sttp/tapir/swagger/http4s/package.scala | 9 -- .../tapir/swagger/http4s/SwaggerHttp4s.scala | 11 +- .../FinatraTestServerInterpreter.scala | 6 +- .../tapir/server/http4s/CatsMonadError.scala | 16 -- .../server/http4s/Http4sRequestBody.scala | 69 --------- .../http4s/Http4sServerInterpreter.scala | 139 ------------------ .../server/http4s/Http4sServerOptions.scala | 98 ------------ .../server/http4s/Http4sServerRequest.scala | 26 ---- .../http4s/Http4sServerSentEvents.scala | 27 ---- .../server/http4s/Http4sToResponseBody.scala | 89 ----------- .../server/http4s/Http4sWebSockets.scala | 109 -------------- .../sttp/tapir/server/http4s/package.scala | 20 --- .../http4s/Http4sServerSentEventsTest.scala | 96 ------------ .../server/http4s/Http4sServerTest.scala | 119 --------------- .../http4s/Http4sTestServerInterpreter.scala | 54 ------- 21 files changed, 48 insertions(+), 924 deletions(-) delete mode 100644 docs/swagger-ui-http4s/src/main/scala-2.13/sttp/tapir/swagger/http4s/package.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala delete mode 100644 server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala delete mode 100644 server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala delete mode 100644 server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala delete mode 100644 server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala 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 da8ebd4d24..9bc7efaf8e 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/test/scalajvm/sttp/tapir/client/sttp/SttpAkkaClientTests.scala b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpAkkaClientTests.scala index e5d633918c..bd3620e62f 100644 --- a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpAkkaClientTests.scala +++ b/client/sttp-client/src/test/scalajvm/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 16319e626f..6a4aadd770 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,7 @@ package sttp.tapir.client.sttp -import cats.effect.{Blocker, ContextShift, IO} +import cats.effect.IO +import cats.effect.unsafe.implicits.global import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.client3._ @@ -11,9 +12,8 @@ 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() + HttpClientFs2Backend[IO]().unsafeRunSync() def wsToPipe: WebSocketToPipe[R] override def send[I, E, O, FN[_]](e: Endpoint[I, E, O, R], port: Port, args: I, scheme: String = "http"): IO[Either[E, O]] = { 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 04cc09c365..54ca31819c 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 @@ -9,9 +12,9 @@ import org.http4s.server.blaze.BlazeServerBuilder import org.http4s.server.middleware._ import org.http4s.server.websocket.WebSocketBuilder import org.http4s.syntax.kleisli._ -import org.http4s.util.CaseInsensitiveString import org.http4s.websocket.WebSocketFrame import org.http4s.{multipart, _} +import org.typelevel.ci.CIString import scodec.bits.ByteVector import sttp.tapir.client.tests.HttpServer._ @@ -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] = _ // @@ -50,23 +49,25 @@ class HttpServer(port: Port) { if (f == "papaya") { Accepted("29") } else { - Ok(s"fruit: $f${amount.map(" " + _).getOrElse("")}", Header("X-Role", f.length.toString)) + Ok(s"fruit: $f${amount.map(" " + _).getOrElse("")}", Header.Raw(CIString("X-Role"), f.length.toString)) } case GET -> Root / "fruit" / f => Ok(s"$f") case GET -> Root / "fruit" / f / "amount" / amount :? colorOptParam(c) => Ok(s"$f $amount $c") case _ @GET -> Root / "api" / "unit" => Ok("{}") case r @ GET -> Root / "api" / "echo" / "params" => Ok(r.uri.query.params.toSeq.sortBy(_._1).map(p => s"${p._1}=${p._2}").mkString("&")) case r @ GET -> Root / "api" / "echo" / "headers" => - val headers = r.headers.toList.map(h => Header(h.name.value, h.value.reverse)) - val filteredHeaders = r.headers.find(_.name.value.equalsIgnoreCase("Cookie")) match { - case Some(c) => headers.filter(_.name.value.equalsIgnoreCase("Cookie")) :+ Header("Set-Cookie", c.value.reverse) + val headers = r.headers.headers.map(h => Header.Raw(CIString(h.name.toString), h.value.reverse)) + val filteredHeaders: List[Header.Raw] = r.headers.headers.find(_.name.toString.equalsIgnoreCase("Cookie")) match { + case Some(c) => headers.filter(_.name.toString.equalsIgnoreCase("Cookie")) :+ Header.Raw(CIString("Set-Cookie"), c.value.reverse) case None => headers } - Ok(headers = filteredHeaders: _*) + okOnlyHeaders(filteredHeaders.map(x => x: Header.ToRaw)) case r @ GET -> Root / "api" / "echo" / "param-to-header" => - Ok(headers = r.uri.multiParams.getOrElse("qq", Nil).reverse.map(v => Header("hh", v)): _*) + okOnlyHeaders(r.uri.multiParams.getOrElse("qq", Nil).reverse.map(v => Header.Raw(CIString("hh"), v): Header.ToRaw)) case r @ GET -> Root / "api" / "echo" / "param-to-upper-header" => - Ok(headers = r.uri.multiParams.map { case (k, v) => Header(k.toUpperCase(), v.headOption.getOrElse("?")) }.toSeq: _*) + okOnlyHeaders(r.uri.multiParams.map { case (k, v) => + Header.Raw(CIString(k.toUpperCase()), v.headOption.getOrElse("?")): Header.ToRaw + }.toSeq) case r @ POST -> Root / "api" / "echo" / "multipart" => r.decode[multipart.Multipart[IO]] { mp => val parts: Vector[multipart.Part[IO]] = mp.parts @@ -78,22 +79,22 @@ class HttpServer(port: Port) { } case r @ POST -> Root / "api" / "echo" => r.as[String].flatMap(Ok(_)) case r @ GET -> Root => - r.headers.get(CaseInsensitiveString("X-Role")) match { - case None => Ok() - case Some(h) => Ok("Role: " + h.value) + r.headers.get(CIString("X-Role")) match { + case None => Ok() + case Some(hs) => Ok("Role: " + hs.head.value) } case r @ GET -> Root / "secret" => - r.headers.get(CaseInsensitiveString("Location")) match { - case None => BadRequest() - case Some(h) => Ok("Location: " + h.value) + r.headers.get(CIString("Location")) match { + case None => BadRequest() + case Some(hs) => Ok("Location: " + hs.head.value) } case DELETE -> Root / "api" / "delete" => Ok() case r @ GET -> Root / "auth" :? apiKeyOptParam(ak) => - val authHeader = r.headers.get(CaseInsensitiveString("Authorization")).map(_.value) - val xApiKey = r.headers.get(CaseInsensitiveString("X-Api-Key")).map(_.value) + val authHeader = r.headers.get(CIString("Authorization")).map(_.head.value) + val xApiKey = r.headers.get(CIString("X-Api-Key")).map(_.head.value) Ok(s"Authorization=$authHeader; X-Api-Key=$xApiKey; Query=$ak") case GET -> Root / "mapping" :? numParam(v) => @@ -102,7 +103,7 @@ class HttpServer(port: Port) { case _ @GET -> Root / "status" :? statusOutParam(status) => status match { case 204 => NoContent() - case 200 => Ok(`Content-Type`(MediaType.text.plain)) + case 200 => Ok.headers(`Content-Type`(MediaType.text.plain)) case _ => BadRequest() } @@ -116,11 +117,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) } @@ -134,11 +135,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) } @@ -155,8 +156,12 @@ class HttpServer(port: Port) { } } + private def okOnlyHeaders(headers: Seq[Header.ToRaw]): IO[Response[IO]] = IO.pure { + Response(headers = Headers(headers)) + } + private def fromAcceptHeader(r: Request[IO])(f: PartialFunction[String, IO[Response[IO]]]): IO[Response[IO]] = - r.headers.get(Accept).map(h => f(h.value)).getOrElse(NotAcceptable()) + r.headers.get[Accept].map(h => f(h.values.head.toString())).getOrElse(NotAcceptable()) private val organizationXml = Ok("sml-xml", `Content-Type`(MediaType.application.xml, Charset.`UTF-8`)) private val organizationJson = Ok("{\"name\": \"sml\"}", `Content-Type`(MediaType.application.json, Charset.`UTF-8`)) @@ -168,7 +173,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/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 c8a86b535c..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,17 +33,17 @@ 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._ - val rootPath = contextPath.foldLeft(Root: Path)(_ / _) + val rootPath = contextPath.foldLeft(Root: Path)(_ / Path.Segment(_)) HttpRoutes.of[F] { case GET -> `rootPath` / "" => Ok(html, `Content-Type`(MediaType.text.html, Charset.`UTF-8`)) case req @ GET -> `rootPath` => - PermanentRedirect(Location(req.uri.withPath(req.uri.path.concat("/")))) + PermanentRedirect(Location(req.uri.withPath(req.uri.path / Path.Segment("")))) case GET -> `rootPath` / `yamlName` => Ok(yaml, `Content-Type`(MediaType.text.yaml, Charset.`UTF-8`)) } 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 092f2f2d75..ce9e681df7 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._ @@ -52,10 +50,7 @@ class SwaggerHttp4s( Ok(yaml) case GET -> Root / `contextPath` / swaggerResource => StaticFile - .fromResource( - s"/META-INF/resources/webjars/swagger-ui/$swaggerVersion/$swaggerResource", - Blocker.liftExecutionContext(ExecutionContext.global) - ) + .fromResource(s"/META-INF/resources/webjars/swagger-ui/$swaggerVersion/$swaggerResource") .getOrElseF(NotFound()) } } 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 37ed7946c3..daf0bed9a4 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 @@ -16,10 +16,6 @@ import scala.concurrent.duration.DurationInt import scala.reflect.ClassTag class FinatraTestServerInterpreter extends TestServerInterpreter[Future, Any, FinatraRoute] { - 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 diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala deleted file mode 100644 index 9793cea610..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/CatsMonadError.scala +++ /dev/null @@ -1,16 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.effect.Sync -import sttp.monad.MonadError - -private[http4s] class CatsMonadError[F[_]](implicit F: Sync[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.suspend(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) -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala deleted file mode 100644 index fd5420bf92..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sRequestBody.scala +++ /dev/null @@ -1,69 +0,0 @@ -package sttp.tapir.server.http4s - -import java.io.ByteArrayInputStream -import cats.effect.{Blocker, ContextShift, Sync} -import cats.syntax.all._ -import cats.~> -import fs2.Chunk -import org.http4s.headers.{`Content-Disposition`, `Content-Type`} -import org.http4s.{Charset, EntityDecoder, Request, multipart} -import sttp.capabilities.fs2.Fs2Streams -import sttp.model.{Header, Part} -import sttp.tapir.model.ServerRequest -import sttp.tapir.server.interpreter.RequestBody -import sttp.tapir.{RawBodyType, RawPart} - -private[http4s] class Http4sRequestBody[F[_]: Sync: ContextShift, G[_]: Sync]( // TODO: constraints? - request: Request[F], - serverRequest: ServerRequest, - serverOptions: Http4sServerOptions[F, G], - t: F ~> G -) extends RequestBody[G, Fs2Streams[F]] { - override val streams: Fs2Streams[F] = Fs2Streams[F] - override def toRaw[R](bodyType: RawBodyType[R]): G[R] = toRawFromStream(request.body, bodyType, request.charset) - override def toStream(): streams.BinaryStream = request.body - - private def toRawFromStream[R](body: fs2.Stream[F, Byte], bodyType: RawBodyType[R], charset: Option[Charset]): G[R] = { - def asChunk: G[Chunk[Byte]] = t(body.compile.to(Chunk)) - def asByteArray: G[Array[Byte]] = t(body.compile.to(Chunk).map(_.toByteBuffer.array())) - - bodyType match { - case RawBodyType.StringBody(defaultCharset) => asByteArray.map(new String(_, charset.map(_.nioCharset).getOrElse(defaultCharset))) - case RawBodyType.ByteArrayBody => asByteArray - case RawBodyType.ByteBufferBody => asChunk.map(_.toByteBuffer) - case RawBodyType.InputStreamBody => asByteArray.map(new ByteArrayInputStream(_)) - case RawBodyType.FileBody => - serverOptions.createFile(serverRequest).flatMap { file => - val fileSink = fs2.io.file.writeAll[F](file.toPath, Blocker.liftExecutionContext(serverOptions.blockingExecutionContext)) - t(body.through(fileSink).compile.drain.map(_ => file)) - } - case m: RawBodyType.MultipartBody => - // TODO: use MultipartDecoder.mixedMultipart once available? - t(implicitly[EntityDecoder[F, multipart.Multipart[F]]].decode(request, strict = false).value.flatMap { - case Left(failure) => Sync[F].raiseError(failure) - case Right(mp) => - val rawPartsF: Vector[F[RawPart]] = mp.parts - .flatMap(part => part.name.flatMap(name => m.partType(name)).map((part, _)).toList) - .map { case (part, codecMeta) => toRawPart(part, codecMeta).asInstanceOf[F[RawPart]] } - - val rawParts: F[Vector[RawPart]] = rawPartsF.sequence - - rawParts.asInstanceOf[F[R]] // R is Seq[RawPart] - }) - } - } - - private def toRawPart[R](part: multipart.Part[F], partType: RawBodyType[R]): G[Part[R]] = { - val dispositionParams = part.headers.get(`Content-Disposition`).map(_.parameters).getOrElse(Map.empty) - val charset = part.headers.get(`Content-Type`).flatMap(_.charset) - toRawFromStream(part.body, partType, charset) - .map(r => - Part( - part.name.getOrElse(""), - r, - otherDispositionParams = dispositionParams - Part.NameDispositionParam, - headers = part.headers.toList.map(h => Header(h.name.value, h.value)) - ) - ) - } -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala deleted file mode 100644 index 42dd49587d..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala +++ /dev/null @@ -1,139 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.arrow.FunctionK -import cats.data.{Kleisli, OptionT} -import cats.effect.{Concurrent, ContextShift, Sync, Timer} -import cats.syntax.all._ -import cats.~> -import fs2.Pipe -import fs2.concurrent.Queue -import org.http4s.server.websocket.WebSocketBuilder -import org.http4s.util.CaseInsensitiveString -import org.http4s.websocket.WebSocketFrame -import org.http4s._ -import org.log4s.{Logger, getLogger} -import sttp.capabilities.WebSockets -import sttp.capabilities.fs2.Fs2Streams -import sttp.model.{Header => SttpHeader} -import sttp.tapir.Endpoint -import sttp.tapir.model.ServerResponse -import sttp.tapir.server.ServerEndpoint -import sttp.tapir.server.interpreter.ServerInterpreter - -import scala.reflect.ClassTag - -trait Http4sServerInterpreter { - def toHttp[I, E, O, F[_], G[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(t: F ~> G)(logic: I => G[Either[E, O]])(implicit - serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] - ): Http[OptionT[G, *], F] = toHttp(e.serverLogic(logic))(t) - - def toHttpRecoverErrors[I, E, O, F[_], G[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(t: F ~> G)(logic: I => G[O])(implicit - serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], - eIsThrowable: E <:< Throwable, - eClassTag: ClassTag[E], - timer: Timer[F] - ): Http[OptionT[G, *], F] = toHttp(e.serverLogicRecoverErrors(logic))(t) - - def toRoutes[I, E, O, F[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])( - logic: I => F[Either[E, O]] - )(implicit serverOptions: Http4sServerOptions[F, F], fs: Concurrent[F], fcs: ContextShift[F], timer: Timer[F]): HttpRoutes[F] = toRoutes( - e.serverLogic(logic) - ) - - def toRouteRecoverErrors[I, E, O, F[_]](e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets])(logic: I => F[O])(implicit - serverOptions: Http4sServerOptions[F, F], - fs: Concurrent[F], - fcs: ContextShift[F], - eIsThrowable: E <:< Throwable, - eClassTag: ClassTag[E], - timer: Timer[F] - ): HttpRoutes[F] = toRoutes(e.serverLogicRecoverErrors(logic)) - - // - - def toHttp[I, E, O, F[_], G[_]](se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, G])( - t: F ~> G - )(implicit - serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] - ): Http[OptionT[G, *], F] = toHttp(List(se))(t) - - def toRoutes[I, E, O, F[_]]( - se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, F] - )(implicit serverOptions: Http4sServerOptions[F, F], fs: Concurrent[F], fcs: ContextShift[F], timer: Timer[F]): HttpRoutes[F] = toRoutes( - List(se) - ) - - // - - def toRoutes[F[_]](serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, F]])(implicit - serverOptions: Http4sServerOptions[F, F], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] - ): HttpRoutes[F] = toHttp(serverEndpoints)(FunctionK.id[F]) - - // - - def toHttp[F[_], G[_]](serverEndpoints: List[ServerEndpoint[_, _, _, Fs2Streams[F] with WebSockets, G]])(t: F ~> G)(implicit - serverOptions: Http4sServerOptions[F, G], - gs: Sync[G], - fs: Concurrent[F], - fcs: ContextShift[F], - timer: Timer[F] - ): Http[OptionT[G, *], F] = { - implicit val monad: CatsMonadError[G] = new CatsMonadError[G] - - Kleisli { (req: Request[F]) => - val serverRequest = new Http4sServerRequest(req) - val interpreter = new ServerInterpreter[Fs2Streams[F] with WebSockets, G, Http4sResponseBody[F], Fs2Streams[F]]( - new Http4sRequestBody[F, G](req, serverRequest, serverOptions, t), - new Http4sToResponseBody[F, G](serverOptions), - serverOptions.interceptors - ) - - OptionT(interpreter(serverRequest, serverEndpoints).flatMap { - case None => none.pure[G] - case Some(response) => t(serverResponseToHttp4s[F](response)).map(_.some) - }) - } - } - - private def serverResponseToHttp4s[F[_]: Concurrent]( - response: ServerResponse[Http4sResponseBody[F]] - ): F[Response[F]] = { - val statusCode = statusCodeToHttp4sStatus(response.code) - val headers = Headers(response.headers.map { case SttpHeader(k, v) => Header.Raw(CaseInsensitiveString(k), v) }.toList) - - response.body match { - 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].build(queue.dequeue, receive, headers = headers, filterPingPongs = false) - } - } - case Some(Right(entity)) => - Response(status = statusCode, headers = headers, body = entity).pure[F] - - case None => Response[F](status = statusCode, headers = headers).pure[F] - } - } - - private def statusCodeToHttp4sStatus(code: sttp.model.StatusCode): Status = - Status.fromInt(code.code).getOrElse(throw new IllegalArgumentException(s"Invalid status code: $code")) -} - -object Http4sServerInterpreter extends Http4sServerInterpreter { - private[http4s] val log: Logger = getLogger -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala deleted file mode 100644 index d2f6b284ed..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerOptions.scala +++ /dev/null @@ -1,98 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.Applicative -import cats.effect.{ContextShift, Sync} -import cats.implicits.catsSyntaxOptionId -import sttp.tapir.Defaults -import sttp.tapir.model.ServerRequest -import sttp.tapir.server.interceptor.log.{DefaultServerLog, ServerLog, ServerLogInterceptor} -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 java.io.File -import scala.concurrent.ExecutionContext - -/** @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. - * Usually the same as `F`. - */ -case class Http4sServerOptions[F[_], G[_]]( - createFile: ServerRequest => G[File], - blockingExecutionContext: ExecutionContext, - ioChunkSize: Int, - interceptors: List[Interceptor[G, Http4sResponseBody[F]]] -) { - def prependInterceptor(i: Interceptor[G, Http4sResponseBody[F]]): Http4sServerOptions[F, G] = - copy(interceptors = i :: interceptors) - def appendInterceptor(i: Interceptor[G, Http4sResponseBody[F]]): Http4sServerOptions[F, G] = - copy(interceptors = interceptors :+ i) -} - -object Http4sServerOptions { - - /** Creates default [[Http4sServerOptions]] 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 exceptionHandler Whether to respond to exceptions, or propagate them to http4s. - * @param serverLog The server log using which an interceptor will be created, if any. To keep the default, use - * `Http4sServerOptions.Log.defaultServerLog` - * @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[_], G[_]: Sync: ContextShift]( - exceptionHandler: Option[ExceptionHandler], - serverLog: Option[ServerLog[G[Unit]]], - 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 - ): Http4sServerOptions[F, G] = - Http4sServerOptions( - defaultCreateFile[G].apply(blockingExecutionContext), - blockingExecutionContext, - 8192, - exceptionHandler.map(new ExceptionInterceptor[G, Http4sResponseBody[F]](_)).toList ++ - serverLog.map(Log.serverLogInterceptor[F, G]).toList ++ - additionalInterceptors ++ - unsupportedMediaTypeInterceptor.toList ++ - List(new DecodeFailureInterceptor[G, Http4sResponseBody[F]](decodeFailureHandler)) - ) - - def defaultCreateFile[F[_]](implicit sync: Sync[F], cs: ContextShift[F]): ExecutionContext => ServerRequest => F[File] = - ec => _ => cs.evalOn(ec)(sync.delay(Defaults.createTempFile())) - - object Log { - def defaultServerLog[F[_]: Sync]: DefaultServerLog[F[Unit]] = - DefaultServerLog[F[Unit]]( - doLogWhenHandled = debugLog[F], - doLogAllDecodeFailures = debugLog[F], - doLogExceptions = (msg: String, ex: Throwable) => Sync[F].delay(Http4sServerInterpreter.log.error(ex)(msg)), - noLog = Applicative[F].unit - ) - - def serverLogInterceptor[F[_], G[_]](serverLog: ServerLog[G[Unit]]): ServerLogInterceptor[G[Unit], G, Http4sResponseBody[F]] = - new ServerLogInterceptor[G[Unit], G, Http4sResponseBody[F]](serverLog, (f, _) => f) - - private def debugLog[F[_]: Sync](msg: String, exOpt: Option[Throwable]): F[Unit] = - exOpt match { - case None => Sync[F].delay(Http4sServerInterpreter.log.debug(msg)) - case Some(ex) => Sync[F].delay(Http4sServerInterpreter.log.debug(ex)(msg)) - } - } - - implicit def default[F[_], G[_]: Sync: ContextShift]: Http4sServerOptions[F, G] = - customInterceptors(Some(DefaultExceptionHandler), Some(Log.defaultServerLog[G])) -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala deleted file mode 100644 index c1e1537076..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerRequest.scala +++ /dev/null @@ -1,26 +0,0 @@ -package sttp.tapir.server.http4s - -import org.http4s.Request -import sttp.model.{Header, Method, QueryParams, Uri} -import sttp.tapir.model.{ConnectionInfo, ServerRequest} - -import scala.collection.immutable.Seq - -private[http4s] class Http4sServerRequest[F[_]](req: Request[F]) extends ServerRequest { - override def protocol: String = req.httpVersion.toString() - override lazy val connectionInfo: ConnectionInfo = ConnectionInfo(req.server, req.remote, req.isSecure) - override def underlying: Any = req - - /** Can differ from `uri.path`, if the endpoint is deployed in a context */ - override lazy val pathSegments: List[String] = { - // if the routes are mounted within a context (e.g. using a router), we have to match against what comes - // after the context. This information is stored in the the PathInfoCaret attribute - req.pathInfo.dropWhile(_ == '/').split("/").toList.map(org.http4s.Uri.decode(_)) - } - - override lazy val queryParameters: QueryParams = QueryParams.fromMultiMap(req.multiParams) - - override def method: Method = Method(req.method.name.toUpperCase) - override def uri: Uri = Uri.unsafeParse(req.uri.toString()) - override lazy val headers: Seq[Header] = req.headers.toList.map(h => Header(h.name.value, h.value)) -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala deleted file mode 100644 index e43ddba0aa..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerSentEvents.scala +++ /dev/null @@ -1,27 +0,0 @@ -package sttp.tapir.server.http4s - -import fs2.{Stream, text} -import sttp.capabilities.fs2.Fs2Streams -import sttp.model.sse.ServerSentEvent - -object Http4sServerSentEvents { - - def serialiseSSEToBytes[F[_]](streams: Fs2Streams[F]): Stream[F, ServerSentEvent] => streams.BinaryStream = sseStream => { - sseStream - .map(sse => { - s"${sse.toString()}\n\n" - }) - .through(text.utf8Encode) - } - - def parseBytesToSSE[F[_]](streams: Fs2Streams[F]): streams.BinaryStream => Stream[F, ServerSentEvent] = stream => { - stream - .through(text.utf8Decode[F]) - .through(text.lines[F]) - .split(_.isEmpty) - .filter(_.nonEmpty) - .map(_.toList) - .map(ServerSentEvent.parse) - } - -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala deleted file mode 100644 index 85d5815ba9..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sToResponseBody.scala +++ /dev/null @@ -1,89 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.effect.{Blocker, Concurrent, ContextShift, Timer} -import cats.syntax.all._ -import fs2.{Chunk, Stream} -import org.http4s -import org.http4s.headers.{`Content-Disposition`, `Content-Type`} -import org.http4s.{EntityBody, EntityEncoder, Header, Headers, multipart} -import org.http4s.util.CaseInsensitiveString -import sttp.capabilities.fs2.Fs2Streams -import sttp.model.{HasHeaders, Part, Header => SttpHeader} -import sttp.tapir.{CodecFormat, RawBodyType, RawPart, WebSocketBodyOutput} -import sttp.tapir.server.interpreter.ToResponseBody - -import java.nio.charset.Charset - -private[http4s] class Http4sToResponseBody[F[_]: Concurrent: Timer: ContextShift, G[_]]( - serverOptions: Http4sServerOptions[F, G] -) extends ToResponseBody[Http4sResponseBody[F], Fs2Streams[F]] { - override val streams: Fs2Streams[F] = Fs2Streams[F] - - override def fromRawValue[R](v: R, headers: HasHeaders, format: CodecFormat, bodyType: RawBodyType[R]): Http4sResponseBody[F] = - Right(rawValueToEntity(bodyType, v)) - - override def fromStreamValue( - v: Stream[F, Byte], - headers: HasHeaders, - format: CodecFormat, - charset: Option[Charset] - ): Http4sResponseBody[F] = - Right(v) - - override def fromWebSocketPipe[REQ, RESP]( - pipe: streams.Pipe[REQ, RESP], - o: WebSocketBodyOutput[streams.Pipe[REQ, RESP], REQ, RESP, _, Fs2Streams[F]] - ): Http4sResponseBody[F] = Left(Http4sWebSockets.pipeToBody(pipe, o)) - - private def rawValueToEntity[CF <: CodecFormat, R](bodyType: RawBodyType[R], r: R): EntityBody[F] = { - 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)) - case RawBodyType.ByteBufferBody => fs2.Stream.chunk(Chunk.byteBuffer(r)) - case RawBodyType.InputStreamBody => - fs2.io.readInputStream( - r.pure[F], - serverOptions.ioChunkSize, - Blocker.liftExecutionContext(serverOptions.blockingExecutionContext) - ) - case RawBodyType.FileBody => - fs2.io.file.readAll[F](r.toPath, Blocker.liftExecutionContext(serverOptions.blockingExecutionContext), 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 - body - } - } - - private def rawPartToBodyPart[T](m: RawBodyType.MultipartBody, part: Part[T]): Option[multipart.Part[F]] = { - m.partType(part.name).map { partType => - val headers = part.headers.map { case SttpHeader(hk, hv) => - Header.Raw(CaseInsensitiveString(hk), hv) - }.toList - - val partContentType = part.contentType.map(parseContentType).getOrElse(`Content-Type`(http4s.MediaType.application.`octet-stream`)) - val entity = rawValueToEntity(partType.asInstanceOf[RawBodyType[Any]], part.body) - - val dispositionParams = part.otherDispositionParams + (Part.NameDispositionParam -> part.name) - val contentDispositionHeader = `Content-Disposition`("form-data", dispositionParams) - - val shouldAddCtHeader = headers.exists(_.name == `Content-Type`.name) - val allHeaders = if (shouldAddCtHeader) { - Headers(partContentType :: contentDispositionHeader :: headers) - } else { - Headers(contentDispositionHeader :: headers) - } - - multipart.Part(allHeaders, entity) - } - } - - private def parseContentType(ct: String): `Content-Type` = - `Content-Type`( - http4s.MediaType - .parse(ct) - .getOrElse(throw new IllegalArgumentException(s"Cannot parse content type: $ct")) - ) -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala deleted file mode 100644 index 064412846f..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/Http4sWebSockets.scala +++ /dev/null @@ -1,109 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.Monad -import cats.effect.{Concurrent, Timer} -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]( - pipe: Pipe[F, REQ, RESP], - o: WebSocketBodyOutput[Pipe[F, REQ, RESP], REQ, RESP, _, Fs2Streams[F]] - ): F[Pipe[F, Http4sWebSocketFrame, Http4sWebSocketFrame]] = { - Queue.bounded[F, WebSocketFrame](1).map { pongs => (in: Stream[F, Http4sWebSocketFrame]) => - val sttpFrames = in.map(http4sFrameToFrame) - val concatenated = optionallyConcatenateFrames(sttpFrames, o.concatenateFragmentedFrames) - val ignorePongs = optionallyIgnorePong(concatenated, o.ignorePong) - val autoPongs = optionallyAutoPong(ignorePongs, pongs, o.autoPongOnPing) - val autoPings = o.autoPing match { - case Some((interval, frame)) => Stream.awakeEvery[F](interval).map(_ => frame) - case None => Stream.empty - } - - autoPongs - .map { - case _: WebSocketFrame.Close if !o.decodeCloseRequests => None - case f => - o.requests.decode(f) match { - case failure: DecodeResult.Failure => throw new WebSocketFrameDecodeFailure(f, failure) - case DecodeResult.Value(v) => Some(v) - } - } - .unNoneTerminate - .through(pipe) - .map(o.responses.encode) - .mergeHaltL(pongs.dequeue) - .mergeHaltL(autoPings) - .map(frameToHttp4sFrame) - } - } - - private def http4sFrameToFrame(f: Http4sWebSocketFrame): WebSocketFrame = - f match { - case t: Http4sWebSocketFrame.Text => WebSocketFrame.Text(t.str, t.last, None) - case Http4sWebSocketFrame.Ping(data) => WebSocketFrame.Ping(data.toArray) - case Http4sWebSocketFrame.Pong(data) => WebSocketFrame.Pong(data.toArray) - case c: Http4sWebSocketFrame.Close => WebSocketFrame.Close(c.closeCode, "") - case _ => WebSocketFrame.Binary(f.data.toArray, f.last, None) - } - - private def frameToHttp4sFrame(w: WebSocketFrame): Http4sWebSocketFrame = { - w match { - case WebSocketFrame.Text(p, finalFragment, _) => Http4sWebSocketFrame.Text(p, finalFragment) - case WebSocketFrame.Binary(p, finalFragment, _) => Http4sWebSocketFrame.Binary(ByteVector(p), finalFragment) - case WebSocketFrame.Ping(p) => Http4sWebSocketFrame.Ping(ByteVector(p)) - case WebSocketFrame.Pong(p) => Http4sWebSocketFrame.Pong(ByteVector(p)) - case WebSocketFrame.Close(code, reason) => Http4sWebSocketFrame.Close(code, reason).fold(throw _, identity) - } - } - - private def optionallyConcatenateFrames[F[_]](s: Stream[F, WebSocketFrame], doConcatenate: Boolean): Stream[F, WebSocketFrame] = { - if (doConcatenate) { - type Accumulator = Option[Either[Array[Byte], String]] - - s.mapAccumulate(None: Accumulator) { - case (None, f: WebSocketFrame.Ping) => (None, Some(f)) - case (None, f: WebSocketFrame.Pong) => (None, Some(f)) - case (None, f: WebSocketFrame.Close) => (None, Some(f)) - case (None, f: WebSocketFrame.Data[_]) if f.finalFragment => (None, Some(f)) - case (Some(Left(acc)), f: WebSocketFrame.Binary) if f.finalFragment => (None, Some(f.copy(payload = acc ++ f.payload))) - case (Some(Left(acc)), f: WebSocketFrame.Binary) if !f.finalFragment => (Some(Left(acc ++ f.payload)), None) - case (Some(Right(acc)), f: WebSocketFrame.Text) if f.finalFragment => (None, Some(f.copy(payload = acc + f.payload))) - case (Some(Right(acc)), f: WebSocketFrame.Text) if !f.finalFragment => (Some(Right(acc + f.payload)), None) - case (acc, f) => throw new IllegalStateException(s"Cannot accumulate web socket frames. Accumulator: $acc, frame: $f.") - }.collect { case (_, Some(f)) => f } - } else { - s - } - } - - private def optionallyIgnorePong[F[_]](s: Stream[F, WebSocketFrame], doIgnore: Boolean): Stream[F, WebSocketFrame] = { - if (doIgnore) { - s.filter { - case WebSocketFrame.Pong(_) => false - case _ => true - } - } else s - } - - private def optionallyAutoPong[F[_]: Monad]( - s: Stream[F, WebSocketFrame], - pongs: Queue[F, WebSocketFrame], - doAuto: Boolean - ): Stream[F, WebSocketFrame] = { - if (doAuto) { - s.evalMap { - case WebSocketFrame.Ping(payload) => pongs.enqueue1(WebSocketFrame.Pong(payload)).map(_ => none[WebSocketFrame]) - case f => f.some.pure[F] - }.collect { case Some(f) => - f - } - } else s - } -} diff --git a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala b/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala deleted file mode 100644 index c202cc00c9..0000000000 --- a/server/http4s-ce2-server/src/main/scala/sttp/tapir/server/http4s/package.scala +++ /dev/null @@ -1,20 +0,0 @@ -package sttp.tapir.server - -import fs2.Pipe -import org.http4s.EntityBody -import org.http4s.websocket.WebSocketFrame -import sttp.capabilities.fs2.Fs2Streams -import sttp.model.sse.ServerSentEvent -import sttp.tapir.{CodecFormat, StreamBodyIO, streamTextBody} - -import java.nio.charset.Charset - -package object http4s { - private[http4s] type Http4sResponseBody[F[_]] = Either[F[Pipe[F, WebSocketFrame, WebSocketFrame]], EntityBody[F]] - - def serverSentEventsBody[F[_]]: StreamBodyIO[fs2.Stream[F, Byte], fs2.Stream[F, ServerSentEvent], Fs2Streams[F]] = { - val fs2Streams = Fs2Streams[F] - streamTextBody(fs2Streams)(CodecFormat.TextEventStream(), Some(Charset.forName("UTF-8"))) - .map(Http4sServerSentEvents.parseBytesToSSE(fs2Streams))(Http4sServerSentEvents.serialiseSSEToBytes(fs2Streams)) - } -} diff --git a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala deleted file mode 100644 index 8e3a5c5c8b..0000000000 --- a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerSentEventsTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.effect.IO -import org.scalatest.funsuite.AnyFunSuite -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 { - - 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 - |event: event - |id: id1 - |retry: 10 - | - |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList - }).unsafeRunSync() - } - - 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 - s"""data: data - |id: id1 - | - |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList - }).unsafeRunSync() - } - - test("serialiseSSEToBytes should successfully serialise multiline data event") { - val sse = fs2.Stream( - ServerSentEvent( - Some("""some data info 1 - |some data info 2 - |some data info 3""".stripMargin), - None, - None, - None - ) - ) - val serialised = Http4sServerSentEvents.serialiseSSEToBytes(Fs2Streams[IO])(sse) - val futureEvents = serialised.compile.toList - 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() - } - - test("parseBytesToSSE should successfully parse SSE bytes to SSE structure") { - val sseBytes = fs2.Stream.iterable( - """data: event1 data - |event: event1 - |id: id1 - |retry: 5 - | - | - |data: event2 data1 - |data: event2 data2 - |data: event2 data3 - |id: id2 - | - |""".stripMargin.getBytes(Charset.forName("UTF-8")) - ) - 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 - |event2 data2 - |event2 data3""".stripMargin), - None, - Some("id2"), - None - ) - ) - ).unsafeRunSync() - } - -} diff --git a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala deleted file mode 100644 index 0d7a840e0f..0000000000 --- a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sServerTest.scala +++ /dev/null @@ -1,119 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.effect._ -import cats.syntax.all._ -import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder -import org.http4s.syntax.kleisli._ -import org.scalatest.{EitherValues, OptionValues} -import org.scalatest.matchers.should.Matchers._ -import sttp.capabilities.WebSockets -import sttp.capabilities.fs2.Fs2Streams -import sttp.client3._ -import sttp.model.sse.ServerSentEvent -import sttp.tapir._ -import sttp.tapir.server.tests.{ - CreateServerTest, - ServerAuthenticationTests, - ServerBasicTests, - ServerStreamingTests, - ServerWebSocketTests, - backendResource -} -import sttp.tapir.tests.{Test, TestSuite} -import sttp.ws.{WebSocket, WebSocketFrame} - -import java.util.UUID -import scala.concurrent.ExecutionContext -import scala.concurrent.duration.DurationInt -import scala.util.Random - -class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite with EitherValues with OptionValues { - - override def tests: Resource[IO, List[Test]] = backendResource.map { backend => - implicit val m: CatsMonadError[IO] = new CatsMonadError[IO] - val interpreter = new Http4sTestServerInterpreter() - val createServerTest = new CreateServerTest(interpreter) - def randomUUID = Some(UUID.randomUUID().toString) - 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])) - val routes = Http4sServerInterpreter.toRoutes(e) - - BlazeServerBuilder[IO](ExecutionContext.global) - .bindHttp(0, "localhost") - .withHttpApp(Router("/api" -> routes).orNotFound) - .resource - .use { server => - val port = server.address.getPort - basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right("ok")) - } - .unsafeRunSync() - }, - createServerTest.testServer( - endpoint.out( - webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain] - .apply(Fs2Streams[IO]) - .autoPing(Some((1.second, WebSocketFrame.ping))) - ), - "automatic pings" - )((_: Unit) => IO(Right((in: fs2.Stream[IO, String]) => in))) { baseUri => - basicRequest - .response(asWebSocket { ws: WebSocket[IO] => - List(ws.receive().timeout(60.seconds), ws.receive().timeout(60.seconds)).sequence - }) - .get(baseUri.scheme("ws")) - .send(backend) - .map(_.body should matchPattern { case Right(List(WebSocketFrame.Ping(_), WebSocketFrame.Ping(_))) => }) - }, - createServerTest.testServer( - endpoint.out(streamBinaryBody(Fs2Streams[IO])), - "streaming should send data according to producer stream rate" - )((_: Unit) => - IO(Right(fs2.Stream.awakeEvery[IO](1.second).map(_.toString()).through(fs2.text.utf8Encode).interruptAfter(5.seconds))) - ) { baseUri => - basicRequest - .response( - asStream(Fs2Streams[IO])(bs => { - bs.through(fs2.text.utf8Decode).mapAccumulate(0)((pings, currentTime) => (pings + 1, currentTime)).compile.last - }) - ) - .get(baseUri) - .send(backend) - .map(_.body match { - case Right(Some((pings, _))) => pings should be >= 2 - case wrongResponse => fail(s"expected to get count of received data chunks, instead got $wrongResponse") - }) - }, - createServerTest.testServer( - endpoint.out(serverSentEventsBody[IO]), - "Send and receive SSE" - )((_: Unit) => IO(Right(fs2.Stream(sse1, sse2)))) { baseUri => - basicRequest - .response(asStream[IO, List[ServerSentEvent], Fs2Streams[IO]](Fs2Streams[IO]) { stream => - Http4sServerSentEvents - .parseBytesToSSE(Fs2Streams[IO]) - .apply(stream) - .compile - .toList - }) - .get(baseUri) - .send(backend) - .map(_.body.value shouldBe List(sse1, sse2)) - } - ) - - new ServerBasicTests(backend, createServerTest, interpreter).tests() ++ - new ServerStreamingTests(backend, createServerTest, Fs2Streams[IO]).tests() ++ - new ServerWebSocketTests(backend, createServerTest, Fs2Streams[IO]) { - override def functionToPipe[A, B](f: A => B): streams.Pipe[A, B] = in => in.map(f) - }.tests() ++ - new ServerAuthenticationTests(backend, createServerTest).tests() ++ - additionalTests() - } -} diff --git a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala b/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala deleted file mode 100644 index 605a0d65e3..0000000000 --- a/server/http4s-ce2-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala +++ /dev/null @@ -1,54 +0,0 @@ -package sttp.tapir.server.http4s - -import cats.data.{Kleisli, NonEmptyList} -import cats.effect.{ContextShift, IO, Resource, Timer} -import cats.syntax.all._ -import org.http4s.syntax.kleisli._ -import org.http4s.{HttpRoutes, Request, Response} -import org.http4s.server.blaze.BlazeServerBuilder -import sttp.capabilities.WebSockets -import sttp.capabilities.fs2.Fs2Streams -import sttp.tapir.Endpoint -import sttp.tapir.server.ServerEndpoint -import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} -import sttp.tapir.server.interceptor.exception.DefaultExceptionHandler -import sttp.tapir.server.tests.TestServerInterpreter -import sttp.tapir.tests.Port - -import scala.concurrent.ExecutionContext -import scala.reflect.ClassTag - -class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[IO] with WebSockets, HttpRoutes[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], - decodeFailureHandler: Option[DecodeFailureHandler] = None - ): HttpRoutes[IO] = { - implicit val serverOptions: Http4sServerOptions[IO, IO] = Http4sServerOptions - .customInterceptors( - exceptionHandler = Some(DefaultExceptionHandler), - serverLog = Some(Http4sServerOptions.Log.defaultServerLog), - decodeFailureHandler = decodeFailureHandler.getOrElse(DefaultDecodeFailureHandler.handler) - ) - Http4sServerInterpreter.toRoutes(e) - } - - override def routeRecoverErrors[I, E <: Throwable, O](e: Endpoint[I, E, O, Fs2Streams[IO] with WebSockets], fn: I => IO[O])(implicit - eClassTag: ClassTag[E] - ): HttpRoutes[IO] = { - Http4sServerInterpreter.toRouteRecoverErrors(e)(fn) - } - - override def server(routes: NonEmptyList[HttpRoutes[IO]]): Resource[IO, Port] = { - val service: Kleisli[IO, Request[IO], Response[IO]] = routes.reduceK.orNotFound - - BlazeServerBuilder[IO](ExecutionContext.global) - .bindHttp(0, "localhost") - .withHttpApp(service) - .resource - .map(_.address.getPort) - } -} From 2377493f9f0782b468be20bb606bfe58fa14a2dd Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Apr 2021 16:45:51 +0200 Subject: [PATCH 03/41] Update versions --- project/Versions.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/Versions.scala b/project/Versions.scala index ad46773a4a..a484a00c52 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,6 +1,6 @@ object Versions { - val http4s = "1.0.0-M20" - val catsEffect = "3.0.1" + val http4s = "1.0.0-M21" + val catsEffect = "3.0.2" val circe = "0.13.0" val circeYaml = "0.13.1" val sttp = "3.3.0-RC1" From dd9919834d361e0556a9b013249f3851e4317d5d Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Apr 2021 16:48:13 +0200 Subject: [PATCH 04/41] Better header conversions --- .../sttp/tapir/client/tests/HttpServer.scala | 17 ++++++++--------- .../server/http4s/Http4sServerInterpreter.scala | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) 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 54ca31819c..d2bc79f098 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 @@ -56,17 +56,18 @@ class HttpServer(port: Port) { case _ @GET -> Root / "api" / "unit" => Ok("{}") case r @ GET -> Root / "api" / "echo" / "params" => Ok(r.uri.query.params.toSeq.sortBy(_._1).map(p => s"${p._1}=${p._2}").mkString("&")) case r @ GET -> Root / "api" / "echo" / "headers" => - val headers = r.headers.headers.map(h => Header.Raw(CIString(h.name.toString), h.value.reverse)) - val filteredHeaders: List[Header.Raw] = r.headers.headers.find(_.name.toString.equalsIgnoreCase("Cookie")) match { - case Some(c) => headers.filter(_.name.toString.equalsIgnoreCase("Cookie")) :+ Header.Raw(CIString("Set-Cookie"), c.value.reverse) + val headers = r.headers.headers.map(h => h.copy(value = h.value.reverse)) + val filteredHeaders: Header.ToRaw = r.headers.headers.find(_.name == CIString("Cookie")) match { + case Some(c) => headers.filter(_.name == CIString("Cookie")) :+ Header.Raw(CIString("Set-Cookie"), c.value.reverse) case None => headers } - okOnlyHeaders(filteredHeaders.map(x => x: Header.ToRaw)) + + okOnlyHeaders(List(filteredHeaders)) case r @ GET -> Root / "api" / "echo" / "param-to-header" => - okOnlyHeaders(r.uri.multiParams.getOrElse("qq", Nil).reverse.map(v => Header.Raw(CIString("hh"), v): Header.ToRaw)) + okOnlyHeaders(r.uri.multiParams.getOrElse("qq", Nil).reverse.map("hh" -> _: Header.ToRaw)) case r @ GET -> Root / "api" / "echo" / "param-to-upper-header" => okOnlyHeaders(r.uri.multiParams.map { case (k, v) => - Header.Raw(CIString(k.toUpperCase()), v.headOption.getOrElse("?")): Header.ToRaw + k -> v.headOption.getOrElse("?"): Header.ToRaw }.toSeq) case r @ POST -> Root / "api" / "echo" / "multipart" => r.decode[multipart.Multipart[IO]] { mp => @@ -156,9 +157,7 @@ class HttpServer(port: Port) { } } - private def okOnlyHeaders(headers: Seq[Header.ToRaw]): IO[Response[IO]] = IO.pure { - Response(headers = Headers(headers)) - } + private def okOnlyHeaders(headers: Seq[Header.ToRaw]): IO[Response[IO]] = IO.pure(Response(headers = Headers(headers))) private def fromAcceptHeader(r: Request[IO])(f: PartialFunction[String, IO[Response[IO]]]): IO[Response[IO]] = r.headers.get[Accept].map(h => f(h.values.head.toString())).getOrElse(NotAcceptable()) 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 820b6222d4..0c231b0793 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 @@ -97,7 +97,7 @@ trait Http4sServerInterpreter { response: ServerResponse[Http4sResponseBody[F]] ): F[Response[F]] = { val statusCode = statusCodeToHttp4sStatus(response.code) - val headers = Headers(response.headers.map { case SttpHeader(k, v) => Header.Raw(CIString(k), v) }.toList) + val headers = Headers(response.headers.map(h => h.name -> h.value)) response.body match { case Some(Left(pipeF)) => From 7eee3db8b9da38ca5cb2f611f8044d0564336428 Mon Sep 17 00:00:00 2001 From: kubinio123 Date: Wed, 5 May 2021 14:06:10 +0200 Subject: [PATCH 05/41] vertx adjusted, other partial adjustments --- build.sbt | 6 +- project/Versions.scala | 2 +- .../cats/FinatraCatsServerInterpreter.scala | 2 +- .../server/http4s/Http4sBodyListener.scala | 6 +- .../http4s/Http4sServerInterpreter.scala | 2 +- .../server/http4s/Http4sServerOptions.scala | 2 +- .../server/http4s/Http4sToResponseBody.scala | 6 +- .../sttp/tapir/server/tests/package.scala | 3 +- .../server/vertx/VertxCatsServerOptions.scala | 9 +- .../VertxCatsServerInterpreter.scala | 31 +- .../sttp/tapir/server/vertx/streams/fs2.scala | 269 +++++++++--------- .../server/vertx/CatsVertxServerTest.scala | 4 +- .../CatsVertxTestServerInterpreter.scala | 8 +- .../vertx/VertxTestServerInterpreter.scala | 2 +- .../server/vertx/ZioVertxServerTest.scala | 7 +- .../vertx/ZioVertxTestServerInterpreter.scala | 15 +- .../server/vertx/streams/Fs2StreamTest.scala | 38 ++- .../ztapir/ZHttp4sServerInterpreter.scala | 17 +- .../scala/sttp/tapir/tests/TestSuite.scala | 5 + 19 files changed, 219 insertions(+), 215 deletions(-) diff --git a/build.sbt b/build.sbt index 37696c2bb4..ededc2f556 100644 --- a/build.sbt +++ b/build.sbt @@ -717,7 +717,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" %% "async-http-client-backend-fs2" % Versions.sttp ) ) .dependsOn(tests) @@ -743,7 +743,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 = allScalaVersions) @@ -809,7 +809,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 ) diff --git a/project/Versions.scala b/project/Versions.scala index 1c347dbc0d..007763c8ba 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -21,7 +21,7 @@ object Versions { val refined = "0.9.24" val enumeratum = "1.6.1" val zio = "1.0.7" - val zioInteropCats = "2.4.1.0" + val zioInteropCats = "3.0.2.0" val zioJson = "0.1.4" val playClient = "2.1.3" val playServer = "2.8.7" 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 5d836118bc..9764ceeee5 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,6 +1,6 @@ package sttp.tapir.server.finatra.cats -import cats.effect.Effect +import cats.effect import com.twitter.inject.Logging import io.catbird.util.Rerunnable import io.catbird.util.effect._ 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/Http4sServerInterpreter.scala b/server/http4s-server/src/main/scala/sttp/tapir/server/http4s/Http4sServerInterpreter.scala index 99df7716a2..f165b0970c 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 @@ -27,7 +27,7 @@ trait Http4sServerInterpreter { e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets] )(fToG: F ~> G)(gToF: G ~> F)(logic: I => G[Either[E, O]])(implicit serverOptions: Http4sServerOptions[F, G] - ): Http[OptionT[G, *], F] = toHttp(e.serverLogic(logic))(t) + ): Http[OptionT[G, *], F] = toHttp(e.serverLogic(logic))(fToG)(gToF) def toHttpRecoverErrors[I, E, O, F[_]: Async, G[_]: Sync]( e: Endpoint[I, E, O, Fs2Streams[F] with WebSockets] 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 d1de567429..e55dde56ed 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 @@ -63,7 +63,7 @@ object Http4sServerOptions { ): Http4sServerOptions[F, G] = Http4sServerOptions( defaultCreateFile[G], - defaultDeleteFile[G] + defaultDeleteFile[G], 8192, metricsInterceptor.toList ++ exceptionHandler.map(new ExceptionInterceptor[G, Http4sResponseBody[F]](_)).toList ++ 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 d379d4dc45..6672b04a3b 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 @@ -5,15 +5,11 @@ import cats.syntax.all._ import fs2.io.file.Files import fs2.{Chunk, Stream} import org.http4s -import org.http4s._ import org.http4s.headers.{`Content-Disposition`, `Content-Type`} -import org.http4s.{EntityBody, EntityEncoder, Header, Headers, multipart} +import org.http4s._ import org.typelevel.ci.CIString -import org.http4s.util.CaseInsensitiveString import sttp.capabilities.fs2.Fs2Streams -import sttp.model.{HasHeaders, Part, Header => SttpHeader} import sttp.model.{HasHeaders, HeaderNames, Part, Header => SttpHeader} -import sttp.tapir.{CodecFormat, RawBodyType, RawPart, WebSocketBodyOutput} import sttp.tapir.server.interpreter.ToResponseBody import sttp.tapir.{CodecFormat, RawBodyType, RawPart, WebSocketBodyOutput} 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 b633b780ec..ac7baedcd6 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 @@ -7,6 +7,5 @@ import sttp.client3.SttpBackend import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend package object tests { - val backendResource: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = - AsyncHttpClientFs2Backend.resource[IO]() + val backendResource: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = AsyncHttpClientFs2Backend.resource() } 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 3e07b47c31..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 { ) } - implicit 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/interpreters/VertxCatsServerInterpreter.scala b/server/vertx/src/main/scala/sttp/tapir/server/vertx/interpreters/VertxCatsServerInterpreter.scala index 954c8cd15f..8759b92f19 100644 --- a/server/vertx/src/main/scala/sttp/tapir/server/vertx/interpreters/VertxCatsServerInterpreter.scala +++ b/server/vertx/src/main/scala/sttp/tapir/server/vertx/interpreters/VertxCatsServerInterpreter.scala @@ -1,6 +1,6 @@ package sttp.tapir.server.vertx.interpreters -import cats.effect.{Async, IO, LiftIO, Sync} +import cats.effect.{Async, Sync} import cats.syntax.all._ import io.vertx.core.{Future, Handler} import io.vertx.ext.web.{Route, Router, RoutingContext} @@ -12,7 +12,7 @@ import sttp.tapir.server.interpreter.{BodyListener, ServerInterpreter} import sttp.tapir.server.vertx.decoders.{VertxRequestBody, VertxServerRequest} import sttp.tapir.server.vertx.encoders.{VertxOutputEncoders, VertxToResponseBody} import sttp.tapir.server.vertx.routing.PathMapping.extractRouteDefinition -import sttp.tapir.server.vertx.streams.ReadStreamCompatible +import sttp.tapir.server.vertx.streams.fs2.fs2ReadStreamCompatible import sttp.tapir.server.vertx.{VertxBodyListener, VertxCatsServerOptions} import scala.reflect.ClassTag @@ -24,7 +24,7 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { * @param endpointOptions options associated to the endpoint, like its logging capabilities, or execution context * @return A function, that given a router, will attach this endpoint to it */ - def route[F[_], I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])(logic: I => F[Either[E, O]])(implicit + def route[F[_]: Async, I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])(logic: I => F[Either[E, O]])(implicit endpointOptions: VertxCatsServerOptions[F] ): Router => Route = route(e.serverLogic(logic)) @@ -34,7 +34,7 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { * @param endpointOptions options associated to the endpoint, like its logging capabilities, or execution context * @return A function, that given a router, will attach this endpoint to it */ - def routeRecoverErrors[F[_], I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])( + def routeRecoverErrors[F[_]: Async, I, E, O](e: Endpoint[I, E, O, Fs2Streams[F]])( logic: I => F[O] )(implicit endpointOptions: VertxCatsServerOptions[F], @@ -47,22 +47,20 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { * @param endpointOptions options associated to the endpoint, like its logging capabilities, or execution context * @return A function, that given a router, will attach this endpoint to it */ - def route[F[_], I, E, O]( + def route[F[_]: Async, I, E, O]( e: ServerEndpoint[I, E, O, Fs2Streams[F], F] - )(implicit - endpointOptions: VertxCatsServerOptions[F] - ): Router => Route = { router => - import sttp.tapir.server.vertx.streams.fs2._ + )(implicit endpointOptions: VertxCatsServerOptions[F]): Router => Route = { router => mountWithDefaultHandlers(e)(router, extractRouteDefinition(e.endpoint)).handler(endpointHandler(e)) } - private def endpointHandler[F[_]: Async: LiftIO, I, E, O, A, S: ReadStreamCompatible]( - e: ServerEndpoint[I, E, O, _, F] + private def endpointHandler[F[_]: Async, I, E, O, A]( + e: ServerEndpoint[I, E, O, Fs2Streams[F], F] )(implicit serverOptions: VertxCatsServerOptions[F]): 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] - val interpreter: ServerInterpreter[Nothing, F, RoutingContext => Unit, S] = new ServerInterpreter( + + val interpreter: ServerInterpreter[Nothing, F, RoutingContext => Unit, Fs2Streams[F]] = new ServerInterpreter( new VertxRequestBody(rc, serverOptions, fFromVFuture), new VertxToResponseBody(serverOptions), serverOptions.interceptors, @@ -77,10 +75,9 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { } .handleError { e => rc.fail(e) } - val cancelToken = effect.toIO(result).unsafeRunCancelable { _ => () } - rc.response.exceptionHandler { _ => - cancelToken.unsafeRunSync() - } + val cancelToken = serverOptions.dispatcher.unsafeRunCancelable(result) + + rc.response.exceptionHandler { _ => cancelToken() } () } @@ -103,7 +100,7 @@ trait VertxCatsServerInterpreter extends CommonServerInterpreter { 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())) 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 048c13de31..18d5fcbc1f 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,171 +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 } - implicit def fs2ReadStreamCompatible[F[_]](implicit opts: VertxCatsServerOptions[F], F: ConcurrentEffect[F]) = + implicit def fs2ReadStreamCompatible[F[_]](implicit opts: VertxCatsServerOptions[F], F: Async[F]) = { new ReadStreamCompatible[Fs2Streams[F]] { override val streams: Fs2Streams[F] = Fs2Streams[F] - override def asReadStream(stream: streams.BinaryStream): 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: streams.BinaryStream): 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]): streams.BinaryStream = - 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]): streams.BinaryStream = + 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 d215029ab9..86fe1076ea 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,7 @@ import cats.effect.{IO, Resource} import io.vertx.core.Vertx import sttp.capabilities.fs2.Fs2Streams import sttp.monad.MonadError -import sttp.tapir.server.tests.{ServerAuthenticationTests, ServerBasicTests, ServerStreamingTests, CreateServerTest, backendResource} +import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} class CatsVertxServerTest extends TestSuite { @@ -16,7 +16,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 CreateServerTest(interpreter) new ServerBasicTests( 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 1fc29e7a89..af08a6d0de 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,6 +1,7 @@ package sttp.tapir.server.vertx import cats.data.NonEmptyList +import cats.effect.std.Dispatcher import cats.effect.{IO, Resource} import io.vertx.core.Vertx import io.vertx.core.http.HttpServerOptions @@ -15,7 +16,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] { import VertxCatsServerInterpreter._ @@ -28,6 +29,7 @@ class CatsVertxTestServerInterpreter(vertx: Vertx) ): Router => Route = { implicit val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.customInterceptors( + dispatcher, metricsInterceptor = metricsInterceptor, decodeFailureHandler = decodeFailureHandler.getOrElse(DefaultDecodeFailureHandler.handler) ) @@ -36,8 +38,10 @@ 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 = + ): Router => Route = { + implicit val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.default(dispatcher) VertxCatsServerInterpreter.routeRecoverErrors(e)(fn) + } override def server(routes: NonEmptyList[Router => Route]): Resource[IO, Port] = { val router = Router.router(vertx) 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 185c723e70..cb21a5b22d 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 d479439ba5..df0cfc2f6e 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 @@ -4,17 +4,14 @@ import cats.effect.{IO, Resource} import io.vertx.core.Vertx import sttp.capabilities.zio.ZioStreams import sttp.monad.MonadError -import sttp.tapir.server.tests.{ServerAuthenticationTests, ServerBasicTests, ServerStreamingTests, CreateServerTest, backendResource} +import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} -import zio.interop.catz._ import zio.Task class ZioVertxServerTest extends TestSuite { - import VertxZioServerInterpreter._ - 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 e42c404a23..1f16796e78 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,8 @@ package sttp.tapir.server.vertx -import cats.arrow.FunctionK import cats.data.NonEmptyList -import cats.effect.{ConcurrentEffect, IO, Resource} +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} @@ -13,7 +13,6 @@ import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, Defaul import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.server.tests.TestServerInterpreter import sttp.tapir.tests.Port -import zio.interop.catz._ import zio.{Runtime, Task} import scala.reflect.ClassTag @@ -45,17 +44,13 @@ 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) + Dispatcher[IO].map { dispatcher => + dispatcher.unsafeRunSync(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 86e926fc08..37174651af 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,24 +1,31 @@ package sttp.tapir.server.vertx.streams -import java.nio.ByteBuffer -import cats.effect.{ContextShift, IO, Timer} -import cats.effect.concurrent.Ref +import _root_.fs2.{Chunk, Stream} +import cats.effect.std.Dispatcher +import cats.effect.unsafe.implicits.global +import cats.effect.{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.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import sttp.tapir.server.vertx.VertxCatsServerOptions +import java.nio.ByteBuffer import scala.concurrent.duration._ import scala.util.control.NonFatal -class Fs2StreamTest extends AnyFlatSpec with Matchers { - implicit val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) - implicit val timer: Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global) - implicit val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.default[IO].copy(maxQueueSizeForReadStream = 4) +class Fs2StreamTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { + + private val (dispatcher, shutdownDispatcher) = Dispatcher[IO].allocated.unsafeRunSync() + + override protected def afterAll(): Unit = { + shutdownDispatcher.unsafeRunSync() + super.afterAll() + } + + implicit val options: VertxCatsServerOptions[IO] = VertxCatsServerOptions.default(dispatcher).copy(maxQueueSizeForReadStream = 4) def intAsBuffer(int: Int): Chunk[Byte] = { val buffer = ByteBuffer.allocate(4) @@ -54,7 +61,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers { io.flatTap(a => IO.delay(cond(a))) .handleErrorWith({ case NonFatal(e) => if (attempts < maxAttempts) { - timer.sleep(frequency) *> internal(attempts + 1) + Temporal[IO].sleep(frequency) *> internal(attempts + 1) } else { IO.raiseError(e) } @@ -101,7 +108,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers { 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].asReadStream(stream) @@ -153,7 +160,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers { } readStream.end() } - result <- resultFiber.join + result <- resultFiber.joinWith(IO.pure(Nil)) } yield { shouldIncreaseMonotonously(result) result should have size count.toLong @@ -183,7 +190,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers { readStream.end() }) .start - result <- resultFiber.join + result <- resultFiber.joinWith(IO.pure(Nil)) } yield { shouldIncreaseMonotonously(result) result should have size count.toLong @@ -193,6 +200,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers { } it should "drain failed read stream" in { + val ex = new Exception("!") val count = 50 val readStream = new FakeStream() val stream = fs2.fs2ReadStreamCompatible[IO].fromReadStream(readStream) @@ -210,12 +218,12 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers { Thread.sleep(25) readStream.handle(intAsVertxBuffer(i)) } - readStream.fail(new Exception("!")) + readStream.fail(ex) }) .start result <- resultFiber.join.attempt } yield { - result.isLeft shouldBe true + result shouldBe Right(Outcome.errored(ex)) }).unsafeRunSync() } } 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 adf1a63125..542de34197 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 @@ -3,23 +3,26 @@ package sttp.tapir.server.http4s.ztapir import org.http4s.HttpRoutes 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 { def from[I, E, O, R](e: ZEndpoint[I, E, O])(logic: I => ZIO[R, E, O])(implicit - serverOptions: Http4sServerOptions[RIO[R with Clock, *], RIO[R with Clock, *]] + serverOptions: Http4sServerOptions[RIO[R with Clock & Blocking, *], RIO[R with Clock & Blocking, *]] ): ServerEndpointsToRoutes[R] = from[R, I, E, O](e.zServerLogic(logic)) def from[R, I, E, O](se: ZServerEndpoint[R, I, E, O])(implicit - serverOptions: Http4sServerOptions[RIO[R with Clock, *], RIO[R with Clock, *]] + serverOptions: Http4sServerOptions[RIO[R with Clock & Blocking, *], RIO[R with Clock & Blocking, *]] ): ServerEndpointsToRoutes[R] = from[R](List(se)) def from[R]( serverEndpoints: List[ZServerEndpoint[R, _, _, _]] - )(implicit serverOptions: Http4sServerOptions[RIO[R with Clock, *], RIO[R with Clock, *]]): ServerEndpointsToRoutes[R] = + )(implicit + serverOptions: Http4sServerOptions[RIO[R with Clock & Blocking, *], RIO[R with Clock & Blocking, *]] + ): ServerEndpointsToRoutes[R] = new ServerEndpointsToRoutes[R](serverEndpoints) // This is needed to avoid too eager type inference. Having ZHttp4sServerInterpreter.toRoutes would require users @@ -27,9 +30,9 @@ trait ZHttp4sServerInterpreter { // Clock class ServerEndpointsToRoutes[R]( serverEndpoints: List[ZServerEndpoint[R, _, _, _]] - )(implicit serverOptions: Http4sServerOptions[RIO[R with Clock, *], RIO[R with Clock, *]]) { - def toRoutes: HttpRoutes[RIO[R with Clock, *]] = { - Http4sServerInterpreter.toRoutes(serverEndpoints.map(_.widen[R with Clock])) + )(implicit serverOptions: Http4sServerOptions[RIO[R with Clock & Blocking, *], RIO[R with Clock & Blocking, *]]) { + def toRoutes: HttpRoutes[RIO[R with Clock & Blocking, *]] = { + Http4sServerInterpreter.toRoutes(serverEndpoints.map(_.widen[R with Clock & Blocking])) } } } diff --git a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala b/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala index 2ed49bfd7b..0907e3a6b4 100644 --- a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala +++ b/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala @@ -1,5 +1,6 @@ package sttp.tapir.tests +import cats.effect.std.Dispatcher import cats.effect.{IO, Resource} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite @@ -9,8 +10,11 @@ trait TestSuite extends AnyFunSuite 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))) { test(t.name)(t.f())(t.pos) @@ -21,6 +25,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() } } From 7a665a2fc237ccf06d7c689385912297decbe1b1 Mon Sep 17 00:00:00 2001 From: kubinio123 Date: Wed, 5 May 2021 16:18:28 +0200 Subject: [PATCH 06/41] examples and test adjustments --- build.sbt | 10 +- .../tapir/client/sttp/SttpClientTests.scala | 26 +++--- .../examples/HelloWorldHttp4sServer.scala | 41 ++++----- .../tapir/examples/Http4sClientExample.scala | 4 +- ...leEndpointsDocumentationHttp4sServer.scala | 35 ++++--- .../examples/OAuth2GithubHttp4sServer.scala | 37 ++++---- .../examples/StreamingHttp4sFs2Server.scala | 41 ++++----- .../examples/WebSocketHttp4sServer.scala | 91 +++++++++---------- .../examples/ZioEnvExampleHttp4sServer.scala | 10 +- .../examples/ZioExampleHttp4sServer.scala | 9 +- .../ZioPartialServerLogicHttp4s.scala | 7 +- .../server/http4s/ztapir/ZEndpointTest.scala | 5 +- .../sttp/tapir/tests/TestSuite.scala | 2 +- 13 files changed, 155 insertions(+), 163 deletions(-) rename tests/src/main/{scala => scalajvm}/sttp/tapir/tests/TestSuite.scala (100%) diff --git a/build.sbt b/build.sbt index ededc2f556..3a50f5a990 100644 --- a/build.sbt +++ b/build.sbt @@ -853,7 +853,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 = allScalaVersions) @@ -872,8 +872,8 @@ lazy val sttpClient: ProjectMatrix = (projectMatrix in file("client/sttp-client" settings = commonJvmSettings ++ Seq( libraryDependencies ++= loggerDependencies.map(_ % Test) ++ Seq( "com.softwaremill.sttp.client3" %% "akka-http-backend" % Versions.sttp % Test, - "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, "com.softwaremill.sttp.shared" %% "akka" % Versions.sttpShared % Optional, "com.typesafe.akka" %% "akka-stream" % Versions.akkaStreams % Optional ) @@ -943,9 +943,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/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 6a4aadd770..551c47046d 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,7 +1,6 @@ package sttp.tapir.client.sttp -import cats.effect.IO -import cats.effect.unsafe.implicits.global +import cats.effect.{IO, Resource} import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.client3._ @@ -9,16 +8,16 @@ 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] { - val backend: SttpBackend[IO, R] = - HttpClientFs2Backend[IO]().unsafeRunSync() + + val backend: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = HttpClientFs2Backend.resource[IO]() def wsToPipe: WebSocketToPipe[R] override def send[I, E, O, FN[_]](e: Endpoint[I, E, O, R], port: Port, args: I, scheme: String = "http"): IO[Either[E, O]] = { - implicit val wst: WebSocketToPipe[R] = wsToPipe - SttpClientInterpreter.toRequestThrowDecodeFailures(e, Some(uri"$scheme://localhost:$port")).apply(args).send(backend).map(_.body) + backend.use { b => + implicit val wst: WebSocketToPipe[R] = wsToPipe + SttpClientInterpreter.toRequestThrowDecodeFailures(e, Some(uri"$scheme://localhost:$port")).apply(args).send(b).map(_.body) + } } override def safeSend[I, E, O, FN[_]]( @@ -26,12 +25,9 @@ abstract class SttpClientTests[R >: WebSockets with Fs2Streams[IO]] extends Clie port: Port, args: I ): IO[DecodeResult[Either[E, O]]] = { - implicit val wst: WebSocketToPipe[R] = wsToPipe - SttpClientInterpreter.toRequest(e, Some(uri"http://localhost:$port")).apply(args).send(backend).map(_.body) - } - - override protected def afterAll(): Unit = { - backend.close().unsafeRunSync() - super.afterAll() + backend.use { b => + implicit val wst: WebSocketToPipe[R] = wsToPipe + SttpClientInterpreter.toRequest(e, Some(uri"http://localhost:$port")).apply(args).send(b).map(_.body) + } } } diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index d780de038f..e34c9c2548 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.server.blaze.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.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 55fa56a115..d7635bebef 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 6606de2cca..1754831d23 100644 --- a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala @@ -1,7 +1,5 @@ package sttp.tapir.examples -import java.util.concurrent.atomic.AtomicReference - import cats.effect._ import cats.syntax.all._ import io.circe.generic.auto._ @@ -11,16 +9,17 @@ import org.http4s.server.blaze.BlazeServerBuilder 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) @@ -41,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( @@ -64,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 d59f6c24ae..6d76b4a8eb 100644 --- a/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala @@ -20,12 +20,9 @@ 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) - implicit val ce: ConcurrentEffect[IO] = IO.ioConcurrentEffect // github application details val clientId = "" @@ -109,20 +106,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 e77b1a6fd1..5e4c7769c8 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala @@ -1,25 +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.server.blaze.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`. @@ -30,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.toRoutes(streamingEndpoint) { _ => @@ -48,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 cd1c3e0d0f..b8dfe6892e 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.server.blaze.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,44 @@ 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 bf0765f596..3ad0fbb63d 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.server.blaze.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 @@ -14,7 +15,7 @@ import sttp.tapir.ztapir._ import zio.clock.Clock import zio.console.Console import zio.interop.catz._ -import zio.{App, ExitCode, Has, IO, RIO, UIO, URIO, ZEnv, ZIO, ZLayer} +import zio.{&, App, ExitCode, Has, IO, RIO, UIO, URIO, ZEnv, ZIO, ZLayer} object ZioEnvExampleHttp4sServer extends App { // Domain classes, services, layers @@ -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 & 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 & 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 & 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 0ab48ac178..87d4856291 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -12,8 +12,9 @@ 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} +import zio.{&, App, ExitCode, IO, RIO, UIO, URIO, ZEnv, ZIO} object ZioExampleHttp4sServer extends App { case class Pet(species: String, url: String) @@ -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 & 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 & 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 & 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 17054c1947..6794230eb9 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 & Console & Clock & 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/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 02968d8279..52cf2b13f2 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 @@ -3,8 +3,9 @@ package sttp.tapir.server.http4s.ztapir import org.http4s.HttpRoutes import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import zio.{Has, RIO, ZIO} +import zio.{&, Has, RIO, ZIO} import sttp.tapir.ztapir._ +import zio.blocking.Blocking import zio.clock.Clock import zio.interop.catz._ @@ -21,7 +22,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 & Clock & Blocking, *]] = ZHttp4sServerInterpreter.from(List(serverEndpoint1.widen[Env], serverEndpoint2.widen[Env])).toRoutes } } diff --git a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala similarity index 100% rename from tests/src/main/scala/sttp/tapir/tests/TestSuite.scala rename to tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala index 0907e3a6b4..339527d8cc 100644 --- a/tests/src/main/scala/sttp/tapir/tests/TestSuite.scala +++ b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala @@ -1,10 +1,10 @@ package sttp.tapir.tests import cats.effect.std.Dispatcher +import cats.effect.unsafe.implicits.global import cats.effect.{IO, Resource} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite -import cats.effect.unsafe.implicits.global trait TestSuite extends AnyFunSuite with BeforeAndAfterAll { def tests: Resource[IO, List[Test]] From 2cae56e7473ffb0623f122819df0f730e52e1c3e Mon Sep 17 00:00:00 2001 From: kubinio123 Date: Wed, 5 May 2021 16:43:22 +0200 Subject: [PATCH 07/41] merge --- .../scala/sttp/tapir/server/vertx/streams/Fs2StreamTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4ecb2a33bc..799a3a32d6 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 @@ -3,7 +3,7 @@ package sttp.tapir.server.vertx.streams import _root_.fs2.{Chunk, Stream} import cats.effect.std.Dispatcher import cats.effect.unsafe.implicits.global -import cats.effect.{IO, Outcome, Ref, Temporal} +import cats.effect.{IO, Outcome, Ref, Temporal, Deferred} import cats.syntax.flatMap._ import cats.syntax.option._ import io.vertx.core.buffer.Buffer From c21967d6728b27da1a32d7967074a3b50bff4a80 Mon Sep 17 00:00:00 2001 From: kubinio123 Date: Mon, 31 May 2021 12:57:47 +0200 Subject: [PATCH 08/41] http4s version 0.23.0-RC1 --- .../scala/sttp/tapir/client/tests/HttpServer.scala | 2 +- .../tapir/examples/HelloWorldHttp4sServer.scala | 2 +- ...MultipleEndpointsDocumentationHttp4sServer.scala | 2 +- .../tapir/examples/OAuth2GithubHttp4sServer.scala | 2 +- .../tapir/examples/StreamingHttp4sFs2Server.scala | 2 +- .../sttp/tapir/examples/WebSocketHttp4sServer.scala | 2 +- .../tapir/examples/ZioEnvExampleHttp4sServer.scala | 2 +- .../tapir/examples/ZioExampleHttp4sServer.scala | 2 +- .../examples/ZioPartialServerLogicHttp4s.scala | 2 +- project/Versions.scala | 2 +- .../server/http4s/Http4sServerInterpreter.scala | 9 ++++----- .../tapir/server/http4s/Http4sToResponseBody.scala | 13 +++++++------ .../sttp/tapir/server/http4s/Http4sServerTest.scala | 2 +- .../server/http4s/Http4sTestServerInterpreter.scala | 2 +- 14 files changed, 23 insertions(+), 23 deletions(-) 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 26f7b1f449..e0c8a978dc 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 @@ -8,7 +8,7 @@ import fs2.{Pipe, Stream} import org.http4s.dsl.io._ import org.http4s.headers.{Accept, `Content-Type`} import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.middleware._ import org.http4s.server.websocket.WebSocketBuilder import org.http4s.syntax.kleisli._ diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index e34c9c2548..6fedb60fc3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -4,7 +4,7 @@ import cats.effect._ import cats.syntax.all._ import org.http4s.HttpRoutes import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import sttp.client3._ import sttp.tapir._ diff --git a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala index 1754831d23..e40c7aca53 100644 --- a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala @@ -5,7 +5,7 @@ import cats.syntax.all._ import io.circe.generic.auto._ import org.http4s.HttpRoutes import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import sttp.tapir._ import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter diff --git a/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala index 6d76b4a8eb..067cd0ec63 100644 --- a/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala @@ -5,7 +5,7 @@ import cats.syntax.all._ import io.circe.generic.auto._ import org.http4s.HttpRoutes import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import pdi.jwt.{JwtAlgorithm, JwtCirce, JwtClaim} import sttp.client3._ diff --git a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala index 5e4c7769c8..af0def43c7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala @@ -5,7 +5,7 @@ import cats.syntax.all._ import fs2._ import org.http4s.HttpRoutes import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import sttp.capabilities.fs2.Fs2Streams import sttp.client3._ diff --git a/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala index b8dfe6892e..ac75fae689 100644 --- a/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala @@ -5,7 +5,7 @@ import io.circe.generic.auto._ import fs2._ import org.http4s.HttpRoutes import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index 68f60f0c8c..be5a2683cb 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -4,7 +4,7 @@ import cats.syntax.all._ import io.circe.generic.auto._ import org.http4s._ import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import zio.blocking.Blocking import sttp.tapir.generic.auto._ diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index 87d4856291..58489ae73a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -4,7 +4,7 @@ import cats.syntax.all._ import io.circe.generic.auto._ import org.http4s._ import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import sttp.tapir.json.circe._ import sttp.tapir.generic.auto._ diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index f5ea9c39cb..87dc30ad76 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -2,7 +2,7 @@ package sttp.tapir.examples import org.http4s._ import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import sttp.client3._ import sttp.client3.asynchttpclient.zio.AsyncHttpClientZioBackend diff --git a/project/Versions.scala b/project/Versions.scala index 143e9f0246..7e9b1ff9a2 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,5 +1,5 @@ object Versions { - val http4s = "1.0.0-M21" + val http4s = "0.23.0-RC1" val catsEffect = "3.0.2" val circe = "0.13.0" val circeYaml = "0.13.1" 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 c9a4d964df..7a64ef4bd5 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 @@ -7,14 +7,13 @@ import cats.effect.{Async, Sync} import cats.syntax.all._ import cats.~> import fs2.{Pipe, Stream} +import org.http4s._ import org.http4s.server.websocket.WebSocketBuilder import org.http4s.websocket.WebSocketFrame -import org.http4s._ import org.log4s.{Logger, getLogger} import org.typelevel.ci.CIString import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams -import sttp.model.{Header => SttpHeader} import sttp.tapir.Endpoint import sttp.tapir.model.ServerResponse import sttp.tapir.server.ServerEndpoint @@ -51,8 +50,8 @@ trait Http4sServerInterpreter { // - def toHttp[I, E, O, F[_]: Async, G[_]: Sync](se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, G])(fToG: F ~> G)(gToF: G ~> F)(implicit - serverOptions: Http4sServerOptions[F, G] + def toHttp[I, E, O, F[_]: Async, G[_]: Sync](se: ServerEndpoint[I, E, O, Fs2Streams[F] with WebSockets, G])(fToG: F ~> G)(gToF: G ~> F)( + implicit serverOptions: Http4sServerOptions[F, G] ): Http[OptionT[G, *], F] = toHttp(List(se))(fToG)(gToF) def toRoutes[I, E, O, F[_]: Async]( @@ -100,7 +99,7 @@ trait Http4sServerInterpreter { response: ServerResponse[Http4sResponseBody[F]] ): F[Response[F]] = { val statusCode = statusCodeToHttp4sStatus(response.code) - val headers = Headers(response.headers.map(header => Header.Raw(CaseInsensitiveString(header.name), header.value)).toList) + val headers = Headers(response.headers.map(header => Header.Raw(CIString(header.name), header.value)).toList) response.body match { case Some(Left(pipeF)) => 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 83a597ebf4..2560897ba6 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 @@ -5,11 +5,12 @@ import cats.syntax.all._ import fs2.io.file.Files import fs2.{Chunk, Stream} import org.http4s -import org.http4s.headers.{`Content-Disposition`, `Content-Type`} +import org.http4s.Header.ToRaw.rawToRaw import org.http4s._ +import org.http4s.headers.{`Content-Disposition`, `Content-Type`} import org.typelevel.ci.CIString import sttp.capabilities.fs2.Fs2Streams -import sttp.model.{HasHeaders, HeaderNames, Part, Header => SttpHeader} +import sttp.model.{HasHeaders, HeaderNames, Part} import sttp.tapir.server.interpreter.ToResponseBody import sttp.tapir.{CodecFormat, RawBodyType, RawPart, WebSocketBodyOutput} @@ -59,11 +60,11 @@ private[http4s] class Http4sToResponseBody[F[_]: Async, G[_]]( private def rawPartToBodyPart[T](m: RawBodyType.MultipartBody, part: Part[T]): Option[multipart.Part[F]] = { m.partType(part.name).map { partType => - val headers = part.headers.map { header => - Header.Raw(CaseInsensitiveString(header.name), header.value) + val headers: List[Header.ToRaw] = part.headers.map { header => + rawToRaw(Header.Raw(CIString(header.name), header.value)) }.toList - val partContentType: `Content-Type` = + val partContentType = part.contentType.map(parseContentType).getOrElse(`Content-Type`(http4s.MediaType.application.`octet-stream`)) val entity = rawValueToEntity(partType.asInstanceOf[RawBodyType[Any]], part.body) @@ -74,7 +75,7 @@ private[http4s] class Http4sToResponseBody[F[_]: Async, G[_]]( val shouldAddCtHeader = part.headers.exists(_.is(HeaderNames.ContentType)) val allHeaders = if (shouldAddCtHeader) { - Headers((partContentType: Header.ToRaw) :: contentDispositionHeader :: headers) + Headers.apply((partContentType: Header.ToRaw) :: contentDispositionHeader :: headers) } else { Headers(contentDispositionHeader :: headers) } 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 e2c511bdb6..7f982ced1e 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 @@ -4,7 +4,7 @@ import cats.effect._ import cats.syntax.all._ import cats.effect.unsafe.implicits.global import org.http4s.server.Router -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import org.scalatest.matchers.should.Matchers._ import org.scalatest.{EitherValues, OptionValues} 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 cb1a0e9f28..842ca240b0 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 @@ -3,7 +3,7 @@ package sttp.tapir.server.http4s import cats.data.{Kleisli, NonEmptyList} import cats.effect.{IO, Resource} import cats.syntax.all._ -import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.syntax.kleisli._ import org.http4s.{HttpRoutes, Request, Response} import sttp.capabilities.WebSockets From 9ee5b4721650bc961d416f82d4081154ae4d4d41 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 10:53:24 +0200 Subject: [PATCH 09/41] Update cats --- project/Versions.scala | 4 ++-- project/build.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/Versions.scala b/project/Versions.scala index d42f28380e..67f49525bd 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,6 +1,6 @@ object Versions { val http4s = "0.23.0-RC1" - val catsEffect = "3.0.2" + val catsEffect = "3.1.1" val circe = "0.14.1" val circeYaml = "0.14.0" val sttp = "3.3.9" @@ -21,7 +21,7 @@ object Versions { val refined = "0.9.26" val enumeratum = "1.7.0" val zio = "1.0.9" - val zioInteropCats = "3.0.2.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 From 7ea3ca0d3f2660b524473a9c10875459b8effe52 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 11:04:38 +0200 Subject: [PATCH 10/41] Fix http4s interpreter --- .../server/http4s/Http4sRequestBody.scala | 1 - .../http4s/Http4sServerInterpreter.scala | 18 ++------- .../server/http4s/Http4sServerOptions.scala | 1 - .../Http4sServerToHttpInterpreter.scala | 40 ++++++------------- .../server/http4s/Http4sToResponseBody.scala | 1 - .../scalajvm/sttp/tapir/tests/TestSuite.scala | 1 + 6 files changed, 18 insertions(+), 44 deletions(-) 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 87629e8d3a..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,6 +1,5 @@ package sttp.tapir.server.http4s -import java.io.ByteArrayInputStream import cats.effect.{Async, Sync} import cats.syntax.all._ import cats.{Monad, ~>} 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 7957c43b8c..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,17 +1,9 @@ package sttp.tapir.server.http4s import cats.arrow.FunctionK -import cats.data.{Kleisli, OptionT} -import cats.effect.{Concurrent, ContextShift, Sync, Timer} -import cats.implicits._ +import cats.effect.{Async, Sync} import cats.~> -import fs2.Pipe -import fs2.concurrent.Queue import org.http4s._ -import org.http4s.server.websocket.WebSocketBuilder -import org.http4s.util.CaseInsensitiveString -import org.http4s.websocket.WebSocketFrame -import org.log4s.{Logger, getLogger} import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.Endpoint @@ -19,7 +11,7 @@ import sttp.tapir.server.ServerEndpoint import scala.reflect.ClassTag -trait Http4sServerInterpreter extends Http4sServerToHttpInterpreter[F, F] { +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( @@ -41,7 +33,7 @@ trait Http4sServerInterpreter extends Http4sServerToHttpInterpreter[F, F] { object Http4sServerInterpreter { def apply[F[_]]()(implicit _fa: Async[F]): Http4sServerInterpreter[F] = { new Http4sServerInterpreter[F] { - override implicit def ga: Async[G] = _fa + 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] @@ -52,12 +44,10 @@ object Http4sServerInterpreter { serverOptions: Http4sServerOptions[F, F] )(implicit _fa: Async[F]): Http4sServerInterpreter[F] = { new Http4sServerInterpreter[F] { - override implicit def ga: Async[G] = _fa + 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 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 7ec459fccc..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 @@ -13,7 +13,6 @@ import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import sttp.tapir.{Defaults, TapirFile} import java.io.File -import scala.concurrent.ExecutionContext /** @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. 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 c1831a50f8..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 @@ -5,7 +5,6 @@ import cats.syntax.all._ import fs2.io.file.Files import fs2.{Chunk, Stream} import org.http4s -import org.http4s.Header.ToRaw.rawToRaw import org.http4s._ import org.http4s.headers.{`Content-Disposition`, `Content-Type`} import org.http4s.Header.ToRaw.rawToRaw diff --git a/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala index a9cdde9f0c..c3864190c5 100644 --- a/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala +++ b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala @@ -3,6 +3,7 @@ package sttp.tapir.tests 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 From 36ae2fd0d905700a1f463f7a918d35ac4945044a Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 11:09:16 +0200 Subject: [PATCH 11/41] Fix aws lambda --- .../serverless/aws/lambda/runtime/AwsLambdaRuntimeLogic.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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/AwsLambdaRuntimeLogic.scala index 8ecc0bb648..b78adb51a1 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/AwsLambdaRuntimeLogic.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 @@ -18,7 +18,7 @@ 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 { - def apply[F[_]: ContextShift: ConcurrentEffect]( + def apply[F[_]: Sync]( route: Route[F], awsRuntimeApi: String, backend: Resource[F, SttpBackend[F, Any]] From dc0909aaa274fe62905a5e995b527ba093df5216 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 11:15:13 +0200 Subject: [PATCH 12/41] Fix http4s client --- build.sbt | 2 +- .../sttp/tapir/client/http4s/Http4sClientRequestTests.scala | 5 ----- .../scala/sttp/tapir/client/http4s/Http4sClientTests.scala | 4 ++-- .../src/main/scala/sttp/tapir/client/tests/HttpServer.scala | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index 17c3fd2cc6..aebde4d79a 100644 --- a/build.sbt +++ b/build.sbt @@ -937,7 +937,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) 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 7f4b63514b..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,7 +1,7 @@ package sttp.tapir.client.http4s import cats.effect.IO -import org.http4s.client.blaze.BlazeClientBuilder +import org.http4s.blaze.client.BlazeClientBuilder import org.http4s.{Request, Response} import sttp.tapir.client.tests.ClientTests import sttp.tapir.{DecodeResult, Endpoint} @@ -10,7 +10,7 @@ import scala.concurrent.ExecutionContext.global abstract class Http4sClientTests[R] extends ClientTests[R] { 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) + val (request, parseResponse) = Http4sClientInterpreter[IO]().toRequestUnsafe(e, Some(s"http://localhost:$port")).apply(args) sendAndParseResponse(request, parseResponse) } 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 5fb44499a0..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 @@ -57,7 +57,7 @@ class HttpServer(port: Port) { case r @ GET -> Root / "api" / "echo" / "params" => Ok(r.uri.query.params.toSeq.sortBy(_._1).map(p => s"${p._1}=${p._2}").mkString("&")) case r @ GET -> Root / "api" / "echo" / "headers" => val headers = r.headers.headers.map(h => h.copy(value = h.value.reverse)) - val filteredHeaders1: Header.ToRaw = r.headers.headers.find(_.name == CIString("Cookie")) match { + val filteredHeaders1 = r.headers.headers.find(_.name == CIString("Cookie")) match { case Some(c) => headers.filter(_.name == CIString("Cookie")) :+ Header.Raw(CIString("Set-Cookie"), c.value.reverse) case None => headers } From df68c3cb38b40ab65621963f8f38f7bf27a267ad Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 11:29:13 +0200 Subject: [PATCH 13/41] Fix vertx server --- .../vertx/VertxCatsServerInterpreter.scala | 27 ++++++++++--------- .../server/vertx/VertxCatsServerOptions.scala | 2 +- .../sttp/tapir/server/vertx/streams/fs2.scala | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) 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 abacd2af2e..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,5 +1,6 @@ package sttp.tapir.server.vertx +import cats.effect.std.Dispatcher import cats.effect.{Async, Sync} import cats.syntax.all._ import io.vertx.core.{Future, Handler} @@ -15,8 +16,8 @@ import sttp.tapir.server.vertx.decoders.{VertxRequestBody, VertxServerRequest} import sttp.tapir.server.vertx.encoders.{VertxOutputEncoders, VertxToResponseBody} import sttp.tapir.server.vertx.interpreters.{CommonServerInterpreter, FromVFuture} import sttp.tapir.server.vertx.routing.PathMapping.extractRouteDefinition +import sttp.tapir.server.vertx.streams.ReadStreamCompatible import sttp.tapir.server.vertx.streams.fs2.fs2ReadStreamCompatible -import sttp.tapir.server.vertx.{VertxBodyListener, VertxCatsServerOptions} import java.util.concurrent.atomic.AtomicReference import scala.reflect.ClassTag @@ -25,7 +26,7 @@ trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter { 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 @@ -81,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 = serverOptions.dispatcher.unsafeRunCancelable(result) + val cancelToken = vertxCatsServerOptions.dispatcher.unsafeRunCancelable(result) cancelRef.getAndSet(Some(Right(cancelToken))).collect { case Left(_) => - cancelToken.unsafeRunSync() + cancelToken() } () @@ -100,16 +102,16 @@ 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 } } @@ -119,8 +121,7 @@ object VertxCatsServerInterpreter { 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) 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 dec88435be..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 @@ -75,5 +75,5 @@ object VertxCatsServerOptions { ) } - def default[F[_]: Async]: 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 d16578a8dd..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 @@ -24,7 +24,7 @@ object fs2 { dfd.get } - implicit def fs2ReadStreamCompatible[F[_]](implicit opts: VertxCatsServerOptions[F], F: Async[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] From c865d840f103eb94d89381f2004d407dcdd6dd1f Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 14:05:08 +0200 Subject: [PATCH 14/41] Fix WS tests --- .../tapir/client/sttp/SttpClientTests.scala | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) 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 982902b7d2..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.{IO, Resource} +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._ @@ -9,15 +11,13 @@ import sttp.tapir.client.tests.ClientTests import sttp.tapir.{DecodeResult, Endpoint} abstract class SttpClientTests[R >: WebSockets with Fs2Streams[IO]] extends ClientTests[R] { - - val backend: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = HttpClientFs2Backend.resource[IO]() + 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]] = { - backend.use { b => - implicit val wst: WebSocketToPipe[R] = wsToPipe - SttpClientInterpreter().toRequestThrowDecodeFailures(e, Some(uri"$scheme://localhost:$port")).apply(args).send(b).map(_.body) - } + implicit val wst: WebSocketToPipe[R] = wsToPipe + SttpClientInterpreter().toRequestThrowDecodeFailures(e, Some(uri"$scheme://localhost:$port")).apply(args).send(backend).map(_.body) } override def safeSend[I, E, O]( @@ -25,9 +25,13 @@ abstract class SttpClientTests[R >: WebSockets with Fs2Streams[IO]] extends Clie port: Port, args: I ): IO[DecodeResult[Either[E, O]]] = { - backend.use { b => - implicit val wst: WebSocketToPipe[R] = wsToPipe - SttpClientInterpreter.toRequest(e, Some(uri"http://localhost:$port")).apply(args).send(b).map(_.body) - } + implicit val wst: WebSocketToPipe[R] = wsToPipe + SttpClientInterpreter().toRequest(e, Some(uri"http://localhost:$port")).apply(args).send(backend).map(_.body) + } + + override protected def afterAll(): Unit = { + backend.close().unsafeRunSync() + closeDispatcher.unsafeRunSync() + super.afterAll() } } From 9e38d0cd393c74072cf4ded039c9ee8454f6cc6c Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 15:15:02 +0200 Subject: [PATCH 15/41] Fix aws lambda --- doc/server/aws.md | 2 +- .../aws/lambda/tests/LambdaHandler.scala | 1 + .../tests/AwsLambdaSamLocalHttpTest.scala | 1 + .../aws/lambda/runtime/AwsLambdaRuntime.scala | 26 ++++++++++--------- ...scala => AwsLambdaRuntimeInvocation.scala} | 9 ++++--- .../AwsLambdaCreateServerStubTest.scala | 1 + ...a => AwsLambdaRuntimeInvocationTest.scala} | 24 ++++++++--------- 7 files changed, 34 insertions(+), 30 deletions(-) rename serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/{AwsLambdaRuntimeLogic.scala => AwsLambdaRuntimeInvocation.scala} (91%) rename serverless/aws/lambda/src/test/scala/sttp/tapir/serverless/aws/lambda/runtime/{AwsLambdaRuntimeLogicTest.scala => AwsLambdaRuntimeInvocationTest.scala} (82%) 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/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..81f8b182df 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 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 b78adb51a1..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 @@ -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[_]: Sync]( + /** 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..f6fcc112d6 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 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 = From d7b4405569691782621f7ca13a4250566afb1085 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 16:40:38 +0200 Subject: [PATCH 16/41] More fixes --- doc/endpoint/zio.md | 6 +++--- .../swagger/http4s/SwaggerHttp4sTest.scala | 16 ++++++---------- .../ztapir/ZHttp4sServerInterpreter.scala | 18 +++++++++++------- .../server/http4s/ztapir/ZEndpointTest.scala | 5 ++--- .../aws/examples/LambdaApiExample.scala | 1 + 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/endpoint/zio.md b/doc/endpoint/zio.md index d01320a7f0..4aa3798eb9 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. @@ -76,7 +76,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/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/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 f500484741..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,16 +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.blocking.Blocking import zio.clock.Clock import zio.interop.catz._ -import zio.{&, RIO, ZIO} +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)) @@ -28,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])) } } } @@ -39,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 575a46c25f..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 @@ -3,11 +3,10 @@ package sttp.tapir.server.http4s.ztapir import org.http4s.HttpRoutes import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import zio.{&, Has, RIO, ZIO} +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 { @@ -22,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 From 11c336a570c3a545750985ddc7da1575257fe5a4 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 12 Jul 2021 16:49:31 +0200 Subject: [PATCH 17/41] Fix sttp client --- build.sbt | 2 +- .../sttp/tapir/client/sttp/SttpClientTests.scala | 3 +-- .../client/tests/ClientStreamingTests.scala | 16 +++++++--------- .../client/tests/ClientWebSocketTests.scala | 9 ++++++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build.sbt b/build.sbt index aebde4d79a..df8283c07e 100644 --- a/build.sbt +++ b/build.sbt @@ -265,7 +265,7 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core")) ) case _ => Seq( - "com.propensive" %% "magnolia" % "0.17.0", + "com.propensive" %%% "magnolia" % "0.17.0", "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, "com.47deg" %%% "scalacheck-toolbox-datetime" % "0.6.0" % Test ) 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/tests/src/main/scala/sttp/tapir/client/tests/ClientStreamingTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientStreamingTests.scala index 99868a5fcc..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 @@ -2,8 +2,7 @@ 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] @@ -13,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/ClientWebSocketTests.scala b/client/tests/src/main/scala/sttp/tapir/client/tests/ClientWebSocketTests.scala index d6075e7670..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 @@ -26,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") { @@ -39,7 +40,8 @@ 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") { @@ -54,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) From c54331c389fa63112d962b05f4d1f5a61c49d2a7 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 10:10:55 +0200 Subject: [PATCH 18/41] Fix finatra --- .../cats/FinatraCatsServerInterpreter.scala | 86 +++++++++++++------ .../cats/FinatraCatsServerOptions.scala | 65 ++++++++++++++ .../FinatraCatsTestServerInterpreter.scala | 20 +++-- .../finatra/cats/FinatraServerCatsTests.scala | 10 ++- .../server/finatra/FinatraServerOptions.scala | 2 +- .../FinatraTestServerInterpreter.scala | 2 +- 6 files changed, 146 insertions(+), 39 deletions(-) create mode 100644 server/finatra-server/finatra-server-cats/src/main/scala/sttp/tapir/server/finatra/cats/FinatraCatsServerOptions.scala 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 ad167f70ea..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 +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 098c9b9e74..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 @@ -41,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")) From b2ffc63c0708070895450e39ac65b31481eb8440 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 10:15:32 +0200 Subject: [PATCH 19/41] Fix examples --- .../ws/akkahttp/TapirSttpClientAkkaHttpWebSockets.scala | 4 ++-- .../sttp/ws/fs2/TapirSttpClientFs2WebSockets.scala | 2 +- .../MultipleEndpointsDocumentationHttp4sServer.scala | 3 +-- .../sttp/tapir/examples/StreamingHttp4sFs2Server.scala | 1 - .../sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala | 9 +++++---- .../sttp/tapir/examples/ZioExampleHttp4sServer.scala | 8 ++++---- .../tapir/examples/ZioPartialServerLogicHttp4s.scala | 2 +- 7 files changed, 14 insertions(+), 15 deletions(-) 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/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala index 6a2d7639fd..f58ffc8a17 100644 --- a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala @@ -1,12 +1,11 @@ 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 diff --git a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala index 00938711eb..456ee1c197 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala @@ -1,6 +1,5 @@ package sttp.tapir.examples -import java.nio.charset.StandardCharsets import cats.effect._ import cats.syntax.all._ import fs2._ diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index 14a0b0ebb2..73200395e1 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -15,7 +15,7 @@ import sttp.tapir.ztapir._ import zio.clock.Clock import zio.console.Console import zio.interop.catz._ -import zio.{&, App, ExitCode, Has, IO, RIO, UIO, URIO, ZEnv, ZIO, ZLayer} +import zio.{App, ExitCode, Has, IO, RIO, UIO, URIO, ZEnv, ZIO, ZLayer} object ZioEnvExampleHttp4sServer extends App { // Domain classes, services, layers @@ -48,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 = { @@ -64,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 & Blocking, *]](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 72edea9969..d05afe7200 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -14,7 +14,7 @@ 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} +import zio.{App, ExitCode, IO, RIO, UIO, URIO, ZEnv, ZIO} object ZioExampleHttp4sServer extends App { case class Pet(species: String, url: String) @@ -23,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")) @@ -41,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 // @@ -54,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 & Blocking, *]](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 8239a515e5..3af3733847 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -57,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 From 694706f7d1da1a9afa2f50a4c87921175de48912 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 10:35:03 +0200 Subject: [PATCH 20/41] Docs --- doc/endpoint/zio.md | 1 + doc/server/finatra.md | 5 ++++- doc/server/http4s.md | 22 ---------------------- doc/server/vertx.md | 43 +++++++++++++++++++++++++------------------ 4 files changed, 30 insertions(+), 41 deletions(-) diff --git a/doc/endpoint/zio.md b/doc/endpoint/zio.md index 4aa3798eb9..2ef058a243 100644 --- a/doc/endpoint/zio.md +++ b/doc/endpoint/zio.md @@ -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._ 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))) } ``` From 834d9495d709a5044370bf51377f51e4ccdb93f6 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 16:37:12 +0200 Subject: [PATCH 21/41] Try forking tests --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index df8283c07e..1bed21ef0f 100644 --- a/build.sbt +++ b/build.sbt @@ -52,6 +52,7 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( ideSkipProject := (scalaVersion.value == scala2_12) || (scalaVersion.value == scala3) || thisProjectRef.value.project.contains("JS"), // slow down for CI Test / parallelExecution := false, + Test / fork := true, // remove false alarms about unused implicit definitions in macros scalacOptions += "-Ywarn-macros:after", evictionErrorLevel := Level.Info From 32829c43967cf9d2c5b99a654d3d7aac13cc7659 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 17:12:36 +0200 Subject: [PATCH 22/41] Try forking tests, part 2 --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 1bed21ef0f..c49a916e5c 100644 --- a/build.sbt +++ b/build.sbt @@ -52,7 +52,6 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( ideSkipProject := (scalaVersion.value == scala2_12) || (scalaVersion.value == scala3) || thisProjectRef.value.project.contains("JS"), // slow down for CI Test / parallelExecution := false, - Test / fork := true, // remove false alarms about unused implicit definitions in macros scalacOptions += "-Ywarn-macros:after", evictionErrorLevel := Level.Info @@ -72,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 / fork := true // tests in js cannot be forked ) // run JS tests inside Gecko, due to jsdom not supporting fetch and to avoid having to install node From 42f468696e9ef4146ff3284b7d8b813f21f1c50d Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 18:13:36 +0200 Subject: [PATCH 23/41] Try to decrease available memory --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cd858e8c4..c47d6659bd 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 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 From 87de0e72296528e1f23a14316fc98299d44e8e18 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 20:56:31 +0200 Subject: [PATCH 24/41] Enable GC logging during builds --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c47d6659bd..5a0e9227e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: matrix: target-platform: [ "JVM", "JS" ] env: - JAVA_OPTS: -Xmx3500M + JAVA_OPTS: "-Xmx3500M -Xlog:gc -XX:+PrintGCDetails -Xlog:gc*::time" steps: - name: Checkout uses: actions/checkout@v2 From de96e7333fcf88e3af9067adf3a8ab1ea9cedbe2 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 20:58:05 +0200 Subject: [PATCH 25/41] Log sbt task timings --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a0e9227e3..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: "-Xmx3500M -Xlog:gc -XX:+PrintGCDetails -Xlog:gc*::time" + JAVA_OPTS: "-Xmx3500M -Xlog:gc -XX:+PrintGCDetails -Xlog:gc*::time -Dsbt.task.timings=true" steps: - name: Checkout uses: actions/checkout@v2 From b489de5e9d02a1125b92baabdc3939ae7f03990d Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 13 Jul 2021 20:58:35 +0200 Subject: [PATCH 26/41] Add test timings --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index c49a916e5c..b795f2d25e 100644 --- a/build.sbt +++ b/build.sbt @@ -52,6 +52,7 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( ideSkipProject := (scalaVersion.value == scala2_12) || (scalaVersion.value == scala3) || thisProjectRef.value.project.contains("JS"), // slow down for CI Test / parallelExecution := false, + Test / testOptions += Tests.Argument("-oD"), // remove false alarms about unused implicit definitions in macros scalacOptions += "-Ywarn-macros:after", evictionErrorLevel := Level.Info From 7339a82e30aaae42cac805efefb9cc5d12c2beb1 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 14:52:31 +0200 Subject: [PATCH 27/41] Properly pass test arguments --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index b795f2d25e..1297861704 100644 --- a/build.sbt +++ b/build.sbt @@ -52,7 +52,6 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( ideSkipProject := (scalaVersion.value == scala2_12) || (scalaVersion.value == scala3) || thisProjectRef.value.project.contains("JS"), // slow down for CI Test / parallelExecution := false, - Test / testOptions += Tests.Argument("-oD"), // remove false alarms about unused implicit definitions in macros scalacOptions += "-Ywarn-macros:after", evictionErrorLevel := Level.Info @@ -73,7 +72,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 / fork := true // tests in js cannot be forked + Test / fork := true, // tests in js cannot be forked + 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 From 674842ff94a62d31e857a0ad7bc0f8be83767d86 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 15:17:48 +0200 Subject: [PATCH 28/41] Imports --- .../main/scala/sttp/tapir/server/tests/CreateServerTest.scala | 1 - 1 file changed, 1 deletion(-) 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 36bbbcff20..275c36f361 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._ From 0d2b9fb920b6304abae0048286479650542ff146 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 15:18:02 +0200 Subject: [PATCH 29/41] More general server tests --- .../sttp/tapir/server/tests/ServerBasicTests.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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")) >> From de8db006ce8a3e4fccfe5b3cfd20bb39486ad0b8 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 15:19:19 +0200 Subject: [PATCH 30/41] Delay request handling in the interpreter when the effect is evaluated --- .../scala/sttp/tapir/server/interpreter/ServerInterpreter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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( From 2a3dac503b426b580c6aa1d4910d65d7c1edcac0 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 16:09:25 +0200 Subject: [PATCH 31/41] Async tests --- .../server/akkahttp/AkkaHttpServerTest.scala | 15 ++++++++++++--- .../tapir/server/tests/CreateServerTest.scala | 2 +- .../tapir/server/vertx/ZioVertxServerTest.scala | 4 ---- .../lambda/AwsLambdaCreateServerStubTest.scala | 6 +++--- tests/src/main/scala/sttp/tapir/tests/Test.scala | 7 +++++-- .../scalajvm/sttp/tapir/tests/TestSuite.scala | 4 ++-- 6 files changed, 23 insertions(+), 15 deletions(-) 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 7528078f9c..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 @@ -18,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 @@ -49,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 @@ -77,7 +86,7 @@ class AkkaHttpServerTest extends TestSuite with EitherValues { ) } } - .unsafeRunSync() + .unsafeToFuture() } ) 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 275c36f361..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 @@ -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/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxServerTest.scala b/server/vertx/src/test/scala/sttp/tapir/server/vertx/ZioVertxServerTest.scala index 67aab88ad7..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 @@ -13,14 +13,10 @@ import sttp.tapir.server.tests.{ ServerStreamingTests, backendResource } -import sttp.tapir.server.vertx.VertxZioServerInterpreter.RioFromVFuture 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(IO.delay(Vertx.vertx()))(vertx => IO.delay(vertx.close()).void) 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 f6fcc112d6..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 @@ -35,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)( @@ -44,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]])( @@ -57,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/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/scalajvm/sttp/tapir/tests/TestSuite.scala b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala index c3864190c5..a005d9bb08 100644 --- a/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala +++ b/tests/src/main/scalajvm/sttp/tapir/tests/TestSuite.scala @@ -5,9 +5,9 @@ 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 +import org.scalatest.funsuite.AsyncFunSuite -trait TestSuite extends AnyFunSuite with BeforeAndAfterAll { +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) From b2567a5566aa48fa355ba63cf32df720e486379d Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 16:09:58 +0200 Subject: [PATCH 32/41] Disable test forking --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index 1297861704..e674483728 100644 --- a/build.sbt +++ b/build.sbt @@ -72,7 +72,6 @@ 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 / fork := true, // tests in js cannot be forked Test / testOptions += Tests.Argument("-oD") // js has other options which conflict with timings ) From d89c770bdab65d85db937dc4e0a78673b0c8a4b6 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 16:45:11 +0200 Subject: [PATCH 33/41] More non-blocking tests --- .../http4s/Http4sServerSentEventsTest.scala | 12 ++++---- .../vertx/ZioVertxTestServerInterpreter.scala | 8 +----- .../server/vertx/streams/Fs2StreamTest.scala | 16 +++++------ .../server/vertx/streams/ZStreamTest.scala | 28 ++++++------------- 4 files changed, 24 insertions(+), 40 deletions(-) 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 3c6f25206c..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 @@ -2,14 +2,14 @@ package sttp.tapir.server.http4s import cats.effect.IO import cats.effect.unsafe.implicits.global -import org.scalatest.funsuite.AnyFunSuite +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))) @@ -25,7 +25,7 @@ class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { | |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList }) - .unsafeRunSync() + .unsafeToFuture() } test("serialiseSSEToBytes should omit fields that are not set") { @@ -40,7 +40,7 @@ class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { | |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList }) - .unsafeRunSync() + .unsafeToFuture() } test("serialiseSSEToBytes should successfully serialise multiline data event") { @@ -65,7 +65,7 @@ class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { | |""".stripMargin.getBytes(Charset.forName("UTF-8")).toList }) - .unsafeRunSync() + .unsafeToFuture() } test("parseBytesToSSE should successfully parse SSE bytes to SSE structure") { @@ -99,7 +99,7 @@ class Http4sServerSentEventsTest extends AnyFunSuite with Matchers { ) ) ) - .unsafeRunSync() + .unsafeToFuture() } } 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 478258baab..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,7 +1,6 @@ package sttp.tapir.server.vertx import cats.data.NonEmptyList -import cats.effect.std.Dispatcher import cats.effect.{IO, Resource} import io.vertx.core.Vertx import io.vertx.core.http.HttpServerOptions @@ -12,7 +11,6 @@ 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.{Runtime, Task} @@ -21,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,9 +41,7 @@ class ZioVertxTestServerInterpreter(vertx: Vertx) extends TestServerInterpreter[ val router = Router.router(vertx) val server = vertx.createHttpServer(new HttpServerOptions().setPort(0)).requestHandler(router) routes.toList.foreach(_.apply(router)) - Dispatcher[IO].map { dispatcher => - dispatcher.unsafeRunSync(VertxTestServerInterpreter.vertxFutureToIo(server.listen(0)).map(_.actualPort())) - } + Resource.eval(VertxTestServerInterpreter.vertxFutureToIo(server.listen(0)).map(_.actualPort())) } } 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 b703629984..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 @@ -3,13 +3,13 @@ package sttp.tapir.server.vertx.streams import _root_.fs2.{Chunk, Stream} import cats.effect.std.Dispatcher import cats.effect.unsafe.implicits.global -import cats.effect.{IO, Outcome, Ref, Temporal, Deferred} +import cats.effect.{Deferred, IO, Outcome, Ref, Temporal} import cats.syntax.flatMap._ import cats.syntax.option._ 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 @@ -17,7 +17,7 @@ import sttp.tapir.server.vertx.VertxCatsServerOptions import java.nio.ByteBuffer import scala.concurrent.duration._ -class Fs2StreamTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { +class Fs2StreamTest extends AsyncFlatSpec with Matchers with BeforeAndAfterAll { private val (dispatcher, shutdownDispatcher) = Dispatcher[IO].allocated.unsafeRunSync() @@ -103,7 +103,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { _ = 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 { @@ -142,7 +142,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { 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 { @@ -169,7 +169,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { 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 { @@ -199,7 +199,7 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { 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 { @@ -227,6 +227,6 @@ class Fs2StreamTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll { result <- resultFiber.join.attempt } yield { result shouldBe Right(Outcome.errored(ex)) - }).unsafeRunSync() + }).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 } } From 2e870ef3d18bc475f0b984d006ad7fad6b5ba356 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 18:10:09 +0200 Subject: [PATCH 34/41] Use different http client for tests --- build.sbt | 2 +- .../src/main/scala/sttp/tapir/server/tests/package.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index e674483728..67fc0b84e7 100644 --- a/build.sbt +++ b/build.sbt @@ -796,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" % Versions.sttp + "com.softwaremill.sttp.client3" %% "httpclient-backend-fs2" % Versions.sttp ) ) .dependsOn(tests) 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 ac7baedcd6..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 @@ -4,8 +4,8 @@ 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 { - val backendResource: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = AsyncHttpClientFs2Backend.resource() + val backendResource: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = HttpClientFs2Backend.resource() } From c333623254ef8aeeff4af5b5cb48b35ccc58bb98 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 18:31:18 +0200 Subject: [PATCH 35/41] Make implementations private & lazy --- .../scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 0842b8ec236ef34826c94637d61c41a9c607ee22 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 18:41:30 +0200 Subject: [PATCH 36/41] Create netty dependencies once in zio-http to speed up tests --- .../server/ziohttp/ZioHttpServerTest.scala | 39 +++++++++++-------- .../ZioHttpTestServerInterpreter.scala | 7 ++-- 2 files changed, 26 insertions(+), 20 deletions(-) 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..b539e8b3fa 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,33 @@ 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] From de5cbcbe431ce02a795176eb3266bb242c757094 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 19:01:21 +0200 Subject: [PATCH 37/41] First add routes, then create vertx server --- .../tapir/server/vertx/CatsVertxTestServerInterpreter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dae9eaf666..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 @@ -45,9 +45,9 @@ class CatsVertxTestServerInterpreter(vertx: Vertx, dispatcher: Dispatcher[IO]) 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()) } } From c6148748509288926cecb5027b637f77ff722f38 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 19:03:27 +0200 Subject: [PATCH 38/41] Fix compile for scala3 --- .../server/ziohttp/ZioHttpServerTest.scala | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) 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 b539e8b3fa..4127568a5b 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 @@ -23,23 +23,24 @@ class ZioHttpServerTest extends TestSuite { 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) + (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() } } } From 5018d30c780379706588fd358d385aea01ebecfd Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 19:03:52 +0200 Subject: [PATCH 39/41] Fix test --- .../serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 81f8b182df..0473d3106e 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 @@ -62,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 = "")( From f3a6ea4b5b4de53624078087f34edca988a8c1a3 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 19:05:55 +0200 Subject: [PATCH 40/41] Really fix compile for scala3 this time --- .../scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4127568a5b..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 @@ -24,7 +24,7 @@ class ZioHttpServerTest extends TestSuite { 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) => + (nettyDeps: EventLoopGroup with ServerChannelFactory) => val interpreter = new ZioHttpTestServerInterpreter(nettyDeps) val createServerTest = new DefaultCreateServerTest(backend, interpreter) From 425dbb089206ba7e09d39170d36328d5879fb803 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 14 Jul 2021 19:54:48 +0200 Subject: [PATCH 41/41] Fix aws test --- .../serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0473d3106e..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 @@ -62,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.toLowerCase -> 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 = "")(