Skip to content

Commit

Permalink
Add convenience method for out header and extend docs (#3054) (#3068)
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil authored Sep 6, 2024
1 parent 9f455e9 commit 6a84e32
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 9 deletions.
112 changes: 112 additions & 0 deletions docs/reference/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,25 @@ val endpoint: Endpoint[Unit, String, ZNothing, List[Book], AuthType.None] =

In the above example, we defined an endpoint that describes a query parameter `q` as input and returns a list of `Book` as output. The `Endpoint#out` method has multiple overloads that can be used to describe other properties of the output, such as the status code, media type, and documentation.

We can also add custom headers to the output using the `Endpoint#outHeader` method:

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

case class Book(title: String, author: String)

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

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

Sometimes based on the condition, we might want to return different types of responses. We can use the `Endpoint#out` method multiple times to describe different output types:

```scala mdoc:compile-only
Expand Down Expand Up @@ -272,8 +291,101 @@ object EndpointWithMultipleOutputTypes extends ZIOAppDefault {

In the above example, we defined an endpoint that describes a path parameter `id` as input and returns either a `Book` or an `Article` as output.

With multiple outputs, we can define if all of them or just some should add an output header, by the order of calling `out` and `outHeader` methods:

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

case class Book(title: String, author: String)

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

case class Article(title: String, author: String)

object Article {
implicit val schema: Schema[Article] = DeriveSchema.gen
}
// header will be added to the first output
val endpoint: Endpoint[Unit, Unit, ZNothing, Either[Article, (Book, Header.Date)], AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Book]
.outHeader(HttpCodec.date)
.out[Article]

// header will be added to all outputs
val endpoint2: Endpoint[Unit, Unit, ZNothing, (Either[Article, Book], Header.Date), AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Book]
.out[Article]
.outHeader(HttpCodec.date)
```

A call to `outHeder` will require to provide the header together with all outputs defined before it.

Sometimes we might want more control over the output properties, in such cases, we can provide a custom `HttpCodec` that describes the output properties using the `Endpoint#outCodec` method.
This can be very useful when we only want to add headers to a subset of outputs for example:

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

case class Book(title: String, author: String)

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

case class Article(title: String, author: String)

object Article {
implicit val schema: Schema[Article] = DeriveSchema.gen
}
val endpoint: Endpoint[Unit, Unit, ZNothing, Either[(Article, Header.Date), Book], AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Book]
.outCodec(HttpCodec.content[Article] ++ HttpCodec.date)
```
Or when we want to reuse the same codec for multiple endpoints:

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

case class Book(title: String, author: String)

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

case class Article(title: String, author: String)

object Article {
implicit val schema: Schema[Article] = DeriveSchema.gen
}
val bookCodec = HttpCodec.content[Book] ++ HttpCodec.date
val articleCodec = HttpCodec.content[Article] ++ HttpCodec.date

val endpoint1: Endpoint[Unit, Unit, ZNothing, (Book, Header.Date), AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.outCodec(bookCodec)

val endpoint2: Endpoint[Unit, Unit, ZNothing, (Article, Header.Date), AuthType.None] =
Endpoint(RoutePattern.GET / "articles")
.outCodec(articleCodec)

val endpoint3: Endpoint[Unit, Unit, ZNothing, Either[(Article, Header.Date), (Book, Header.Date)], AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.outCodec(articleCodec | bookCodec)
```
## Describing Failures

For failure outputs, we can describe the output properties using the `Endpoint#outError*` methods. Let's see an example:
Expand Down
45 changes: 45 additions & 0 deletions zio-http/jvm/src/test/scala/zio/http/endpoint/RequestSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,51 @@ object RequestSpec extends ZIOHttpSpec {
assertTrue(contentType == Some(ContentType(MediaType.text.`plain`)))
}
},
test("custom out header") {
val endpoint =
Endpoint(GET / "posts")
.out[String](MediaType.text.plain)
.outHeader(HttpCodec.age)
val routes = endpoint.implementAs(("test", Header.Age(1.minute))).toRoutes
for {
response <- routes.runZIO(Request.get(URL.decode("/posts").toOption.get))
age = response.header(Header.Age)
body <- response.body.asString.orDie
} yield assertTrue(age.contains(Header.Age(1.minute)), body == "test")
},
test("custom out header for only one output") {
val endpoint =
Endpoint(GET / "posts")
.out[String](MediaType.text.plain)
.outHeader(HttpCodec.age)
.out[Int](MediaType.text.plain)
val routesRight = endpoint.implementAs(Right(("test", Header.Age(1.minute)))).toRoutes
val routesLeft = endpoint.implementAs(Left(1)).toRoutes
for {
responseRight <- routesRight.runZIO(Request.get(URL.decode("/posts").toOption.get))
ageRight = responseRight.header(Header.Age)
bodyRight <- responseRight.body.asString.orDie
responseLeft <- routesLeft.runZIO(Request.get(URL.decode("/posts").toOption.get))
ageLeft = responseLeft.header(Header.Age)
bodyLeft <- responseLeft.body.asString.orDie
} yield assertTrue(
ageRight.contains(Header.Age(1.minute)),
bodyRight == "test",
ageLeft.isEmpty,
bodyLeft == "1",
)
},
test("custom out header with outCodec") {
val endpoint =
Endpoint(GET / "posts")
.outCodec((HttpCodec.content[String](MediaType.text.plain) | HttpCodec.content[Int]) ++ HttpCodec.age)
val routes = endpoint.implementAs((Left("test"), Header.Age(1.minute))).toRoutes
for {
response <- routes.runZIO(Request.get(URL.decode("/posts").toOption.get))
age = response.header(Header.Age)
body <- response.body.asString.orDie
} yield assertTrue(age.contains(Header.Age(1.minute)), body == "test")
},
test("multiple Accept headers") {
check(Gen.int) { id =>
val endpoint =
Expand Down
23 changes: 14 additions & 9 deletions zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,15 @@ final case class Endpoint[PathInput, Input, Err, Output, Auth <: AuthType](
authType,
)

/**
* Returns a new endpoint derived from this one, whose response must satisfy
* the specified codec.
*/
def outCodec[Output2](codec: HttpCodec[HttpCodecType.ResponseType, Output2])(implicit
alt: Alternator[Output2, Output],
): Endpoint[PathInput, Input, Err, alt.Out, Auth] =
copy(output = codec | self.output)

/**
* Converts a codec error into a specific error type. The given media types
* are sorted by q-factor. Beginning with the highest q-factor.
Expand Down Expand Up @@ -655,14 +664,10 @@ final case class Endpoint[PathInput, Input, Err, Output, Auth <: AuthType](

def outErrors[Err2]: OutErrors[PathInput, Input, Err, Output, Auth, Err2] = OutErrors(self)

/**
* Returns a new endpoint derived from this one, whose response must satisfy
* the specified codec.
*/
def outCodec[Output2](codec: HttpCodec[HttpCodecType.ResponseType, Output2])(implicit
alt: Alternator[Output2, Output],
): Endpoint[PathInput, Input, Err, alt.Out, Auth] =
copy(output = codec | self.output)
def outHeader[A](codec: HeaderCodec[A])(implicit
combiner: Combiner[Output, A],
): Endpoint[PathInput, Input, Err, combiner.Out, Auth] =
copy(output = self.output ++ codec)

/**
* Returns a new endpoint derived from this one, whose output type is a stream
Expand Down Expand Up @@ -799,7 +804,7 @@ final case class Endpoint[PathInput, Input, Err, Output, Auth <: AuthType](
copy(input = self.input ++ codec)

/**
* Adds tags to the endpoint. The are used for documentation generation. For
* Adds tags to the endpoint. They are used for documentation generation. For
* example to group endpoints for OpenAPI.
*/
def tag(tag: String, tags: String*): Endpoint[PathInput, Input, Err, Output, Auth] =
Expand Down

0 comments on commit 6a84e32

Please sign in to comment.