Skip to content

Commit

Permalink
Merge branch 'main' into ci_0
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil authored Feb 14, 2025
2 parents 1b29718 + 5e5c012 commit 0c8479f
Show file tree
Hide file tree
Showing 19 changed files with 378 additions and 125 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ jobs:

- uses: actions/checkout@v4
with:
repository: khajavi/FrameworkBenchmarks
repository: zio/FrameworkBenchmarks
path: FrameworkBenchMarks

- id: result
Expand Down Expand Up @@ -1059,7 +1059,7 @@ jobs:

- uses: actions/checkout@v4
with:
repository: khajavi/FrameworkBenchmarks
repository: zio/FrameworkBenchmarks
path: FrameworkBenchMarks

- id: result
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ object GreetingServer extends ZIOAppDefault {
Routes(
Method.GET / Root -> handler(Response.text("Greetings at your service")),
Method.GET / "greet" -> handler { (req: Request) =>
val name = req.queryParamToOrElse("name", "World")
val name = req.queryOrElse[String]("name", "World")
Response.text(s"Hello $name!")
}
)
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ val endpoint =
// typed path parameter "user"
Endpoint(Method.GET / "hello" / string("user"))
// reads the two query parameters city and age from the request and validates the age
.query(HttpCodec.queryAll[UserParams])
.query(HttpCodec.query[UserParams])
// support for HTML templates included
.out[Dom]

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ object GreetingServer extends ZIOAppDefault {
Routes(
Method.GET / Root -> handler(Response.text("Greetings at your service")),
Method.GET / "greet" -> handler { (req: Request) =>
val name = req.queryParamToOrElse("name", "World")
val name = req.queryOrElse[String]("name", "World")
Response.text(s"Hello $name!")
}
)
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/http-codec.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ val uuidQueryCodec : QueryCodec[UUID] = HttpCodec.query[UUID]("uuid")
We can combine multiple query codecs with `++`:


If we have multiple query parameters we can use `HttpCodec.queryAll`, `HttpCodec.queryAllBool`, `HttpCodec.queryAllInt`, and `HttpCodec.queryAllTo`:
If we have multiple query parameter values we can use `HttpCodec.query[A]` with a collection for the type parameter.

```scala mdoc:compile-only
import zio._
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ object ExampleServer extends ZIOAppDefault {
Method.GET / "greet" ->
// The handler is a function that takes a Request and returns a Response
handler { (req: Request) =>
val name = req.queryParamToOrElse("name", "World")
val name = req.queryOrElse[String]("name", "World")
Response.text(s"Hello $name!")
}

Expand Down
37 changes: 24 additions & 13 deletions docs/reference/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,27 @@ object QueryParamExample extends ZIOAppDefault {
}
```

The typed version of `Request#queryParam` is `Request#queryParamTo` which takes a key and a type parameter of type `T` and finally returns a `Either[QueryParamsError, T]` value:
The typed version of `Request#queryParam` is `Request#query[T](key: String)` which returns a `Either[QueryParamError, T]`:

```scala mdoc:compile-only
// curl -X GET https://localhost:8080/search?age=42 -i
import zio.http._
import zio.http.codec._
object TypedQueryParamExample extends ZIOAppDefault {
val app =
Routes(
Method.GET / "search" -> Handler.fromFunctionHandler { (req: Request) =>
val response: ZIO[Any, QueryParamsError, Response] =
ZIO.fromEither(req.queryParamTo[Int]("age"))
val response: ZIO[Any, HttpCodecError.QueryParamError, Response] =
ZIO.fromEither(req.query[Int]("age"))
.map(value => Response.text(s"The value of age query param is: $value"))

Handler.fromZIO(response).catchAll {
case QueryParamsError.Missing(name) =>
Handler.badRequest(s"The $name query param is missing")
case QueryParamsError.Malformed(name, codec, values) =>
Handler.badRequest(s"The value of $name query param is malformed")
case HttpCodecError.MissingQueryParams(names) =>
Handler.badRequest(s"The query params ${names.mkString(", ")} are missing")
case error: HttpCodecError.MalformedQueryParam =>
Handler.badRequest(error.getMessage())
case error: HttpCodecError.InvalidQueryParamCount =>
Handler.badRequest(error.getMessage())
}
},
)
Expand All @@ -191,10 +194,10 @@ object TypedQueryParamExample extends ZIOAppDefault {
```

:::info
In the above example, instead of using `ZIO.fromEither(req.queryParamTo[Int]("age"))` we can use `req.queryParamToZIO[Int]("age")` to get a `ZIO` value directly which encodes the error type in the ZIO effect.
In the above example, instead of using `ZIO.fromEither(req.query[Int]("age"))` we can use `req.queryZIO[Int]("age")` to get a `ZIO` value directly which encodes the error type in the ZIO effect.
:::

To retrieve all query parameter values for a key, we can use the `Request#queryParams` method that takes a `String` as the input key and returns a `Chunk[String]`:
To retrieve all query parameter values for a key, we can use the `req.query[Chunk[Int]]("age")` method:

```scala mdoc:compile-only
// curl -X GET https://localhost:8080/search?q=value1&q=value2 -i
Expand All @@ -220,21 +223,28 @@ object QueryParamsExample extends ZIOAppDefault {
}
```

The typed version of `Request#queryParams` is `Request#queryParamsTo` which takes a key and a type parameter of type `T` and finally returns a `Either[QueryParamsError, Chunk[T]]` value.
The typed version of `Request#queryParams` is `Request#query[Chunk[T]](key: String)` which returns a `Either[QueryParamError, Chunk[T]]`.

:::note
All the above methods also have `OrElse` versions which take a default value as input and return the default value if the query parameter is not found, e.g. `Request#queryParamOrElse`, `Request#queryParamToOrElse`, `Request#queryParamsOrElse`, `Request#queryParamsToOrElse`.
All the above methods also have `OrElse` versions which take a default value as input and return the default value if the query parameter can not be found or decoded.
:::

Using the `Request#queryParameters` method, we can access the query parameters of the request which returns a `QueryParams` object.
To get a `QueryParams` instance for a request use `Request#queryParameters`.

### Modifying Query Parameters

When we are working with ZIO HTTP Client, we need to create a new `Request` and may need to set/update/remove query parameters. In such cases, we have the following methods available: `addQueryParam`, `addQueryParams`, `removeQueryParam`, `removeQueryParams`, `setQueryParams`, and `updateQueryParams`.
When we are working with the ZIO HTTP Client, we need to create a new `Request` and may need to set/update/remove query parameters. In such cases, we have the following methods available: `addQueryParam`, `addQueryParams`, `removeQueryParam`, `removeQueryParams`, `setQueryParams`, and `updateQueryParams`.

```scala mdoc:compile-only
import zio._
import zio.http._
import zio.schema._

case class AuthorQueryParams(age: Int, name: String)

object AuthorQueryParams {
implicit val schema: Schema[AuthorQueryParams] = DeriveSchema.gen[AuthorQueryParams]
}

object QueryParamClientExample extends ZIOAppDefault {
def run =
Expand All @@ -243,6 +253,7 @@ object QueryParamClientExample extends ZIOAppDefault {
.get("http://localhost:8080/search")
.addQueryParam("language", "scala")
.addQueryParam("q", "How to Write HTTP App")
.addQueryParam(AuthorQueryParams(42, "John"))
.addQueryParams("tag", Chunk("zio", "http", "scala")),
).provide(Client.default)
}
Expand Down
2 changes: 1 addition & 1 deletion project/BenchmarkWorkFlow.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object BenchmarkWorkFlow {
WorkflowStep.Use(
UseRef.Public("actions", "checkout", s"v4"),
Map(
"repository" -> "khajavi/FrameworkBenchmarks",
"repository" -> "zio/FrameworkBenchmarks",
"path" -> "FrameworkBenchMarks",
),
),
Expand Down
140 changes: 116 additions & 24 deletions zio-http/jvm/src/test/scala/zio/http/QueryParamsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,24 @@

package zio.http

import java.time.Instant
import java.util.UUID

import scala.jdk.CollectionConverters._

import zio.test.Assertion.{anything, equalTo, fails, hasSize}
import zio.test.Assertion.{anything, equalTo, fails, hasSize, succeeds}
import zio.test._
import zio.{Chunk, ZIO}
import zio.{Chunk, NonEmptyChunk, ZIO}

import zio.schema.{DeriveSchema, Schema}

object QueryParamsSpec extends ZIOHttpSpec {

case class SimpleWrapper(a: String)
implicit val simpleWrapperSchema: Schema[SimpleWrapper] = DeriveSchema.gen[SimpleWrapper]
case class Foo(a: Int, b: SimpleWrapper, c: NonEmptyChunk[String], chunk: Chunk[String])
implicit val fooSchema: Schema[Foo] = DeriveSchema.gen[Foo]

def spec =
suite("QueryParams")(
suite("-")(
Expand Down Expand Up @@ -142,6 +152,55 @@ object QueryParamsSpec extends ZIOHttpSpec {
}
},
),
suite("add typed")(
test("primitives") {
val uuid = "123e4567-e89b-12d3-a456-426614174000"
assertTrue(
QueryParams.empty.addQueryParam("a", 1).queryParam("a").get == "1",
QueryParams.empty.addQueryParam("a", 1.0d).queryParam("a").get == "1.0",
QueryParams.empty.addQueryParam("a", 1.0f).queryParam("a").get == "1.0",
QueryParams.empty.addQueryParam("a", 1L).queryParam("a").get == "1",
QueryParams.empty.addQueryParam("a", 1.toShort).queryParam("a").get == "1",
QueryParams.empty.addQueryParam("a", true).queryParam("a").get == "true",
QueryParams.empty.addQueryParam("a", 'a').queryParam("a").get == "a",
QueryParams.empty.addQueryParam("a", Instant.EPOCH).queryParam("a").get == "1970-01-01T00:00:00Z",
QueryParams.empty
.addQueryParam("a", UUID.fromString(uuid))
.queryParam("a")
.get == uuid,
)

},
test("collections") {
assertTrue(
// Chunk
QueryParams.empty.addQueryParam("a", Chunk.empty[Int]).queryParam("a").isEmpty,
QueryParams.empty.addQueryParam("a", Chunk(1)).queryParams("a") == Chunk("1"),
QueryParams.empty.addQueryParam("a", Chunk(1, 2)).queryParams("a") == Chunk("1", "2"),
QueryParams.empty.addQueryParam("a", Chunk(1.0, 2.0)).queryParams("a") == Chunk("1.0", "2.0"),
// List
QueryParams.empty.addQueryParam("a", List.empty[Int]).queryParam("a").isEmpty,
QueryParams.empty.addQueryParam("a", List(1)).queryParams("a") == Chunk("1"),
// NonEmptyChunk
QueryParams.empty.addQueryParam("a", NonEmptyChunk(1)).queryParams("a") == Chunk("1"),
QueryParams.empty.addQueryParam("a", NonEmptyChunk(1, 2)).queryParams("a") == Chunk("1", "2"),
)
},
test("case class") {
val foo = Foo(1, SimpleWrapper("foo"), NonEmptyChunk("1", "2"), Chunk("foo", "bar"))
val fooEmpty = Foo(0, SimpleWrapper(""), NonEmptyChunk("1"), Chunk.empty)
assertTrue(
QueryParams.empty.addQueryParam(foo).queryParam("a").get == "1",
QueryParams.empty.addQueryParam(foo).queryParam("b").get == "foo",
QueryParams.empty.addQueryParam(foo).queryParams("c") == Chunk("1", "2"),
QueryParams.empty.addQueryParam(foo).queryParams("chunk") == Chunk("foo", "bar"),
QueryParams.empty.addQueryParam(fooEmpty).queryParam("a").get == "0",
QueryParams.empty.addQueryParam(fooEmpty).queryParam("b").get == "",
QueryParams.empty.addQueryParam(fooEmpty).queryParams("c") == Chunk("1"),
QueryParams.empty.addQueryParam(fooEmpty).queryParams("chunk").isEmpty,
)
},
),
suite("apply")(
test("from tuples") {
val gens = Gen.fromIterable(
Expand Down Expand Up @@ -251,32 +310,65 @@ object QueryParamsSpec extends ZIOHttpSpec {
},
),
suite("getAs - getAllAs")(
test("success") {
test("pure") {
val typed = "typed"
val default = 3
val invalidTyped = "invalidTyped"
val unknown = "non-existent"
val queryParams = QueryParams(typed -> "1", typed -> "2", invalidTyped -> "str")
val single = QueryParams(typed -> "1")
val queryParamsFoo = QueryParams("a" -> "1", "b" -> "foo", "c" -> "2", "chunk" -> "foo", "chunk" -> "bar")
assertTrue(
single.query[Int](typed) == Right(1),
queryParams.query[Int](invalidTyped).isLeft,
queryParams.query[Int](unknown).isLeft,
single.queryOrElse[Int](typed, default) == 1,
queryParams.queryOrElse[Int](invalidTyped, default) == default,
queryParams.queryOrElse[Int](unknown, default) == default,
queryParams.query[Chunk[Int]](typed) == Right(Chunk(1, 2)),
queryParams.query[Chunk[Int]](invalidTyped).isLeft,
queryParams.query[Chunk[Int]](unknown) == Right(Chunk.empty),
queryParams.query[NonEmptyChunk[Int]](unknown).isLeft,
queryParams.queryOrElse[Chunk[Int]](typed, Chunk(default)) == Chunk(1, 2),
queryParams.queryOrElse[Chunk[Int]](invalidTyped, Chunk(default)) == Chunk(default),
queryParams.queryOrElse[Chunk[Int]](unknown, Chunk(default)) == Chunk.empty,
queryParams.queryOrElse[NonEmptyChunk[Int]](unknown, NonEmptyChunk(default)) == NonEmptyChunk(default),
// case class
queryParamsFoo.query[Foo] == Right(Foo(1, SimpleWrapper("foo"), NonEmptyChunk("2"), Chunk("foo", "bar"))),
queryParamsFoo.query[SimpleWrapper] == Right(SimpleWrapper("1")),
queryParamsFoo.query[SimpleWrapper]("b") == Right(SimpleWrapper("foo")),
queryParams.query[Foo].isLeft,
queryParamsFoo.queryOrElse[Foo](Foo(0, SimpleWrapper(""), NonEmptyChunk("1"), Chunk.empty)) == Foo(
1,
SimpleWrapper("foo"),
NonEmptyChunk("2"),
Chunk("foo", "bar"),
),
queryParams.queryOrElse[Foo](Foo(0, SimpleWrapper(""), NonEmptyChunk("1"), Chunk.empty)) == Foo(
0,
SimpleWrapper(""),
NonEmptyChunk("1"),
Chunk.empty,
),
)
},
test("as ZIO") {
val typed = "typed"
val default = 3
val invalidTyped = "invalidTyped"
val unknown = "non-existent"
val queryParams = QueryParams(typed -> "1", typed -> "2", invalidTyped -> "str")
assertTrue(
queryParams.queryParamTo[Int](typed) == Right(1),
queryParams.queryParamTo[Int](invalidTyped).isLeft,
queryParams.queryParamTo[Int](unknown).isLeft,
queryParams.queryParamToOrElse[Int](typed, default) == 1,
queryParams.queryParamToOrElse[Int](invalidTyped, default) == default,
queryParams.queryParamToOrElse[Int](unknown, default) == default,
queryParams.queryParamsTo[Int](typed).map(_.length) == Right(2),
queryParams.queryParamsTo[Int](invalidTyped).isLeft,
queryParams.queryParamsTo[Int](unknown).isLeft,
queryParams.queryParamsToOrElse[Int](typed, Chunk(default)).length == 2,
queryParams.queryParamsToOrElse[Int](invalidTyped, Chunk(default)).length == 1,
queryParams.queryParamsToOrElse[Int](unknown, Chunk(default)).length == 1,
)
assertZIO(queryParams.queryParamToZIO[Int](typed))(equalTo(1)) &&
assertZIO(queryParams.queryParamToZIO[Int](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.queryParamToZIO[Int](unknown).exit)(fails(anything)) &&
assertZIO(queryParams.queryParamsToZIO[Int](typed))(hasSize(equalTo(2))) &&
assertZIO(queryParams.queryParamsToZIO[Int](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.queryParamsToZIO[Int](unknown).exit)(fails(anything))
val single = QueryParams(typed -> "1")
assertZIO(single.queryZIO[Int](typed))(equalTo(1)) &&
assertZIO(single.queryZIO[Int](unknown).exit)(fails(anything)) &&
assertZIO(single.queryZIO[Chunk[Int]](typed))(hasSize(equalTo(1))) &&
assertZIO(single.queryZIO[Chunk[Int]](unknown).exit)(succeeds(equalTo(Chunk.empty[Int]))) &&
assertZIO(single.queryZIO[NonEmptyChunk[Int]](unknown).exit)(fails(anything)) &&
assertZIO(queryParams.queryZIO[Int](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.queryZIO[Int](unknown).exit)(fails(anything)) &&
assertZIO(queryParams.queryZIO[Chunk[Int]](typed))(hasSize(equalTo(2))) &&
assertZIO(queryParams.queryZIO[Chunk[Int]](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.queryZIO[Chunk[Int]](unknown).exit)(succeeds(equalTo(Chunk.empty[Int]))) &&
assertZIO(queryParams.queryZIO[NonEmptyChunk[Int]](unknown).exit)(fails(anything))
},
),
suite("encode - decode")(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ object QueryParameterSpec extends ZIOHttpSpec {
val testRoutes = testEndpoint(
Routes(
Endpoint(GET / "users")
.query(HttpCodec.queryAll[Params])
.query(HttpCodec.query[Params])
.out[String]
.implementPurely(_.toString),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ object RoundtripSpec extends ZIOHttpSpec {
},
test("simple get with query params from case class") {
val endpoint = Endpoint(GET / "query")
.query(HttpCodec.queryAll[Params])
.query(HttpCodec.query[Params])
.out[Params]
val route = endpoint.implementPurely(params => params)

Expand Down Expand Up @@ -246,7 +246,7 @@ object RoundtripSpec extends ZIOHttpSpec {
.query(HttpCodec.query[Int]("id"))
.query(HttpCodec.query[String]("name").optional)
.query(HttpCodec.query[String]("details").optional)
.query(HttpCodec.queryAll[Age].optional)
.query(HttpCodec.query[Age].optional)
.out[PostWithAge]

val handler =
Expand All @@ -269,7 +269,7 @@ object RoundtripSpec extends ZIOHttpSpec {
.query(HttpCodec.query[Int]("id"))
.query(HttpCodec.query[String]("name").optional)
.query(HttpCodec.query[String]("details").optional)
.query(HttpCodec.queryAll[Age].optional)
.query(HttpCodec.query[Age].optional)
.out[PostWithAge]

val handler =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
private val queryParamValidationEndpoint =
Endpoint(GET / "withQuery")
.in[SimpleInputBody]
.query(HttpCodec.queryAll[AgeParam])
.query(HttpCodec.query[AgeParam])
.out[SimpleOutputBody]
.outError[NotFoundError](Status.NotFound)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import zio.test._

import zio.http._
import zio.http.codec.HttpCodec
import zio.http.codec.HttpCodec.queryAll
import zio.http.codec.PathCodec.path
import zio.http.endpoint.Endpoint

Expand Down
Loading

2 comments on commit 0c8479f

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 : Performance Benchmarks (SimpleEffectBenchmarkServer)

concurrency: 256
requests/sec: 365139

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 : Performance Benchmarks (PlainTextBenchmarkServer)

concurrency: 256
requests/sec: 361695

Please sign in to comment.