scalascala-3ziotapirzio-http

zio-http and Tapir: provide layers globally


Hello I'm starting a project with ZIO, zio-http, Tapir and Scala 3, I'm configuring ZLayers:

my routes:

object DeckRoute:

  val deckRoutes: List[ZServerEndpoint[Any, Any]] =
    List(
      listRoute(),
      findByIdRoute(),
      insertRoute(),
      updateRoute(),
      deleteRoute()
    )

  private def listRoute(): ZServerEndpoint[Any, Any] =
    def listRouteLogic() =
      ListDeck
        .list()
        .mapBoth(
          _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
          d => d.map(_.into[DeckListResponse].transform)
        )
        .either
        .provideLayer(appEnvironment)

    DeckEndpoint.listEndpoint.serverLogic(_ => listRouteLogic())

  private def findByIdRoute(): ZServerEndpoint[Any, Any] =
    def findByIdRouteLogic(id: Long) =
      FindByIdDeck
        .findById(id)
        .mapBoth(
          _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
          _.into[DeckDetailsResponse].transform
        )
        .either
        .provideLayer(appEnvironment)

    DeckEndpoint.findByIdEndpoint.serverLogic(p => findByIdRouteLogic(p))

  private def insertRoute(): ZServerEndpoint[Any, Any] =
    def insertRouteLogic(request: DeckInsertRequest) =
      InsertDeck
        .insert(request.into[InsertDeckDomain].transform)
        .mapBoth(
          _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
          _.into[DeckInsertedResponse].transform
        )
        .either
        .provideLayer(appEnvironment)

    DeckEndpoint.insertEndpoint.serverLogic(p => insertRouteLogic(p))

  private def updateRoute(): ZServerEndpoint[Any, Any] =
    def updateRouteLogic(id: Long, request: DeckUpdateRequest) =
      UpdateDeck
        .update(
          request.into[UpdateDeckDomain].withFieldConst(_.id, id).transform
        )
        .mapBoth(
          _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
          _.into[DeckUpdatedResponse].transform
        )
        .either
        .provideLayer(appEnvironment)

    DeckEndpoint.updateEndpoint.serverLogic(p => updateRouteLogic(p._1, p._2))

  private def deleteRoute(): ZServerEndpoint[Any, Any] =
    def deleteRouteLogic(id: Long) =
      DeleteDeck
        .delete(id)
        .orElseFail(DeckError.GenericError("", "", 500, OffsetDateTime.now()))
        .either
        .provideLayer(appEnvironment)

    DeckEndpoint.deleteEndpoint.serverLogic(p => deleteRouteLogic(p))

my layers config

object Environment:

  type AppType = ListDeck & FindByIdDeck & InsertDeck & UpdateDeck & DeleteDeck

  val appEnvironment: ZLayer[Any, Nothing, AppType] =
    ZLayer.make[AppType](DeckService.layer, DeckQueryService.layer)

my main class

object App extends ZIOAppDefault:

  private val routes: Routes[Any, Response] =
    ZioHttpInterpreter().toHttp(swaggerEndpoints ++ deckRoutes)

  val run: ZIO[Any, Throwable, Nothing] = Server
    .serve(routes)
    .provide(
      ZLayer.succeed(Server.Config.default.port(8080)),
      Server.live
    )

Im my DeckRoutes I defined for each route .provideLayer(appEnvironment). I wanted do use a global config to define my layers insteade of set for each endpoint, it's possible ?

my dependencies:

ThisBuild / scalaVersion     := "3.3.4"
ThisBuild / version          := "0.1.0-SNAPSHOT"
ThisBuild / organization     := "br.com.flashcards"
ThisBuild / organizationName := "zio-flashcards"

val tapirVersion = "1.11.9"
val zioVersion = "2.1.13"
val circeVersion = "0.14.10"
val catsVersion = "2.12.0"

lazy val root = (project in file("."))
  .settings(
    name := "zio-flashcards",
    libraryDependencies ++= Seq(
      "dev.zio" %% "zio" % zioVersion,
      "dev.zio" %% "zio-http" % "3.0.1",
      "com.softwaremill.sttp.tapir" %% "tapir-zio" % tapirVersion,
      "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion,
      "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
      "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
      "com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % tapirVersion,
      "io.circe" %% "circe-core" % circeVersion,
      "io.circe" %% "circe-generic" % circeVersion,
      "io.circe" %% "circe-parser" % circeVersion,
      "io.scalaland" %% "chimney" % "1.5.0",
      "dev.zio" %% "zio-test" % zioVersion % Test
    ),
    testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")
  )

Solution

  • To solve my problem I follow a example shared by @Gastón Schabas, I transformed my route object in a Class, create a ZLayer and insert it in a ZIO provide in a Main class :

    package br.com.flashcards
    
    import br.com.flashcards.adapter.endpoint.DeckEndpoint
    import br.com.flashcards.config.EndpointConfig
    import br.com.flashcards.core.service.impl.DeckService
    import br.com.flashcards.core.service.query.impl.DeckQueryService
    import sttp.tapir.server.interceptor.cors.CORSConfig.AllowedOrigin
    import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor}
    import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions}
    import zio.*
    import zio.http.*
    
    object App extends ZIOAppDefault:
    
      override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] =
        val options: ZioHttpServerOptions[Any] =
          ZioHttpServerOptions.customiseInterceptors
            .corsInterceptor(
              CORSInterceptor.customOrThrow(
                CORSConfig.default.copy(
                  allowedOrigin = AllowedOrigin.All
                )
              )
            )
            .options
    
        (for {
          endpoints <- ZIO.service[EndpointConfig]
          httpApp = ZioHttpInterpreter(options).toHttp(endpoints.endpoints)
          actualPort <- Server.install(httpApp)
          _ <- Console.printLine(s"Application zio-flashcards started")
          _ <- Console.printLine(
            s"Go to http://localhost:8080/docs to open SwaggerUI"
          )
          _ <- ZIO.never
        } yield ())
          .provide(
            EndpointConfig.layer,
            DeckRoute.layer,
            DeckService.layer,
            DeckQueryService.layer,
            Server.defaultWithPort(8080)
          )
          .exitCode
    
    

    my route. Obs: I refactored my traits with insert, update, delete and find in two traits, Read and Write Traits

    package br.com.flashcards.adapter.endpoint
    
    import br.com.flashcards.adapter.endpoint.doc.DeckDocEndpoint
    import br.com.flashcards.adapter.endpoint.request.{
      DeckInsertRequest,
      DeckUpdateRequest
    }
    import br.com.flashcards.adapter.endpoint.response.error.DeckError
    import br.com.flashcards.adapter.endpoint.response.{
      DeckDetailsResponse,
      DeckInsertedResponse,
      DeckListResponse,
      DeckUpdatedResponse
    }
    import br.com.flashcards.core.exception.DeckException
    import br.com.flashcards.core.service.query.DeckRead
    import br.com.flashcards.core.service.{
      DeckWrite,
      InsertDeckDomain,
      UpdateDeckDomain
    }
    import io.scalaland.chimney.dsl.*
    import sttp.tapir.ztapir.*
    import zio.*
    
    import java.time.OffsetDateTime
    
    case class DeckEndpoint(
        write: DeckWrite,
        read: DeckRead
    ):
    
      val endpoints: List[ZServerEndpoint[Any, Any]] =
        List(
          listRoute(),
          findByIdRoute(),
          insertRoute(),
          updateRoute(),
          deleteRoute()
        )
    
      private def listRoute(): ZServerEndpoint[Any, Any] =
        def listRouteLogic() =
          read
            .list()
            .mapBoth(
              _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
              d => d.map(_.into[DeckListResponse].transform)
            )
    
        DeckDocEndpoint.listEndpoint.zServerLogic(_ => listRouteLogic())
    
      private def findByIdRoute(): ZServerEndpoint[Any, Any] =
        def findByIdRouteLogic(
            id: Long
        ) =
          read
            .findById(id)
            .mapBoth(
              _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
              _.into[DeckDetailsResponse].transform
            )
    
        DeckDocEndpoint.findByIdEndpoint.zServerLogic(p => findByIdRouteLogic(p))
    
      private def insertRoute(): ZServerEndpoint[Any, Any] =
        def insertRouteLogic(
            request: DeckInsertRequest
        ) =
          write
            .insert(request.into[InsertDeckDomain].transform)
            .mapBoth(
              _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
              _.into[DeckInsertedResponse].transform
            )
    
        DeckDocEndpoint.insertEndpoint.zServerLogic(p => insertRouteLogic(p))
    
      private def updateRoute(): ZServerEndpoint[Any, Any] =
        def updateRouteLogic(
            id: Long,
            request: DeckUpdateRequest
        ) =
          write
            .update(
              request.into[UpdateDeckDomain].withFieldConst(_.id, id).transform
            )
            .mapBoth(
              _ => DeckError.GenericError("", "", 500, OffsetDateTime.now()),
              _.into[DeckUpdatedResponse].transform
            )
    
        DeckDocEndpoint.updateEndpoint.zServerLogic(p =>
          updateRouteLogic(p._1, p._2)
        )
    
      private def deleteRoute(): ZServerEndpoint[Any, Any] =
        def deleteRouteLogic(
            id: Long
        ) =
          write
            .delete(id)
            .orElseFail(DeckError.GenericError("", "", 500, OffsetDateTime.now()))
    
        DeckDocEndpoint.deleteEndpoint.zServerLogic(p => deleteRouteLogic(p))
    
    object DeckRoute:
    
      val layer: ZLayer[
        DeckWrite & DeckRead,
        DeckException,
        DeckRoute
      ] = ZLayer.fromFunction(DeckEndpoint(_, _))
    
    

    Thank you :)