All Downloads are FREE. Search and download functionalities are using the official Maven repository.

caliban.Http4sAdapter.scala Maven / Gradle / Ivy

There is a newer version: 2.9.0
Show newest version
package caliban

import caliban.interop.cats.{ CatsInterop, ToEffect }
import caliban.interop.tapir.TapirAdapter.{ zioMonadError, CalibanPipe, ZioWebSockets }
import caliban.interop.tapir.{ HttpInterpreter, HttpUploadInterpreter, StreamConstructor, WebSocketInterpreter }
import cats.data.Kleisli
import cats.effect.Async
import cats.~>
import org.http4s._
import org.http4s.server.websocket.WebSocketBuilder2
import sttp.capabilities.WebSockets
import sttp.capabilities.fs2.Fs2Streams
import sttp.capabilities.zio.ZioStreams
import sttp.tapir.Codec.JsonCodec
import sttp.tapir.Endpoint
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

object Http4sAdapter {

  def makeHttpService[R, E](interpreter: HttpInterpreter[R, E]): HttpRoutes[RIO[R, *]] =
    ZHttp4sServerInterpreter()
      .from(interpreter.serverEndpoints[R, ZioStreams](ZioStreams))
      .toRoutes

  def makeHttpServiceF[F[_]: Async, R, E](
    interpreter: HttpInterpreter[R, E]
  )(implicit interop: ToEffect[F, R]): HttpRoutes[F] = {
    val endpoints  = interpreter.serverEndpoints[R, Fs2Streams[F]](Fs2Streams[F])
    val endpointsF = endpoints.map(convertHttpEndpointToF[F, R])
    Http4sServerInterpreter().toRoutes(endpointsF)
  }

  def makeHttpUploadService[R, E](interpreter: HttpUploadInterpreter[R, E])(implicit
    requestCodec: JsonCodec[GraphQLRequest],
    mapCodec: JsonCodec[Map[String, Seq[String]]]
  ): HttpRoutes[RIO[R, *]] =
    ZHttp4sServerInterpreter().from(interpreter.serverEndpoint[R, ZioStreams](ZioStreams)).toRoutes

  def makeHttpUploadServiceF[F[_]: Async, R, E](interpreter: HttpUploadInterpreter[R, E])(implicit
    interop: ToEffect[F, R],
    requestCodec: JsonCodec[GraphQLRequest],
    mapCodec: JsonCodec[Map[String, Seq[String]]]
  ): HttpRoutes[F] = {
    val endpoint  = interpreter.serverEndpoint[R, Fs2Streams[F]](Fs2Streams[F])
    val endpointF = convertHttpEndpointToF[F, R](endpoint)
    Http4sServerInterpreter().toRoutes(endpointF)
  }

  def makeWebSocketService[R, R1 <: R, E](
    builder: WebSocketBuilder2[RIO[R, *]],
    interpreter: WebSocketInterpreter[R1, E]
  ): HttpRoutes[RIO[R1, *]] =
    ZHttp4sServerInterpreter[R1]()
      .fromWebSocket(interpreter.serverEndpoint[R1])
      .toRoutes[Any](builder.asInstanceOf[WebSocketBuilder2[RIO[R1, *]]])

  def makeWebSocketServiceF[F[_]: Async, R, E](builder: WebSocketBuilder2[F], interpreter: WebSocketInterpreter[R, E])(
    implicit
    interop: CatsInterop[F, R],
    runtime: Runtime[R]
  ): HttpRoutes[F] = {
    val endpointF = convertWebSocketEndpointToF[F, R](interpreter.serverEndpoint[R])
    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
   * @param route an http4s route
   * @param f a function from a request to a ZLayer
   * @tparam R the environment type to eliminate
   * @return a new route without the R requirement
   */
  def provideLayerFromRequest[R](route: HttpRoutes[RIO[R, *]], f: Request[Task] => TaskLayer[R]): HttpRoutes[Task] =
    Kleisli { (req: Request[Task[*]]) =>
      val to: Task ~> RIO[R, *] = new (Task ~> RIO[R, *]) {
        def apply[A](fa: Task[A]): RIO[R, A] = fa
      }

      val from: RIO[R, *] ~> Task = new (RIO[R, *] ~> Task) {
        def apply[A](fa: RIO[R, A]): Task[A] = fa.provideLayer(f(req))
      }

      route(req.mapK(to)).mapK(from).map(_.mapK(from))
    }

  /**
   * Utility function to create an http4s middleware that can extracts something from each request
   * and provide a layer to eliminate some part of the ZIO environment
   * @param route an http4s route
   * @param f a function from a request to a ZLayer
   * @tparam R the remaining environment
   * @tparam R1 the environment to eliminate
   * @return a new route that requires only R
   */
  def provideSomeLayerFromRequest[R, R1](
    route: HttpRoutes[RIO[R with R1, *]],
    f: Request[RIO[R, *]] => RLayer[R, R1]
  )(implicit tagged: Tag[R1]): HttpRoutes[RIO[R, *]] =
    Kleisli { (req: Request[RIO[R, *]]) =>
      val to: RIO[R, *] ~> RIO[R with R1, *] = new (RIO[R, *] ~> RIO[R with R1, *]) {
        def apply[A](fa: RIO[R, A]): RIO[R with R1, A] = fa
      }

      val from: RIO[R with R1, *] ~> RIO[R, *] = new (RIO[R with R1, *] ~> RIO[R, *]) {
        def apply[A](fa: RIO[R with R1, A]): RIO[R, A] = fa.provideSomeLayer[R](f(req))
      }

      route(req.mapK(to)).mapK(from).map(_.mapK(from))
    }

  /**
   * 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[F[_], R](
    endpoint: ServerEndpoint[Fs2Streams[F], RIO[R, *]]
  )(implicit interop: ToEffect[F, R]): ServerEndpoint[Fs2Streams[F], F] =
    ServerEndpoint[
      endpoint.SECURITY_INPUT,
      endpoint.PRINCIPAL,
      endpoint.INPUT,
      endpoint.ERROR_OUTPUT,
      endpoint.OUTPUT,
      Fs2Streams[F],
      F
    ](
      endpoint.endpoint,
      _ => a => interop.toEffect(endpoint.securityLogic(zioMonadError)(a)),
      _ => u => req => interop.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[_], R](
    endpoint: ServerEndpoint[ZioWebSockets, RIO[R, *]]
  )(implicit interop: CatsInterop[F, R], runtime: Runtime[R]): ServerEndpoint[Fs2Streams[F] with WebSockets, F] = {
    type Fs2Pipe = fs2.Pipe[F, GraphQLWSInput, Either[GraphQLWSClose, GraphQLWSOutput]]

    val e = endpoint
      .asInstanceOf[
        ServerEndpoint.Full[
          endpoint.SECURITY_INPUT,
          endpoint.PRINCIPAL,
          endpoint.INPUT,
          endpoint.ERROR_OUTPUT,
          (String, CalibanPipe),
          ZioWebSockets,
          RIO[R, *]
        ]
      ]

    ServerEndpoint[
      endpoint.SECURITY_INPUT,
      endpoint.PRINCIPAL,
      endpoint.INPUT,
      endpoint.ERROR_OUTPUT,
      (String, Fs2Pipe),
      Fs2Streams[F] with WebSockets,
      F
    ](
      e.endpoint
        .asInstanceOf[Endpoint[endpoint.SECURITY_INPUT, endpoint.INPUT, endpoint.ERROR_OUTPUT, (String, Fs2Pipe), Any]],
      _ => a => interop.toEffect(e.securityLogic(zioMonadError)(a)),
      _ =>
        u =>
          req =>
            interop.toEffect(
              e.logic(zioMonadError)(u)(req)
                .map(_.map { case (protocol, zioPipe) =>
                  import zio.stream.interop.fs2z._
                  (
                    protocol,
                    fs2InputStream =>
                      zioPipe(
                        fs2InputStream
                          .translate(interop.fromEffectK)
                          .toZStream()
                          .provideEnvironment(runtime.environment)
                      ).toFs2Stream
                        .translate(interop.toEffectK)
                  )
                })
            )
    )
  }

  private implicit def streamConstructor[F[_], R](implicit
    toEffect: ToEffect[F, R]
  ): StreamConstructor[Fs2Streams[F]#BinaryStream] =
    new StreamConstructor[Fs2Streams[F]#BinaryStream] {
      override def apply(stream: ZStream[Any, Throwable, Byte]): Fs2Streams[F]#BinaryStream =
        stream.toFs2Stream
          .translate(toEffect.toEffectK)
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy