diff --git a/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala b/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala index 18d7668c5..b834366eb 100644 --- a/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala +++ b/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala @@ -2,15 +2,20 @@ package caliban import caliban.execution.QueryExecution import caliban.interop.cats.CatsInterop -import caliban.interop.tapir.TapirAdapter.zioMonadError +import caliban.interop.tapir.TapirAdapter.{ zioMonadError, CalibanPipe, ZioWebSockets } import caliban.interop.tapir.{ RequestInterceptor, TapirAdapter, WebSocketHooks } import cats.data.Kleisli import cats.effect.Async +import cats.effect.std.Dispatcher import cats.~> import org.http4s._ import org.http4s.server.websocket.WebSocketBuilder2 +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams +import sttp.tapir.Endpoint import sttp.tapir.json.circe._ import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import zio._ import zio.blocking.Blocking @@ -38,6 +43,24 @@ object Http4sAdapter { ZHttp4sServerInterpreter().from(endpoints).toRoutes } + def makeHttpServiceF[F[_]: Async, R, E]( + interpreter: GraphQLInterpreter[R, E], + skipValidation: Boolean = false, + enableIntrospection: Boolean = true, + queryExecution: QueryExecution = QueryExecution.Parallel, + requestInterceptor: RequestInterceptor[R] = RequestInterceptor.empty + )(implicit runtime: Runtime[R]): HttpRoutes[F] = { + val endpoints = TapirAdapter.makeHttpService[R, E]( + interpreter, + skipValidation, + enableIntrospection, + queryExecution, + requestInterceptor + ) + val endpointsF = endpoints.map(convertHttpEndpointToF[F, R, E]) + Http4sServerInterpreter().toRoutes(endpointsF) + } + def makeHttpUploadService[R <: Has[_] with Random, E]( interpreter: GraphQLInterpreter[R, E], skipValidation: Boolean = false, @@ -55,6 +78,24 @@ object Http4sAdapter { ZHttp4sServerInterpreter().from(endpoint).toRoutes } + def makeHttpUploadServiceF[F[_]: Async, R <: Has[_] with Random, E]( + interpreter: GraphQLInterpreter[R, E], + skipValidation: Boolean = false, + enableIntrospection: Boolean = true, + queryExecution: QueryExecution = QueryExecution.Parallel, + requestInterceptor: RequestInterceptor[R] = RequestInterceptor.empty + )(implicit runtime: Runtime[R]): HttpRoutes[F] = { + val endpoint = TapirAdapter.makeHttpUploadService[R, E]( + interpreter, + skipValidation, + enableIntrospection, + queryExecution, + requestInterceptor + ) + val endpointF = convertHttpEndpointToF[F, R, E](endpoint) + Http4sServerInterpreter().toRoutes(endpointF) + } + def makeWebSocketService[R, R1 <: R, E]( builder: WebSocketBuilder2[RIO[R with Clock with Blocking, *]], interpreter: GraphQLInterpreter[R1, E], @@ -79,6 +120,29 @@ object Http4sAdapter { .toRoutes(builder.asInstanceOf[WebSocketBuilder2[RIO[R1 with Clock with Blocking, *]]]) } + def makeWebSocketServiceF[F[_]: Async: Dispatcher, R, E]( + builder: WebSocketBuilder2[F], + interpreter: GraphQLInterpreter[R, E], + skipValidation: Boolean = false, + enableIntrospection: Boolean = true, + keepAliveTime: Option[Duration] = None, + queryExecution: QueryExecution = QueryExecution.Parallel, + requestInterceptor: RequestInterceptor[R] = RequestInterceptor.empty, + webSocketHooks: WebSocketHooks[R, E] = WebSocketHooks.empty + )(implicit runtime: Runtime[R]): HttpRoutes[F] = { + val endpoint = TapirAdapter.makeWebSocketService[R, E]( + interpreter, + skipValidation, + enableIntrospection, + keepAliveTime, + queryExecution, + requestInterceptor, + webSocketHooks + ) + val endpointF = convertWebSocketEndpointToF[F, R, E](endpoint) + Http4sServerInterpreter().toWebSocketRoutes(endpointF)(builder) + } + /** * Utility function to create an http4s middleware that can extracts something from each request * and provide a layer to eliminate the ZIO environment @@ -131,7 +195,7 @@ object Http4sAdapter { * If you wish to use `Http4sServerInterpreter` with cats-effect IO instead of `ZHttp4sServerInterpreter`, * you can use this function to convert the tapir endpoints to their cats-effect counterpart. */ - def convertHttpEndpointToF[E, R, F[_]: Async]( + def convertHttpEndpointToF[F[_]: Async, R, E]( endpoint: ServerEndpoint[Any, RIO[R, *]] )(implicit runtime: Runtime[R]): ServerEndpoint[Any, F] = ServerEndpoint[endpoint.A, endpoint.U, endpoint.I, endpoint.E, endpoint.O, Any, F]( @@ -140,4 +204,36 @@ object Http4sAdapter { _ => u => req => CatsInterop.toEffect(endpoint.logic(zioMonadError)(u)(req)) ) + /** + * If you wish to use `Http4sServerInterpreter` with cats-effect IO instead of `ZHttp4sServerInterpreter`, + * you can use this function to convert the tapir endpoints to their cats-effect counterpart. + */ + def convertWebSocketEndpointToF[F[_]: Async: Dispatcher, R, E]( + endpoint: ServerEndpoint[ZioWebSockets, RIO[R, *]] + )(implicit runtime: Runtime[R]): ServerEndpoint[Fs2Streams[F] with WebSockets, F] = { + type Fs2Pipe = fs2.Pipe[F, GraphQLWSInput, GraphQLWSOutput] + + val e = endpoint + .asInstanceOf[ + ServerEndpoint.Full[endpoint.A, endpoint.U, endpoint.I, endpoint.E, CalibanPipe, ZioWebSockets, RIO[R, *]] + ] + + ServerEndpoint[endpoint.A, endpoint.U, endpoint.I, endpoint.E, Fs2Pipe, Fs2Streams[F] with WebSockets, F]( + e.endpoint.asInstanceOf[Endpoint[endpoint.A, endpoint.I, endpoint.E, Fs2Pipe, Any]], + _ => a => CatsInterop.toEffect(e.securityLogic(zioMonadError)(a)), + _ => + u => + req => + CatsInterop.toEffect( + e.logic(zioMonadError)(u)(req) + .map(_.map { zioPipe => + import zio.stream.interop.fs2z._ + fs2InputStream => + zioPipe(fs2InputStream.translate(CatsInterop.fromEffectK[F, Any]).toZStream()).toFs2Stream + .translate(CatsInterop.toEffectK) + }) + ) + ) + } + } diff --git a/examples/src/main/scala/example/http4s/ExampleAppF.scala b/examples/src/main/scala/example/http4s/ExampleAppF.scala new file mode 100644 index 000000000..59c8e830f --- /dev/null +++ b/examples/src/main/scala/example/http4s/ExampleAppF.scala @@ -0,0 +1,49 @@ +package example.http4s + +import caliban.interop.cats.implicits._ +import caliban.{ CalibanError, Http4sAdapter } +import cats.data.Kleisli +import cats.effect.std.Dispatcher +import cats.effect.{ ExitCode, IO, IOApp } +import example.ExampleData.sampleCharacters +import example.ExampleService.ExampleService +import example.{ ExampleApi, ExampleService } +import org.http4s.StaticFile +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.implicits._ +import org.http4s.server.Router +import org.http4s.server.middleware.CORS +import zio.Runtime +import zio.clock.Clock +import zio.console.Console +import zio.internal.Platform + +object ExampleAppF extends IOApp { + + type MyEnv = Console with Clock with ExampleService + + implicit val zioRuntime: Runtime[MyEnv] = + Runtime.unsafeFromLayer(ExampleService.make(sampleCharacters) ++ Console.live ++ Clock.live, Platform.default) + + override def run(args: List[String]): IO[ExitCode] = + Dispatcher[IO].use { implicit dispatcher => + for { + interpreter <- ExampleApi.api.interpreterAsync[IO] + _ <- BlazeServerBuilder[IO] + .bindHttp(8088, "localhost") + .withHttpWebSocketApp(wsBuilder => + Router[IO]( + "/api/graphql" -> + CORS.policy(Http4sAdapter.makeHttpServiceF[IO, MyEnv, CalibanError](interpreter)), + "/ws/graphql" -> + CORS.policy(Http4sAdapter.makeWebSocketServiceF[IO, MyEnv, CalibanError](wsBuilder, interpreter)), + "/graphiql" -> + Kleisli.liftF(StaticFile.fromResource("/graphiql.html", None)) + ).orNotFound + ) + .serve + .compile + .drain + } yield ExitCode.Success + } +}