diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d68e4c115..f29c56ed9b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ jobs: - checkout - restore_cache: key: sbtcache - - run: sbt ++2.12.18 core/test http4s/test akkaHttp/test pekkoHttp/test play/test zioHttp/test quickAdapter/test examples/compile catsInterop/test tools/test codegenSbt/test clientJVM/test monixInterop/compile tapirInterop/test federation/test reporting/test tracing/test + - run: sbt ++2.12.18 core/test http4s/test akkaHttp/test pekkoHttp/test zioHttp/test quickAdapter/test catsInterop/test tools/test codegenSbt/test clientJVM/test monixInterop/compile tapirInterop/test federation/test reporting/test tracing/test - save_cache: key: sbtcache paths: @@ -98,7 +98,7 @@ jobs: - checkout - restore_cache: key: sbtcache - - run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test federation/test tools/test reporting/test tracing/test + - run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test play/test federation/test tools/test reporting/test tracing/test - save_cache: key: sbtcache paths: @@ -113,7 +113,7 @@ jobs: - checkout - restore_cache: key: sbtcache - - run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test federation/test tools/test reporting/test tracing/test + - run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test play/test federation/test tools/test reporting/test tracing/test - save_cache: key: sbtcache paths: @@ -177,8 +177,8 @@ jobs: - checkout - restore_cache: key: sbtcache - - run: sbt ++2.13.12 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues - - run: sbt ++3.3.1 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues + - run: sbt ++2.13.12 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues + - run: sbt ++3.3.1 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues - save_cache: key: sbtcache paths: diff --git a/adapters/play/src/main/scala/caliban/PlayAdapter.scala b/adapters/play/src/main/scala/caliban/PlayAdapter.scala index 1c39dd0d59..4d929b0dd5 100644 --- a/adapters/play/src/main/scala/caliban/PlayAdapter.scala +++ b/adapters/play/src/main/scala/caliban/PlayAdapter.scala @@ -1,16 +1,16 @@ package caliban -import akka.stream.scaladsl.{ Flow, Sink, Source } -import akka.stream.{ Materializer, OverflowStrategy } -import akka.util.ByteString import caliban.PlayAdapter.convertHttpStreamingEndpoint import caliban.interop.tapir.TapirAdapter._ import caliban.interop.tapir.{ HttpInterpreter, HttpUploadInterpreter, StreamConstructor, WebSocketInterpreter } +import org.apache.pekko.stream.scaladsl.{ Flow, Sink, Source } +import org.apache.pekko.stream.{ Materializer, OverflowStrategy } +import org.apache.pekko.util.ByteString import play.api.routing.Router.Routes -import sttp.capabilities.{ akka, WebSockets } -import sttp.capabilities.akka.AkkaStreams -import sttp.capabilities.akka.AkkaStreams.Pipe -import sttp.model.{ MediaType, StatusCode } +import sttp.capabilities.WebSockets +import sttp.capabilities.pekko.PekkoStreams +import sttp.capabilities.pekko.PekkoStreams.Pipe +import sttp.model.StatusCode import sttp.tapir.Codec.JsonCodec import sttp.tapir.PublicEndpoint import sttp.tapir.model.ServerRequest @@ -30,7 +30,7 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) { )(implicit runtime: Runtime[R], materializer: Materializer): Routes = playInterpreter.toRoutes( interpreter - .serverEndpoints[R, AkkaStreams](AkkaStreams) + .serverEndpoints[R, PekkoStreams](PekkoStreams) .map(convertHttpStreamingEndpoint[R, (GraphQLRequest, ServerRequest)]) ) @@ -40,7 +40,7 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) { requestCodec: JsonCodec[GraphQLRequest], mapCodec: JsonCodec[Map[String, Seq[String]]] ): Routes = - playInterpreter.toRoutes(convertHttpStreamingEndpoint(interpreter.serverEndpoint[R, AkkaStreams](AkkaStreams))) + playInterpreter.toRoutes(convertHttpStreamingEndpoint(interpreter.serverEndpoint[R, PekkoStreams](PekkoStreams))) def makeWebSocketService[R, E]( interpreter: WebSocketInterpreter[R, E] @@ -66,7 +66,7 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) { private implicit def streamConstructor(implicit runtime: Runtime[Any], mat: Materializer - ): StreamConstructor[AkkaStreams.BinaryStream] = + ): StreamConstructor[PekkoStreams.BinaryStream] = new StreamConstructor[Source[ByteString, Any]] { override def apply(stream: ZStream[Any, Throwable, Byte]): Source[ByteString, Any] = Unsafe.unsafe(implicit u => @@ -111,18 +111,18 @@ object PlayAdapter extends PlayAdapter(None) { Unit, Input, TapirResponse, - CalibanResponse[AkkaStreams.BinaryStream], - AkkaStreams, + CalibanResponse[PekkoStreams.BinaryStream], + PekkoStreams, RIO[R, *] ] - )(implicit runtime: Runtime[R], mat: Materializer): ServerEndpoint[AkkaStreams, Future] = + )(implicit runtime: Runtime[R], mat: Materializer): ServerEndpoint[PekkoStreams, Future] = ServerEndpoint[ Unit, Unit, Input, TapirResponse, - CalibanResponse[akka.AkkaStreams.BinaryStream], - AkkaStreams, + CalibanResponse[PekkoStreams.BinaryStream], + PekkoStreams, Future ]( endpoint.endpoint, @@ -151,14 +151,14 @@ object PlayAdapter extends PlayAdapter(None) { )(implicit runtime: Runtime[R], materializer: Materializer - ): ServerEndpoint[AkkaStreams with WebSockets, Future] = + ): ServerEndpoint[PekkoStreams with WebSockets, Future] = ServerEndpoint[ Unit, Unit, (ServerRequest, String), StatusCode, (String, AkkaPipe), - AkkaStreams with WebSockets, + PekkoStreams with WebSockets, Future ]( endpoint.endpoint diff --git a/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala b/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala index 4d3c392c75..a809445b02 100644 --- a/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala +++ b/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala @@ -1,7 +1,5 @@ package caliban -import akka.actor.ActorSystem -import akka.stream.Materializer import caliban.interop.tapir.TestData.sampleCharacters import caliban.interop.tapir.{ FakeAuthorizationInterceptor, @@ -13,10 +11,12 @@ import caliban.interop.tapir.{ WebSocketInterpreter } import caliban.uploads.Uploads +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.stream.Materializer import play.api.Mode import play.api.routing._ import play.api.routing.sird._ -import play.core.server.{ AkkaHttpServer, ServerConfig } +import play.core.server.{ PekkoHttpServer, ServerConfig } import sttp.client3.UriContext import zio._ import zio.test.{ Live, ZIOSpecDefault } @@ -24,7 +24,7 @@ import zio.test.{ Live, ZIOSpecDefault } import scala.language.postfixOps object PlayAdapterSpec extends ZIOSpecDefault { - import sttp.tapir.json.circe._ + import sttp.tapir.json.play._ private val envLayer = TestService.make(sampleCharacters) ++ Uploads.empty @@ -51,7 +51,7 @@ object PlayAdapterSpec extends ZIOSpecDefault { } _ <- ZIO .attempt( - AkkaHttpServer.fromRouterWithComponents( + PekkoHttpServer.fromRouterWithComponents( ServerConfig( mode = Mode.Dev, port = Some(8088), diff --git a/adapters/quick/src/main/scala/caliban/QuickAdapter.scala b/adapters/quick/src/main/scala/caliban/QuickAdapter.scala index ac57ce66a8..3f2778f078 100644 --- a/adapters/quick/src/main/scala/caliban/QuickAdapter.scala +++ b/adapters/quick/src/main/scala/caliban/QuickAdapter.scala @@ -7,29 +7,28 @@ import zio.http._ final class QuickAdapter[-R, E] private (requestHandler: QuickRequestHandler[R, E]) { /** - * Converts this adapter to a [[zio.http.RequestHandler]] which can be used to create zio-http [[zio.http.App]]s + * Converts this adapter to a [[zio.http.RequestHandler]] which can be used to create zio-http `HttpApp` */ val handler: RequestHandler[R, Nothing] = Handler.fromFunctionZIO[Request](requestHandler.handleRequest) /** - * Converts this adapter to an [[zio.http.App]] serving the GraphQL API at the specified path. + * Converts this adapter to an `HttpApp` serving the GraphQL API at the specified path. * * @param apiPath The path where the GraphQL API will be served. * @param graphiqlPath The path where the GraphiQL UI will be served. If None, GraphiQL will not be served. */ - def toApp(apiPath: Path, graphiqlPath: Option[Path] = None): App[R] = { - val apiApp = Http.collectHandler[Request] { - case (Method.GET | Method.POST) -> path if path == apiPath => handler - } - graphiqlPath match { - case None => apiApp - case Some(uiPath) => - val uiHandler = GraphiQLHandler.handler(apiPath.toString(), uiPath.toString) - apiApp ++ Http.collectHandler[Request] { - case Method.GET -> path if path == uiPath => uiHandler - } + def toApp(apiPath: Path, graphiqlPath: Option[Path] = None): HttpApp[R] = { + val apiRoutes = List( + RoutePattern(Method.POST, apiPath) -> handler, + RoutePattern(Method.GET, apiPath) -> handler + ) + val graphiqlRoute = graphiqlPath.fold(List.empty[Route[R, Nothing]]) { uiPath => + val uiHandler = GraphiQLHandler.handler(apiPath.toString(), uiPath.toString) + List(RoutePattern(Method.GET, uiPath) -> uiHandler) } + + Routes.fromIterable(apiRoutes ::: graphiqlRoute).toHttpApp } /** diff --git a/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala b/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala index c4d0ea20ba..175d9dfdcc 100644 --- a/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala +++ b/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala @@ -39,12 +39,7 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R val queryParams = httpReq.url.queryParams def extractFields(key: String): Either[Response, Option[Map[String, InputValue]]] = - try Right( - queryParams - .get(key) - .flatMap(_.headOption) - .map(readFromString[InputValue.ObjectValue](_).fields) - ) + try Right(queryParams.get(key).map(readFromString[InputValue.ObjectValue](_).fields)) catch { case NonFatal(_) => Left(badRequest(s"Invalid $key query param")) } def fromQueryParams: Either[Response, GraphQLRequest] = @@ -52,30 +47,34 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R vars <- extractFields("variables") exts <- extractFields("extensions") } yield GraphQLRequest( - query = queryParams.get("query").flatMap(_.headOption), - operationName = queryParams.get("operationName").flatMap(_.headOption), + query = queryParams.get("query"), + operationName = queryParams.get("operationName"), variables = vars, extensions = exts ) - def isApplicationGql = - httpReq.headers.get("content-type").fold(false)(_.startsWith("application/graphql")) + def isGqlJson = + httpReq.header(Header.ContentType).exists { h => + h.mediaType.subType.equalsIgnoreCase("graphql") && + h.mediaType.mainType.equalsIgnoreCase("application") + } + + def decodeApplicationGql() = + httpReq.body.asString.mapBoth(_ => BodyDecodeErrorResponse, b => GraphQLRequest(Some(b))) + + def decodeJson() = + httpReq.body.asArray + .flatMap(arr => ZIO.attempt(readFromArray[GraphQLRequest](arr))) + .orElseFail(BodyDecodeErrorResponse) val resp = { if (httpReq.method == Method.GET || queryParams.get("query").isDefined) ZIO.fromEither(fromQueryParams) else { - val req = - if (isApplicationGql) - httpReq.body.asString.mapBoth(_ => BodyDecodeErrorResponse, b => GraphQLRequest(Some(b))) - else - httpReq.body.asArray - .flatMap(arr => ZIO.attempt(readFromArray[GraphQLRequest](arr))) - .orElseFail(BodyDecodeErrorResponse) - + val req = if (isGqlJson) decodeApplicationGql() else decodeJson() httpReq.headers .get(GraphQLRequest.`apollo-federation-include-trace`) - .collect { case GraphQLRequest.ftv1 => req.map(_.withFederatedTracing) } + .collect { case s if s.equalsIgnoreCase(GraphQLRequest.ftv1) => req.map(_.withFederatedTracing) } .getOrElse(req) } } @@ -95,15 +94,16 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R private def transformResponse(httpReq: Request, resp: GraphQLResponse[E])(implicit trace: Trace): Response = { def acceptsGqlJson: Boolean = - httpReq.header(Header.Accept).fold(false) { h => - h.mimeTypes.exists(mt => - mt.mediaType.subType == "graphql-response+json" && mt.mediaType.mainType == "application" - ) + httpReq.header(Header.Accept).exists { h => + h.mimeTypes.exists { mt => + mt.mediaType.subType.equalsIgnoreCase("graphql-response+json") && + mt.mediaType.mainType.equalsIgnoreCase("application") + } } val cacheDirective = HttpUtils.computeCacheDirective(resp.extensions) - (resp match { + resp match { case resp @ GraphQLResponse(StreamValue(stream), _, _, _) => Response( Status.Ok, @@ -125,7 +125,7 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R headers = responseHeaders(ContentTypeJson, cacheDirective), body = encodeSingleResponse(resp, keepDataOnErrors = true, hasCacheDirective = cacheDirective.isDefined) ) - }).withServerTime + } } private def encodeSingleResponse( diff --git a/adapters/quick/src/main/scala/caliban/quick/package.scala b/adapters/quick/src/main/scala/caliban/quick/package.scala index b53d5bbc07..b88589713b 100644 --- a/adapters/quick/src/main/scala/caliban/quick/package.scala +++ b/adapters/quick/src/main/scala/caliban/quick/package.scala @@ -1,8 +1,8 @@ package caliban import caliban.Configurator.ExecutionConfiguration +import zio.http.{ HttpApp, Path, RequestHandler, Response } import zio.{ RIO, Trace, ZIO } -import zio.http.{ RequestHandler, Response } package object quick { @@ -21,6 +21,17 @@ package object quick { ): RIO[R, Nothing] = gql.interpreter.flatMap(QuickAdapter(_).runServer(port, apiPath, graphiqlPath)) + /** + * Creates zio-http `HttpApp` from the GraphQL API + * + * @param apiPath The route to serve the API on, e.g., `/api/graphql` + * @param graphiqlPath Optionally define a route to serve the GraphiQL UI on, e.g., `/graphiql` + */ + def toApp(apiPath: String, graphiqlPath: Option[String] = None)(implicit + trace: Trace + ): RIO[R, HttpApp[R]] = + gql.interpreter.map(QuickAdapter(_).toApp(Path.decode(apiPath), graphiqlPath.map(Path.decode))) + /** * Creates a zio-http handler for the GraphQL API * diff --git a/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala b/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala index c61ffaef4a..aa35afc806 100644 --- a/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala +++ b/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala @@ -2,7 +2,6 @@ package caliban import caliban.interop.tapir.TestData.sampleCharacters import caliban.interop.tapir.{ TapirAdapterSpec, TestApi, TestService } -import caliban.quick._ import caliban.uploads.Uploads import sttp.client3.UriContext import zio._ @@ -12,11 +11,12 @@ import zio.test.{ Live, ZIOSpecDefault } import scala.language.postfixOps object QuickAdapterSpec extends ZIOSpecDefault { + import caliban.quick._ import sttp.tapir.json.jsoniter._ private val envLayer = TestService.make(sampleCharacters) ++ Uploads.empty - private val auth = HttpAppMiddleware.intercept { case (req, resp) => + private val auth = Middleware.intercept { case (req, resp) => if (req.headers.get("X-Invalid").nonEmpty) Response(Status.Unauthorized, body = Body.fromString("You are unauthorized!")) else resp @@ -24,11 +24,8 @@ object QuickAdapterSpec extends ZIOSpecDefault { private val apiLayer = envLayer >>> ZLayer.fromZIO { for { - handler <- TestApi.api.handler.map(_ @@ auth) - _ <- - Server - .serve(Http.collectHandler { case _ -> Root / "api" / "graphql" => handler }) - .forkScoped + app <- TestApi.api.toApp("/api/graphql").map(_ @@ auth) + _ <- Server.serve(app).forkScoped _ <- Live.live(Clock.sleep(3 seconds)) service <- ZIO.service[TestService] } yield service diff --git a/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala b/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala index ad00b514a3..29cacb7a36 100644 --- a/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala +++ b/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala @@ -3,27 +3,38 @@ package caliban import caliban.interop.tapir.ws.Protocol import caliban.interop.tapir.{ HttpInterpreter, WebSocketInterpreter } import sttp.capabilities.zio.ZioStreams +import sttp.model.HeaderNames import sttp.tapir.server.ziohttp.{ ZioHttpInterpreter, ZioHttpServerOptions } import zio.http._ object ZHttpAdapter { + @deprecated("Defining subprotocols in the server config is no longer required") val defaultWebSocketConfig: WebSocketConfig = { val subProtocols = List(Protocol.Legacy.name, Protocol.GraphQLWS.name).mkString(",") - WebSocketConfig.default.withSubProtocol(Some(subProtocols)) + WebSocketConfig.default.subProtocol(Some(subProtocols)) } def makeHttpService[R, E](interpreter: HttpInterpreter[R, E])(implicit serverOptions: ZioHttpServerOptions[R] = ZioHttpServerOptions.default[R] - ): App[R] = + ): RequestHandler[R, Nothing] = ZioHttpInterpreter(serverOptions) .toHttp(interpreter.serverEndpoints[R, ZioStreams](ZioStreams)) - .withDefaultErrorResponse + .toHandler def makeWebSocketService[R, E](interpreter: WebSocketInterpreter[R, E])(implicit serverOptions: ZioHttpServerOptions[R] = ZioHttpServerOptions.default[R] - ): App[R] = - ZioHttpInterpreter(serverOptions) + ): RequestHandler[R, Nothing] = + ZioHttpInterpreter(patchWsServerOptions(serverOptions)) .toHttp(interpreter.serverEndpoint[R]) - .withDefaultErrorResponse + .toHandler + + private def patchWsServerOptions[R](serverOptions: ZioHttpServerOptions[R]) = + serverOptions.withCustomWebSocketConfig { req => + val protocol = req.header(HeaderNames.SecWebSocketProtocol).fold(Protocol.Legacy: Protocol)(Protocol.fromName) + serverOptions.customWebSocketConfig(req) match { + case Some(existing) => existing.subProtocol(Some(protocol.name)) + case _ => WebSocketConfig.default.subProtocol(Some(protocol.name)) + } + } } diff --git a/adapters/zio-http/src/test/scala/caliban/ZHttpAdapterSpec.scala b/adapters/zio-http/src/test/scala/caliban/ZHttpAdapterSpec.scala index c91a660f44..1350aec791 100644 --- a/adapters/zio-http/src/test/scala/caliban/ZHttpAdapterSpec.scala +++ b/adapters/zio-http/src/test/scala/caliban/ZHttpAdapterSpec.scala @@ -28,15 +28,15 @@ object ZHttpAdapterSpec extends ZIOSpecDefault { _ <- Server .serve( - Http - .collectHttp[Request] { - case _ -> Root / "api" / "graphql" => - ZHttpAdapter.makeHttpService( + Routes( + Method.ANY / "api" / "graphql" -> + ZHttpAdapter + .makeHttpService( HttpInterpreter(interpreter).intercept(FakeAuthorizationInterceptor.bearer[TestService & Uploads]) - ) - case _ -> Root / "ws" / "graphql" => - ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)) - } + ), + Method.ANY / "ws" / "graphql" -> + ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)) + ).toHttpApp ) .forkScoped _ <- Live.live(Clock.sleep(3 seconds)) @@ -53,11 +53,7 @@ object ZHttpAdapterSpec extends ZIOSpecDefault { suite.provideShared( apiLayer, Scope.default, - Server.defaultWith( - _.withWebSocketConfig(ZHttpAdapter.defaultWebSocketConfig) - .port(8089) - .responseCompression() - ) + Server.defaultWith(_.port(8089).responseCompression()) ) } } diff --git a/build.sbt b/build.sbt index 36a027692f..1dd51a5da7 100644 --- a/build.sbt +++ b/build.sbt @@ -19,11 +19,11 @@ val laminextVersion = "0.16.2" val magnoliaScala2Version = "1.1.6" val magnoliaScala3Version = "1.3.4" val pekkoHttpVersion = "1.0.0" -val playVersion = "2.8.21" -val playJsonVersion = "2.10.3" +val playVersion = "3.0.0" +val playJsonVersion = "3.0.1" val scalafmtVersion = "3.7.17" val sttpVersion = "3.9.1" -val tapirVersion = "1.8.2" +val tapirVersion = "1.9.4" val zioVersion = "2.0.19" val zioInteropCats2Version = "22.0.0.0" val zioInteropCats3Version = "23.0.0.8" @@ -31,7 +31,7 @@ val zioInteropReactiveVersion = "2.0.2" val zioConfigVersion = "3.0.7" val zqueryVersion = "0.5.1" val zioJsonVersion = "0.6.2" -val zioHttpVersion = "3.0.0-RC2" +val zioHttpVersion = "3.0.0-RC4" val zioOpenTelemetryVersion = "3.0.0-RC15" val zioPreludeVersion = "1.0.0-RC21" @@ -153,7 +153,7 @@ lazy val core = project "io.circe" %% "circe-parser" % circeVersion % Test, "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterVersion % Optional, "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % Provided, - "com.typesafe.play" %% "play-json" % playJsonVersion % Optional + "org.playframework" %% "play-json" % playJsonVersion % Optional ) ) .dependsOn(macros) @@ -410,20 +410,22 @@ lazy val play = project .settings(commonSettings) .settings(enableMimaSettingsJVM) .settings( - skip := (scalaVersion.value == scala3), - ideSkipProject := (scalaVersion.value == scala3), - crossScalaVersions -= scala3, + skip := (scalaVersion.value == scala212), + ideSkipProject := (scalaVersion.value == scala212), + crossScalaVersions -= scala212, testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")), + libraryDependencies ++= { + if (scalaVersion.value == scala3) Seq() + else Seq(compilerPlugin(("org.typelevel" %% "kind-projector" % "0.13.2").cross(CrossVersion.full))) + }, libraryDependencies ++= Seq( - "com.typesafe.play" %% "play" % playVersion, - "com.softwaremill.sttp.tapir" %% "tapir-play-server" % tapirVersion, - "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion % Test, - "dev.zio" %% "zio-test" % zioVersion % Test, - "dev.zio" %% "zio-test-sbt" % zioVersion % Test, - "com.typesafe.play" %% "play-akka-http-server" % playVersion % Test, - "io.circe" %% "circe-generic" % circeVersion % Test, - "com.softwaremill.sttp.client3" %% "circe" % sttpVersion % Test, - compilerPlugin(("org.typelevel" %% "kind-projector" % "0.13.2").cross(CrossVersion.full)) + "org.playframework" %% "play" % playVersion, + "com.softwaremill.sttp.tapir" %% "tapir-play-server" % tapirVersion, + "com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapirVersion % Test, + "dev.zio" %% "zio-test" % zioVersion % Test, + "dev.zio" %% "zio-test-sbt" % zioVersion % Test, + "org.playframework" %% "play-pekko-http-server" % playVersion % Test, + "org.playframework" %% "play-json" % playJsonVersion % Test ) ) .dependsOn(core, tapirInterop % "compile->compile;test->test") @@ -503,24 +505,24 @@ lazy val examples = project run / connectInput := true ) .settings( - skip := (scalaVersion.value == scala3), - ideSkipProject := (scalaVersion.value == scala3), - crossScalaVersions -= scala3, + skip := (scalaVersion.value != scala213), + ideSkipProject := (scalaVersion.value != scala213), + crossScalaVersions := Seq(scala213), libraryDependencySchemes += "org.scala-lang.modules" %% "scala-java8-compat" % "always", libraryDependencies ++= Seq( - "org.typelevel" %% "cats-mtl" % catsMtlVersion, - "org.http4s" %% "http4s-ember-server" % http4sVersion, - "org.http4s" %% "http4s-dsl" % http4sVersion, - "com.softwaremill.sttp.client3" %% "zio" % sttpVersion, - "io.circe" %% "circe-generic" % circeVersion, - "dev.zio" %% "zio-http" % zioHttpVersion, - "com.typesafe.play" %% "play-akka-http-server" % playVersion, - "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, - "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion, - "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion, - "com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapirVersion, - "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion, - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % Provided + "org.typelevel" %% "cats-mtl" % catsMtlVersion, + "org.http4s" %% "http4s-ember-server" % http4sVersion, + "org.http4s" %% "http4s-dsl" % http4sVersion, + "com.softwaremill.sttp.client3" %% "zio" % sttpVersion, + "io.circe" %% "circe-generic" % circeVersion, + "dev.zio" %% "zio-http" % zioHttpVersion, + "org.playframework" %% "play-pekko-http-server" % playVersion, + "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, + "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion, + "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion, + "com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapirVersion, + "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion, + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % Provided ) ) .dependsOn( diff --git a/examples/README.md b/examples/README.md index ed25b1056c..31ca23bbbd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,6 +7,7 @@ val calibanVersion = "2.4.3" libraryDependencies ++= Seq( "com.github.ghostdogpr" %% "caliban" % calibanVersion, + "com.github.ghostdogpr" %% "caliban-quick" % calibanVersion, "com.github.ghostdogpr" %% "caliban-http4s" % calibanVersion, "com.github.ghostdogpr" %% "caliban-play" % calibanVersion, "com.github.ghostdogpr" %% "caliban-akka-http" % calibanVersion, @@ -21,9 +22,9 @@ libraryDependencies ++= Seq( "org.http4s" %% "http4s-dsl" % "0.23.23", "com.softwaremill.sttp.client3" %% "zio" % "3.9.0", "io.circe" %% "circe-generic" % "0.14.6", - "org.playframework" %% "play-akka-http-server" % "2.8.14", + "org.playframework" %% "play-pekko-http-server" % "3.0.0", "com.typesafe.akka" %% "akka-actor-typed" % "2.6.18", - "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.8.2" + "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.9.4" ) ``` diff --git a/examples/src/main/scala/example/federation/v2/FederatedApp.scala b/examples/src/main/scala/example/federation/v2/FederatedApp.scala index a827dd548e..73cdccb662 100644 --- a/examples/src/main/scala/example/federation/v2/FederatedApp.scala +++ b/examples/src/main/scala/example/federation/v2/FederatedApp.scala @@ -1,42 +1,17 @@ package example.federation.v2 -import caliban.ZHttpAdapter -import caliban.interop.tapir.HttpInterpreter +import caliban.quick._ import example.federation.v2.FederationData.characters.sampleCharacters import example.federation.v2.FederationData.episodes.sampleEpisodes -import zio.http._ import zio._ object FederatedApp extends ZIOAppDefault { - import sttp.tapir.json.circe._ - val characterServer = for { - interpreter <- FederatedApi.Characters.api.interpreter - config = ZLayer.succeed(Server.Config.default.port(8088)) - _ <- - Server - .serve( - Http - .collectHttp[Request] { case _ -> Root / "api" / "graphql" => - ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)) - } - ) - .provideSome[CharacterService](Server.live, config) - } yield () + val characterServer = + FederatedApi.Characters.api.runServer(8088, "/api/graphql") - val episodeServer = for { - interpreter <- FederatedApi.Episodes.api.interpreter - config = ZLayer.succeed(Server.Config.default.port(8089)) - _ <- - Server - .serve( - Http - .collectHttp[Request] { case _ -> Root / "api" / "graphql" => - ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)) - } - ) - .provideSome[EpisodeService](Server.live, config) - } yield () + val episodeServer = + FederatedApi.Episodes.api.runServer(8089, "/api/graphql") override def run = (characterServer race episodeServer) diff --git a/examples/src/main/scala/example/play/AuthExampleApp.scala b/examples/src/main/scala/example/play/AuthExampleApp.scala index f26ab747c9..09738da233 100644 --- a/examples/src/main/scala/example/play/AuthExampleApp.scala +++ b/examples/src/main/scala/example/play/AuthExampleApp.scala @@ -1,6 +1,6 @@ package example.play -import akka.actor.ActorSystem +import org.apache.pekko.actor.ActorSystem import caliban._ import caliban.interop.tapir.{ HttpInterpreter, WebSocketInterpreter } import caliban.interop.tapir.TapirAdapter.TapirResponse @@ -8,7 +8,7 @@ import caliban.schema.GenericSchema import play.api.Mode import play.api.routing._ import play.api.routing.sird._ -import play.core.server.{ AkkaHttpServer, ServerConfig } +import play.core.server.{ PekkoHttpServer, ServerConfig } import sttp.model.StatusCode import sttp.tapir.json.play._ import sttp.tapir.model.ServerRequest @@ -53,7 +53,7 @@ object AuthExampleApp extends App { val interpreter = Unsafe.unsafe(implicit u => runtime.unsafe.run(api.interpreter).getOrThrow()) - val server = AkkaHttpServer.fromRouterWithComponents( + val server = PekkoHttpServer.fromRouterWithComponents( ServerConfig( mode = Mode.Dev, port = Some(8088), diff --git a/examples/src/main/scala/example/play/ExampleApp.scala b/examples/src/main/scala/example/play/ExampleApp.scala index 6d2e3a747b..e7c4a6a501 100644 --- a/examples/src/main/scala/example/play/ExampleApp.scala +++ b/examples/src/main/scala/example/play/ExampleApp.scala @@ -1,7 +1,7 @@ package example.play -import akka.actor.ActorSystem -import akka.stream.Materializer +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.stream.Materializer import caliban.interop.tapir.{ HttpInterpreter, WebSocketInterpreter } import caliban.{ GraphQL, PlayAdapter } import example.ExampleData.sampleCharacters @@ -9,7 +9,7 @@ import example.{ ExampleApi, ExampleService } import play.api.Mode import play.api.routing._ import play.api.routing.sird._ -import play.core.server.{ AkkaHttpServer, ServerConfig } +import play.core.server.{ PekkoHttpServer, ServerConfig } import zio.{ Runtime, Scope, ZIO, ZIOAppDefault } object ExampleApp extends ZIOAppDefault { @@ -23,7 +23,7 @@ object ExampleApp extends ZIOAppDefault { interpreter <- ZIO.serviceWithZIO[GraphQL[Any]](_.interpreter) _ <- ZIO.acquireRelease( ZIO.attempt( - AkkaHttpServer.fromRouterWithComponents( + PekkoHttpServer.fromRouterWithComponents( ServerConfig( mode = Mode.Dev, port = Some(8088), diff --git a/examples/src/main/scala/example/quick/AuthExampleApp.scala b/examples/src/main/scala/example/quick/AuthExampleApp.scala index f9956fdb3f..396da8549b 100644 --- a/examples/src/main/scala/example/quick/AuthExampleApp.scala +++ b/examples/src/main/scala/example/quick/AuthExampleApp.scala @@ -36,7 +36,7 @@ object Auth { } val middleware = - HttpAppMiddleware.customAuthZIO { req => + Middleware.customAuthZIO { req => val user = req.headers.get(Header.Authorization).map(_.renderedValue) ZIO.serviceWithZIO[Auth](_.setUser(user)).as(true) } @@ -64,10 +64,10 @@ object AuthExampleApp extends ZIOAppDefault { apiHandler <- (exampleApi |+| Authed.api).handler.map(_ @@ Auth.middleware) graphiqlHandler = GraphiQLHandler.handler(apiPath = "/api/graphql", graphiqlPath = "/graphiql") port <- Server.install( - Http.collectHandler[Request] { - case _ -> Root / "api" / "graphql" => apiHandler - case _ -> Root / "graphiql" => graphiqlHandler - } + Routes( + Method.POST / "api" / "graphql" -> apiHandler, + Method.GET / "graphiql" -> graphiqlHandler + ).toHttpApp ) _ <- ZIO.logInfo(s"Server started on port $port") _ <- ZIO.never diff --git a/examples/src/main/scala/example/stitching/ExampleApp.scala b/examples/src/main/scala/example/stitching/ExampleApp.scala index dc02f57640..536fd06db1 100644 --- a/examples/src/main/scala/example/stitching/ExampleApp.scala +++ b/examples/src/main/scala/example/stitching/ExampleApp.scala @@ -105,7 +105,7 @@ import caliban.ZHttpAdapter object ExampleApp extends ZIOAppDefault { import sttp.tapir.json.circe._ - private val graphiql = Handler.fromStream(ZStream.fromResource("graphiql.html")).toHttp.withDefaultErrorResponse + private val graphiql = Handler.fromResource("graphiql.html").sandbox override def run = (for { @@ -114,12 +114,14 @@ object ExampleApp extends ZIOAppDefault { _ <- Server .serve( - Http - .collectHttp[Request] { - case _ -> Root / "api" / "graphql" => ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)) - case _ -> Root / "ws" / "graphql" => ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)) - case _ -> Root / "graphiql" => graphiql - } + Routes( + Method.ANY / "api" / "graphql" -> + ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)), + Method.ANY / "ws" / "graphql" -> + ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)), + Method.ANY / "graphiql" -> + graphiql + ).toHttpApp ) } yield ()) .provide( diff --git a/examples/src/main/scala/example/ziohttp/AuthExampleApp.scala b/examples/src/main/scala/example/ziohttp/AuthExampleApp.scala index 6ea35b4adb..c263db4db5 100644 --- a/examples/src/main/scala/example/ziohttp/AuthExampleApp.scala +++ b/examples/src/main/scala/example/ziohttp/AuthExampleApp.scala @@ -40,7 +40,7 @@ object Auth { object WebSockets { def live[R <: Auth]( interpreter: GraphQLInterpreter[R, CalibanError] - ): App[R] = { + ): RequestHandler[R, Nothing] = { val webSocketHooks = WebSocketHooks.init[R, CalibanError](payload => ZIO .fromOption(payload match { @@ -61,7 +61,7 @@ object Auth { } val middleware = - HttpAppMiddleware.customAuthZIO { req => + Middleware.customAuthZIO { req => val user = req.headers.get(Header.Authorization).map(_.renderedValue) ZIO.serviceWithZIO[Auth](_.setUser(user)).as(true) } @@ -82,7 +82,7 @@ object Authed extends GenericSchema[Auth] { } object AuthExampleApp extends ZIOAppDefault { - private val graphiql = Handler.fromStream(ZStream.fromResource("graphiql.html")).toHttp.withDefaultErrorResponse + private val graphiql = Handler.fromResource("graphiql.html").sandbox override def run: URIO[Any, ExitCode] = (for { @@ -90,13 +90,12 @@ object AuthExampleApp extends ZIOAppDefault { interpreter <- (exampleApi |+| Authed.api).interpreter port <- Server .install( - Http - .collectHttp[Request] { - case _ -> Root / "api" / "graphql" => - ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)) @@ Auth.middleware - case _ -> Root / "ws" / "graphql" => Auth.WebSockets.live(interpreter) - case _ -> Root / "graphiql" => graphiql - } + Routes( + Method.ANY / "api" / "graphql" -> + ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)) @@ Auth.middleware, + Method.ANY / "ws" / "graphql" -> Auth.WebSockets.live(interpreter), + Method.ANY / "graphiql" -> graphiql + ).toHttpApp ) _ <- ZIO.logInfo(s"Server started on port $port") _ <- ZIO.never @@ -105,11 +104,7 @@ object AuthExampleApp extends ZIOAppDefault { ExampleService.make(sampleCharacters), ExampleApi.layer, Auth.http, - ZLayer.succeed( - Server.Config.default - .port(8088) - .withWebSocketConfig(ZHttpAdapter.defaultWebSocketConfig) - ), + ZLayer.succeed(Server.Config.default.port(8088)), Server.live ) .exitCode diff --git a/examples/src/main/scala/example/ziohttp/ExampleApp.scala b/examples/src/main/scala/example/ziohttp/ExampleApp.scala index 5d24b687e8..3497b3a9e3 100644 --- a/examples/src/main/scala/example/ziohttp/ExampleApp.scala +++ b/examples/src/main/scala/example/ziohttp/ExampleApp.scala @@ -5,13 +5,12 @@ import example.{ ExampleApi, ExampleService } import caliban.{ GraphQL, ZHttpAdapter } import caliban.interop.tapir.{ HttpInterpreter, WebSocketInterpreter } import zio._ -import zio.stream._ import zio.http._ object ExampleApp extends ZIOAppDefault { import sttp.tapir.json.circe._ - private val graphiql = Handler.fromStream(ZStream.fromResource("graphiql.html")).toHttp.withDefaultErrorResponse + private val graphiql = Handler.fromResource("graphiql.html").sandbox override def run: ZIO[Any, Throwable, Unit] = (for { @@ -19,13 +18,12 @@ object ExampleApp extends ZIOAppDefault { _ <- Server .serve( - Http - .collectHttp[Request] { - case _ -> Root / "api" / "graphql" => ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)) - case _ -> Root / "ws" / "graphql" => - ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)) - case _ -> Root / "graphiql" => graphiql - } + Routes( + Method.ANY / "api" / "graphql" -> ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)), + Method.ANY / "ws" / "graphql" -> + ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)), + Method.ANY / "graphiql" -> graphiql + ).toHttpApp ) _ <- Console.printLine("Server online at http://localhost:8088/") _ <- Console.printLine("Press RETURN to stop...") *> Console.readLine @@ -33,11 +31,7 @@ object ExampleApp extends ZIOAppDefault { .provide( ExampleService.make(sampleCharacters), ExampleApi.layer, - ZLayer.succeed( - Server.Config.default - .port(8088) - .withWebSocketConfig(ZHttpAdapter.defaultWebSocketConfig) - ), + ZLayer.succeed(Server.Config.default.port(8088)), Server.live ) } diff --git a/vuepress/docs/docs/adapters.md b/vuepress/docs/docs/adapters.md index 805d7b01b2..bdd53de799 100644 --- a/vuepress/docs/docs/adapters.md +++ b/vuepress/docs/docs/adapters.md @@ -160,11 +160,11 @@ for { handler2 <- api.interpreter.map(QuickAdapter(_).handler) // Creates a handler which serves the GraphiQL API from CDN graphiql = GraphiQLHandler.handler(apiPath = "/api/graphql", graphiqlPath = "/graphiql") - app = Http.collectHandler[Request] { - case _ -> Root / "api" / "graphql" => handler - case Method.GET -> Root / "graphiql" => graphiql + app = Routes( + Method.ANY / "api" / "graphql" -> handler, + Method.GET / "graphiql" -> graphiql // Add more routes, apply middleware, etc. - } + ).toHttpApp _ <- Server.serve(app).provide(Server.defaultWithPort(8080)) } yield () ```