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

sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.scala Maven / Gradle / Ivy

The newest version!
package sttp.tapir.server.vertx.zio

import io.vertx.core.{Future, Handler, Promise}
import io.vertx.ext.web.{Route, Router, RoutingContext}
import sttp.capabilities.WebSockets
import sttp.capabilities.zio.ZioStreams
import sttp.tapir.server.interceptor.RequestResult
import sttp.tapir.server.interpreter.{BodyListener, ServerInterpreter}
import sttp.tapir.server.vertx.VertxBodyListener
import sttp.tapir.server.vertx.VertxErrorHandler
import sttp.tapir.server.vertx.decoders.{VertxRequestBody, VertxServerRequest}
import sttp.tapir.server.vertx.encoders.{VertxOutputEncoders, VertxToResponseBody}
import sttp.tapir.server.vertx.interpreters.{CommonServerInterpreter, FromVFuture, RunAsync}
import sttp.tapir.server.vertx.routing.PathMapping.extractRouteDefinition
import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.{RioFromVFuture, VertxFutureToRIO, ZioRunAsync}
import sttp.tapir.server.vertx.zio.streams._
import sttp.tapir.ztapir._
import zio._

import java.util.concurrent.atomic.AtomicReference

trait VertxZioServerInterpreter[R] extends CommonServerInterpreter with VertxErrorHandler {
  def vertxZioServerOptions[R2 <: R]: VertxZioServerOptions[R2] = VertxZioServerOptions.default[R].widen

  def route[R2](e: ZServerEndpoint[R2, ZioStreams with WebSockets])(implicit
      runtime: Runtime[R & R2]
  ): Router => Route = { router =>
    mountWithDefaultHandlers(e.widen)(router, extractRouteDefinition(e.endpoint), vertxZioServerOptions)
      .handler(endpointHandler(e))
  }

  private def endpointHandler[R2](
      e: ZServerEndpoint[R2, ZioStreams with WebSockets]
  )(implicit runtime: Runtime[R & R2]): Handler[RoutingContext] = {
    val fromVFuture = new RioFromVFuture[R & R2]
    implicit val monadError: RIOMonadError[R & R2] = new RIOMonadError[R & R2]
    implicit val bodyListener: BodyListener[RIO[R & R2, *], RoutingContext => Future[Void]] =
      new VertxBodyListener[RIO[R & R2, *]](new ZioRunAsync(runtime))
    val zioReadStream = zioReadStreamCompatible(vertxZioServerOptions[R & R2])
    val interpreter = new ServerInterpreter[ZioStreams with WebSockets, RIO[R & R2, *], RoutingContext => Future[Void], ZioStreams](
      _ => List(e.widen[R & R2]),
      new VertxRequestBody[RIO[R & R2, *], ZioStreams](vertxZioServerOptions[R & R2], fromVFuture)(zioReadStream),
      new VertxToResponseBody(vertxZioServerOptions)(zioReadStream),
      vertxZioServerOptions.interceptors,
      vertxZioServerOptions.deleteFile
    )
    new Handler[RoutingContext] {
      override def handle(rc: RoutingContext) = {
        val serverRequest = VertxServerRequest(rc)

        val result: ZIO[R & R2, Throwable, Any] =
          interpreter(serverRequest)
            .flatMap {
              // in vertx, endpoints are attempted to be decoded individually; if this endpoint didn't match - another one might
              case RequestResult.Failure(_) => ZIO.succeed(rc.next())
              case RequestResult.Response(response) =>
                ZIO.async((k: Task[Unit] => Unit) => {
                  VertxOutputEncoders(response)
                    .apply(rc)
                    .onComplete(d => {
                      if (d.succeeded()) k(ZIO.unit) else k(ZIO.fail(d.cause()))
                    })
                })
            }
            .catchAll { t => handleError(rc, t).asRIO }

        // we obtain the cancel token only after the effect is run, so we need to pass it to the exception handler
        // via a mutable ref; however, before this is done, it's possible an exception has already been reported;
        // if so, we need to use this fact to cancel the operation nonetheless
        val cancelRef = new AtomicReference[Option[Either[Throwable, FiberId => Exit[Throwable, Any]]]](None)

        rc.response.exceptionHandler { (t: Throwable) =>
          cancelRef.getAndSet(Some(Left(t))).collect { case Right(c) =>
            rc.vertx()
              .executeBlocking[Unit](
                (promise: Promise[Unit]) => {
                  c(FiberId.None)
                  promise.complete(())
                },
                false
              )
          }
          ()
        }

        val canceler: FiberId => Exit[Throwable, Any] = {
          Unsafe.unsafe(implicit u => {
            val value = runtime.unsafe.fork(ZIO.yieldNow *> result)
            (fiberId: FiberId) => runtime.unsafe.run(value.interruptAs(fiberId)).flattenExit
          })
        }

        cancelRef.getAndSet(Some(Right(canceler))).collect { case Left(_) =>
          rc.vertx()
            .executeBlocking[Unit](
              (promise: Promise[Unit]) => {
                canceler(FiberId.None)
                promise.complete(())
              },
              false
            )
        }

        ()
      }
    }
  }
}

object VertxZioServerInterpreter {
  def apply[R](serverOptions: VertxZioServerOptions[R] = VertxZioServerOptions.default[R]): VertxZioServerInterpreter[R] = {
    new VertxZioServerInterpreter[R] {
      override def vertxZioServerOptions[R2 <: R]: VertxZioServerOptions[R2] = serverOptions.widen[R2]
    }
  }

  private[vertx] class RioFromVFuture[R] extends FromVFuture[RIO[R, *]] {
    def apply[T](f: => Future[T]): RIO[R, T] = f.asRIO
  }

  private[vertx] class ZioRunAsync[R](runtime: Runtime[R]) extends RunAsync[RIO[R, *]] {
    override def apply[T](f: => RIO[R, T]): Unit = Unsafe.unsafe(implicit u => {
      runtime.unsafe.runToFuture(f)
    })
  }

  implicit class VertxFutureToRIO[A](f: => Future[A]) {
    def asRIO[R]: RIO[R, A] = {
      ZIO.async { cb =>
        f.onComplete { handler =>
          if (handler.succeeded()) {
            cb(ZIO.succeed(handler.result()))
          } else {
            cb(ZIO.fail(handler.cause()))
          }
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy