Skip to content

Instantly share code, notes, and snippets.

@kciesielski
Created September 5, 2023 08:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kciesielski/96151668c66be7392d39072ab9f251f4 to your computer and use it in GitHub Desktop.
Save kciesielski/96151668c66be7392d39072ab9f251f4 to your computer and use it in GitHub Desktop.
Tapir + ZIO + interceptor
package com.softwaremill
import sttp.model.StatusCode
import sttp.monad.MonadError
import sttp.monad.syntax._
import sttp.tapir.AttributeKey
import sttp.tapir.server.interceptor.*
import sttp.tapir.server.interpreter.BodyListener
import sttp.tapir.server.model.ServerResponse
import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions}
import zio.http.*
import zio.logging.LogFormat
import zio.logging.backend.SLF4J
import zio.*
case class Permission(value: String)
case class PermissionError(msg: String)
type Eff[A] = RIO[SessionContext, A]
def enforcePermission(permission: Permission): ZIO[SessionContext, PermissionError, Unit] =
ZIO.succeed { println(s"Enforcing permission: $permission") }.flatMap(_ => ZIO.fail(PermissionError("Failed to resolve permissions!")))
class PermissionsEndpointInterceptor() extends EndpointInterceptor[Eff] {
override def apply[B](responder: Responder[Eff, B], decodeHandler: EndpointHandler[Eff, B]): EndpointHandler[Eff, B] =
new EndpointHandler[Eff, B] {
override def onDecodeSuccess[A, U, I](ctx: DecodeSuccessContext[Eff, A, U, I])(implicit
monad: MonadError[Eff],
bodyListener: BodyListener[Eff, B]
): Eff[ServerResponse[B]] = {
(ctx.endpoint
.attribute(AttributeKey[Permission]) match {
case None =>
ZIO.unit
case Some(endpointPermission) =>
enforcePermission(endpointPermission)
})
.flatMap(_ =>
decodeHandler
.onDecodeSuccess(ctx)
)
.catchSome {
case p: PermissionError =>
ZIO.succeed(ServerResponse[B](StatusCode.Unauthorized, Nil, None, None))
}
.orDieWith(permError => new RuntimeException(s"Unexpected permission error? $permError"))
}
override def onSecurityFailure[A](
ctx: SecurityFailureContext[Eff, A]
)(implicit monad: MonadError[Eff], bodyListener: BodyListener[Eff, B]): Eff[ServerResponse[B]] =
decodeHandler.onSecurityFailure(ctx)
override def onDecodeFailure(
ctx: DecodeFailureContext
)(implicit monad: MonadError[Eff], bodyListener: BodyListener[Eff, B]): Eff[Option[ServerResponse[B]]] =
decodeHandler.onDecodeFailure(ctx)
}
}
class SessionContext()
object Main extends ZIOAppDefault:
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = SLF4J.slf4j(LogLevel.Debug, LogFormat.default)
override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] =
val serverOptions: ZioHttpServerOptions[SessionContext] =
ZioHttpServerOptions.default[SessionContext].prependInterceptor(new PermissionsEndpointInterceptor())
val app: HttpApp[SessionContext, Throwable] = ZioHttpInterpreter(serverOptions).toHttp(Endpoints.all)
val port = sys.env.get("HTTP_PORT").flatMap(_.toIntOption).getOrElse(8080)
val logConfig = RequestHandlerMiddlewares.requestLogging(logRequestBody = true, logResponseBody = true)
val appWithLogging = app.withDefaultErrorResponse @@ logConfig
(
for
actualPort <- Server.serve(appWithLogging)
_ <- Console.printLine(s"Server started at http://localhost:${actualPort}. Press ENTER key to exit.")
_ <- Console.readLine
yield ()
).provide(
Server.defaultWithPort(port),
ZLayer.succeed(new SessionContext())
).exitCode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment