Skip to content

Commit

Permalink
breaking: Make Root a PathCodec, remove Empty (#2796)
Browse files Browse the repository at this point in the history
* Add info about home routes

It took me a lot of time to find out how to express a route to the root path, a lot of friction. Lets make sure this never happens again by making this part of the very first example.

Also: remove ZERO WIDTH NON-JOINER character.

* breaking: Make `Root` a PathCodec

... to improve the DSL for root routes.

Also: remove the 'watch mode' section from the docs as watch mode is not intended for starting users.

* Replace `Root` by `Path.root`, `Empty` by `Path.empty`

Remove `Empty`.

* Document migration

* Update documentation

* sbt fmt

* Merge 2798
  • Loading branch information
erikvanoosten authored Apr 25, 2024
1 parent 8d3acf8 commit 58e23c3
Show file tree
Hide file tree
Showing 19 changed files with 144 additions and 141 deletions.
13 changes: 5 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ libraryDependencies += "dev.zio" %% "zio-http" % "@VERSION@"

ZIO HTTP provides a simple and expressive API for building HTTP applications. It supports both server and client-side APIs.

ZIO HTTP is designed in terms of **HTTP as function**, where both server and client are a function from `Request` to `Response`.
ZIO HTTP is designed in terms of **HTTP as function**, where both server and client are a function from `Request` to `Response`.

### Greeting Server

The following example demonstrates how to build a simple greeting server that responds with a greeting message based on the query parameter `name`.
The following example demonstrates how to build a simple greeting server. It contains 2 routes: one on the root
path, it responds with a fixed string, and one route on the path `/greet` that responds with a greeting message
based on the query parameter `name`.

```scala mdoc:silent
import zio._
Expand All @@ -38,6 +40,7 @@ import zio.http._
object GreetingServer extends ZIOAppDefault {
val routes =
Routes(
Method.GET / Root -> handler(Response.text("Greetings at your service")),
Method.GET / "greet" -> handler { (req: Request) =>
val name = req.queryParamToOrElse("name", "World")
Response.text(s"Hello $name!")
Expand Down Expand Up @@ -69,9 +72,3 @@ object GreetingClient extends ZIOAppDefault {
def run = app.provide(Client.default, Scope.default)
}
```

## Watch Mode

We can use the [sbt-revolver] plugin to start the server and run it in watch mode using `~ reStart` command on the SBT console.

[sbt-revolver]: https://github.com/spray/sbt-revolver
3 changes: 3 additions & 0 deletions docs/migration/RC6-to-xx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**`Root` and `Empty`**
- replace `Root` with `Path.root`
- replace `Empty` with `Path.empty`
2 changes: 1 addition & 1 deletion docs/reference/cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ responseCookie.copy(domain = Some("example.com"))
- `path` updates the path of the cookie:

```scala mdoc:compile-only
responseCookie.copy(path = Some(Root / "cookie"))
responseCookie.copy(path = Some(Path.root / "cookie"))
```

- `isSecure` enables cookie only on https server:
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The below snippet creates a request with default params, `headers` as `Headers.e
```scala mdoc
import zio.http._

Request(method = Method.GET, url = URL(Root))
Request(method = Method.GET, url = URL(Path.root))
```

There are also some helper methods to create requests for different HTTP methods inside the `Request`'s companion object: `delete`, `get`, `head`, `options`, `patch`, `post`, and `put`.
Expand Down Expand Up @@ -71,7 +71,7 @@ The below snippet creates a request with query params: `?q=a&q=b&q=c`
import zio._
import zio.http._

Request.get(url = URL(Root, queryParams = QueryParams("q" -> Chunk("a","b","c"))))
Request.get(url = URL(Path.root, queryParams = QueryParams("q" -> Chunk("a","b","c"))))
```

The `Request#url.queryParams` can be used to read query params from the request.
Expand Down Expand Up @@ -266,7 +266,7 @@ import zio.http._

val request = Request(
method = Method.GET,
url = URL(Root),
url = URL(Path.root),
headers = Headers(
Header.Cookie(
NonEmptyChunk(
Expand Down
2 changes: 1 addition & 1 deletion docs/testing-http-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object ExampleSpec extends ZIOSpecDefault {
def spec = suite("http")(
test("should be ok") {
val app = Handler.ok.toHttpApp
val req = Request.get(URL(Root))
val req = Request.get(URL(Path.root))
assertZIO(app.runZIO(req))(equalTo(Response.ok))
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class HttpCollectEval {

@Benchmark
def benchmarkHttp(): Unit = {
(0 to MAX).foreach(_ => http(Request.get(url = URL(Root / "text"))))
(0 to MAX).foreach(_ => http(Request.get(url = URL(Path.root / "text"))))
()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class HttpRouteTextPerf {

private val res = Response.text("HELLO WORLD")
private val app = Handler.succeed(res)
private val req: Request = Request.get(URL(Root))
private val req: Request = Request.get(URL(Path.root))
private val httpProgram = ZIO.foreachDiscard(0 to 1000) { _ => app(req) }
private val UIOProgram = ZIO.foreachDiscard(0 to 1000) { _ => ZIO.succeed(res) }

Expand Down
8 changes: 5 additions & 3 deletions zio-http-example/src/main/scala/example/HelloWorld.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import zio._
import zio.http._

object HelloWorld extends ZIOAppDefault {
val textRoute =
Method.GET / "text" -> handler(Response.text("Hello World!"))
// Responds with plain text
val homeRoute =
Method.GET / Root -> handler(Response.text("Hello World!"))

// Responds with JSON
val jsonRoute =
Method.GET / "json" -> handler(Response.json("""{"greetings": "Hello World!"}"""))

// Create HTTP route
val app = Routes(textRoute, jsonRoute)
val app = Routes(homeRoute, jsonRoute)

// Run it like any simple app
override val run = Server.serve(app).provide(Server.default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ object GetBodyAsStringSpec extends ZIOHttpSpec {
check(charsetGen) { charset =>
val request = Request
.post(
URL(Root),
URL(Path.root),
Body.fromChunk(Chunk.fromArray("abc".getBytes(charset))),
)
.addHeader(Header.ContentType(MediaType.text.html, charset = Some(charset)))
Expand All @@ -47,7 +47,7 @@ object GetBodyAsStringSpec extends ZIOHttpSpec {
}
},
test("should map bytes to default utf-8 if no charset given") {
val request = Request.post(URL(Root), Body.fromChunk(Chunk.fromArray("abc".getBytes())))
val request = Request.post(URL(Path.root), Body.fromChunk(Chunk.fromArray("abc".getBytes())))
val encoded = request.body.asString
val expected = new String(Chunk.fromArray("abc".getBytes()).toArray, Charsets.Http)
assertZIO(encoded)(equalTo(expected))
Expand Down
160 changes: 80 additions & 80 deletions zio-http/jvm/src/test/scala/zio/http/PathSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("other cases") {
val gen = Gen.fromIterable(
Seq(
Root / "a" -> List("", "a"),
Root / "a" / "b" -> List("", "a", "b"),
Root / "a" / "b" / "c" / "" -> List("", "a", "b", "c", ""),
Empty / "a" -> List("a"),
Empty / "a" / "b" -> List("a", "b"),
Empty / "a" / "b" / "c" / "" -> List("a", "b", "c", ""),
Path.root / "a" -> List("", "a"),
Path.root / "a" / "b" -> List("", "a", "b"),
Path.root / "a" / "b" / "c" / "" -> List("", "a", "b", "c", ""),
Path.empty / "a" -> List("a"),
Path.empty / "a" / "b" -> List("a", "b"),
Path.empty / "a" / "b" / "c" / "" -> List("a", "b", "c", ""),
),
)

Expand All @@ -101,12 +101,12 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("other cases") {
val gen = Gen.fromIterable(
Seq(
Root / "a" -> List("a", ""),
Root / "a" / "b" -> List("b", "a", ""),
Root / "a" / "b" / "c" / "" -> List("", "c", "b", "a", ""),
Empty / "a" -> List("a"),
Empty / "a" / "b" -> List("b", "a"),
Empty / "a" / "b" / "c" / "" -> List("", "c", "b", "a"),
Path.root / "a" -> List("a", ""),
Path.root / "a" / "b" -> List("b", "a", ""),
Path.root / "a" / "b" / "c" / "" -> List("", "c", "b", "a", ""),
Path.empty / "a" -> List("a"),
Path.empty / "a" / "b" -> List("b", "a"),
Path.empty / "a" / "b" / "c" / "" -> List("", "c", "b", "a"),
),
)

Expand All @@ -122,23 +122,23 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
// Internal representation of a path
val paths = Gen.fromIterable(
Seq(
"/" -> Root -> Path(Flags(Flag.LeadingSlash), Chunk.empty),
"/a" -> Root / a -> Path(Flags(Flag.LeadingSlash), Chunk("a")),
"/a/b" -> Root / a / b -> Path(Flags(Flag.LeadingSlash), Chunk("a", "b")),
"/a/b/c" -> Root / a / b / c -> Path(Flags(Flag.LeadingSlash), Chunk("a", "b", "c")),
"a/b/c" -> Empty / a / b / c -> Path(Flags.none, Chunk("a", "b", "c")),
"a/b" -> Empty / a / b -> Path(Flags.none, Chunk("a", "b")),
"a" -> Empty / a -> Path(Flags.none, Chunk("a")),
"" -> Empty -> Path(Flags.none, Chunk.empty),
"a/" -> Empty / a / "" -> Path(Flags(Flag.TrailingSlash), Chunk("a")),
"a/b/" -> Empty / a / b / "" -> Path(Flags(Flag.TrailingSlash), Chunk("a", "b")),
"a/b/c/" -> Empty / a / b / c / "" -> Path(Flags(Flag.TrailingSlash), Chunk("a", "b", "c")),
"/a/b/c/" -> Root / a / b / c / "" -> Path(
"/" -> Path.root -> Path(Flags(Flag.LeadingSlash), Chunk.empty),
"/a" -> Path.root / a -> Path(Flags(Flag.LeadingSlash), Chunk("a")),
"/a/b" -> Path.root / a / b -> Path(Flags(Flag.LeadingSlash), Chunk("a", "b")),
"/a/b/c" -> Path.root / a / b / c -> Path(Flags(Flag.LeadingSlash), Chunk("a", "b", "c")),
"a/b/c" -> Path.empty / a / b / c -> Path(Flags.none, Chunk("a", "b", "c")),
"a/b" -> Path.empty / a / b -> Path(Flags.none, Chunk("a", "b")),
"a" -> Path.empty / a -> Path(Flags.none, Chunk("a")),
"" -> Path.empty -> Path(Flags.none, Chunk.empty),
"a/" -> Path.empty / a / "" -> Path(Flags(Flag.TrailingSlash), Chunk("a")),
"a/b/" -> Path.empty / a / b / "" -> Path(Flags(Flag.TrailingSlash), Chunk("a", "b")),
"a/b/c/" -> Path.empty / a / b / c / "" -> Path(Flags(Flag.TrailingSlash), Chunk("a", "b", "c")),
"/a/b/c/" -> Path.root / a / b / c / "" -> Path(
Flags(Flag.LeadingSlash, Flag.TrailingSlash),
Chunk("a", "b", "c"),
),
"/a/b/" -> Root / a / b / "" -> Path(Flags(Flag.LeadingSlash, Flag.TrailingSlash), Chunk("a", "b")),
"/a/" -> Root / a / "" -> Path(Flags(Flag.LeadingSlash, Flag.TrailingSlash), Chunk("a")),
"/a/b/" -> Path.root / a / b / "" -> Path(Flags(Flag.LeadingSlash, Flag.TrailingSlash), Chunk("a", "b")),
"/a/" -> Path.root / a / "" -> Path(Flags(Flag.LeadingSlash, Flag.TrailingSlash), Chunk("a")),
),
)
checkAll(paths) { case ((encoded, path1), path2) =>
Expand All @@ -152,12 +152,12 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("multiple leading slashes") {
val encoded = "///a/b/c"
val decoded = Path.decode(encoded)
assertTrue(decoded == Root / a / b / c)
assertTrue(decoded == Path.root / a / b / c)
},
test("multiple trailing slashes") {
val encoded = "a/b/c///"
val decoded = Path.decode(encoded)
assertTrue(decoded == (Empty / a / b / c).addTrailingSlash)
assertTrue(decoded == (Path.empty / a / b / c).addTrailingSlash)
},
),
suite("isRoot")(
Expand Down Expand Up @@ -215,9 +215,9 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("simplifies internal representation") {
val urls = Gen.fromIterable(
Seq(
Root -> Root.addLeadingSlash,
Root -> Root.addLeadingSlash.addTrailingSlash,
Empty.addTrailingSlash -> Empty.addTrailingSlash.addTrailingSlash.addTrailingSlash,
Path.root -> Path.root.addLeadingSlash,
Path.root -> Path.root.addLeadingSlash.addTrailingSlash,
Path.empty.addTrailingSlash -> Path.empty.addTrailingSlash.addTrailingSlash.addTrailingSlash,
),
)
checkAll(urls) { case (actual, expected) => assertTrue(actual == expected) }
Expand All @@ -228,27 +228,27 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
assertTrue(Path.root == Path(Path.Flags(Flag.LeadingSlash, Flag.TrailingSlash), Chunk.empty))
},
test("prepending turns a root into a path with a trailing slash") {
assertTrue("a" /: Root == Path("a/"))
assertTrue("a" /: Path.root == Path("a/"))
},
test("appending turns a root into a path with a leading slash") {
assertTrue(Root / "a" == Path("/a"))
assertTrue(Path.root / "a" == Path("/a"))
},
test("root is not empty, but empty is") {
assertTrue(Root.isEmpty == false) &&
assertTrue(Empty.isEmpty == true)
assertTrue(Path.root.isEmpty == false) &&
assertTrue(Path.empty.isEmpty == true)
},
),
suite("startsWith")(
test("isTrue") {
val gen = Gen.fromIterable(
Seq(
Root -> Root,
Root / "a" -> Root / "a",
Root / "a" / "b" -> Root / "a" / "b",
Root / "a" / "b" / "c" -> Root / "a",
Root / "a" / "b" / "c" -> Root / "a" / "b" / "c",
Root / "a" / "b" / "c" -> Root / "a" / "b" / "c",
Root / "a" / "b" / "c" -> Root / "a" / "b" / "c",
Path.root -> Path.root,
Path.root / "a" -> Path.root / "a",
Path.root / "a" / "b" -> Path.root / "a" / "b",
Path.root / "a" / "b" / "c" -> Path.root / "a",
Path.root / "a" / "b" / "c" -> Path.root / "a" / "b" / "c",
Path.root / "a" / "b" / "c" -> Path.root / "a" / "b" / "c",
Path.root / "a" / "b" / "c" -> Path.root / "a" / "b" / "c",
),
)

Expand All @@ -260,11 +260,11 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("isFalse") {
val gen = Gen.fromIterable(
Seq(
Root -> Root / "a",
Root / "a" -> Root / "a" / "b",
Root / "a" -> Root / "b",
Root / "a" / "b" -> Root / "a" / "b" / "c",
Empty / "a" -> Root / "a",
Path.root -> Path.root / "a",
Path.root / "a" -> Path.root / "a" / "b",
Path.root / "a" -> Path.root / "b",
Path.root / "a" / "b" -> Path.root / "a" / "b" / "c",
Path.empty / "a" -> Path.root / "a",
),
)

Expand All @@ -277,17 +277,17 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("take") {
val gen = Gen.fromIterable(
Seq(
(1, Root) -> Root,
(1, Root / "a") -> Root,
(1, Root / "a" / "b") -> Root,
(1, Root / "a" / "b" / "c") -> Root,
(2, Root / "a" / "b" / "c") -> Root / "a",
(3, Root / "a" / "b" / "c") -> Root / "a" / "b",
(4, Root / "a" / "b" / "c") -> Root / "a" / "b" / "c",
(1, Root) -> Root / "",
(2, Root / "a" / "") -> Root / "a",
(3, Root / "a" / "b" / "") -> Root / "a" / "b",
(4, Root / "a" / "b" / "c" / "") -> Root / "a" / "b" / "c",
(1, Path.root) -> Path.root,
(1, Path.root / "a") -> Path.root,
(1, Path.root / "a" / "b") -> Path.root,
(1, Path.root / "a" / "b" / "c") -> Path.root,
(2, Path.root / "a" / "b" / "c") -> Path.root / "a",
(3, Path.root / "a" / "b" / "c") -> Path.root / "a" / "b",
(4, Path.root / "a" / "b" / "c") -> Path.root / "a" / "b" / "c",
(1, Path.root) -> Path.root / "",
(2, Path.root / "a" / "") -> Path.root / "a",
(3, Path.root / "a" / "b" / "") -> Path.root / "a" / "b",
(4, Path.root / "a" / "b" / "c" / "") -> Path.root / "a" / "b" / "c",
),
)

Expand All @@ -299,13 +299,13 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("drop") {
val gen = Gen.fromIterable(
Seq(
(1, Root) -> Empty,
(1, Root / "a") -> Empty / "a",
(1, Root / "a" / "b") -> Empty / "a" / "b",
(1, Root / "a" / "b" / "c") -> Empty / "a" / "b" / "c",
(2, Root / "a" / "b" / "c") -> Empty / "b" / "c",
(3, Root / "a" / "b" / "c") -> Empty / "c",
(4, Root / "a" / "b" / "c") -> Empty,
(1, Path.root) -> Path.empty,
(1, Path.root / "a") -> Path.empty / "a",
(1, Path.root / "a" / "b") -> Path.empty / "a" / "b",
(1, Path.root / "a" / "b" / "c") -> Path.empty / "a" / "b" / "c",
(2, Path.root / "a" / "b" / "c") -> Path.empty / "b" / "c",
(3, Path.root / "a" / "b" / "c") -> Path.empty / "c",
(4, Path.root / "a" / "b" / "c") -> Path.empty,
),
)

Expand All @@ -317,19 +317,19 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
test("dropRight") {
val gen = Gen.fromIterable(
Seq(
(1, Root) -> Empty,
(1, Root / "a") -> Root,
(1, Root / "a" / "b") -> Root / "a",
(1, Root / "a" / "b" / "c") -> Root / "a" / "b",
(2, Root / "a" / "b" / "c") -> Root / "a",
(3, Root / "a" / "b" / "c") -> Root,
(4, Root / "a" / "b" / "c") -> Empty,
(1, Empty / "a" / "") -> Empty / "a",
(1, Root / "a" / "") -> Root / "a",
(2, Empty / "a" / "") -> Empty,
(2, Root / "a" / "") -> Root,
(3, Empty / "a" / "") -> Empty,
(3, Root / "a" / "") -> Empty,
(1, Path.root) -> Path.empty,
(1, Path.root / "a") -> Path.root,
(1, Path.root / "a" / "b") -> Path.root / "a",
(1, Path.root / "a" / "b" / "c") -> Path.root / "a" / "b",
(2, Path.root / "a" / "b" / "c") -> Path.root / "a",
(3, Path.root / "a" / "b" / "c") -> Path.root,
(4, Path.root / "a" / "b" / "c") -> Path.empty,
(1, Path.empty / "a" / "") -> Path.empty / "a",
(1, Path.root / "a" / "") -> Path.root / "a",
(2, Path.empty / "a" / "") -> Path.empty,
(2, Path.root / "a" / "") -> Path.root,
(3, Path.empty / "a" / "") -> Path.empty,
(3, Path.root / "a" / "") -> Path.empty,
),
)

Expand All @@ -345,7 +345,7 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
val weirdRoot2 = Path(Path.Flags(Flag.LeadingSlash, Flag.TrailingSlash), Chunk.empty)
val weirdRoot3 = Path(Path.Flags(Flag.TrailingSlash), Chunk.empty)

assertTrue((path ++ Root).hasTrailingSlash) &&
assertTrue((path ++ Path.root).hasTrailingSlash) &&
assertTrue((path ++ weirdRoot1).hasTrailingSlash) &&
assertTrue((path ++ weirdRoot2).hasTrailingSlash) &&
assertTrue((path ++ weirdRoot3).hasTrailingSlash)
Expand All @@ -357,7 +357,7 @@ object PathSpec extends ZIOHttpSpec with ExitAssertion {
val weirdRoot2 = Path(Path.Flags(Flag.LeadingSlash, Flag.TrailingSlash), Chunk.empty)
val weirdRoot3 = Path(Path.Flags(Flag.TrailingSlash), Chunk.empty)

assertTrue((Root ++ path).hasLeadingSlash) &&
assertTrue((Path.root ++ path).hasLeadingSlash) &&
assertTrue((weirdRoot1 ++ path).hasLeadingSlash) &&
assertTrue((weirdRoot2 ++ path).hasLeadingSlash) &&
assertTrue((weirdRoot3 ++ path).hasLeadingSlash)
Expand Down
Loading

0 comments on commit 58e23c3

Please sign in to comment.