From 78725c9b4ab5536b6ec4c8eb26793422ba0389eb Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 17 Jun 2024 13:13:55 +1000 Subject: [PATCH 1/2] Change `caliban-zio-http` to depend on caliban-quick --- .circleci/config.yml | 4 +- .../src/main/scala/caliban/QuickAdapter.scala | 2 +- .../test/scala/caliban/QuickAdapterSpec.scala | 6 +- .../src/main/scala/caliban/ZHttpAdapter.scala | 46 +++---- .../test/scala/caliban/ZHttpAdapterSpec.scala | 61 ---------- build.sbt | 10 +- .../scala/caliban/ws/WebSocketHooks.scala | 4 +- .../example/ziohttp/AuthExampleApp.scala | 114 ------------------ .../scala/example/ziohttp/ExampleApp.scala | 38 ------ vuepress/docs/docs/adapters.md | 2 +- 10 files changed, 25 insertions(+), 262 deletions(-) delete mode 100644 adapters/zio-http/src/test/scala/caliban/ZHttpAdapterSpec.scala delete mode 100644 examples/src/main/scala/example/ziohttp/AuthExampleApp.scala delete mode 100644 examples/src/main/scala/example/ziohttp/ExampleApp.scala diff --git a/.circleci/config.yml b/.circleci/config.yml index 91c43196c..04f6f977e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -177,8 +177,8 @@ jobs: - checkout - restore_cache: key: sbtcache - - run: sbt ++2.13 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues quickAdapter/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues # tools/mimaReportBinaryIssues - - run: sbt ++3.3 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues quickAdapter/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues # tools/mimaReportBinaryIssues + - run: sbt ++2.13 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues quickAdapter/mimaReportBinaryIssues play/mimaReportBinaryIssues # tools/mimaReportBinaryIssues + - run: sbt ++3.3 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues quickAdapter/mimaReportBinaryIssues play/mimaReportBinaryIssues # tools/mimaReportBinaryIssues - save_cache: key: sbtcache paths: diff --git a/adapters/quick/src/main/scala/caliban/QuickAdapter.scala b/adapters/quick/src/main/scala/caliban/QuickAdapter.scala index f78244b77..704867408 100644 --- a/adapters/quick/src/main/scala/caliban/QuickAdapter.scala +++ b/adapters/quick/src/main/scala/caliban/QuickAdapter.scala @@ -23,7 +23,7 @@ final class QuickAdapter[R] private (requestHandler: QuickRequestHandler[R]) { Handler.fromFunctionZIO[Request](requestHandler.handleHttpRequest) /** - * Converts this adapter to an `Routes` serving the GraphQL API at the specified path. + * Converts this adapter to a `Routes` 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. diff --git a/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala b/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala index 2b7e43094..e232e8d1b 100644 --- a/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala +++ b/adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala @@ -6,7 +6,7 @@ import caliban.uploads.Uploads import sttp.client3.UriContext import zio._ import zio.http._ -import zio.test.{ Live, TestAspect, ZIOSpecDefault } +import zio.test.{ Live, ZIOSpecDefault } import scala.language.postfixOps @@ -24,10 +24,10 @@ object QuickAdapterSpec extends ZIOSpecDefault { private val apiLayer = envLayer >>> ZLayer.fromZIO { for { - app <- TestApi.api + _ <- TestApi.api .routes("/api/graphql", uploadPath = Some("/upload/graphql"), webSocketPath = Some("/ws/graphql")) .map(_ @@ auth) - _ <- Server.serve(app).forkScoped + .flatMap(_.serve[TestService & Uploads].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 9a1650b68..c6d4b2372 100644 --- a/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala +++ b/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala @@ -1,11 +1,8 @@ package caliban -import caliban.interop.tapir.{ HttpInterpreter, WebSocketInterpreter } -import caliban.ws.Protocol -import sttp.capabilities.zio.ZioStreams -import sttp.model.HeaderNames -import sttp.tapir.server.ziohttp.{ ZioHttpInterpreter, ZioHttpServerOptions } -import zio.http._ +import caliban.ws.WebSocketHooks +import zio.Duration +import zio.http.{ WebSocketConfig => ZWebSocketConfig, _ } @deprecated( "The `caliban-zio-http` package is deprecated and scheduled to be removed in a future release. To use Caliban with zio-http, use the `caliban-quick` module instead", @@ -13,32 +10,17 @@ 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.subProtocol(Some(subProtocols)) - } - - def makeHttpService[R, E](interpreter: HttpInterpreter[R, E])(implicit - serverOptions: ZioHttpServerOptions[R] = ZioHttpServerOptions.default[R] - ): RequestHandler[R, Nothing] = - ZioHttpInterpreter(serverOptions) - .toHttp(interpreter.serverEndpoints[R, ZioStreams](ZioStreams)) - .toHandler + def makeHttpService[R, E](interpreter: GraphQLInterpreter[R, E]): RequestHandler[R, Nothing] = + QuickAdapter(interpreter).handlers.api - def makeWebSocketService[R, E](interpreter: WebSocketInterpreter[R, E])(implicit - serverOptions: ZioHttpServerOptions[R] = ZioHttpServerOptions.default[R] - ): RequestHandler[R, Nothing] = - ZioHttpInterpreter(patchWsServerOptions(serverOptions)) - .toHttp(interpreter.serverEndpoint[R]) - .toHandler + def makeWebSocketService[R, E]( + interpreter: GraphQLInterpreter[R, E], + keepAliveTime: Option[Duration] = None, + webSocketHooks: WebSocketHooks[R, E] = WebSocketHooks.empty, + zHttpConfig: ZWebSocketConfig = ZWebSocketConfig.default + ): RequestHandler[R, Nothing] = { + val config = quick.WebSocketConfig(keepAliveTime, webSocketHooks, zHttpConfig) + QuickAdapter(interpreter).configureWebSocket(config).handlers.webSocket + } - 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 deleted file mode 100644 index 41e4e8871..000000000 --- a/adapters/zio-http/src/test/scala/caliban/ZHttpAdapterSpec.scala +++ /dev/null @@ -1,61 +0,0 @@ -package caliban - -import caliban.interop.tapir.TestData.sampleCharacters -import caliban.interop.tapir.{ - FakeAuthorizationInterceptor, - HttpInterpreter, - TapirAdapterSpec, - TestApi, - TestService, - WebSocketInterpreter -} -import caliban.uploads.Uploads -import sttp.client3.UriContext -import zio._ -import zio.http._ -import zio.test.{ Live, TestAspect, ZIOSpecDefault } - -import scala.annotation.nowarn -import scala.language.postfixOps - -@nowarn -object ZHttpAdapterSpec extends ZIOSpecDefault { - import sttp.tapir.json.zio._ - - private val envLayer = TestService.make(sampleCharacters) ++ Uploads.empty - - private val apiLayer = envLayer >>> ZLayer.fromZIO { - for { - interpreter <- TestApi.api.interpreter - _ <- - Server - .serve( - Routes( - Method.ANY / "api" / "graphql" -> - ZHttpAdapter - .makeHttpService( - HttpInterpreter(interpreter).intercept(FakeAuthorizationInterceptor.bearer[TestService & Uploads]) - ), - Method.ANY / "ws" / "graphql" -> - ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)) - ) - ) - .forkScoped - _ <- Live.live(Clock.sleep(3 seconds)) - service <- ZIO.service[TestService] - } yield service - } - - override def spec = suite("ZIO Http") { - val suite = TapirAdapterSpec.makeSuite( - "ZHttpAdapterSpec", - uri"http://localhost:8089/api/graphql", - wsUri = Some(uri"ws://localhost:8089/ws/graphql") - ) - suite.provideShared( - apiLayer, - Scope.default, - Server.defaultWith(_.port(8089).responseCompression()) - ) - } -} diff --git a/build.sbt b/build.sbt index 832aa64f2..729752123 100644 --- a/build.sbt +++ b/build.sbt @@ -373,15 +373,7 @@ lazy val zioHttp = project .settings(commonSettings) .settings(enableMimaSettingsJVM) .disablePlugins(AssemblyPlugin) - .settings( - libraryDependencies ++= Seq( - "dev.zio" %% "zio-http" % zioHttpVersion, - "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion, - "dev.zio" %% "zio-json" % zioJsonVersion % Test, - "com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion % Test - ) - ) - .dependsOn(core, tapirInterop % "compile->compile;test->test") + .dependsOn(core, quickAdapter) lazy val quickAdapter = project .in(file("adapters/quick")) diff --git a/core/src/main/scala/caliban/ws/WebSocketHooks.scala b/core/src/main/scala/caliban/ws/WebSocketHooks.scala index 2b0253189..d2e41a307 100644 --- a/core/src/main/scala/caliban/ws/WebSocketHooks.scala +++ b/core/src/main/scala/caliban/ws/WebSocketHooks.scala @@ -69,7 +69,9 @@ trait WebSocketHooks[-R, +E] { self => } object WebSocketHooks { - def empty[R, E]: WebSocketHooks[R, E] = new WebSocketHooks[R, E] {} + def empty[R, E]: WebSocketHooks[R, E] = Empty + + private case object Empty extends WebSocketHooks[Any, Nothing] /** * Specifies a callback that will be run before an incoming subscription diff --git a/examples/src/main/scala/example/ziohttp/AuthExampleApp.scala b/examples/src/main/scala/example/ziohttp/AuthExampleApp.scala deleted file mode 100644 index 400277deb..000000000 --- a/examples/src/main/scala/example/ziohttp/AuthExampleApp.scala +++ /dev/null @@ -1,114 +0,0 @@ -package example.ziohttp - -import caliban.Value.StringValue -import caliban._ -import caliban.interop.tapir.{ HttpInterpreter, WebSocketInterpreter } -import caliban.schema.GenericSchema -import caliban.ws.WebSocketHooks -import example.ExampleData._ -import example.{ ExampleApi, ExampleService } -import sttp.tapir.json.circe._ -import zio._ -import zio.http._ -import zio.stream._ - -import scala.annotation.nowarn - -case object Unauthorized extends RuntimeException("Unauthorized") - -trait Auth { - type Unauthorized = Unauthorized.type - - def currentUser: IO[Unauthorized, String] - def setUser(name: Option[String]): UIO[Unit] -} - -@nowarn -object Auth { - - val http: ULayer[Auth] = ZLayer.scoped { - FiberRef - .make[Option[String]](None) - .map { ref => - new Auth { - def currentUser: IO[Unauthorized, String] = - ref.get.flatMap { - case Some(v) => ZIO.succeed(v) - case None => ZIO.fail(Unauthorized) - } - def setUser(name: Option[String]): UIO[Unit] = ref.set(name) - } - } - } - - object WebSockets { - def live[R <: Auth]( - interpreter: GraphQLInterpreter[R, CalibanError] - ): RequestHandler[R, Nothing] = { - val webSocketHooks = WebSocketHooks.init[R, CalibanError](payload => - ZIO - .fromOption(payload match { - case InputValue.ObjectValue(fields) => - fields.get("Authorization").flatMap { - case StringValue(s) => Some(s) - case _ => None - } - case x => None - }) - .orElseFail(CalibanError.ExecutionError("Unable to decode payload")) - .flatMap(user => ZIO.serviceWithZIO[Auth](_.setUser(Some(user)))) - .debug("connect") - ) ++ WebSocketHooks.afterInit(ZIO.failCause(Cause.empty).delay(10.seconds)) - - ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter, webSocketHooks = webSocketHooks)) - } - } - - val middleware = - Middleware.customAuthZIO { req => - val user = req.headers.get(Header.Authorization).map(_.renderedValue) - ZIO.serviceWithZIO[Auth](_.setUser(user)).as(true) - } -} - -object Authed extends GenericSchema[Auth] { - import auto._ - - case class Queries( - whoAmI: ZIO[Auth, Unauthorized.type, String] = ZIO.serviceWithZIO[Auth](_.currentUser) - ) - case class Subscriptions( - whoAmI: ZStream[Auth, Unauthorized.type, String] = - ZStream.fromZIO(ZIO.serviceWithZIO[Auth](_.currentUser)).repeat(Schedule.spaced(2.seconds)) - ) - - val api = graphQL(RootResolver(Queries(), None, Subscriptions())) -} - -@nowarn -object AuthExampleApp extends ZIOAppDefault { - private val graphiql = Handler.fromResource("graphiql.html").sandbox - - def run = - (for { - exampleApi <- ZIO.service[GraphQL[Any]] - interpreter <- (exampleApi |+| Authed.api).interpreter - port <- Server.install( - Routes( - Method.ANY / "api" / "graphql" -> - ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)) @@ Auth.middleware, - Method.ANY / "ws" / "graphql" -> Auth.WebSockets.live(interpreter), - Method.ANY / "graphiql" -> graphiql - ) - ) - _ <- ZIO.logInfo(s"Server started on port $port") - _ <- ZIO.never - } yield ()) - .provide( - ExampleService.make(sampleCharacters), - ExampleApi.layer, - Auth.http, - ZLayer.succeed(Server.Config.default.port(8088)), - Server.live - ) -} diff --git a/examples/src/main/scala/example/ziohttp/ExampleApp.scala b/examples/src/main/scala/example/ziohttp/ExampleApp.scala deleted file mode 100644 index de7b76172..000000000 --- a/examples/src/main/scala/example/ziohttp/ExampleApp.scala +++ /dev/null @@ -1,38 +0,0 @@ -package example.ziohttp - -import example.ExampleData._ -import example.{ ExampleApi, ExampleService } -import caliban.{ GraphQL, ZHttpAdapter } -import caliban.interop.tapir.{ HttpInterpreter, WebSocketInterpreter } -import zio._ -import zio.http._ - -import scala.annotation.nowarn - -@nowarn -object ExampleApp extends ZIOAppDefault { - import sttp.tapir.json.circe._ - - private val graphiql = Handler.fromResource("graphiql.html").sandbox - - override def run: ZIO[Any, Throwable, Unit] = - (for { - interpreter <- ZIO.serviceWithZIO[GraphQL[Any]](_.interpreter) - _ <- - Server.serve( - Routes( - Method.ANY / "api" / "graphql" -> ZHttpAdapter.makeHttpService(HttpInterpreter(interpreter)), - Method.ANY / "ws" / "graphql" -> ZHttpAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)), - Method.ANY / "graphiql" -> graphiql - ) - ) - _ <- Console.printLine("Server online at http://localhost:8088/") - _ <- Console.printLine("Press RETURN to stop...") *> Console.readLine - } yield ()) - .provide( - ExampleService.make(sampleCharacters), - ExampleApi.layer, - ZLayer.succeed(Server.Config.default.port(8088)), - Server.live - ) -} diff --git a/vuepress/docs/docs/adapters.md b/vuepress/docs/docs/adapters.md index f557da3bf..e87fffbe4 100644 --- a/vuepress/docs/docs/adapters.md +++ b/vuepress/docs/docs/adapters.md @@ -166,7 +166,7 @@ for { Method.POST / "upload" / "graphql" -> handlers.upload // Add more routes, apply middleware, etc. ) - _ <- Server.serve(app).provide(Server.defaultWithPort(8080)) + _ <- app.serve[Any].provide(Server.defaultWithPort(8080)) } yield () ``` From 74bc787e8686ea1cf0f8a4cdfae768d18406e473 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 17 Jun 2024 13:42:56 +1000 Subject: [PATCH 2/2] Add argument for ExecutionConfiguration --- .../zio-http/src/main/scala/caliban/ZHttpAdapter.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala b/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala index c6d4b2372..298ab1f7e 100644 --- a/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala +++ b/adapters/zio-http/src/main/scala/caliban/ZHttpAdapter.scala @@ -1,5 +1,6 @@ package caliban +import caliban.Configurator.ExecutionConfiguration import caliban.ws.WebSocketHooks import zio.Duration import zio.http.{ WebSocketConfig => ZWebSocketConfig, _ } @@ -10,8 +11,11 @@ import zio.http.{ WebSocketConfig => ZWebSocketConfig, _ } ) object ZHttpAdapter { - def makeHttpService[R, E](interpreter: GraphQLInterpreter[R, E]): RequestHandler[R, Nothing] = - QuickAdapter(interpreter).handlers.api + def makeHttpService[R, E]( + interpreter: GraphQLInterpreter[R, E], + config: ExecutionConfiguration = ExecutionConfiguration() + ): RequestHandler[R, Nothing] = + QuickAdapter(interpreter).configure(config).handlers.api def makeWebSocketService[R, E]( interpreter: GraphQLInterpreter[R, E],