Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graphiql endpoints for tapir adapters #2379

Merged
merged 2 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions adapters/akka-http/src/main/scala/caliban/AkkaHttpAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import sttp.capabilities.WebSockets
import sttp.capabilities.akka.AkkaStreams
import sttp.capabilities.akka.AkkaStreams.Pipe
import sttp.model.StatusCode
import sttp.monad.{ FutureMonad, MonadError }
import sttp.tapir.Codec.JsonCodec
import sttp.tapir.PublicEndpoint
import sttp.tapir.model.ServerRequest
Expand All @@ -22,6 +23,8 @@ import zio.stream.ZStream
import scala.concurrent.{ ExecutionContext, Future }

class AkkaHttpAdapter private (private val options: AkkaHttpServerOptions)(implicit ec: ExecutionContext) {
private implicit val monadErrorFuture: MonadError[Future] = new FutureMonad

private val akkaInterpreter = AkkaHttpServerInterpreter(options)(ec)

def makeHttpService[R, E](
Expand Down Expand Up @@ -58,6 +61,18 @@ class AkkaHttpAdapter private (private val options: AkkaHttpServerOptions)(impli
)
)

/**
* Creates a route which serves the GraphiQL UI from CDN.
*
* @param apiPath The path at which the API can be introspected.
*
* @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]]
*/
def makeGraphiqlService(apiPath: String): Route =
akkaInterpreter.toRoute(
HttpInterpreter.makeGraphiqlEndpoint[Future](apiPath)
)

private implicit def streamConstructor(implicit
runtime: Runtime[Any],
mat: Materializer
Expand Down
17 changes: 16 additions & 1 deletion adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import sttp.capabilities.fs2.Fs2Streams
import sttp.capabilities.zio.ZioStreams
import sttp.tapir.Codec.JsonCodec
import sttp.tapir.Endpoint
import sttp.tapir.integ.cats.effect.CatsMonadError
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.http4s.Http4sServerInterpreter
import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
import zio._
import zio.interop.catz.concurrentInstance
import zio.stream.interop.fs2z._
import zio.stream.ZStream
import zio.stream.interop.fs2z._

object Http4sAdapter {

Expand Down Expand Up @@ -52,6 +53,20 @@ object Http4sAdapter {
Http4sServerInterpreter().toRoutes(endpointF)
}

/**
* Creates a route which serves the GraphiQL UI from CDN.
*
* @param apiPath The path at which the API can be introspected.
*
* @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]]
*/
def makeGraphiqlService[F[_]: Async](apiPath: String): HttpRoutes[F] = {
implicit val F: CatsMonadError[F] = new CatsMonadError[F]
Http4sServerInterpreter().toRoutes(
HttpInterpreter.makeGraphiqlEndpoint[F](apiPath)
)
}

def makeWebSocketService[R, R1 <: R, E](
builder: WebSocketBuilder2[RIO[R, *]],
interpreter: WebSocketInterpreter[R1, E]
Expand Down
13 changes: 13 additions & 0 deletions adapters/pekko-http/src/main/scala/caliban/PekkoHttpAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.capabilities.pekko.PekkoStreams.Pipe
import sttp.model.StatusCode
import sttp.monad.{ FutureMonad, MonadError }
import sttp.tapir.Codec.JsonCodec
import sttp.tapir.PublicEndpoint
import sttp.tapir.model.ServerRequest
Expand All @@ -22,6 +23,8 @@ import zio.stream.ZStream
import scala.concurrent.{ ExecutionContext, Future }

class PekkoHttpAdapter private (val options: PekkoHttpServerOptions)(implicit ec: ExecutionContext) {
private implicit val monadErrorFuture: MonadError[Future] = new FutureMonad

private val pekkoInterpreter = PekkoHttpServerInterpreter(options)(ec)

def makeHttpService[R, E](
Expand Down Expand Up @@ -58,6 +61,16 @@ class PekkoHttpAdapter private (val options: PekkoHttpServerOptions)(implicit ec
)
)

/**
* Creates a route which serves the GraphiQL UI from CDN.
*
* @param apiPath The path at which the API can be introspected.
*
* @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]]
*/
def makeGraphiqlService(apiPath: String): Route =
pekkoInterpreter.toRoute(HttpInterpreter.makeGraphiqlEndpoint[Future](apiPath))

private implicit def streamConstructor(implicit
runtime: Runtime[Any],
mat: Materializer
Expand Down
13 changes: 13 additions & 0 deletions adapters/play/src/main/scala/caliban/PlayAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.capabilities.pekko.PekkoStreams.Pipe
import sttp.model.StatusCode
import sttp.monad.FutureMonad
import sttp.tapir.Codec.JsonCodec
import sttp.tapir.PublicEndpoint
import sttp.tapir.model.ServerRequest
Expand Down Expand Up @@ -37,6 +38,18 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) {
): Routes =
playInterpreter.toRoutes(interpreter.serverEndpointFuture[PekkoStreams](PekkoStreams)(runtime))

/**
* Creates a route which serves the GraphiQL UI from CDN.
*
* @param apiPath The path at which the API can be introspected.
*
* @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]]
*/
def makeGraphiqlService(apiPath: String)(implicit materializer: Materializer): Routes = {
implicit val F: FutureMonad = new FutureMonad()(materializer.executionContext)
playInterpreter.toRoutes(HttpInterpreter.makeGraphiqlEndpoint[Future](apiPath))
}

def makeWebSocketService[R, E](
interpreter: WebSocketInterpreter[R, E]
)(implicit runtime: Runtime[R], materializer: Materializer): Routes = {
Expand Down
83 changes: 13 additions & 70 deletions adapters/quick/src/main/scala/caliban/GraphiQLHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,84 +11,27 @@ object GraphiQLHandler {
* Creates a handler which serves the GraphiQL UI from CDN.
*
* @param apiPath The path at which the API can be introspected.
* @param graphiqlPath The path at which the GraphiQL UI will be served.
*
* @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]]
*/
def handler(apiPath: String): RequestHandler[Any, Nothing] = {
val headers = Headers(Header.ContentType(MediaType.text.html).untyped)
zio.http.handler { (req: Request) =>
Response(
Status.Ok,
headers,
Body.fromString(html(apiPath, req.path.encode))
)
}
}

@deprecated("Use overloaded method without providing the graphiqlPath param", since = "2.8.2")
def handler(apiPath: String, graphiqlPath: String): RequestHandler[Any, Nothing] =
Response(
Status.Ok,
Headers(Header.ContentType(MediaType.text.html).untyped),
Body.fromString(html(apiPath, graphiqlPath))
).toHandler

def html(apiPath: String, uiPath: String): String =
s"""
|<!--
| * Copyright (c) 2021 GraphQL Contributors
| * All rights reserved.
| *
| * This source code is licensed under the license found in the
| * LICENSE file in the root directory of this source tree.
|-->
|<!doctype html>
|<html lang="en">
|<head>
| <title>GraphiQL</title>
| <style>
| body {
| height: 100%;
| margin: 0;
| width: 100%;
| overflow: hidden;
| }
|
| #graphiql {
| height: 100vh;
| }
| </style>
| <script
| crossorigin
| src="https://unpkg.com/react@18/umd/react.development.js"
| ></script>
| <script
| crossorigin
| src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
| ></script>
| <script
| src="https://unpkg.com/graphiql/graphiql.min.js"
| type="application/javascript"
| ></script>
| <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css"/>
| <script
| src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
| crossorigin
| ></script>
|
| <link
| rel="stylesheet"
| href="https://unpkg.com/@graphiql/plugin-explorer/dist/style.css"
| />
|</head>
|
|<body>
|<div id="graphiql">Loading...</div>
|<script>
| const root = ReactDOM.createRoot(document.getElementById('graphiql'));
| const fetcher = GraphiQL.createFetcher({
| url: window.location.href.replace("$uiPath", "$apiPath")
| });
| const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
| root.render(
| React.createElement(GraphiQL, {
| fetcher,
| defaultEditorToolsVisibility: true,
| plugins: [explorerPlugin],
| }),
| );
|</script>
|</body>
|</html>
|""".stripMargin

def html(apiPath: String, uiPath: String): String = HttpUtils.graphiqlHtml(apiPath, uiPath)
}
2 changes: 1 addition & 1 deletion adapters/quick/src/main/scala/caliban/QuickAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ final class QuickAdapter[R] private (requestHandler: QuickRequestHandler[R]) {
RoutePattern(Method.GET, apiPath) -> handlers.api
)
val graphiqlRoute = graphiqlPath.toList.map { uiPath =>
RoutePattern(Method.GET, uiPath) -> GraphiQLHandler.handler(apiPath, uiPath)
RoutePattern(Method.GET, uiPath) -> GraphiQLHandler.handler(apiPath)
}
val uploadRoute = uploadPath.toList.map { uPath =>
RoutePattern(Method.POST, uPath) -> handlers.upload
Expand Down
61 changes: 61 additions & 0 deletions core/src/main/scala/caliban/HttpUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,65 @@ private[caliban] object HttpUtils {

def serverSentEvents: Boolean = length >= 17 && header.contains("text/event-stream")
}

def graphiqlHtml(apiPath: String, uiPath: String): String =
s"""<!doctype html>
|<html lang="en">
| <head>
| <title>GraphiQL</title>
| <style>
| body {
| height: 100%;
| margin: 0;
| width: 100%;
| overflow: hidden;
| }
|
| #graphiql {
| height: 100vh;
| }
| </style>
| <script
| crossorigin
| src="https://unpkg.com/react@18/umd/react.production.min.js"
| ></script>
| <script
| crossorigin
| src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
| ></script>
| <script
| src="https://unpkg.com/graphiql/graphiql.min.js"
| type="application/javascript"
| ></script>
| <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
| <script
| src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
| crossorigin
| ></script>
|
| <link
| rel="stylesheet"
| href="https://unpkg.com/@graphiql/plugin-explorer/dist/style.css"
| />
| </head>
|
| <body>
| <div id="graphiql">Loading...</div>
| <script>
| const root = ReactDOM.createRoot(document.getElementById('graphiql'));
| const fetcher = GraphiQL.createFetcher({
| url: window.location.href.replace("$uiPath", "$apiPath")
| });
| const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
| root.render(
| React.createElement(GraphiQL, {
| fetcher,
| defaultEditorToolsVisibility: true,
| plugins: [explorerPlugin],
| }),
| );
| </script>
| </body>
|</html>
|""".stripMargin
}
Loading
Loading