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

caliban.interop.tapir.HttpUploadInterpreter.scala Maven / Gradle / Ivy

The newest version!
package caliban.interop.tapir

import caliban._
import caliban.interop.tapir.TapirAdapter._
import caliban.uploads.{ FileMeta, GraphQLUploadRequest, Uploads }
import sttp.capabilities.Streams
import sttp.model._
import sttp.shared.Identity
import sttp.tapir._
import sttp.tapir.model.ServerRequest
import sttp.tapir.server.ServerEndpoint
import zio._

import java.nio.charset.StandardCharsets
import scala.concurrent.Future

sealed trait HttpUploadInterpreter[-R, E] { self =>
  protected def endpoint[S](
    streams: Streams[S]
  ): PublicEndpoint[UploadRequest, TapirResponse, CalibanResponse[streams.BinaryStream], S]

  protected def executeRequest[BS](
    graphQLRequest: GraphQLRequest,
    serverRequest: ServerRequest
  )(implicit streamConstructor: StreamConstructor[BS]): ZIO[R, TapirResponse, CalibanResponse[BS]]

  private def parsePath(path: String): List[PathValue] = path.split('.').map(PathValue.parse).toList

  def serverEndpoint[R1 <: R, S](streams: Streams[S])(implicit
    streamConstructor: StreamConstructor[streams.BinaryStream]
  ): CalibanUploadsEndpoint[R1, streams.BinaryStream, S] = {
    def logic(request: UploadRequest): RIO[R1, Either[TapirResponse, CalibanResponse[streams.BinaryStream]]] = {
      val (parts, serverRequest) = request
      val partsMap               = parts.map(part => part.name -> part).toMap

      val io =
        for {
          rawOperations <- ZIO.fromOption(partsMap.get("operations")) orElseFail TapirResponse(StatusCode.BadRequest)
          request       <-
            JsonCodecs.requestCodec.rawDecode(new String(rawOperations.body, StandardCharsets.UTF_8)) match {
              case _: DecodeResult.Failure => ZIO.fail(TapirResponse(StatusCode.BadRequest))
              case DecodeResult.Value(v)   => ZIO.succeed(v)
            }
          rawMap        <- ZIO.fromOption(partsMap.get("map")) orElseFail TapirResponse(StatusCode.BadRequest)
          map           <- JsonCodecs.listMapCodec.rawDecode(new String(rawMap.body, StandardCharsets.UTF_8)) match {
                             case _: DecodeResult.Failure => ZIO.fail(TapirResponse(StatusCode.BadRequest))
                             case DecodeResult.Value(v)   => ZIO.succeed(v)
                           }
          filePaths      = map.map { case (key, value) => (key, value.map(parsePath).toList) }.toList
                             .flatMap(kv => kv._2.map(kv._1 -> _))
          handler        = Uploads.handler(handle =>
                             ZIO
                               .succeed(partsMap.get(handle))
                               .some
                               .flatMap(fp =>
                                 Random.nextUUID.asSomeError
                                   .map(uuid =>
                                     FileMeta(
                                       uuid.toString,
                                       fp.body,
                                       fp.contentType,
                                       fp.fileName.getOrElse(""),
                                       fp.body.length
                                     )
                                   )
                               )
                               .unsome
                           )
          uploadQuery    = GraphQLUploadRequest(request, filePaths, handler)
          query          = serverRequest.headers
                             .find(isFtv1Header)
                             .fold(uploadQuery.remap)(_ => uploadQuery.remap.withFederatedTracing)
          response      <- executeRequest(query, serverRequest)
                             .provideSomeLayer[R](ZLayer(uploadQuery.fileHandle))
        } yield response

      io.either
    }

    endpoint(streams).serverLogic(logic(_))
  }

  def serverEndpointFuture[S](streams: Streams[S])(runtime: Runtime[R])(implicit
    streamConstructor: StreamConstructor[streams.BinaryStream]
  ): ServerEndpoint[S, Future] = {
    implicit val r: Runtime[R] = runtime
    convertHttpEndpointToFuture[R, streams.BinaryStream, S, UploadRequest](serverEndpoint(streams))
  }

  def serverEndpointIdentity[S](streams: Streams[S])(runtime: Runtime[R])(implicit
    streamConstructor: StreamConstructor[streams.BinaryStream]
  ): ServerEndpoint[S, Identity] = {
    implicit val r: Runtime[R] = runtime
    convertHttpEndpointToIdentity[R, streams.BinaryStream, S, UploadRequest](serverEndpoint(streams))
  }

  def intercept[R1](interceptor: Interceptor[R1, R]): HttpUploadInterpreter[R1, E] =
    HttpUploadInterpreter.Intercepted(self, interceptor)

  def prependPath(path: List[String]): HttpUploadInterpreter[R, E] =
    HttpUploadInterpreter.Prepended(self, path)

  def configure[R1](configurator: Configurator[R1]): HttpUploadInterpreter[R & R1, E] =
    intercept[R & R1](ZLayer.scopedEnvironment[R & R1 & ServerRequest](configurator *> ZIO.environment[R]))
}

object HttpUploadInterpreter {
  private case class Base[R, E](interpreter: GraphQLInterpreter[R, E]) extends HttpUploadInterpreter[R, E] {
    def endpoint[S](
      streams: Streams[S]
    ): PublicEndpoint[UploadRequest, TapirResponse, CalibanResponse[streams.BinaryStream], S] =
      makeHttpUploadEndpoint(streams)

    def executeRequest[BS](
      graphQLRequest: GraphQLRequest,
      serverRequest: ServerRequest
    )(implicit streamConstructor: StreamConstructor[BS]): ZIO[R, TapirResponse, CalibanResponse[BS]] =
      interpreter.executeRequest(graphQLRequest).map(buildHttpResponse[E, BS](serverRequest))
  }

  private case class Intercepted[R1, R, E](
    interpreter: HttpUploadInterpreter[R, E],
    layer: ZLayer[R1 & ServerRequest, TapirResponse, R]
  ) extends HttpUploadInterpreter[R1, E] {
    override def intercept[R2](interceptor: Interceptor[R2, R1]): HttpUploadInterpreter[R2, E] =
      Intercepted[R2, R, E](interpreter, ZLayer.makeSome[R2 & ServerRequest, R](interceptor, layer))

    def endpoint[S](
      streams: Streams[S]
    ): PublicEndpoint[UploadRequest, TapirResponse, CalibanResponse[streams.BinaryStream], S] =
      interpreter.endpoint(streams)

    def executeRequest[BS](
      graphQLRequest: GraphQLRequest,
      serverRequest: ServerRequest
    )(implicit streamConstructor: StreamConstructor[BS]): ZIO[R1, TapirResponse, CalibanResponse[BS]] =
      interpreter.executeRequest(graphQLRequest, serverRequest).provideSome[R1](ZLayer.succeed(serverRequest), layer)
  }

  private case class Prepended[R, E](
    interpreter: HttpUploadInterpreter[R, E],
    path: List[String]
  ) extends HttpUploadInterpreter[R, E] {
    override def endpoint[S](
      streams: Streams[S]
    ): PublicEndpoint[UploadRequest, TapirResponse, CalibanResponse[streams.BinaryStream], S] = {
      val endpoints = interpreter.endpoint(streams)
      if (path.nonEmpty) {
        val p: List[EndpointInput[Unit]]   = path.map(stringToPath)
        val fixedPath: EndpointInput[Unit] = p.tail.foldLeft(p.head)(_ / _)

        endpoints.prependIn(fixedPath)
      } else {
        endpoints
      }
    }

    def executeRequest[BS](
      graphQLRequest: GraphQLRequest,
      serverRequest: ServerRequest
    )(implicit streamConstructor: StreamConstructor[BS]): ZIO[R, TapirResponse, CalibanResponse[BS]] =
      interpreter.executeRequest(graphQLRequest, serverRequest)
  }

  def apply[R, E](
    interpreter: GraphQLInterpreter[R, E]
  ): HttpUploadInterpreter[R, E] =
    Base(interpreter)

  def makeHttpUploadEndpoint[S](
    streams: Streams[S]
  ): PublicEndpoint[UploadRequest, TapirResponse, CalibanResponse[streams.BinaryStream], S] =
    endpoint.post
      .in(multipartBody)
      .in(extractFromRequest(identity))
      .out(header[MediaType](HeaderNames.ContentType))
      .out(statusCode)
      .out(header[Option[String]](HeaderNames.CacheControl))
      .out(outputBody(streams))
      .errorOut(errorBody)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy