diff --git a/build.sbt b/build.sbt index 36490ac34b..ebaa132997 100644 --- a/build.sbt +++ b/build.sbt @@ -124,6 +124,6 @@ lazy val zhttpTest = (project in file("zio-http-test")) lazy val example = (project in file("./example")) .settings(stdSettings("example")) .settings(publishSetting(false)) - .settings(runSettings("example.FileStreaming")) + .settings(runSettings("example.Main")) .settings(libraryDependencies ++= Seq(`jwt-core`)) .dependsOn(zhttp) diff --git a/docs/website/docs/v1.x/dsl/server/config.md b/docs/website/docs/v1.x/dsl/server/config.md deleted file mode 100644 index 50b2454a71..0000000000 --- a/docs/website/docs/v1.x/dsl/server/config.md +++ /dev/null @@ -1,3 +0,0 @@ -# Config - -Work in progress \ No newline at end of file diff --git a/docs/website/docs/v1.x/dsl/server/index.md b/docs/website/docs/v1.x/dsl/server/index.md new file mode 100644 index 0000000000..eff8c9feff --- /dev/null +++ b/docs/website/docs/v1.x/dsl/server/index.md @@ -0,0 +1,109 @@ +# ZIO HTTP Server + +This section describes, ZIO HTTP Server and different configurations you can provide while creating the Server + +## Start a ZIO HTTP Server with default configurations +```scala + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + Server.start(8090, app.silent).exitCode +``` +## Start a ZIO HTTP Server with custom configurations. +1. Imports required by the customised server. + ```scala + import zhttp.http._ + import zhttp.service.server.ServerChannelFactory + import zhttp.service.{EventLoopGroup, Server} + import zio._ + import scala.util.Try + ``` +2. The Server can be built incrementally with a `++` each returning a new Server overriding any default configuration. (More properties are given in the [Server Configurations](#server-configurations) section below.) + ```scala + private val server = + Server.port(PORT) ++ // Setup port + Server.maxRequestSize(8 * 1024) ++ // handle max request size of 8 KB (default 4 KB) + Server.app(fooBar ++ app) // Setup the Http app + ``` +3. And then use ```Server.make``` to get a "managed" instance use it to run a server forever + ```scala + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { + server.make + .use(start => + console.putStrLn(s"Server started on port ${start.port}") + *> ZIO.never, + ).provideCustomLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(2)) + .exitCode + ``` + **Tip :** `ServerChannelFactory.auto ++ EventLoopGroup.auto(num Threads)` is supplied as an external dependency to choose netty transport type. One can leave it as `auto` to let the application handle it for you. + Also in `EventLoopGroup.auto(numThreads)` you can choose number of threads based on number of available processors. + +### Binding Server to a socket address +One can bind server to Inet address in multiple ways, either by providing a port number or +- If no port is provided, the default port is 8080 +- If specified port is 0, it will use a dynamically selected port. + +
+A complete example + +- Example below shows how the server can be started in forever mode to serve HTTP requests: + +```scala +import zhttp.http._ +import zhttp.service._ +import zhttp.service.server.ServerChannelFactory +import zio._ + +import scala.util.Try + +object HelloWorldAdvanced extends App { + // Set a port + private val PORT = 8090 + + private val fooBar: HttpApp[Any, Nothing] = Http.collect[Request] { + case Method.GET -> !! / "foo" => Response.text("bar") + case Method.GET -> !! / "bar" => Response.text("foo") + } + + private val app = Http.collectM[Request] { + case Method.GET -> !! / "random" => random.nextString(10).map(Response.text) + case Method.GET -> !! / "utc" => clock.currentDateTime.map(s => Response.text(s.toString)) + } + + private val server = + Server.port(PORT) ++ // Setup port + Server.paranoidLeakDetection ++ // Paranoid leak detection (affects performance) + Server.app(fooBar +++ app) // Setup the Http app + + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { + // Configure thread count using CLI + val nThreads: Int = args.headOption.flatMap(x => Try(x.toInt).toOption).getOrElse(0) + + // Create a new server + server.make + .use(_ => + // Waiting for the server to start + console.putStrLn(s"Server started on port $PORT") + + // Ensures the server doesn't die after printing + *> ZIO.never, + ) + .provideCustomLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(nThreads)) + .exitCode + } +} + ``` +
+ +## Server Configurations + +| **Configuration** | **Purpose and usage** | +| ----------- | ----------- | +| `Server.app(httpApp)` | Mount routes. Refer to complete example above | +| `Server.maxRequestSize(8 * 1024)` | handle max request size of 8 KB (default 4 KB) | +| `Server.port(portNum)` or `Server.bind(portNum)` | Bind server to the port, refer to examples above | +| `Server.ssl(sslOptions)` | Creates a new server with ssl options. [HttpsHelloWorld](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/example/HttpsHelloWorld.scala) | +| `Server.acceptContinue` | Sends a [100 CONTINUE](https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3) | +| `Server.disableFlowControl` | Refer [Netty FlowControlHandler](https://netty.io/4.1/api/io/netty/handler/flow/FlowControlHandler.html) | +| `Server.disableLeakDetection` | Disable any leak detection Refer netty's [ResourceLeakDetector](https://netty.io/4.0/api/io/netty/util/ResourceLeakDetector.Level.html) | +| `Server.simpleLeakDetection` | Simplistic leak detection comes with small over head. Refer netty's [ResourceLeakDetector](https://netty.io/4.0/api/io/netty/util/ResourceLeakDetector.Level.html) | +| `Server.paranoidLeakDetection` | Comes with highest possible overhead (for testing purposes only). Refer netty's [ResourceLeakDetector](https://netty.io/4.0/api/io/netty/util/ResourceLeakDetector.Level.html) | +| `Server.consolidateFlush` | Flushing content is done in batches. Can potentially improve performance. | diff --git a/docs/website/docs/v1.x/getting-started.md b/docs/website/docs/v1.x/getting-started.md index e3a09048b8..2d6e72ab8d 100644 --- a/docs/website/docs/v1.x/getting-started.md +++ b/docs/website/docs/v1.x/getting-started.md @@ -4,20 +4,30 @@ sidebar_position: 2 # Getting Started +**ZIO HTTP** is a powerful library that is used to build highly performant HTTP-based services and clients using functional scala and ZIO and uses [Netty](https://netty.io/) as its core. +ZIO HTTP has powerful functional domains which help in creating, modifying, composing apps easily. Let's start with the HTTP domain. +The first step when using ZIO HTTP is creating an HTTP app. + ## Http +`Http` is a domain that models HTTP apps using ZIO and works over any request and response types. `Http` Domain provides different constructors to create HTTP apps, `Http.text`, `Http.html`, `Http.fromFile`, `Http.fromData`, `Http.fromStream`, `Http.fromEffect`. + ### Creating a "_Hello World_" app +Creating an HTTP app using ZIO Http is as simple as given below, this app will always respond with "Hello World!" + ```scala import zhttp.http._ val app = Http.text("Hello World!") ``` - -An application can be made using any of the available operators on `zhttp.Http`. In the above program for any Http request, the response is always `"Hello World!"`. +An app can be made using any of the available constructors on `zhttp.Http`. ### Routing + For handling routes, Http Domain has a `collect` method that, accepts different requests and produces responses. Pattern matching on the route is supported by the framework +The example below shows how to create routes: + ```scala import zhttp.http._ @@ -26,34 +36,56 @@ val app = Http.collect[Request] { case Method.GET -> !! / "fruits" / "b" => Response.text("Banana") } ``` - -Pattern matching on route is supported by the framework +You can create typed routes as well. The below example shows how to accept count as `Int` only. + ```scala + import zhttp.http._ + + val app = Http.collect[Request] { + case Method.GET -> !! / "Apple" / int(count) => Response.text(s"Apple: $count") + } + ``` ### Composition +Apps can be composed using operators in `Http`: + +- Using the `++` operator. The way it works is, if none of the routes match in `a`, then the control is passed on to the `b` app. + +```scala + import zhttp.http._ + + val a = Http.collect[Request] { case Method.GET -> !! / "a" => Response.ok } + val b = Http.collect[Request] { case Method.GET -> !! / "b" => Response.ok } + + val app = a ++ b + ``` + + +- Using the `<>` operator. The way it works is, if `a` fails, then the control is passed on to the `b` app. + ```scala import zhttp.http._ -val a = Http.collect[Request] { case Method.GET -> !! / "a" => Response.ok } -val b = Http.collect[Request] { case Method.GET -> !! / "b" => Response.ok } +val a = Http.fail(new Error("SERVER_ERROR")) +val b = Http.text("OK") val app = a <> b ``` -Apps can be composed using the `<>` operator. The way it works is, if none of the routes match in `a` , or a `NotFound` error is thrown from `a`, and then the control is passed on to the `b` app. - ### ZIO Integration +For creating effectful apps, you can use `collectZIO` and wrap `Response` using `wrapZIO` to produce ZIO effect value. + ```scala val app = Http.collectZIO[Request] { case Method.GET -> !! / "hello" => Response.text("Hello World").wrapZIO } ``` -`Http.collectZIO` allow routes to return a ZIO effect value. - ### Accessing the Request +To access the request use `@` as it binds a matched pattern to a variable and can be used while creating a response. + ```scala import zhttp.http._ @@ -67,29 +99,36 @@ val app = Http.collectZIO[Request] { ### Testing -zhttp provides a `zhttp-test` package for use in unit tests. You can utilize it as follows: +Since `Http` is a function of the form `A => ZIO[R, Option[E], B]` to test it you can simply call an `Http` like a function. ```scala import zio.test._ -import zhttp.test._ import zhttp.http._ object Spec extends DefaultRunnableSpec { - + def spec = suite("http")( testM("should be ok") { val app = Http.ok val req = Request() - assertM(app(req))(equalTo(Response.ok)) // an apply method is added via `zhttp.test` package + assertM(app(req))(equalTo(Response.ok)) } ) } ``` +When we call the `app` with the `request` it calls the apply method of `Http` via `zhttp.test` package ## Socket +`Socket` is functional domain in ZIO HTTP. It provides constructors to create socket apps. +A socket app is an app that handles WebSocket connections. + ### Creating a socket app +Socket app can be created by using `Socket` constructors. To create a socket app, you need to create a socket that accepts `WebSocketFrame` and produces `ZStream` of `WebSocketFrame`. +Finally, we need to convert socketApp to `Response` using `toResponse`, so that we can run it like any other HTTP app. +The below example shows a simple socket app, we are using `collect` which returns a stream with WebsSocketTextFrame "BAR" on receiving WebsSocketTextFrame "FOO". + ```scala import zhttp.socket._ @@ -105,7 +144,12 @@ private val socket = Socket.collect[WebSocketFrame] { case WebSocketFrame.Text(" ## Server -### Starting an Http App +As we have seen how to create HTTP apps, the only thing left is to run an HTTP server and serve requests. +ZIO HTTP provides a way to set configurations for your server. The server can be configured according to the leak detection level, request size, address etc. + +### Starting an HTTP App + +To launch our app, we need to start the server on a port. The below example shows a simple HTTP app that responds with empty content and a `200` status code, deployed on port `8090` using `Server.start`. ```scala import zhttp.http._ @@ -120,14 +164,12 @@ object HelloWorld extends App { } ``` -A simple Http app that responds with empty content and a `200` status code is deployed on port `8090` using `Server.start`. - ## Examples -- [Simple Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/HelloWorld.scala) -- [Advanced Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/HelloWorldAdvanced.scala) -- [WebSocket Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/SocketEchoServer.scala) -- [Streaming Response](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/StreamingResponse.scala) -- [Simple Client](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/SimpleClient.scala) -- [File Streaming](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/FileStreaming.scala) -- [Authentication](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/Authentication.scala) +- [Simple Server](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/hello-world) +- [Advanced Server](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/hello-world-advanced) +- [WebSocket Server](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/web-socket) +- [Streaming Response](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/stream-response) +- [Simple Client](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/simple-client) +- [File Streaming](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/stream-file) +- [Authentication](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/authentication) diff --git a/docs/website/docs/v1.x/index.md b/docs/website/docs/v1.x/index.md index 5819dcca26..1523440356 100644 --- a/docs/website/docs/v1.x/index.md +++ b/docs/website/docs/v1.x/index.md @@ -4,4 +4,23 @@ sidebar_label: "Setup" --- # Setup -Work in progress \ No newline at end of file + +In this guide, you'll learn how to get started with a new zio-http project. + +Before we dive in, make sure that you have the following on your computer: + +* JDK 1.8 or higher +* sbt (scalaVersion >= 2.12) + +## As a dependency + +To use zio-http, add the following dependencies in your project: + +```scala +val ZHTTPVersion = "1.0.0.0-RC23" + +libraryDependencies ++= Seq( + "io.d11" %% "zhttp" % ZHTTPVersion, + "io.d11" %% "zhttp-test" % ZHTTPVersion % Test +) +``` diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 01979e7170..6e5999d2d0 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -3,7 +3,7 @@ import sbt._ object Dependencies { val JwtCoreVersion = "9.0.3" val NettyVersion = "4.1.73.Final" - val NettyIncubatorVersion = "0.0.11.Final" + val NettyIncubatorVersion = "0.0.12.Final" val ScalaCompactCollectionVersion = "2.6.0" val ZioVersion = "1.0.13" val SttpVersion = "3.3.18" diff --git a/zio-http/src/main/scala/zhttp/http/Http.scala b/zio-http/src/main/scala/zhttp/http/Http.scala index da986539c8..566ec45fa7 100644 --- a/zio-http/src/main/scala/zhttp/http/Http.scala +++ b/zio-http/src/main/scala/zhttp/http/Http.scala @@ -102,6 +102,11 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => ): Http[R1, E1, A1, C] = self >>> Http.collectZIO(pf) + final def collectManaged[R1 <: R, E1 >: E, A1 <: A, B1 >: B, C]( + pf: PartialFunction[B1, ZManaged[R1, E1, C]], + ): Http[R1, E1, A1, C] = + self >>> Http.collectManaged(pf) + /** * Named alias for `<<<` */ @@ -465,6 +470,11 @@ object Http { */ def collectZIO[A]: Http.PartialCollectZIO[A] = Http.PartialCollectZIO(()) + /** + * Creates an Http app which accepts a request and produces response from a managed resource + */ + def collectManaged[A]: Http.PartialCollectManaged[A] = Http.PartialCollectManaged(()) + /** * Combines multiple Http apps into one */ @@ -619,6 +629,11 @@ object Http { Http.collect[A] { case a if pf.isDefinedAt(a) => Http.fromZIO(pf(a)) }.flatten } + final case class PartialCollectManaged[A](unit: Unit) extends AnyVal { + def apply[R, E, B](pf: PartialFunction[A, ZManaged[R, E, B]]): Http[R, E, A, B] = + Http.collect[A] { case a if pf.isDefinedAt(a) => Http.fromZIO(pf(a).useNow) }.flatten + } + final case class PartialCollect[A](unit: Unit) extends AnyVal { def apply[B](pf: PartialFunction[A, B]): Http[Any, Nothing, A, B] = Collect(pf) } diff --git a/zio-http/src/main/scala/zhttp/http/Scheme.scala b/zio-http/src/main/scala/zhttp/http/Scheme.scala index 14e348031b..8e6585c851 100644 --- a/zio-http/src/main/scala/zhttp/http/Scheme.scala +++ b/zio-http/src/main/scala/zhttp/http/Scheme.scala @@ -2,7 +2,7 @@ package zhttp.http import io.netty.handler.codec.http.HttpScheme sealed trait Scheme { self => - def asString: String = Scheme.asString(self) + def encode: String = Scheme.asString(self) } object Scheme { def asString(self: Scheme): String = self match { diff --git a/zio-http/src/main/scala/zhttp/http/URL.scala b/zio-http/src/main/scala/zhttp/http/URL.scala index af54863417..43f4c8d2c5 100644 --- a/zio-http/src/main/scala/zhttp/http/URL.scala +++ b/zio-http/src/main/scala/zhttp/http/URL.scala @@ -28,7 +28,7 @@ final case class URL( case _ => self.copy(kind = URL.Location.Relative) } - def asString: String = URL.asString(self) + def encode: String = URL.asString(self) } object URL { sealed trait Location @@ -95,8 +95,8 @@ object URL { url.kind match { case Location.Relative => path case Location.Absolute(scheme, host, port) => - if (port == 80 || port == 443) s"${scheme.asString}://$host$path" - else s"${scheme.asString}://$host:$port$path" + if (port == 80 || port == 443) s"${scheme.encode}://$host$path" + else s"${scheme.encode}://$host:$port$path" } } diff --git a/zio-http/src/main/scala/zhttp/http/middleware/Web.scala b/zio-http/src/main/scala/zhttp/http/middleware/Web.scala index 9dee5307c7..d457861cb4 100644 --- a/zio-http/src/main/scala/zhttp/http/middleware/Web.scala +++ b/zio-http/src/main/scala/zhttp/http/middleware/Web.scala @@ -40,7 +40,7 @@ private[zhttp] trait Web extends Cors with Csrf with Auth with HeaderModifier[Ht for { end <- clock.nanoTime _ <- console - .putStrLn(s"${response.status.asJava.code()} ${method} ${url.asString} ${(end - start) / 1000000}ms") + .putStrLn(s"${response.status.asJava.code()} ${method} ${url.encode} ${(end - start) / 1000000}ms") .mapError(Option(_)) } yield Patch.empty } diff --git a/zio-http/src/main/scala/zhttp/service/Client.scala b/zio-http/src/main/scala/zhttp/service/Client.scala index 140d7a338c..40beb22f0f 100644 --- a/zio-http/src/main/scala/zhttp/service/Client.scala +++ b/zio-http/src/main/scala/zhttp/service/Client.scala @@ -43,7 +43,7 @@ final case class Client(rtm: HttpRuntime[Any], cf: JChannelFactory[Channel], el: } val scheme = req.url.kind match { case Location.Relative => "" - case Location.Absolute(scheme, _, _) => scheme.asString + case Location.Absolute(scheme, _, _) => scheme.encode } val init = ClientChannelInitializer(hand, scheme, sslOption) diff --git a/zio-http/src/main/scala/zhttp/service/EncodeClientParams.scala b/zio-http/src/main/scala/zhttp/service/EncodeClientParams.scala index 81c508a0d5..b6964e6e02 100644 --- a/zio-http/src/main/scala/zhttp/service/EncodeClientParams.scala +++ b/zio-http/src/main/scala/zhttp/service/EncodeClientParams.scala @@ -15,7 +15,7 @@ trait EncodeClientParams { // As per the spec, the path should contain only the relative path. // Host and port information should be in the headers. - val path = url.relative.asString + val path = url.relative.encode // val content = req.getBodyAsString match { // case Some(text) => Unpooled.copiedBuffer(text, HTTP_CHARSET) diff --git a/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala b/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala index 05b67735e2..2d3e0888ab 100644 --- a/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala +++ b/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala @@ -1,9 +1,9 @@ package zhttp.service import io.netty.channel.{ChannelHandlerContext, EventLoopGroup => JEventLoopGroup} -import io.netty.util.concurrent.{EventExecutor, Future} +import io.netty.util.concurrent.{EventExecutor, Future, GenericFutureListener} +import zio._ import zio.internal.Executor -import zio.{Exit, Runtime, URIO, ZIO} import scala.collection.mutable import scala.concurrent.{ExecutionContext => JExecutionContext} @@ -14,16 +14,23 @@ import scala.jdk.CollectionConverters._ * cancel the execution when the channel closes. */ final class HttpRuntime[+R](strategy: HttpRuntime.Strategy[R]) { - def unsafeRun(ctx: ChannelHandlerContext)(program: ZIO[R, Throwable, Any]): Unit = { + val rtm = strategy.getRuntime(ctx) + + // Close the connection if the program fails + // When connection closes, interrupt the program + rtm .unsafeRunAsync(for { fiber <- program.fork - _ <- ZIO.effect { - ctx.channel().closeFuture.addListener((_: Future[_ <: Void]) => rtm.unsafeRunAsync_(fiber.interrupt): Unit) + close <- UIO { + val close = closeListener(rtm, fiber) + ctx.channel().closeFuture.addListener(close) + close } _ <- fiber.join + _ <- UIO(ctx.channel().closeFuture().removeListener(close)) } yield ()) { case Exit.Success(_) => () case Exit.Failure(cause) => @@ -31,18 +38,39 @@ final class HttpRuntime[+R](strategy: HttpRuntime.Strategy[R]) { case None => () case Some(_) => System.err.println(cause.prettyPrint) } - ctx.close() + if (ctx.channel().isOpen) ctx.close() } } + + private def closeListener(rtm: Runtime[Any], fiber: Fiber.Runtime[_, _]): GenericFutureListener[Future[_ >: Void]] = + (_: Future[_ >: Void]) => rtm.unsafeRunAsync_(fiber.interrupt): Unit } object HttpRuntime { + def dedicated[R](group: JEventLoopGroup): URIO[R, HttpRuntime[R]] = + Strategy.dedicated(group).map(runtime => new HttpRuntime[R](runtime)) + + def default[R]: URIO[R, HttpRuntime[R]] = + Strategy.default().map(runtime => new HttpRuntime[R](runtime)) + + def sticky[R](group: JEventLoopGroup): URIO[R, HttpRuntime[R]] = + Strategy.sticky(group).map(runtime => new HttpRuntime[R](runtime)) + sealed trait Strategy[R] { def getRuntime(ctx: ChannelHandlerContext): Runtime[R] } object Strategy { + def dedicated[R](group: JEventLoopGroup): ZIO[R, Nothing, Strategy[R]] = + ZIO.runtime[R].map(runtime => Dedicated(runtime, group)) + + def default[R](): ZIO[R, Nothing, Strategy[R]] = + ZIO.runtime[R].map(runtime => Default(runtime)) + + def sticky[R](group: JEventLoopGroup): ZIO[R, Nothing, Strategy[R]] = + ZIO.runtime[R].map(runtime => Group(runtime, group)) + case class Default[R](runtime: Runtime[R]) extends Strategy[R] { override def getRuntime(ctx: ChannelHandlerContext): Runtime[R] = runtime } @@ -73,23 +101,5 @@ object HttpRuntime { override def getRuntime(ctx: ChannelHandlerContext): Runtime[R] = localRuntime.getOrElse(ctx.executor(), runtime) } - - def sticky[R](group: JEventLoopGroup): ZIO[R, Nothing, Strategy[R]] = - ZIO.runtime[R].map(runtime => Group(runtime, group)) - - def default[R](): ZIO[R, Nothing, Strategy[R]] = - ZIO.runtime[R].map(runtime => Default(runtime)) - - def dedicated[R](group: JEventLoopGroup): ZIO[R, Nothing, Strategy[R]] = - ZIO.runtime[R].map(runtime => Dedicated(runtime, group)) } - - def sticky[R](group: JEventLoopGroup): URIO[R, HttpRuntime[R]] = - Strategy.sticky(group).map(runtime => new HttpRuntime[R](runtime)) - - def dedicated[R](group: JEventLoopGroup): URIO[R, HttpRuntime[R]] = - Strategy.dedicated(group).map(runtime => new HttpRuntime[R](runtime)) - - def default[R]: URIO[R, HttpRuntime[R]] = - Strategy.default().map(runtime => new HttpRuntime[R](runtime)) } diff --git a/zio-http/src/main/scala/zhttp/service/Server.scala b/zio-http/src/main/scala/zhttp/service/Server.scala index cc2137a754..711f957734 100644 --- a/zio-http/src/main/scala/zhttp/service/Server.scala +++ b/zio-http/src/main/scala/zhttp/service/Server.scala @@ -215,7 +215,7 @@ object Server { for { channelFactory <- ZManaged.access[ServerChannelFactory](_.get) eventLoopGroup <- ZManaged.access[EventLoopGroup](_.get) - zExec <- HttpRuntime.default[R].toManaged_ + zExec <- HttpRuntime.sticky[R](eventLoopGroup).toManaged_ reqHandler = settings.app.compile(zExec, settings) respHandler = ServerResponseHandler(zExec, settings, ServerTimeGenerator.make) init = ServerChannelInitializer(zExec, settings, reqHandler, respHandler) diff --git a/zio-http/src/main/scala/zhttp/socket/Socket.scala b/zio-http/src/main/scala/zhttp/socket/Socket.scala index 54190c52eb..1833aa031d 100644 --- a/zio-http/src/main/scala/zhttp/socket/Socket.scala +++ b/zio-http/src/main/scala/zhttp/socket/Socket.scala @@ -57,8 +57,14 @@ sealed trait Socket[-R, +E, -A, +B] { self => } object Socket { + def collect[A]: PartialCollect[A] = new PartialCollect[A](()) + /** + * Simply echos the incoming message back + */ + def echo[A]: Socket[Any, Nothing, A, A] = Socket.collect[A] { case a => ZStream.succeed(a) } + def end: ZStream[Any, Nothing, Nothing] = ZStream.halt(Cause.empty) def fromFunction[A]: PartialFromFunction[A] = new PartialFromFunction[A](()) diff --git a/zio-http/src/test/scala/zhttp/http/EncodeRequestSpec.scala b/zio-http/src/test/scala/zhttp/http/EncodeRequestSpec.scala index d62176f5b6..0dcf1c3b5e 100644 --- a/zio-http/src/test/scala/zhttp/http/EncodeRequestSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/EncodeRequestSpec.scala @@ -46,13 +46,13 @@ object EncodeRequestSpec extends DefaultRunnableSpec with EncodeClientParams { testM("uri") { checkM(anyClientParam) { params => val uri = encodeClientParams(HttpVersion.HTTP_1_1, params).map(_.uri()) - assertM(uri)(equalTo(params.url.relative.asString)) + assertM(uri)(equalTo(params.url.relative.encode)) } } + testM("uri on HttpData.File") { checkM(HttpGen.clientParamsForFileHttpData()) { params => val uri = encodeClientParams(HttpVersion.HTTP_1_1, params).map(_.uri()) - assertM(uri)(equalTo(params.url.relative.asString)) + assertM(uri)(equalTo(params.url.relative.encode)) } } } + diff --git a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala index 944656f5f0..7fcaa2fd48 100644 --- a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala @@ -114,6 +114,11 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { val actual = a.execute(1) assert(actual)(isEffect) } + + test("should resolve managed") { + val a = Http.collectManaged[Int] { case 1 => ZManaged.succeed("A") } + val actual = a.execute(1) + assert(actual)(isEffect) + } + test("should resolve second effect") { val a = Http.empty.flatten val b = Http.succeed("B") diff --git a/zio-http/src/test/scala/zhttp/http/URLSpec.scala b/zio-http/src/test/scala/zhttp/http/URLSpec.scala index 7451b59ae8..38beb65221 100644 --- a/zio-http/src/test/scala/zhttp/http/URLSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/URLSpec.scala @@ -47,18 +47,18 @@ object URLSpec extends DefaultRunnableSpec { val asStringSpec = { def roundtrip(url: String) = - assert(URL.fromString(url).map(_.asString))(isRight(equalTo(url))) + assert(URL.fromString(url).map(_.encode))(isRight(equalTo(url))) suite("asString")( testM("using gen") { checkAll(HttpGen.url) { case url => - val source = url.asString - val decoded = URL.fromString(source).map(_.asString) + val source = url.encode + val decoded = URL.fromString(source).map(_.encode) assert(decoded)(isRight(equalTo(source))) } } + test("empty") { - val actual = URL.fromString("/").map(_.asString) + val actual = URL.fromString("/").map(_.encode) assert(actual)(isRight(equalTo("/"))) } + test("relative with pathname only") { diff --git a/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala b/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala index 6313830056..04109fa95c 100644 --- a/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala +++ b/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala @@ -1,17 +1,19 @@ package zhttp.socket import zio._ +import zio.duration.durationInt import zio.stream.ZStream import zio.test.Assertion._ +import zio.test.TestAspect.timeout import zio.test._ object SocketSpec extends DefaultRunnableSpec { def spec = suite("SocketSpec") { - OperationsSpec - } + operationsSpec + } @@ timeout(5 seconds) - def OperationsSpec = suite("Operations Spec") { + def operationsSpec = suite("OperationsSpec") { testM("fromStream provide") { val text = "Cat ipsum dolor sit amet" val environment = ZStream.environment[String] @@ -23,38 +25,44 @@ object SocketSpec extends DefaultRunnableSpec { assertM(socket.runCollect) { equalTo(Chunk(text)) } - } + testM("fromFunction provide") { - val environmentFunction = (_: Any) => ZStream.environment[WebSocketFrame] - val socket = Socket - .fromFunction(environmentFunction) - .provide(WebSocketFrame.text("Foo")) - .execute(WebSocketFrame.text("Bar")) + } + + testM("fromFunction provide") { + val environmentFunction = (_: Any) => ZStream.environment[WebSocketFrame] + val socket = Socket + .fromFunction(environmentFunction) + .provide(WebSocketFrame.text("Foo")) + .execute(WebSocketFrame.text("Bar")) - assertM(socket.runCollect) { - equalTo(Chunk(WebSocketFrame.text("Foo"))) - } - } + testM("collect provide") { - val environment = ZStream.environment[WebSocketFrame] - val socket = Socket - .collect[WebSocketFrame] { case WebSocketFrame.Pong => - environment + assertM(socket.runCollect) { + equalTo(Chunk(WebSocketFrame.text("Foo"))) } - .provide(WebSocketFrame.ping) - .execute(WebSocketFrame.pong) + } + + testM("collect provide") { + val environment = ZStream.environment[WebSocketFrame] + val socket = Socket + .collect[WebSocketFrame] { case WebSocketFrame.Pong => + environment + } + .provide(WebSocketFrame.ping) + .execute(WebSocketFrame.pong) - assertM(socket.runCollect) { - equalTo(Chunk(WebSocketFrame.ping)) - } - } + testM("ordered provide") { - val socket = Socket.collect[Int] { case _ => - ZStream.environment[Int] - } + assertM(socket.runCollect) { + equalTo(Chunk(WebSocketFrame.ping)) + } + } + + testM("ordered provide") { + val socket = Socket.collect[Int] { case _ => + ZStream.environment[Int] + } - val socketA: Socket[Int, Nothing, Int, Int] = socket.provide(12) - val socketB: Socket[Int, Nothing, Int, Int] = socketA.provide(1) - val socketC: Socket[Any, Nothing, Int, Int] = socketB.provide(42) + val socketA: Socket[Int, Nothing, Int, Int] = socket.provide(12) + val socketB: Socket[Int, Nothing, Int, Int] = socketA.provide(1) + val socketC: Socket[Any, Nothing, Int, Int] = socketB.provide(42) - assertM(socketC.execute(1000).runCollect)(equalTo(Chunk(12))) - } + assertM(socketC.execute(1000).runCollect)(equalTo(Chunk(12))) + } + + testM("echo") { + assertM(Socket.echo(1).runCollect)(equalTo(Chunk(1))) + } } }