Search code examples
scalahttp4szio

Running http4s server with ZIO Env


Trying to learn using ZIO library, so I decided to create a basic web service app. Idea pretty basic, use http4s lib for server and route endpoints, print "hello world" on endpoint call.

With the help of docs and examples I found, produces code:

object Main extends ManagedApp {

  type AppEnvironment = Clock with Console with HelloRepository
  type AppTask[A] = RIO[AppEnvironment, A]

  override def run(args: List[String]): ZManaged[ZEnv, Nothing, Int] = {
    val httpApp: HttpApp[AppTask] = Router[AppTask]("/" -> helloWorldService).orNotFound

    val server = ZIO.runtime[AppEnvironment].flatMap { implicit rts =>
      BlazeServerBuilder[AppTask]
        .bindHttp(8080, "0.0.0.0")
        .withHttpApp(CORS(httpApp))
        .serve
        .compile[AppTask, AppTask, ExitCode]
        .drain
    }

    (for {
      _ <- ZManaged.environment[ZEnv] >>> server.toManaged_
    } yield ())
      .foldM(err => putStrLn(s"Execution failed with: $err").as(1).toManaged_, _ => ZManaged.succeed(0))
  }

  val dsl: Http4sDsl[AppTask] = Http4sDsl[AppTask]
  import dsl._

  val helloWorldService: HttpRoutes[AppTask] = HttpRoutes.of[AppTask] {
    case GET -> Root / "hello" / name => Ok(Repo.getHello(name))
  }
}

trait HelloRepository extends Serializable {
  val helloRepository: HelloRepository.Service[Any]
}

object HelloRepository extends Serializable {
  trait Service[R] extends Serializable {
    def getHello(name: String): ZIO[R, Nothing, String]
  }
}

object Repo extends HelloRepository.Service[HelloRepository] {
  override def getHello(name: String): ZIO[HelloRepository, Nothing, String] = ZIO.succeed(s"Hello $name")
}
  • I create router: Router[AppTask]("/" ...
  • I create server: ZIO.runtime[AppEnvironment].flatMap ...
  • Then trying to start server with ZIO enviroment, but something I am missing as this line: _ <- ZManaged.environment[ZEnv] >>> server.toManaged_ is incorected, and throws error on build: Error:(34, 39) inferred type arguments [touch.Main.AppEnvironment,Throwable,Unit] do not conform to method >>>'s type parameter bounds [R1 >: zio.ZEnv,E1,B] _ <- ZManaged.environment[ZEnv] >>> server.toManaged_

Error:(34, 39) inferred type arguments [touch.Main.AppEnvironment,Throwable,Unit] do not conform to method >>>'s type parameter bounds [R1 >: zio.ZEnv,E1,B]

Error:(34, 50) type mismatch; found : zio.ZManaged[touch.Main.AppEnvironment,Throwable,Unit] (which expands to) zio.ZManaged[zio.clock.Clock with zio.console.Console with touch.HelloRepository,Throwable,Unit] required: zio.ZManaged[R1,E1,B]

maybe someone can help me with the correct syntax? also would appriacete some explanation, or link to docs, where this is explained.


Solution

  • I would like to explain more but I don't know where you got your code sample or what your build.sbt looks like but I happen to have some http4s code lying around so I took the liberty of adding some import statements and simplifying it a bit. You can always add back the complexity I took out.

    Here's what worked for me.

    /tmp/http4s/test.scala

    import org.http4s.implicits._
    import org.http4s.server.blaze._
    import org.http4s.server.Router
    import org.http4s.server.middleware.CORS
    
    import org.http4s._
    import org.http4s.dsl.Http4sDsl
    
    import zio._
    import zio.clock._
    import zio.console._
    import zio.interop.catz._
    
    trait HelloRepository
    {
      def getHello(name: String): ZIO[AppEnvironment, Nothing, String]
    }
    
    trait AppEnvironment extends Console with Clock
    {
      val helloRepository: HelloRepository
    }
    
    object Main extends App {
    
      type AppTask[A] = RIO[AppEnvironment, A]
    
      val dsl: Http4sDsl[AppTask] = Http4sDsl[AppTask]
      import dsl._
    
      val httpApp: HttpApp[AppTask] = Router[AppTask](
        "/" -> HttpRoutes.of[AppTask] {
          case GET -> Root / "hello" / name => Ok( ZIO.accessM[AppEnvironment](_.helloRepository.getHello(name)) )
        }
      ).orNotFound
    
      val program = for {
        server <- ZIO.runtime[AppEnvironment]
        .flatMap {
          implicit rts =>
            BlazeServerBuilder[AppTask]
              .bindHttp(8080, "0.0.0.0")
              .withHttpApp(CORS(httpApp))
              .serve
              .compile
              .drain
        }
      } yield server
    
      val runEnv = new AppEnvironment with Console.Live with Clock.Live
      {
        val helloRepository = new HelloRepository
        {
          def getHello(name: String): ZIO[AppEnvironment, Nothing, String] = ZIO.succeed(s"Hello $name")
        }
      }
    
      def run(args: List[String]) =
        program
          .provide(runEnv)
          .foldM(err => putStrLn(s"Execution failed with: $err") *> ZIO.succeed(1), _ => ZIO.succeed(0))
    }
    

    /tmp/http4s/build.sbt

    val Http4sVersion       = "0.20.0"
    val CatsVersion         = "2.0.0"
    val ZioCatsVersion      = "2.0.0.0-RC3"
    val ZioVersion          = "1.0.0-RC13"
    val LogbackVersion      = "1.2.3"
    
    lazy val root = (project in file("."))
      .settings(
        organization := "example",
        name := "example",
        version := "0.0.1-SNAPSHOT",
        scalaVersion := "2.12.8",
        scalacOptions ++= Seq("-Ypartial-unification"),
        libraryDependencies ++= Seq(
          "org.typelevel"              %% "cats-effect"         % CatsVersion,
          "dev.zio"                    %% "zio"                 % ZioVersion,
          "dev.zio"                    %% "zio-interop-cats"    % ZioCatsVersion,
          "org.http4s"                 %% "http4s-blaze-server" % Http4sVersion,
          "org.http4s"                 %% "http4s-dsl"          % Http4sVersion,
          "ch.qos.logback"             %  "logback-classic"     % LogbackVersion,
       ),
        addCompilerPlugin("org.spire-math" %% "kind-projector"     % "0.9.6"),
        addCompilerPlugin("com.olegpy"     %% "better-monadic-for" % "0.2.4")
      )
    
    scalacOptions ++= Seq(
      "-deprecation",               // Emit warning and location for usages of deprecated APIs.
      "-encoding", "UTF-8",         // Specify character encoding used by source files.
      "-language:higherKinds",      // Allow higher-kinded types
      "-language:postfixOps",       // Allows operator syntax in postfix position (deprecated since Scala 2.10)
      "-feature",                   // Emit warning and location for usages of features that should be imported explicitly.
      "-Ypartial-unification",      // Enable partial unification in type constructor inference
      "-Xfatal-warnings",           // Fail the compilation if there are any warnings
    )
    

    sample execution

    bash-3.2$ cd /tmp/http4s
    bash-3.2$ sbt
    ...
    sbt:example> compile
    ...
    [info] Done compiling.
    [success] Total time: 5 s, completed Oct 24, 2019 11:20:53 PM
    sbt:example> run
    ...
    [info] Running Main 
    23:21:03.720 [zio-default-async-1-163838348] INFO org.http4s.blaze.channel.nio1.NIO1SocketServerGroup - Service bound to address /0:0:0:0:0:0:0:0:8080
    23:21:03.725 [blaze-selector-0] DEBUG org.http4s.blaze.channel.nio1.SelectorLoop - Channel initialized.
    23:21:03.732 [zio-default-async-1-163838348] INFO org.http4s.server.blaze.BlazeServerBuilder - 
      _   _   _        _ _
     | |_| |_| |_ _ __| | | ___
     | ' \  _|  _| '_ \_  _(_-<
     |_||_\__|\__| .__/ |_|/__/
                 |_|
    23:21:03.796 [zio-default-async-1-163838348] INFO org.http4s.server.blaze.BlazeServerBuilder - http4s v0.20.0 on blaze v0.14.0 started at http://[0:0:0:0:0:0:0:0]:8080/
    23:21:11.070 [blaze-selector-1] DEBUG org.http4s.blaze.channel.nio1.SelectorLoop - Channel initialized.
    23:21:11.070 [blaze-selector-1] DEBUG org.http4s.blaze.channel.nio1.NIO1HeadStage - Starting up.
    23:21:11.070 [blaze-selector-1] DEBUG org.http4s.blaze.channel.nio1.NIO1HeadStage - Stage NIO1HeadStage sending inbound command: Connected
    23:21:11.070 [blaze-selector-1] DEBUG org.http4s.server.blaze.Http1ServerStage$$anon$1 - Starting HTTP pipeline
    23:21:11.072 [blaze-selector-1] DEBUG org.http4s.blazecore.IdleTimeoutStage - Starting idle timeout stage with timeout of 30000 ms
    

    At this point after opening http://localhost:8080/hello/there I observed the expected output in the browser.

    Hope this helps.