Skip to content

Commit

Permalink
Efficient query optional encoding (zio#2942)
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil committed Aug 15, 2024
1 parent fe42b85 commit 662e685
Show file tree
Hide file tree
Showing 43 changed files with 946 additions and 357 deletions.
22 changes: 11 additions & 11 deletions docs/reference/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ object Book {

val endpoint =
Endpoint(RoutePattern.GET / "books")
.query(QueryCodec.queryTo[String]("q") examples (("example1", "scala"), ("example2", "zio")))
.query(HttpCodec.query[String]("q") examples (("example1", "scala"), ("example2", "zio")))
.out[List[Book]]
```

Expand Down Expand Up @@ -109,33 +109,33 @@ import zio.http.codec._
```scala mdoc:compile-only
val endpoint: Endpoint[Unit, String, ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(QueryCodec.queryTo[String]("q"))
.query(HttpCodec.query[String]("q"))
```

QueryCodecs are composable, so we can combine multiple query parameters:

```scala mdoc:compile-only
val endpoint: Endpoint[Unit, (String, Int), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(QueryCodec.queryTo[String]("q") ++ QueryCodec.queryTo[Int]("limit"))
.query(HttpCodec.query[String]("q") ++ HttpCodec.query[Int]("limit"))
```

Or we can use the `query` method multiple times:

```scala mdoc:compile-only
val endpoint: Endpoint[Unit, (String, Int), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(QueryCodec.queryTo[String]("q"))
.query(QueryCodec.queryTo[Int]("limit"))
.query(HttpCodec.query[String]("q"))
.query(HttpCodec.query[Int]("limit"))
```

Please note that as we add more properties to the endpoint, the input and output types of the endpoint change accordingly. For example, in the following example, we have an endpoint with a path parameter of type `String` and two query parameters of type `String` and `Int`. So the input type of the endpoint is `(String, String, Int)`:

```scala mdoc:compile-only
val endpoint: Endpoint[String, (String, String, Int), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.string("genre"))
.query(QueryCodec.queryTo[String]("q"))
.query(QueryCodec.queryTo[Int]("limit"))
.query(HttpCodec.query[String]("q"))
.query(HttpCodec.query[Int]("limit"))
```

When we implement the endpoint, the handler function should take the input type of a tuple that the first element is the "genre" path parameter, and the second and third elements are the query parameters "q" and "limit" respectively.
Expand Down Expand Up @@ -215,7 +215,7 @@ object Book {

val endpoint: Endpoint[Unit, String, ZNothing, List[Book], AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(QueryCodec.query("q"))
.query(HttpCodec.query[String]("q"))
.out[List[Book]]
```

Expand Down Expand Up @@ -369,8 +369,8 @@ case class BookQuery(query: String, genre: String, title: String)

val endpoint: Endpoint[String, (String, String, String), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.POST / "books" / PathCodec.string("genre"))
.query(QueryCodec.query("q"))
.query(QueryCodec.query("title"))
.query(HttpCodec.query[String]("q"))
.query(HttpCodec.query[String]("title"))

val mappedEndpoint: Endpoint[String, BookQuery, ZNothing, ZNothing, AuthType.None] =
endpoint.transformIn[BookQuery] { case (genre, q, title) => BookQuery(q, genre, title) } { i =>
Expand All @@ -390,7 +390,7 @@ Every property of an `Endpoint` API can be annotated with documentation, may be
val endpoint =
Endpoint((RoutePattern.GET / "books") ?? Doc.p("Route for querying books"))
.query(
QueryCodec.queryTo[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
HttpCodec.query[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
"Query parameter for searching books",
),
)
Expand Down
22 changes: 11 additions & 11 deletions docs/reference/http-codec.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,19 @@ There are also predefined codecs for all the HTTP methods, e.g. `HttpCodec.conne

### QueryCodec

The `QueryCodec[A]` is a codec for the query parameters of the HTTP message with type `A`. To be able to encode and decode query parameters, ZIO HTTP provides a wide range of query codecs. If we are dealing with a single query parameter we can use `HttpCodec.query`, `HttpCodec.queryBool`, `HttpCodec.queryInt`, and `HttpCodec.queryTo`:
The `QueryCodec[A]` is a codec for the query parameters of the HTTP message with type `A`. To be able to encode and decode query parameters, ZIO HTTP provides a wide range of query codecs. If we are dealing with a single query parameter we can use `HttpCodec.query`, `HttpCodec.query[Boolean]`, `HttpCodec.query[Boolean]`, and `HttpCodec.queryTo`:

```scala mdoc:compile-only
import zio.http._
import zio.http.codec._
import java.util.UUID

val nameQueryCodec : QueryCodec[String] = HttpCodec.query("name") // e.g. ?name=John
val ageQueryCodec : QueryCodec[Int] = HttpCodec.queryInt("age") // e.g. ?age=30
val activeQueryCodec: QueryCodec[Boolean] = HttpCodec.queryBool("active") // e.g. ?active=true
val nameQueryCodec : QueryCodec[String] = HttpCodec.query[String]("name") // e.g. ?name=John
val ageQueryCodec : QueryCodec[Int] = HttpCodec.query[Int]("age") // e.g. ?age=30
val activeQueryCodec: QueryCodec[Boolean] = HttpCodec.query[Boolean]("active") // e.g. ?active=true

// e.g. ?uuid=43abea9e-0b0e-11ef-8d07-e755ec5cd767
val uuidQueryCodec : QueryCodec[UUID] = HttpCodec.queryTo[UUID]("uuid")
val uuidQueryCodec : QueryCodec[UUID] = HttpCodec.query[UUID]("uuid")
```

We can combine multiple query codecs with `++`:
Expand All @@ -171,11 +171,11 @@ import zio.http._
import zio.http.codec._
import java.util.UUID

val queryAllCodec : QueryCodec[Chunk[String]] = HttpCodec.queryAll("q") // e.g. ?q=one&q=two&q=three
val queryAllIntCodec : QueryCodec[Chunk[Int]] = HttpCodec.queryAllInt("id") // e.g. ?ids=1&ids=2&ids=3
val queryAllCodec : QueryCodec[Chunk[String]] = HttpCodec.query[Chunk[String]]("q") // e.g. ?q=one&q=two&q=three
val queryAllIntCodec : QueryCodec[Chunk[Int]] = HttpCodec.query[Chunk[Int]]("id") // e.g. ?ids=1&ids=2&ids=3

// e.g. ?uuid=43abea9e-0b0e-11ef-8d07-e755ec5cd767&uuid=43abea9e-0b0e-11ef-8d07-e755ec5cd768
val queryAllUUIDCodec: QueryCodec[Chunk[UUID]] = HttpCodec.queryAllTo[UUID]("uuid")
val queryAllUUIDCodec: QueryCodec[Chunk[UUID]] = HttpCodec.query[Chunk[UUID]]("uuid")
```

### StatusCodec
Expand Down Expand Up @@ -203,7 +203,7 @@ By combining two codecs using the `++` operator, we can create a new codec that
import zio.http.codec._

// e.g. ?name=John&age=30
val queryCodec: QueryCodec[(String, Int)] = HttpCodec.query("name") ++ HttpCodec.queryInt("age")
val queryCodec: QueryCodec[(String, Int)] = HttpCodec.query[String]("name") ++ HttpCodec.query[Int]("age")
```

### Combining Codecs Alternatively
Expand All @@ -213,7 +213,7 @@ There is also a `|` operator that allows us to create a codec that can decode ei
```scala mdoc:silent
import zio.http.codec._

val eitherQueryCodec: QueryCodec[String] = HttpCodec.query("q") | HttpCodec.query("query")
val eitherQueryCodec: QueryCodec[String] = HttpCodec.query[String]("q") | HttpCodec.query[String]("query")
```

Assume we have a request
Expand Down Expand Up @@ -244,7 +244,7 @@ import zio._
import zio.http._
import zio.http.codec._

val optionalQueryCodec: QueryCodec[Option[String]] = HttpCodec.query("q").optional
val optionalQueryCodec: QueryCodec[Option[String]] = HttpCodec.query[String]("q").optional

val request = Request(url = URL.root.copy(queryParams = QueryParams("query" -> "foo")))
val result: Task[Option[String]] = optionalQueryCodec.decodeRequest(request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import zio.{Scope => _, _}
import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.QueryCodec
import zio.http.codec.HttpCodec
import zio.http.endpoint.Endpoint

import cats.effect.unsafe.implicits.global
Expand Down Expand Up @@ -91,7 +91,7 @@ class EndpointBenchmark {
// API DSL
val usersPosts =
Endpoint(Method.GET / "users" / int("userId") / "posts" / int("limit"))
.query(QueryCodec.query("query"))
.query(HttpCodec.query[String]("query"))
.out[ExampleData]

val handledUsersPosts =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zio.http.endpoint.cli

import zio.http._
import zio.http.codec.HttpCodec.Query.QueryType
import zio.http.codec._
import zio.http.endpoint._

Expand Down Expand Up @@ -127,8 +128,20 @@ private[cli] object CliEndpoint {
case HttpCodec.Path(pathCodec, _) =>
CliEndpoint(url = HttpOptions.Path(pathCodec) :: List())

case HttpCodec.Query(name, codec, _, _) =>
CliEndpoint(url = HttpOptions.Query(name, codec) :: List())
case HttpCodec.Query(queryType, _) =>
queryType match {
case QueryType.Primitive(name, codec) =>
CliEndpoint(url = HttpOptions.Query(name, codec) :: List())
case record @ QueryType.Record(_) =>
val queryOptions = record.fieldAndCodecs.map { case (field, codec) =>
HttpOptions.Query(field.name, codec)
}
CliEndpoint(url = queryOptions.toList)
case QueryType.Collection(_, elements, _) =>
val queryOptions =
HttpOptions.Query(elements.name, elements.codec)
CliEndpoint(url = queryOptions :: List())
}

case HttpCodec.Status(_, _) => CliEndpoint.empty

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

import zio.http._
import zio.http.codec.HttpCodec.Query.QueryParamHint
import zio.http.codec._
import zio.http.codec.internal.TextBinaryCodec
import zio.http.endpoint._
Expand Down Expand Up @@ -106,7 +105,7 @@ object EndpointGen {
val schema = schema0.asInstanceOf[Schema[Any]]
val codec = BinaryCodecWithSchema(TextBinaryCodec.fromSchema(schema), schema)
CliRepr(
HttpCodec.Query(name, codec, QueryParamHint.Any),
HttpCodec.Query(HttpCodec.Query.QueryType.Primitive(name, codec)),
CliEndpoint(url = HttpOptions.Query(name, codec) :: Nil),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import zio.http.codec._

object CombinerTypesExample extends App {

val foo = query("foo")
val bar = query("bar")
val foo = HttpCodec.query[String]("foo")
val bar = HttpCodec.query[String]("bar")

val combine1L1R: HttpCodec[HttpCodecType.Query, (String, String)] = foo & bar
val combine1L2R: HttpCodec[HttpCodecType.Query, (String, String, String)] = foo & (bar & bar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import zio._

import zio.http.Header.Authorization
import zio.http._
import zio.http.codec.{HttpCodec, PathCodec}
import zio.http.codec.PathCodec.path
import zio.http.codec._
import zio.http.endpoint._
import zio.http.endpoint.openapi.{OpenAPIGen, SwaggerUI}
import zio.http.endpoint.{Endpoint, EndpointExecutor, EndpointLocator}

object EndpointExamples extends ZIOAppDefault {
import HttpCodec.query
import PathCodec._

// MiddlewareSpec can be added at the service level as well
val getUser =
Expand All @@ -21,7 +20,7 @@ object EndpointExamples extends ZIOAppDefault {

val getUserPosts =
Endpoint(Method.GET / "users" / int("userId") / "posts" / int("postId"))
.query(query("name"))
.query(HttpCodec.query[String]("name"))
.out[List[String]]

val getUserPostsRoute =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ object BooksEndpointExample extends ZIOAppDefault {
val endpoint =
Endpoint((RoutePattern.GET / "books") ?? Doc.p("Route for querying books"))
.query(
QueryCodec.queryTo[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
HttpCodec.query[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
"Query parameter for searching books",
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ object Post {
}

trait TestCliEndpoints {
import HttpCodec._
import zio.http.codec.PathCodec._

val getUser =
Endpoint(Method.GET / "users" / int("userId") ?? Doc.p("The unique identifier of the user"))
Expand All @@ -51,7 +49,7 @@ trait TestCliEndpoints {
"posts" / int("postId") ?? Doc.p("The unique identifier of the post"),
)
.query(
query("user-name") ?? Doc.p(
HttpCodec.query[String]("user-name") ?? Doc.p(
"The user's name",
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import zio._
import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.QueryCodec
import zio.http.codec._
import zio.http.endpoint.{AuthType, Endpoint}

object DeclarativeProgrammingExample extends ZIOAppDefault {
Expand All @@ -32,7 +32,7 @@ object DeclarativeProgrammingExample extends ZIOAppDefault {

val endpoint: Endpoint[Unit, String, NotFoundError, Book, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(QueryCodec.query("id"))
.query(HttpCodec.query[String]("id"))
.out[Book]
.outError[NotFoundError](Status.NotFound)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ object CodeGen {
val (imports, _) = renderQueryCode(Code.QueryParamCode(name, underlying))
(Code.Import.FromBase(s"components.$newtypeName") :: imports) -> (newtypeName + ".Type")
}
imports -> s""".query(QueryCodec.queryTo[$tpe]("$name"))"""
imports -> s""".query(HttpCodec.query[$tpe]("$name"))"""
}

def renderInCode(inCode: Code.InCode): String = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ object Species {
import test.components.Age
val list_by_species =
Endpoint(Method.GET / "api" / "v1" / "zoo" / "list" / string("species").transform(Species.wrap)(Species.unwrap))
.query(QueryCodec.queryTo[Age.Type]("max-age"))
.query(HttpCodec.query[Age.Type]("max-age"))
.in[Unit]
.out[Chunk[Animal]](status = Status.Ok)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ object Species {
import zio.http.endpoint._
import zio.http.codec._
val list_by_species = Endpoint(Method.GET / "api" / "v1" / "zoo" / "list" / string("species"))
.query(QueryCodec.queryTo[Int]("max-age"))
.query(HttpCodec.query[Int]("max-age"))
.in[Unit]
.out[Chunk[Animal]](status = Status.Ok)

}
}
4 changes: 2 additions & 2 deletions zio-http-gen/src/test/resources/EndpointWithQueryParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ object Users {
import zio.http.endpoint._
import zio.http.codec._
val get = Endpoint(Method.GET / "api" / "v1" / "users")
.query(QueryCodec.queryTo[Int]("limit"))
.query(QueryCodec.queryTo[String]("name"))
.query(HttpCodec.query[Int]("limit"))
.query(HttpCodec.query[String]("name"))
.in[Unit]

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object Pets {
import zio.http.endpoint._
import zio.http.codec._
val listPets = Endpoint(Method.GET / "pets")
.query(QueryCodec.queryTo[Int]("limit"))
.query(HttpCodec.query[Int]("limit"))
.in[Unit]
.out[Pets](status = Status.Ok)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,11 @@ package zio.http.gen.grpc

import java.nio.file._

import scala.jdk.CollectionConverters._

import zio._
import zio.test._

import zio.http._
import zio.http.codec.HeaderCodec
import zio.http.codec.HttpCodec.{query, queryInt}
import zio.http.endpoint._
import zio.http.gen.model._
import zio.http.gen.scala.Code
import zio.http.gen.scala.Code.Collection.Opt

object EndpointGenSpec extends ZIOSpecDefault {

Expand Down
Loading

0 comments on commit 662e685

Please sign in to comment.