diff --git a/docs/website/docs/getting-started.md b/docs/website/docs/getting-started.md deleted file mode 100644 index 0524799b71..0000000000 --- a/docs/website/docs/getting-started.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Getting Started - -## Http - -### Creating a "_Hello World_" app - -```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!"`. - -### Routing - -```scala -import zhttp.http._ - -val app = Http.collect[Request] { - case Method.GET -> !! / "fruits" / "a" => Response.text("Apple") - case Method.GET -> !! / "fruits" / "b" => Response.text("Banana") -} -``` - -Pattern matching on route is supported by the framework - -### Composition - -```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 -``` - -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 - -```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 - -```scala -import zhttp.http._ - -val app = Http.collectZIO[Request] { - case req @ Method.GET -> !! / "fruits" / "a" => - Response.text("URL:" + req.url.path.asString + " Headers: " + req.getHeaders).wrapZIO - case req @ Method.POST -> !! / "fruits" / "a" => - req.getBodyAsString.map(Response.text(_)) - } -``` - -### Testing - -zhttp provides a `zhttp-test` package for use in unit tests. You can utilize it as follows: - -```scala -import zio.test._ -import zhttp.test._ -import zhttp.http._ - -object Spec extends DefaultRunnableSpec { - - def spec = suite("http")( - test("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 - } - ) -} -``` - -## Socket - -### Creating a socket app - -```scala -import zhttp.socket._ - -private val socket = Socket.collect[WebSocketFrame] { case WebSocketFrame.Text("FOO") => - ZStream.succeed(WebSocketFrame.text("BAR")) - } - - private val app = Http.collectZIO[Request] { - case Method.GET -> !! / "greet" / name => Response.text(s"Greetings {$name}!").wrapZIO - case Method.GET -> !! / "ws" => socket.toResponse - } -``` - -## Server - -### Starting an Http App - -```scala -import zhttp.http._ -import zhttp.service.Server -import zio._ - -object HelloWorld extends App { - val app = Http.ok - - override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = - Server.start(8090, app).exitCode -} -``` - -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) diff --git a/docs/website/docs/v1.x/dsl/http.md b/docs/website/docs/v1.x/dsl/http.md index 64af2a87ce..33e5514de2 100644 --- a/docs/website/docs/v1.x/dsl/http.md +++ b/docs/website/docs/v1.x/dsl/http.md @@ -229,7 +229,7 @@ The below snippet tests an app that takes `Int` as input and responds by adding object Spec extends DefaultRunnableSpec { def spec = suite("http")( - test("1 + 1 = 2") { + testM("1 + 1 = 2") { val app: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](_ + 1) assertM(app(1))(equalTo(2)) } diff --git a/docs/website/docs/v1.x/dsl/server.md b/docs/website/docs/v1.x/dsl/server.md index 30564897b3..33310d0993 100644 --- a/docs/website/docs/v1.x/dsl/server.md +++ b/docs/website/docs/v1.x/dsl/server.md @@ -31,7 +31,7 @@ This section describes, ZIO HTTP Server and different configurations you can pro override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { server.make .use(start => - Console.printLine(s"Server started on port ${start.port}") + console.putStrLn(s"Server started on port ${start.port}") *> ZIO.never, ).provideCustomLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(2)) .exitCode @@ -84,7 +84,7 @@ object HelloWorldAdvanced extends App { server.make .use(_ => // Waiting for the server to start - Console.printLine(s"Server started on port $PORT") + console.putStrLn(s"Server started on port $PORT") // Ensures the server doesn't die after printing *> ZIO.never, diff --git a/docs/website/docs/v1.x/examples/advanced-examples/advanced_server.md b/docs/website/docs/v1.x/examples/advanced-examples/advanced_server.md index 39976c8a0b..a49feb9487 100644 --- a/docs/website/docs/v1.x/examples/advanced-examples/advanced_server.md +++ b/docs/website/docs/v1.x/examples/advanced-examples/advanced_server.md @@ -35,7 +35,7 @@ object HelloWorldAdvanced extends App { server.make .use(start => // Waiting for the server to start - Console.printLine(s"Server started on port ${start.port}") + console.putStrLn(s"Server started on port ${start.port}") // Ensures the server doesn't die after printing *> ZIO.never, diff --git a/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_client.md b/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_client.md index 369bd3e881..854c191f9b 100644 --- a/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_client.md +++ b/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_client.md @@ -32,7 +32,7 @@ object HttpsClient extends App { val program = for { res <- Client.request(url, headers, sslOption) data <- res.bodyAsString - _ <- Console.printLine { data } + _ <- console.putStrLn { data } } yield () override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] diff --git a/docs/website/docs/v1.x/getting-started.md b/docs/website/docs/v1.x/getting-started.md index dbfe29616b..626260202e 100644 --- a/docs/website/docs/v1.x/getting-started.md +++ b/docs/website/docs/v1.x/getting-started.md @@ -108,7 +108,7 @@ import zhttp.http._ object Spec extends DefaultRunnableSpec { def spec = suite("http")( - test("should be ok") { + testM("should be ok") { val app = Http.ok val req = Request() assertM(app(req))(equalTo(Response.ok)) diff --git a/docs/website/docs/v1.x/index.md b/docs/website/docs/v1.x/index.md index 7b61c7ad64..6937c5745f 100644 --- a/docs/website/docs/v1.x/index.md +++ b/docs/website/docs/v1.x/index.md @@ -42,3 +42,36 @@ sbt new dream11/zio-http.g8 * [scalafix-organize-imports](https://github.com/liancheng/scalafix-organize-imports) * [sbt-revolver](https://github.com/spray/sbt-revolver) +## Efficient development process + +The dependencies in the Dream11 g8 template were added to enable an efficient development process. + +### sbt-revolver "hot-reload" changes + +Sbt-revolver can watch application resources for change and automatically re-compile and then re-start the application under development. This provides a fast development-turnaround, the closest you can get to real hot-reloading. + +Start your application from _sbt_ with the following command + +```shell +~reStart +``` + +Pressing enter will stop watching for changes, but not stop the application. Use the following command to stop the application (shutdown hooks will not be executed). + +``` +reStop +``` + +In case you already have an _sbt_ server running, i.e. to provide your IDE with BSP information, use _sbtn_ instead of _sbt_ to run `~reStart`, this let's both _sbt_ sessions share one server. + +### scalafmt automatically format source code + +scalafmt will automatically format all source code and assert that all team members use consistent formatting. + +### scalafix refactoring and linting + +scalafix will mainly be used as a linting tool during everyday development, for example by removing unused dependencies or reporting errors for disabled features. Additionally it can simplify upgrades of Scala versions and dependencies, by executing predefined migration paths. + +### sbt-native-packager + +sbt-native-packager can package the application in the most popular formats, for example Docker images, rpm packages or graalVM native images. diff --git a/example/src/main/scala/example/PlainTextBenchmarkServer.scala b/example/src/main/scala/example/PlainTextBenchmarkServer.scala index 7b21ad574a..09d5866337 100644 --- a/example/src/main/scala/example/PlainTextBenchmarkServer.scala +++ b/example/src/main/scala/example/PlainTextBenchmarkServer.scala @@ -1,7 +1,7 @@ package example import io.netty.util.AsciiString -import zhttp.http._ +import zhttp.http.{Http, _} import zhttp.service.server.ServerChannelFactory import zhttp.service.{EventLoopGroup, Server} import zio._ diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 92e71cec5d..40e4923fdc 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,7 +4,7 @@ object Dependencies { val JwtCoreVersion = "9.0.4" val NettyVersion = "4.1.75.Final" val NettyIncubatorVersion = "0.0.13.Final" - val ScalaCompactCollectionVersion = "2.6.0" + val ScalaCompactCollectionVersion = "2.7.0" val ZioVersion = "2.0.0-RC3" val SttpVersion = "3.3.18" diff --git a/project/plugins.sbt b/project/plugins.sbt index f16a3b5332..e6a85d62a5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,5 +5,5 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.2") addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.14.2") -addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.5.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.5.1") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") diff --git a/zio-http/src/main/scala/zhttp/html/Dom.scala b/zio-http/src/main/scala/zhttp/html/Dom.scala index 04408600f2..f1f2940525 100644 --- a/zio-http/src/main/scala/zhttp/html/Dom.scala +++ b/zio-http/src/main/scala/zhttp/html/Dom.scala @@ -10,7 +10,7 @@ package zhttp.html * elements. */ sealed trait Dom { self => - def encode: String = self match { + def encode: CharSequence = self match { case Dom.Element(name, children) => val attributes = children.collect { case self: Dom.Attribute => self.encode } @@ -35,19 +35,19 @@ sealed trait Dom { self => } object Dom { - def attr(name: String, value: String): Dom = Dom.Attribute(name, value) + def attr(name: CharSequence, value: CharSequence): Dom = Dom.Attribute(name, value) - def element(name: String, children: Dom*): Dom = Dom.Element(name, children) + def element(name: CharSequence, children: Dom*): Dom = Dom.Element(name, children) def empty: Dom = Empty - def text(data: String): Dom = Dom.Text(data) + def text(data: CharSequence): Dom = Dom.Text(data) - private[zhttp] final case class Element(name: String, children: Seq[Dom]) extends Dom + private[zhttp] final case class Element(name: CharSequence, children: Seq[Dom]) extends Dom - private[zhttp] final case class Text(data: String) extends Dom + private[zhttp] final case class Text(data: CharSequence) extends Dom - private[zhttp] final case class Attribute(name: String, value: String) extends Dom + private[zhttp] final case class Attribute(name: CharSequence, value: CharSequence) extends Dom private[zhttp] object Empty extends Dom } diff --git a/zio-http/src/main/scala/zhttp/html/Elements.scala b/zio-http/src/main/scala/zhttp/html/Elements.scala index be870a4d84..f1f8731b32 100644 --- a/zio-http/src/main/scala/zhttp/html/Elements.scala +++ b/zio-http/src/main/scala/zhttp/html/Elements.scala @@ -250,12 +250,12 @@ trait Elements { } object Element { - private[zhttp] val voidElementNames: Set[String] = + private[zhttp] val voidElementNames: Set[CharSequence] = Set(area, base, br, col, embed, hr, img, input, link, meta, param, source, track, wbr).map(_.name) - private[zhttp] def isVoid(name: String): Boolean = voidElementNames.contains(name) + private[zhttp] def isVoid(name: CharSequence): Boolean = voidElementNames.contains(name) - case class PartialElement(name: String) { + case class PartialElement(name: CharSequence) { def apply(children: Html*): Dom = Dom.element( name, children.collect { diff --git a/zio-http/src/main/scala/zhttp/html/Html.scala b/zio-http/src/main/scala/zhttp/html/Html.scala index d5dbc72141..2f004a750b 100644 --- a/zio-http/src/main/scala/zhttp/html/Html.scala +++ b/zio-http/src/main/scala/zhttp/html/Html.scala @@ -6,7 +6,7 @@ import scala.language.implicitConversions * A view is a domain that used generate HTML. */ sealed trait Html { self => - def encode: String = { + def encode: CharSequence = { self match { case Html.Empty => "" case Html.Single(element) => element.encode @@ -16,7 +16,7 @@ sealed trait Html { self => } object Html { - implicit def fromString(string: String): Html = Html.Single(Dom.text(string)) + implicit def fromString(string: CharSequence): Html = Html.Single(Dom.text(string)) implicit def fromSeq(elements: Seq[Dom]): Html = Html.Multiple(elements) diff --git a/zio-http/src/main/scala/zhttp/html/Template.scala b/zio-http/src/main/scala/zhttp/html/Template.scala index 95d9f0ace8..25e65bae96 100644 --- a/zio-http/src/main/scala/zhttp/html/Template.scala +++ b/zio-http/src/main/scala/zhttp/html/Template.scala @@ -5,7 +5,7 @@ package zhttp.html */ object Template { - def container(heading: String)(element: Html): Html = { + def container(heading: CharSequence)(element: Html): Html = { html( head( title(s"ZIO Http - ${heading}"), diff --git a/zio-http/src/main/scala/zhttp/http/Http.scala b/zio-http/src/main/scala/zhttp/http/Http.scala index 1e29d0a222..ab433852e0 100644 --- a/zio-http/src/main/scala/zhttp/http/Http.scala +++ b/zio-http/src/main/scala/zhttp/http/Http.scala @@ -7,7 +7,7 @@ import zhttp.html._ import zhttp.http.headers.HeaderModifier import zhttp.service.server.ServerTime import zhttp.service.{Handler, HttpRuntime, Server} -import zio.ZIO.attemptBlockingIO +import zio.ZIO.attemptBlocking import zio._ import zio.stream.ZStream @@ -27,6 +27,78 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => import Http._ + /** + * Extracts body as a ByteBuf + */ + private[zhttp] final def bodyAsByteBuf(implicit + eb: B <:< Response, + ee: E <:< Throwable, + ): Http[R, Throwable, A, ByteBuf] = + self.widen[Throwable, B].mapZIO(_.bodyAsByteBuf) + + /** + * Evaluates the app and returns an HExit that can be resolved further + * + * NOTE: `execute` is not a stack-safe method for performance reasons. Unlike + * ZIO, there is no reason why the execute should be stack safe. The + * performance improves quite significantly if no additional heap allocations + * are required this way. + */ + final private[zhttp] def execute(a: A): HExit[R, E, B] = + self match { + + case Http.Empty => HExit.empty + case Http.Identity => HExit.succeed(a.asInstanceOf[B]) + case Succeed(b) => HExit.succeed(b) + case Fail(e) => HExit.fail(e) + case Die(e) => HExit.die(e) + case Attempt(a) => + try { HExit.succeed(a()) } + catch { case e: Throwable => HExit.fail(e.asInstanceOf[E]) } + case FromFunctionHExit(f) => + try { f(a) } + catch { case e: Throwable => HExit.die(e) } + case FromHExit(h) => h + case Chain(self, other) => self.execute(a).flatMap(b => other.execute(b)) + case Race(self, other) => + (self.execute(a), other.execute(a)) match { + case (HExit.Effect(self), HExit.Effect(other)) => + Http.fromOptionFunction[Any](_ => self.raceFirst(other)).execute(a) + case (HExit.Effect(_), other) => other + case (self, _) => self + } + case FoldHttp(self, ee, df, bb, dd) => + try { + self.execute(a).foldExit(ee(_).execute(a), df(_).execute(a), bb(_).execute(a), dd.execute(a)) + } catch { + case e: Throwable => HExit.die(e) + } + + case RunMiddleware(app, mid) => + try { + mid(app).execute(a) + } catch { + case e: Throwable => HExit.die(e) + } + + case When(f, other) => + try { + if (f(a)) other.execute(a) else HExit.empty + } catch { + case e: Throwable => HExit.die(e) + } + + case Combine(self, other) => { + self.execute(a) match { + case HExit.Empty => other.execute(a) + case exit: HExit.Success[_] => exit.asInstanceOf[HExit[R, E, B]] + case exit: HExit.Failure[_] => exit.asInstanceOf[HExit[R, E, B]] + case exit: HExit.Die => exit + case exit @ HExit.Effect(_) => exit.defaultWith(other.execute(a)).asInstanceOf[HExit[R, E, B]] + } + } + } + /** * Attaches the provided middleware to the Http app */ @@ -34,6 +106,13 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => mid: Middleware[R1, E1, A1, B1, A2, B2], ): Http[R1, E1, A2, B2] = mid(self) + /** + * Combines two Http instances into a middleware that works a codec for + * incoming and outgoing messages. + */ + final def \/[R1 <: R, E1 >: E, C, D](other: Http[R1, E1, C, D]): Middleware[R1, E1, B, C, A, D] = + self codecMiddleware other + /** * Alias for flatmap */ @@ -164,6 +243,13 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => ): Http[R1, E1, A1, B1] = unrefineWith(pf)(Http.fail).catchAll(e => e) + /** + * Combines two Http instances into a middleware that works a codec for + * incoming and outgoing messages. + */ + final def codecMiddleware[R1 <: R, E1 >: E, C, D](other: Http[R1, E1, C, D]): Middleware[R1, E1, B, C, A, D] = + Middleware.codecHttp(self, other) + /** * Collects some of the results of the http and converts it to another type. */ @@ -533,82 +619,16 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => self.asInstanceOf[Http[R, E1, A, B1]] /** - * Combines the two apps and returns the result of the one on the right + * Narrows the type of the input */ - final def zipRight[R1 <: R, E1 >: E, A1 <: A, C1](other: Http[R1, E1, A1, C1]): Http[R1, E1, A1, C1] = - self.flatMap(_ => other) + final def narrow[A1](implicit a: A1 <:< A): Http[R, E, A1, B] = + self.asInstanceOf[Http[R, E, A1, B]] /** - * Extracts body as a ByteBuf - */ - private[zhttp] final def bodyAsByteBuf(implicit - eb: B <:< Response, - ee: E <:< Throwable, - ): Http[R, Throwable, A, ByteBuf] = - self.widen[Throwable, B].mapZIO(_.bodyAsByteBuf) - - /** - * Evaluates the app and returns an HExit that can be resolved further - * - * NOTE: `execute` is not a stack-safe method for performance reasons. Unlike - * ZIO, there is no reason why the execute should be stack safe. The - * performance improves quite significantly if no additional heap allocations - * are required this way. + * Combines the two apps and returns the result of the one on the right */ - final private[zhttp] def execute(a: A): HExit[R, E, B] = - self match { - - case Http.Empty => HExit.empty - case Http.Identity => HExit.succeed(a.asInstanceOf[B]) - case Succeed(b) => HExit.succeed(b) - case Fail(e) => HExit.fail(e) - case Die(e) => HExit.die(e) - case Attempt(a) => - try { HExit.succeed(a()) } - catch { case e: Throwable => HExit.fail(e.asInstanceOf[E]) } - case FromFunctionHExit(f) => - try { f(a) } - catch { case e: Throwable => HExit.die(e) } - case FromHExit(h) => h - case Chain(self, other) => self.execute(a).flatMap(b => other.execute(b)) - case Race(self, other) => - (self.execute(a), other.execute(a)) match { - case (HExit.Effect(self), HExit.Effect(other)) => - Http.fromOptionFunction[Any](_ => self.raceFirst(other)).execute(a) - case (HExit.Effect(_), other) => other - case (self, _) => self - } - case FoldHttp(self, ee, df, bb, dd) => - try { - self.execute(a).foldExit(ee(_).execute(a), df(_).execute(a), bb(_).execute(a), dd.execute(a)) - } catch { - case e: Throwable => HExit.die(e) - } - - case RunMiddleware(app, mid) => - try { - mid(app).execute(a) - } catch { - case e: Throwable => HExit.die(e) - } - - case When(f, other) => - try { - if (f(a)) other.execute(a) else HExit.empty - } catch { - case e: Throwable => HExit.die(e) - } - - case Combine(self, other) => { - self.execute(a) match { - case HExit.Empty => other.execute(a) - case exit: HExit.Success[_] => exit.asInstanceOf[HExit[R, E, B]] - case exit: HExit.Failure[_] => exit.asInstanceOf[HExit[R, E, B]] - case exit: HExit.Die => exit - case exit @ HExit.Effect(_) => exit.defaultWith(other.execute(a)).asInstanceOf[HExit[R, E, B]] - } - } - } + final def zipRight[R1 <: R, E1 >: E, A1 <: A, C1](other: Http[R1, E1, A1, C1]): Http[R1, E1, A1, C1] = + self.flatMap(_ => other) } object Http { @@ -616,6 +636,15 @@ object Http { implicit final class HttpAppSyntax[-R, +E](val http: HttpApp[R, E]) extends HeaderModifier[HttpApp[R, E]] { self => + private[zhttp] def compile[R1 <: R]( + zExec: HttpRuntime[R1], + settings: Server.Config[R1, Throwable], + serverTimeGenerator: ServerTime, + )(implicit + evE: E <:< Throwable, + ): ChannelHandler = + Handler(http.asInstanceOf[HttpApp[R1, Throwable]], zExec, settings, serverTimeGenerator) + /** * Patches the response produced by the app */ @@ -655,15 +684,6 @@ object Http { * Applies Http based on the path as string */ def whenPathEq(p: String): HttpApp[R, E] = http.when(_.unsafeEncode.uri().contentEquals(p)) - - private[zhttp] def compile[R1 <: R]( - zExec: HttpRuntime[R1], - settings: Server.Config[R1, Throwable], - serverTimeGenerator: ServerTime, - )(implicit - evE: E <:< Throwable, - ): ChannelHandler = - Handler(http.asInstanceOf[HttpApp[R1, Throwable]], zExec, settings, serverTimeGenerator) } /** @@ -784,7 +804,7 @@ object Http { /** * Creates an Http app from the contents of a file. */ - def fromFile(file: => java.io.File): HttpApp[Any, Throwable] = Http.fromFileZIO(ZIO.attempt(file)) + def fromFile(file: => java.io.File): HttpApp[Any, Throwable] = Http.fromFileZIO(ZIO.succeed(file)) /** * Creates an Http app from the contents of a file which is produced from an @@ -889,7 +909,7 @@ object Http { */ def getResource(path: String): Http[Any, Throwable, Any, net.URL] = Http - .fromZIO(attemptBlockingIO(getClass.getClassLoader.getResource(path))) + .fromZIO(attemptBlocking(getClass.getClassLoader.getResource(path))) .flatMap { resource => if (resource == null) Http.empty else Http.succeed(resource) } /** @@ -954,14 +974,14 @@ object Http { * Creates an Http app which responds with an Html page using the built-in * template. */ - def template(heading: String)(view: Html): HttpApp[Any, Nothing] = + def template(heading: CharSequence)(view: Html): HttpApp[Any, Nothing] = Http.response(Response.html(Template.container(heading)(view))) /** * Creates an Http app which always responds with the same plain text. */ - def text(str: String, charset: Charset = HTTP_CHARSET): HttpApp[Any, Nothing] = - Http.succeed(Response.text(str, charset)) + def text(charSeq: CharSequence): HttpApp[Any, Nothing] = + Http.succeed(Response.text(charSeq)) /** * Creates an Http app that responds with a 408 status code after the provided diff --git a/zio-http/src/main/scala/zhttp/http/HttpData.scala b/zio-http/src/main/scala/zhttp/http/HttpData.scala index 76db8345e3..2b677805ed 100644 --- a/zio-http/src/main/scala/zhttp/http/HttpData.scala +++ b/zio-http/src/main/scala/zhttp/http/HttpData.scala @@ -3,7 +3,8 @@ package zhttp.http import io.netty.buffer.{ByteBuf, Unpooled} import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.{HttpContent, LastHttpContent} -import zio.ZIO.attemptBlocking +import io.netty.util.AsciiString +import zhttp.http.HttpData.ByteBufConfig import zio.stream.ZStream import zio.{Chunk, Task, ZIO} @@ -16,12 +17,23 @@ import java.nio.charset.Charset sealed trait HttpData { self => /** - * Returns true if HttpData is a stream + * Encodes the HttpData into a ByteBuf. Takes in ByteBufConfig to have a more + * fine grained control over the encoding. */ - final def isChunked: Boolean = self match { - case HttpData.BinaryStream(_) => true - case _ => false - } + def toByteBuf(config: ByteBufConfig): Task[ByteBuf] + + /** + * Encodes the HttpData into a Stream of ByteBufs. Takes in ByteBufConfig to + * have a more fine grained control over the encoding. + */ + def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] + + /** + * Encodes the HttpData into a Http of ByeBuf. This could be more performant + * in certain cases. Takes in ByteBufConfig to have a more fine grained + * control over the encoding. + */ + def toHttp(config: ByteBufConfig): Http[Any, Throwable, Any, ByteBuf] /** * Returns true if HttpData is empty @@ -31,31 +43,48 @@ sealed trait HttpData { self => case _ => false } - final def toByteBuf: Task[ByteBuf] = { - self match { - case self: HttpData.Incoming => self.encode - case self: HttpData.Outgoing => self.encode - } - } + /** + * Encodes the HttpData into a ByteBuf. + */ + final def toByteBuf: Task[ByteBuf] = toByteBuf(ByteBufConfig.default) - final def toByteBufStream: ZStream[Any, Throwable, ByteBuf] = self match { - case self: HttpData.Incoming => self.encodeAsStream - case self: HttpData.Outgoing => ZStream.fromZIO(self.encode) - } + /** + * Encodes the HttpData into a Stream of ByteBufs + */ + final def toByteBufStream: ZStream[Any, Throwable, ByteBuf] = toByteBufStream(ByteBufConfig.default) + + /** + * A bit more efficient version of toByteBuf in certain cases + */ + final def toHttp: Http[Any, Throwable, Any, ByteBuf] = toHttp(ByteBufConfig.default) } object HttpData { + private def collectStream[R, E](stream: ZStream[R, E, ByteBuf]): ZIO[R, E, ByteBuf] = + stream.runFold(Unpooled.compositeBuffer()) { case (cmp, buf) => cmp.addComponent(true, buf) } + /** * Helper to create empty HttpData */ def empty: HttpData = Empty + /** + * Helper to create HttpData from AsciiString + */ + def fromAsciiString(asciiString: AsciiString): HttpData = FromAsciiString(asciiString) + /** * Helper to create HttpData from ByteBuf */ def fromByteBuf(byteBuf: ByteBuf): HttpData = HttpData.BinaryByteBuf(byteBuf) + /** + * Helper to create HttpData from CharSequence + */ + def fromCharSequence(charSequence: CharSequence, charset: Charset = HTTP_CHARSET): HttpData = + fromAsciiString(new AsciiString(charSequence, charset)) + /** * Helper to create HttpData from chunk of bytes */ @@ -64,9 +93,7 @@ object HttpData { /** * Helper to create HttpData from contents of a file */ - def fromFile(file: => java.io.File): HttpData = { - RandomAccessFile(() => new java.io.RandomAccessFile(file, "r")) - } + def fromFile(file: => java.io.File): HttpData = JavaFile(() => file) /** * Helper to create HttpData from Stream of string @@ -83,27 +110,20 @@ object HttpData { /** * Helper to create HttpData from String */ - def fromString(text: String, charset: Charset = HTTP_CHARSET): HttpData = Text(text, charset) - - private[zhttp] sealed trait Outgoing extends HttpData { self => - def encode: ZIO[Any, Throwable, ByteBuf] = - self match { - case HttpData.Text(text, charset) => ZIO.succeed(Unpooled.copiedBuffer(text, charset)) - case HttpData.BinaryChunk(data) => ZIO.succeed(Unpooled.copiedBuffer(data.toArray)) - case HttpData.BinaryByteBuf(data) => ZIO.succeed(data) - case HttpData.Empty => ZIO.succeed(Unpooled.EMPTY_BUFFER) - case HttpData.BinaryStream(stream) => - stream - .asInstanceOf[ZStream[Any, Throwable, ByteBuf]] - .runFold(Unpooled.compositeBuffer())((c, b) => c.addComponent(true, b)) - case HttpData.RandomAccessFile(raf) => - attemptBlocking { - val fis = new FileInputStream(raf().getFD) - val fileContent: Array[Byte] = new Array[Byte](raf().length().toInt) - fis.read(fileContent) - Unpooled.copiedBuffer(fileContent) - } - } + def fromString(text: String, charset: Charset = HTTP_CHARSET): HttpData = fromCharSequence(text, charset) + + private[zhttp] sealed trait Complete extends HttpData + + /** + * Provides a more fine grained control while encoding HttpData into ByteBUfs + */ + case class ByteBufConfig(chunkSize: Int = 1024 * 4) { + def chunkSize(fileLength: Long): Int = { + val actualInt = fileLength.toInt + if (actualInt < 0) chunkSize + else if (actualInt < chunkSize) actualInt + else chunkSize + } } private[zhttp] final class UnsafeContent(private val httpContent: HttpContent) extends AnyVal { @@ -116,9 +136,13 @@ object HttpData { def read(): Unit = ctx.read(): Unit } - private[zhttp] final case class Incoming(unsafeRun: (UnsafeChannel => UnsafeContent => Unit) => Unit) + private[zhttp] final case class UnsafeAsync(unsafeRun: (UnsafeChannel => UnsafeContent => Unit) => Unit) extends HttpData { - def encode: ZIO[Any, Nothing, ByteBuf] = for { + + /** + * Encodes the HttpData into a ByteBuf. + */ + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = for { body <- ZIO.async[Any, Nothing, ByteBuf](cb => unsafeRun(ch => { val buffer = Unpooled.compositeBuffer() @@ -130,21 +154,150 @@ object HttpData { ) } yield body - def encodeAsStream: ZStream[Any, Nothing, ByteBuf] = ZStream - .async[Any, Nothing, ByteBuf](cb => - unsafeRun(ch => - msg => { - cb(ZIO.succeed(Chunk(msg.content))) - if (msg.isLast) cb(ZIO.fail(None)) else ch.read() - }, - ), - ) + /** + * Encodes the HttpData into a Stream of ByteBufs + */ + override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = + ZStream + .async[Any, Nothing, ByteBuf](cb => + unsafeRun(ch => + msg => { + cb(ZIO.succeed(Chunk(msg.content))) + if (msg.isLast) cb(ZIO.fail(None)) else ch.read() + }, + ), + ) + + override def toHttp(config: ByteBufConfig): Http[Any, Throwable, Any, ByteBuf] = + Http.fromZIO(toByteBuf(config)) + } + + private[zhttp] case class FromAsciiString(asciiString: AsciiString) extends Complete { + + /** + * Encodes the HttpData into a ByteBuf. Takes in ByteBufConfig to have a + * more fine grained control over the encoding. + */ + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = + ZIO.attempt(Unpooled.wrappedBuffer(asciiString.array())) + + /** + * Encodes the HttpData into a Stream of ByteBufs. Takes in ByteBufConfig to + * have a more fine grained control over the encoding. + */ + override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = + ZStream.fromZIO(toByteBuf(config)) + + /** + * Encodes the HttpData into a Http of ByeBuf. This could be more performant + * in certain cases. Takes in ByteBufConfig to have a more fine grained + * control over the encoding. + */ + override def toHttp(config: ByteBufConfig): Http[Any, Throwable, Any, ByteBuf] = ??? + } + + private[zhttp] final case class BinaryChunk(data: Chunk[Byte]) extends Complete { + + private def encode = Unpooled.wrappedBuffer(data.toArray) + + /** + * Encodes the HttpData into a ByteBuf. + */ + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = ZIO.succeed(encode) + + /** + * Encodes the HttpData into a Stream of ByteBufs + */ + override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = + ZStream.fromZIO(toByteBuf(config)) + + override def toHttp(config: ByteBufConfig): UHttp[Any, ByteBuf] = Http.succeed(encode) + } + + private[zhttp] final case class BinaryByteBuf(data: ByteBuf) extends Complete { + + /** + * Encodes the HttpData into a ByteBuf. + */ + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = ZIO.attempt(data) + + /** + * Encodes the HttpData into a Stream of ByteBufs + */ + override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = + ZStream.fromZIO(toByteBuf(config)) + + override def toHttp(config: ByteBufConfig): UHttp[Any, ByteBuf] = Http.succeed(data) + } + + private[zhttp] final case class BinaryStream(stream: ZStream[Any, Throwable, ByteBuf]) extends Complete { + + /** + * Encodes the HttpData into a ByteBuf. + */ + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = + collectStream(toByteBufStream(config)) + + /** + * Encodes the HttpData into a Stream of ByteBufs + */ + override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = + stream + + override def toHttp(config: ByteBufConfig): Http[Any, Throwable, Any, ByteBuf] = + Http.fromZIO(toByteBuf(config)) + } + + private[zhttp] final case class JavaFile(unsafeFile: () => java.io.File) extends Complete { + + /** + * Encodes the HttpData into a ByteBuf. + */ + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = + collectStream(toByteBufStream(config)) + + /** + * Encodes the HttpData into a Stream of ByteBufs + */ + override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = + ZStream.unwrap { + for { + file <- ZIO.attempt(unsafeFile()) + fs <- ZIO.attempt(new FileInputStream(file)) + size = config.chunkSize(file.length()) + buffer = new Array[Byte](size) + } yield ZStream + .repeatZIOOption[Any, Throwable, ByteBuf] { + for { + len <- ZIO.attempt(fs.read(buffer)).mapError(Some(_)) + bytes <- if (len > 0) ZIO.succeed(Unpooled.copiedBuffer(buffer, 0, len)) else ZIO.fail(None) + } yield bytes + } + .ensuring(ZIO.succeed(fs.close())) + } + + override def toHttp(config: ByteBufConfig): Http[Any, Throwable, Any, ByteBuf] = + Http.fromZIO(toByteBuf(config)) + } + + object ByteBufConfig { + val default: ByteBufConfig = ByteBufConfig() + } + + private[zhttp] case object Empty extends Complete { + + /** + * Encodes the HttpData into a ByteBuf. + */ + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = ZIO.succeed(Unpooled.EMPTY_BUFFER) + + /** + * Encodes the HttpData into a Stream of ByteBufs + */ + override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = + ZStream.fromZIO(toByteBuf(config)) + + override def toHttp(config: ByteBufConfig): UHttp[Any, ByteBuf] = Http.empty } - private[zhttp] final case class Text(text: String, charset: Charset) extends Outgoing - private[zhttp] final case class BinaryChunk(data: Chunk[Byte]) extends Outgoing - private[zhttp] final case class BinaryByteBuf(data: ByteBuf) extends Outgoing - private[zhttp] final case class BinaryStream(stream: ZStream[Any, Throwable, ByteBuf]) extends Outgoing - private[zhttp] final case class RandomAccessFile(unsafeGet: () => java.io.RandomAccessFile) extends Outgoing - private[zhttp] case object Empty extends Outgoing } diff --git a/zio-http/src/main/scala/zhttp/http/HttpDataExtension.scala b/zio-http/src/main/scala/zhttp/http/HttpDataExtension.scala index 1a2fe07b22..8deb770eba 100644 --- a/zio-http/src/main/scala/zhttp/http/HttpDataExtension.scala +++ b/zio-http/src/main/scala/zhttp/http/HttpDataExtension.scala @@ -1,19 +1,15 @@ package zhttp.http import io.netty.buffer.{ByteBuf, ByteBufUtil} +import io.netty.util.AsciiString import zhttp.http.headers.HeaderExtension import zio.stream.ZStream import zio.{Chunk, Task, ZIO} private[zhttp] trait HttpDataExtension[+A] extends HeaderExtension[A] { self: A => - def data: HttpData - private[zhttp] final def bodyAsByteBuf: Task[ByteBuf] = data.toByteBuf - final def bodyAsByteArray: Task[Array[Byte]] = - bodyAsByteBuf.flatMap(buf => - ZIO.attempt(ByteBufUtil.getBytes(buf)).ensuring(ZIO.succeed(buf.release(buf.refCnt()))), - ) + def data: HttpData /** * Decodes the content of request as a Chunk of Bytes @@ -21,11 +17,16 @@ private[zhttp] trait HttpDataExtension[+A] extends HeaderExtension[A] { self: A final def body: Task[Chunk[Byte]] = bodyAsByteArray.map(Chunk.fromArray) + final def bodyAsByteArray: Task[Array[Byte]] = + bodyAsByteBuf.flatMap(buf => + ZIO.attempt(ByteBufUtil.getBytes(buf)).ensuring(ZIO.succeed(buf.release(buf.refCnt()))), + ) + /** - * Decodes the content of request as string + * Decodes the content of request as CharSequence */ - final def bodyAsString: Task[String] = - bodyAsByteArray.map(new String(_, charset)) + final def bodyAsCharSequence: ZIO[Any, Throwable, CharSequence] = + bodyAsByteArray.map { buf => new AsciiString(buf, false) } /** * Decodes the content of request as stream of bytes @@ -39,4 +40,10 @@ private[zhttp] trait HttpDataExtension[+A] extends HeaderExtension[A] { self: A } } .flattenChunks + + /** + * Decodes the content of request as string + */ + final def bodyAsString: Task[String] = + bodyAsByteArray.map(new String(_, charset)) } diff --git a/zio-http/src/main/scala/zhttp/http/Middleware.scala b/zio-http/src/main/scala/zhttp/http/Middleware.scala index d2b03f71d0..c2f246fcca 100644 --- a/zio-http/src/main/scala/zhttp/http/Middleware.scala +++ b/zio-http/src/main/scala/zhttp/http/Middleware.scala @@ -1,7 +1,7 @@ package zhttp.http import zhttp.http.middleware.Web -import zio._ +import zio.{Clock, Duration, ZIO} /** * Middlewares are essentially transformations that one can apply on any Http to @@ -20,6 +20,11 @@ import zio._ */ trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self => + /** + * Applies middleware on Http and returns new Http. + */ + def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut] + /** * Creates a new middleware that passes the output Http of the current * middleware as the input to the provided middleware. @@ -54,11 +59,6 @@ trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self => other(self(http)) } - /** - * Applies middleware on Http and returns new Http. - */ - def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut] - /** * Makes the middleware resolve with a constant Middleware */ @@ -176,10 +176,15 @@ trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self => object Middleware extends Web { /** - * Creates a middleware using specified encoder and decoder + * Creates a middleware using the specified encoder and decoder functions */ def codec[A, B]: PartialCodec[A, B] = new PartialCodec[A, B](()) + /** + * Creates a codec middleware using two Http. + */ + def codecHttp[A, B]: PartialCodecHttp[A, B] = new PartialCodecHttp[A, B](()) + /** * Creates a middleware using specified effectful encoder and decoder */ @@ -326,6 +331,17 @@ object Middleware extends Web { Middleware.identity.mapZIO(encoder).contramapZIO(decoder) } + final class PartialCodecHttp[AOut, BIn](val unit: Unit) extends AnyVal { + def apply[R, E, AIn, BOut]( + decoder: Http[R, E, AOut, AIn], + encoder: Http[R, E, BIn, BOut], + ): Middleware[R, E, AIn, BIn, AOut, BOut] = + new Middleware[R, E, AIn, BIn, AOut, BOut] { + override def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut] = + decoder >>> http >>> encoder + } + } + final class PartialContraMapZIO[-R, +E, +AIn, -BIn, -AOut, +BOut, AOut0]( val self: Middleware[R, E, AIn, BIn, AOut, BOut], ) extends AnyVal { diff --git a/zio-http/src/main/scala/zhttp/http/Request.scala b/zio-http/src/main/scala/zhttp/http/Request.scala index 6bf4736bff..7ec1a4cac6 100644 --- a/zio-http/src/main/scala/zhttp/http/Request.scala +++ b/zio-http/src/main/scala/zhttp/http/Request.scala @@ -78,6 +78,14 @@ trait Request extends HeaderExtension[Request] with HttpDataExtension[Request] { */ def setUrl(url: URL): Request = self.copy(url = url) + /** + * Returns a string representation of the request, useful for debugging, + * logging or other purposes. It contains the essential properties of HTTP + * request: protocol version, method, URL, headers and remote address. + */ + override def toString = + s"Request($version, $method, $url, $headers, $remoteAddress)" + /** * Gets the HttpRequest */ @@ -142,6 +150,8 @@ object Request { override def version: Version = req.version override def unsafeEncode: HttpRequest = req.unsafeEncode override def data: HttpData = req.data + override def toString: String = + s"ParameterizedRequest($req, $params)" } object ParameterizedRequest { diff --git a/zio-http/src/main/scala/zhttp/http/Response.scala b/zio-http/src/main/scala/zhttp/http/Response.scala index 1851578b48..f82335e9fa 100644 --- a/zio-http/src/main/scala/zhttp/http/Response.scala +++ b/zio-http/src/main/scala/zhttp/http/Response.scala @@ -6,10 +6,9 @@ import io.netty.handler.codec.http.{FullHttpResponse, HttpHeaderNames, HttpRespo import zhttp.html._ import zhttp.http.headers.HeaderExtension import zhttp.socket.{IsWebSocket, Socket, SocketApp} -import zio.{Chunk, UIO, ZIO} +import zio.{UIO, ZIO} import java.io.{PrintWriter, StringWriter} -import java.nio.charset.Charset final case class Response private ( status: Status, @@ -19,6 +18,47 @@ final case class Response private ( ) extends HeaderExtension[Response] with HttpDataExtension[Response] { self => + /** + * Encodes the Response into a Netty HttpResponse. Sets default headers such + * as `content-length`. For performance reasons, it is possible that it uses a + * FullHttpResponse if the complete data is available. Otherwise, it would + * create a DefaultHttpResponse without any content. + */ + private[zhttp] def unsafeEncode(): HttpResponse = { + import io.netty.handler.codec.http._ + + val jHeaders = self.headers.encode + val jContent = self.data match { + case HttpData.UnsafeAsync(_) => null + case data: HttpData.Complete => + data match { + case HttpData.FromAsciiString(text) => Unpooled.wrappedBuffer(text.array()) + case HttpData.BinaryChunk(data) => Unpooled.wrappedBuffer(data.toArray) + case HttpData.BinaryByteBuf(data) => data + case HttpData.BinaryStream(_) => null + case HttpData.Empty => Unpooled.EMPTY_BUFFER + case HttpData.JavaFile(_) => null + } + } + + val hasContentLength = jHeaders.contains(HttpHeaderNames.CONTENT_LENGTH) + if (jContent == null) { + // TODO: Unit test for this + // Client can't handle chunked responses and currently treats them as a FullHttpResponse. + // Due to this client limitation it is not possible to write a unit-test for this. + // Alternative would be to use sttp client for this use-case. + + if (!hasContentLength) jHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED) + + new DefaultHttpResponse(HttpVersion.HTTP_1_1, self.status.asJava, jHeaders) + } else { + val jResponse = new DefaultFullHttpResponse(HTTP_1_1, self.status.asJava, jContent, false) + if (!hasContentLength) jHeaders.set(HttpHeaderNames.CONTENT_LENGTH, jContent.readableBytes()) + jResponse.headers().add(jHeaders) + jResponse + } + } + /** * Adds cookies in the response headers. */ @@ -49,6 +89,11 @@ final case class Response private ( def setStatus(status: Status): Response = self.copy(status = status) + /** + * Creates an Http from a Response + */ + def toHttp: Http[Any, Nothing, Any, Response] = Http.succeed(self) + /** * Updates the headers using the provided function */ @@ -59,50 +104,16 @@ final case class Response private ( * A more efficient way to append server-time to the response headers. */ def withServerTime: Response = self.copy(attribute = self.attribute.withServerTime) - - /** - * Encodes the Response into a Netty HttpResponse. Sets default headers such - * as `content-length`. For performance reasons, it is possible that it uses a - * FullHttpResponse if the complete data is available. Otherwise, it would - * create a DefaultHttpResponse without any content. - */ - private[zhttp] def unsafeEncode(): HttpResponse = { - import io.netty.handler.codec.http._ - - val jHeaders = self.headers.encode - val jContent = self.data match { - case HttpData.Incoming(_) => null - case data: HttpData.Outgoing => - data match { - case HttpData.Text(text, charset) => Unpooled.wrappedBuffer(text.getBytes(charset)) - case HttpData.BinaryChunk(data) => Unpooled.copiedBuffer(data.toArray) - case HttpData.BinaryByteBuf(data) => data - case HttpData.BinaryStream(_) => null - case HttpData.Empty => Unpooled.EMPTY_BUFFER - case HttpData.RandomAccessFile(_) => null - } - } - - val hasContentLength = jHeaders.contains(HttpHeaderNames.CONTENT_LENGTH) - if (jContent == null) { - // TODO: Unit test for this - // Client can't handle chunked responses and currently treats them as a FullHttpResponse. - // Due to this client limitation it is not possible to write a unit-test for this. - // Alternative would be to use sttp client for this use-case. - - if (!hasContentLength) jHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED) - - new DefaultHttpResponse(HttpVersion.HTTP_1_1, self.status.asJava, jHeaders) - } else { - val jResponse = new DefaultFullHttpResponse(HTTP_1_1, self.status.asJava, jContent, false) - if (!hasContentLength) jHeaders.set(HttpHeaderNames.CONTENT_LENGTH, jContent.readableBytes()) - jResponse.headers().add(jHeaders) - jResponse - } - } } object Response { + private[zhttp] def unsafeFromJResponse(jRes: FullHttpResponse): Response = { + val status = Status.fromHttpResponseStatus(jRes.status()) + val headers = Headers.decode(jRes.headers()) + val data = HttpData.fromByteBuf(Unpooled.copiedBuffer(jRes.content())) + Response(status, headers, data) + } + def apply[R, E]( status: Status = Status.Ok, headers: Headers = Headers.empty, @@ -181,9 +192,9 @@ object Response { /** * Creates a response with content-type set to application/json */ - def json(data: String): Response = + def json(data: CharSequence): Response = Response( - data = HttpData.fromChunk(Chunk.fromArray(data.getBytes(HTTP_CHARSET))), + data = HttpData.fromCharSequence(data), headers = Headers(HeaderNames.contentType, HeaderValues.applicationJson), ) @@ -196,7 +207,7 @@ object Response { * Creates an empty response with status 301 or 302 depending on if it's * permanent or not. */ - def redirect(location: String, isPermanent: Boolean = false): Response = { + def redirect(location: CharSequence, isPermanent: Boolean = false): Response = { val status = if (isPermanent) Status.PermanentRedirect else Status.TemporaryRedirect Response(status, Headers.location(location)) } @@ -209,19 +220,12 @@ object Response { /** * Creates a response with content-type set to text/plain */ - def text(text: String, charset: Charset = HTTP_CHARSET): Response = + def text(text: CharSequence): Response = Response( - data = HttpData.fromString(text, charset), + data = HttpData.fromCharSequence(text), headers = Headers(HeaderNames.contentType, HeaderValues.textPlain), ) - private[zhttp] def unsafeFromJResponse(jRes: FullHttpResponse): Response = { - val status = Status.fromHttpResponseStatus(jRes.status()) - val headers = Headers.decode(jRes.headers()) - val data = HttpData.fromByteBuf(Unpooled.copiedBuffer(jRes.content())) - Response(status, headers, data) - } - /** * Attribute holds meta data for the backend */ diff --git a/zio-http/src/main/scala/zhttp/service/Handler.scala b/zio-http/src/main/scala/zhttp/service/Handler.scala index 6bd4cf5ea1..cebe09a8a1 100644 --- a/zio-http/src/main/scala/zhttp/service/Handler.scala +++ b/zio-http/src/main/scala/zhttp/service/Handler.scala @@ -72,7 +72,7 @@ private[zhttp] final case class Handler[R]( jReq, app, new Request { - override def data: HttpData = HttpData.Incoming(callback => + override def data: HttpData = HttpData.UnsafeAsync(callback => ctx .pipeline() .addAfter(HTTP_REQUEST_HANDLER, HTTP_CONTENT_HANDLER, new RequestBodyHandler(callback)): Unit, diff --git a/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala b/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala index 2d4058c53f..11ec03c84a 100644 --- a/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala +++ b/zio-http/src/main/scala/zhttp/service/HttpRuntime.scala @@ -47,7 +47,7 @@ final class HttpRuntime[+R](strategy: HttpRuntime.Strategy[R]) { .unsafeRunAsyncWith(program) { case Exit.Success(_) => () case Exit.Failure(cause) => - cause.failureOption match { + cause.failureOption.orElse(cause.dieOption) match { case None => () case Some(_) => java.lang.System.err.println(cause.prettyPrint) } diff --git a/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala b/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala index b0026a7838..2566a4ee85 100644 --- a/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala +++ b/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala @@ -9,7 +9,7 @@ import zhttp.service.{ChannelFuture, HttpRuntime, Server} import zio.ZIO import zio.stream.ZStream -import java.io.RandomAccessFile +import java.io.File private[zhttp] trait ServerResponseHandler[R] { type Ctx = ChannelHandlerContext @@ -20,10 +20,20 @@ private[zhttp] trait ServerResponseHandler[R] { def writeResponse(msg: Response, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { ctx.write(encodeResponse(msg)) - writeData(msg.data.asInstanceOf[HttpData.Outgoing], jReq) + writeData(msg.data.asInstanceOf[HttpData.Complete], jReq) () } + /** + * Enables auto-read if possible. Also performs the first read. + */ + private def attemptAutoRead()(implicit ctx: Ctx): Unit = { + if (!config.useAggregator && !ctx.channel().config().isAutoRead) { + ctx.channel().config().setAutoRead(true) + ctx.read(): Unit + } + } + /** * Checks if an encoded version of the response exists, uses it if it does. * Otherwise, it will return a fresh response. It will also set the server @@ -49,6 +59,16 @@ private[zhttp] trait ServerResponseHandler[R] { jResponse } + private def flushReleaseAndRead(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + ctx.flush() + releaseAndRead(jReq) + } + + private def releaseAndRead(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + releaseRequest(jReq) + attemptAutoRead() + } + /** * Releases the FullHttpRequest safely. */ @@ -62,10 +82,9 @@ private[zhttp] trait ServerResponseHandler[R] { /** * Writes file content to the Channel. Does not use Chunked transfer encoding */ - private def unsafeWriteFileContent(raf: RandomAccessFile)(implicit ctx: ChannelHandlerContext): Unit = { - val fileLength = raf.length() + private def unsafeWriteFileContent(file: File)(implicit ctx: ChannelHandlerContext): Unit = { // Write the content. - ctx.write(new DefaultFileRegion(raf.getChannel, 0, fileLength)) + ctx.write(new DefaultFileRegion(file, 0, file.length())) // Write the end marker. ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT): Unit } @@ -73,32 +92,25 @@ private[zhttp] trait ServerResponseHandler[R] { /** * Writes data on the channel */ - private def writeData(data: HttpData.Outgoing, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + private def writeData(data: HttpData.Complete, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { data match { - case HttpData.BinaryStream(stream) => + + case _: HttpData.FromAsciiString => flushReleaseAndRead(jReq) + + case _: HttpData.BinaryChunk => flushReleaseAndRead(jReq) + + case _: HttpData.BinaryByteBuf => flushReleaseAndRead(jReq) + + case HttpData.Empty => flushReleaseAndRead(jReq) + + case HttpData.BinaryStream(stream) => rt.unsafeRun(ctx) { - writeStreamContent(stream).ensuring(ZIO.succeed { - releaseRequest(jReq) - if (!config.useAggregator && !ctx.channel().config().isAutoRead) { - ctx.channel().config().setAutoRead(true) - ctx.read(): Unit - } // read next HttpContent - }) + writeStreamContent(stream).ensuring(ZIO.succeed(releaseAndRead(jReq))) } - case HttpData.RandomAccessFile(raf) => - unsafeWriteFileContent(raf()) - releaseRequest(jReq) - if (!config.useAggregator && !ctx.channel().config().isAutoRead) { - ctx.channel().config().setAutoRead(true) - ctx.read(): Unit - } // read next HttpContent - case _ => - ctx.flush() - releaseRequest(jReq) - if (!config.useAggregator && !ctx.channel().config().isAutoRead) { - ctx.channel().config().setAutoRead(true) - ctx.read(): Unit - } // read next HttpContent + + case HttpData.JavaFile(unsafeGet) => + unsafeWriteFileContent(unsafeGet()) + releaseAndRead(jReq) } } diff --git a/zio-http/src/test/scala/zhttp/html/DomSpec.scala b/zio-http/src/test/scala/zhttp/html/DomSpec.scala index f4ac9aa85b..c1272cef63 100644 --- a/zio-http/src/test/scala/zhttp/html/DomSpec.scala +++ b/zio-http/src/test/scala/zhttp/html/DomSpec.scala @@ -69,8 +69,8 @@ object DomSpec extends DefaultRunnableSpec { assertTrue(dom.encode == """zio-http""") } + suite("Self Closing") { - val voidTagGen: Gen[Any, String] = Gen.fromIterable(Element.voidElementNames) - val tagGen: Gen[Random, String] = + val voidTagGen: Gen[Any, CharSequence] = Gen.fromIterable(Element.voidElementNames) + val tagGen: Gen[Random, String] = Gen.stringBounded(1, 5)(Gen.alphaChar).filterNot(Element.voidElementNames.contains) test("void") { diff --git a/zio-http/src/test/scala/zhttp/http/HttpDataSpec.scala b/zio-http/src/test/scala/zhttp/http/HttpDataSpec.scala index 6cd7d873e1..2005d29e8e 100644 --- a/zio-http/src/test/scala/zhttp/http/HttpDataSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/HttpDataSpec.scala @@ -1,29 +1,47 @@ package zhttp.http +import zhttp.http.HttpData.ByteBufConfig +import zio.durationInt import zio.stream.ZStream -import zio.test.Assertion.equalTo +import zio.test.Assertion.{anything, equalTo, isLeft, isSubtype} +import zio.test.TestAspect.timeout import zio.test.{DefaultRunnableSpec, Gen, assertM, checkAll} import java.io.File object HttpDataSpec extends DefaultRunnableSpec { - // TODO : Add tests for othe HttpData types override def spec = - suite("HttpDataSpec")( - suite("Test toByteBuf")( - test("HttpData.fromFile") { - val file = new File(getClass.getResource("/TestFile.txt").getPath) - val res = HttpData.fromFile(file).toByteBuf.map(_.toString(HTTP_CHARSET)) - assertM(res)(equalTo("abc\nfoo")) - }, - test("HttpData.fromStream") { - checkAll(Gen.string) { payload => - val stringBuffer = payload.toString.getBytes(HTTP_CHARSET) - val responseContent = ZStream.fromIterable(stringBuffer) - val res = HttpData.fromStream(responseContent).toByteBuf.map(_.toString(HTTP_CHARSET)) - assertM(res)(equalTo(payload)) - } - }, - ), - ) + suite("HttpDataSpec") { + val testFile = new File(getClass.getResource("/TestFile.txt").getPath) + suite("outgoing") { + suite("encode")( + suite("fromStream") { + test("success") { + checkAll(Gen.string) { payload => + val stringBuffer = payload.getBytes(HTTP_CHARSET) + val responseContent = ZStream.fromIterable(stringBuffer) + val res = HttpData.fromStream(responseContent).toByteBuf.map(_.toString(HTTP_CHARSET)) + assertM(res)(equalTo(payload)) + } + } + }, + suite("fromFile")( + test("failure") { + val res = HttpData.fromFile(throw new Error("Failure")).toByteBuf.either + assertM(res)(isLeft(isSubtype[Error](anything))) + }, + test("success") { + lazy val file = testFile + val res = HttpData.fromFile(file).toByteBuf.map(_.toString(HTTP_CHARSET)) + assertM(res)(equalTo("abc\nfoo")) + }, + test("success small chunk") { + lazy val file = testFile + val res = HttpData.fromFile(file).toByteBuf(ByteBufConfig(3)).map(_.toString(HTTP_CHARSET)) + assertM(res)(equalTo("abc\nfoo")) + }, + ), + ) + } + } @@ timeout(5 seconds) } diff --git a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala index 541a41fc88..ab104807f1 100644 --- a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala @@ -79,6 +79,25 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { assert(actual)(isEmpty) }, ) + + suite("codecMiddleware")( + test("codec success") { + val a = Http.collect[Int] { case v => v.toString } + val b = Http.collect[String] { case v => v.toInt } + val app = Http.identity[String] @@ (a \/ b) + val actual = app.execute(2) + assert(actual)(isSuccess(equalTo(2))) + } + + test("encoder failure") { + val app = Http.identity[Int] @@ (Http.succeed(1) \/ Http.fail("fail")) + val actual = app.execute(()) + assert(actual)(isFailure(equalTo("fail"))) + } + + test("decoder failure") { + val app = Http.identity[Int] @@ (Http.fail("fail") \/ Http.succeed(1)) + val actual = app.execute(()) + assert(actual)(isFailure(equalTo("fail"))) + }, + ) + suite("collectHExit")( test("should succeed") { val a = Http.collectHExit[Int] { case 1 => HExit.succeed("OK") } diff --git a/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala b/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala index 66c6f9a19d..46e1f0fe73 100644 --- a/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala @@ -150,6 +150,25 @@ object MiddlewareSpec extends DefaultRunnableSpec with HExitAssertion { val app = Http.identity[Int] @@ mid assertM(app("1").exit)(fails(anything)) } + } + + suite("codecHttp") { + test("codec success") { + val a = Http.collect[Int] { case v => v.toString } + val b = Http.collect[String] { case v => v.toInt } + val mid = Middleware.codecHttp[String, Int](b, a) + val app = Http.identity[Int] @@ mid + assertM(app("2"))(equalTo("2")) + } + + test("encoder failure") { + val mid = Middleware.codecHttp[String, Int](Http.succeed(1), Http.fail("fail")) + val app = Http.identity[Int] @@ mid + assertM(app("2").exit)(fails(anything)) + } + + test("decoder failure") { + val mid = Middleware.codecHttp[String, Int](Http.fail("fail"), Http.succeed(2)) + val app = Http.identity[Int] @@ mid + assertM(app("2").exit)(fails(anything)) + } } } } diff --git a/zio-http/src/test/scala/zhttp/http/RequestSpec.scala b/zio-http/src/test/scala/zhttp/http/RequestSpec.scala new file mode 100644 index 0000000000..0820ca5723 --- /dev/null +++ b/zio-http/src/test/scala/zhttp/http/RequestSpec.scala @@ -0,0 +1,28 @@ +package zhttp.http + +import zhttp.internal.HttpGen +import zio.test.Assertion._ +import zio.test._ + +object RequestSpec extends DefaultRunnableSpec { + def spec = suite("Request")( + suite("toString") { + test("should produce string representation of a request") { + check(HttpGen.request) { req => + assert(req.toString)( + equalTo(s"Request(${req.version}, ${req.method}, ${req.url}, ${req.headers}, ${req.remoteAddress})"), + ) + } + } + + test("should produce string representation of a parameterized request") { + check(HttpGen.parameterizedRequest(Gen.alphaNumericString)) { req => + assert(req.toString)( + equalTo( + s"ParameterizedRequest(Request(${req.version}, ${req.method}, ${req.url}, ${req.headers}, ${req.remoteAddress}), ${req.params})", + ), + ) + } + } + }, + ) +} diff --git a/zio-http/src/test/scala/zhttp/http/ResponseSpec.scala b/zio-http/src/test/scala/zhttp/http/ResponseSpec.scala index 8b13789179..4bf3c41667 100644 --- a/zio-http/src/test/scala/zhttp/http/ResponseSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/ResponseSpec.scala @@ -1 +1,41 @@ +package zhttp.http +import zio.test.Assertion._ +import zio.test._ + +object ResponseSpec extends DefaultRunnableSpec { + def spec = suite("Response")( + suite("redirect") { + val location = "www.google.com" + test("Temporary redirect should produce a response with a TEMPORARY_REDIRECT") { + val x = Response.redirect(location) + assertTrue(x.status == Status.TemporaryRedirect) && + assertTrue(x.headerValue(HeaderNames.location).contains(location)) + } + + test("Temporary redirect should produce a response with a location") { + val x = Response.redirect(location) + assertTrue(x.headerValue(HeaderNames.location).contains(location)) + } + + test("Permanent redirect should produce a response with a PERMANENT_REDIRECT") { + val x = Response.redirect(location, true) + assertTrue(x.status == Status.PermanentRedirect) + } + + test("Permanent redirect should produce a response with a location") { + val x = Response.redirect(location, true) + assertTrue(x.headerValue(HeaderNames.location).contains(location)) + } + } + + suite("json")( + test("Json should set content type to ApplicationJson") { + val x = Response.json("""{"message": "Hello"}""") + assertTrue(x.headerValue(HeaderNames.contentType).contains(HeaderValues.applicationJson.toString)) + }, + ) + + suite("toHttp")( + test("should convert response to Http") { + val http = Http(Response.ok) + assertM(http(()))(equalTo(Response.ok)) + }, + ), + ) +} diff --git a/zio-http/src/test/scala/zhttp/internal/DynamicServer.scala b/zio-http/src/test/scala/zhttp/internal/DynamicServer.scala index 8d3f209054..888e5a2538 100644 --- a/zio-http/src/test/scala/zhttp/internal/DynamicServer.scala +++ b/zio-http/src/test/scala/zhttp/internal/DynamicServer.scala @@ -1,31 +1,19 @@ package zhttp.internal import zhttp.http._ -import zhttp.internal.DynamicServer.{HttpEnv, Id} import zhttp.service.Server.Start import zio._ import java.util.UUID -sealed trait DynamicServer { - def add(app: HttpApp[HttpEnv, Throwable]): UIO[Id] - def get(id: Id): UIO[Option[HttpApp[HttpEnv, Throwable]]] +object DynamicServer { - def port: ZIO[Any, Nothing, Int] + type Id = String - def start: IO[Nothing, Start] - - def setStart(n: Start): UIO[Boolean] -} -object DynamicServer { - - type Id = String - type HttpEnv = DynamicServer with Console - type HttpAppTest = HttpApp[HttpEnv, Throwable] val APP_ID = "X-APP_ID" - def app: HttpApp[HttpEnv, Throwable] = Http - .fromOptionFunction[Request] { case req => + def app: HttpApp[DynamicServer, Throwable] = Http + .fromOptionFunction[Request] { req => for { id <- req.headerValue(APP_ID) match { case Some(id) => ZIO.succeed(id) @@ -42,52 +30,57 @@ object DynamicServer { def baseURL(scheme: Scheme): ZIO[DynamicServer, Nothing, String] = port.map(port => s"${scheme.encode}://localhost:$port") - def deploy(app: HttpApp[HttpEnv, Throwable]): ZIO[DynamicServer, Nothing, String] = - ZIO.serviceWithZIO[DynamicServer](_.add(app)) + def deploy[R](app: HttpApp[R, Throwable]): ZIO[DynamicServer with R, Nothing, String] = + for { + env <- ZIO.environment[R] + id <- ZIO.environmentWithZIO[DynamicServer](_.get.add(app.provideEnvironment(env))) + } yield id - def get(id: Id): ZIO[DynamicServer, Nothing, Option[HttpApp[HttpEnv, Throwable]]] = - ZIO.serviceWithZIO[DynamicServer](_.get(id)) + def get(id: Id): ZIO[DynamicServer, Nothing, Option[HttpApp[Any, Throwable]]] = + ZIO.environmentWithZIO[DynamicServer](_.get.get(id)) def httpURL: ZIO[DynamicServer, Nothing, String] = baseURL(Scheme.HTTP) def live: ZLayer[Any, Nothing, DynamicServer] = { for { - ref <- Ref.make(Map.empty[Id, HttpApp[HttpEnv, Throwable]]) + ref <- Ref.make(Map.empty[Id, HttpApp[Any, Throwable]]) pr <- Promise.make[Nothing, Start] } yield new Live(ref, pr) }.toLayer - def port: ZIO[DynamicServer, Nothing, Int] = ZIO.serviceWithZIO[DynamicServer](_.port) + def port: ZIO[DynamicServer, Nothing, Int] = ZIO.environmentWithZIO[DynamicServer](_.get.port) - def setStart(s: Start): ZIO[DynamicServer, Nothing, Boolean] = ZIO.serviceWithZIO[DynamicServer](_.setStart(s)) + def setStart(s: Start): ZIO[DynamicServer, Nothing, Boolean] = + ZIO.environmentWithZIO[DynamicServer](_.get.setStart(s)) - def start: ZIO[DynamicServer, Nothing, Start] = ZIO.serviceWithZIO[DynamicServer](_.start) + def start: ZIO[DynamicServer, Nothing, Start] = ZIO.environmentWithZIO[DynamicServer](_.get.start) def wsURL: ZIO[DynamicServer, Nothing, String] = baseURL(Scheme.WS) sealed trait Service { - def add(app: HttpApp[HttpEnv, Throwable]): UIO[Id] - def get(id: Id): UIO[Option[HttpApp[HttpEnv, Throwable]]] + def add(app: HttpApp[Any, Throwable]): UIO[Id] - def port: ZIO[Any, Nothing, Int] + def get(id: Id): UIO[Option[HttpApp[Any, Throwable]]] - def start: IO[Nothing, Start] + def port: ZIO[Any, Nothing, Int] def setStart(n: Start): UIO[Boolean] + + def start: IO[Nothing, Start] } - final class Live(ref: Ref[Map[Id, HttpApp[HttpEnv, Throwable]]], pr: Promise[Nothing, Start]) extends DynamicServer { - def add(app: HttpApp[HttpEnv, Throwable]): UIO[Id] = for { + final class Live(ref: Ref[Map[Id, HttpApp[Any, Throwable]]], pr: Promise[Nothing, Start]) extends Service { + def add(app: HttpApp[Any, Throwable]): UIO[Id] = for { id <- ZIO.succeed(UUID.randomUUID().toString) _ <- ref.update(map => map + (id -> app)) } yield id - def get(id: Id): UIO[Option[HttpApp[HttpEnv, Throwable]]] = ref.get.map(_.get(id)) + def get(id: Id): UIO[Option[HttpApp[Any, Throwable]]] = ref.get.map(_.get(id)) def port: ZIO[Any, Nothing, Int] = start.map(_.port) - def start: IO[Nothing, Start] = pr.await - def setStart(s: Start): UIO[Boolean] = pr.complete(ZIO.attempt(s).orDie) + + def start: IO[Nothing, Start] = pr.await } } diff --git a/zio-http/src/test/scala/zhttp/internal/HttpGen.scala b/zio-http/src/test/scala/zhttp/internal/HttpGen.scala index 41ceb6d8fb..d3b5c99db1 100644 --- a/zio-http/src/test/scala/zhttp/internal/HttpGen.scala +++ b/zio-http/src/test/scala/zhttp/internal/HttpGen.scala @@ -1,12 +1,13 @@ package zhttp.internal import io.netty.buffer.Unpooled +import zhttp.http.Request.ParameterizedRequest import zhttp.http.Scheme.{HTTP, HTTPS, WS, WSS} import zhttp.http.URL.Location import zhttp.http._ import zio.stream.ZStream import zio.test.{Gen, Sized} -import zio.{Chunk, ZIO, _} +import zio.{Chunk, Random, ZIO} import java.io.File @@ -124,6 +125,13 @@ object HttpGen { } yield p } + def parameterizedRequest[R, A](paramsGen: Gen[R, A]): Gen[R with Random with Sized, ParameterizedRequest[A]] = { + for { + req <- request + params <- paramsGen + } yield ParameterizedRequest(req, params) + } + def request: Gen[Random with Sized, Request] = for { version <- httpVersion method <- HttpGen.method diff --git a/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala b/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala index 219a9d4e1b..ed9a0f42c0 100644 --- a/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala +++ b/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala @@ -2,8 +2,6 @@ package zhttp.internal import zhttp.http.URL.Location import zhttp.http._ -import zhttp.internal.DynamicServer.HttpEnv -import zhttp.internal.HttpRunnableSpec.HttpTestClient import zhttp.service.Client.Config import zhttp.service._ import zhttp.service.client.ClientSSLHandler.ClientSSLOptions @@ -47,7 +45,10 @@ abstract class HttpRunnableSpec extends DefaultRunnableSpec { self => } } - implicit class RunnableHttpClientAppSyntax(app: HttpApp[HttpEnv, Throwable]) { + implicit class RunnableHttpClientAppSyntax[R, E](http: HttpApp[R, E]) { + + def app(implicit e: E <:< Throwable): HttpApp[R, Throwable] = + http.asInstanceOf[HttpApp[R, Throwable]] /** * Deploys the http application on the test server and returns a Http of @@ -56,7 +57,7 @@ abstract class HttpRunnableSpec extends DefaultRunnableSpec { self => * while writing tests. It also allows us to simply pass a request in the * end, to execute, and resolve it with a response, like a normal HttpApp. */ - def deploy: HttpTestClient[Any, Request, Response] = + def deploy(implicit e: E <:< Throwable): Http[R with HttpEnv, Throwable, Request, Response] = for { port <- Http.fromZIO(DynamicServer.port) id <- Http.fromZIO(DynamicServer.deploy(app)) @@ -70,11 +71,11 @@ abstract class HttpRunnableSpec extends DefaultRunnableSpec { self => } } yield response - def deployWS: HttpTestClient[Any, SocketApp[Any], Response] = + def deployWS(implicit e: E <:< Throwable): Http[R with HttpEnv, Throwable, SocketApp[HttpEnv], Response] = for { id <- Http.fromZIO(DynamicServer.deploy(app)) url <- Http.fromZIO(DynamicServer.wsURL) - response <- Http.fromFunctionZIO[SocketApp[Any]] { app => + response <- Http.fromFunctionZIO[SocketApp[HttpEnv]] { app => Client.socket( url = url, headers = Headers(DynamicServer.APP_ID, id), @@ -111,13 +112,3 @@ abstract class HttpRunnableSpec extends DefaultRunnableSpec { self => } yield status } } - -object HttpRunnableSpec { - type HttpTestClient[-R, -A, +B] = - Http[ - R with EventLoopGroup with ChannelFactory with DynamicServer with ServerChannelFactory, - Throwable, - A, - B, - ] -} diff --git a/zio-http/src/test/scala/zhttp/internal/package.scala b/zio-http/src/test/scala/zhttp/internal/package.scala new file mode 100644 index 0000000000..d889c50934 --- /dev/null +++ b/zio-http/src/test/scala/zhttp/internal/package.scala @@ -0,0 +1,8 @@ +package zhttp + +import zhttp.service.{ChannelFactory, EventLoopGroup, ServerChannelFactory} + +package object internal { + type DynamicServer = DynamicServer.Service + type HttpEnv = EventLoopGroup with ChannelFactory with DynamicServer with ServerChannelFactory +}