Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation of Endpoint API #2741

Merged
merged 22 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ lazy val zioHttpExample = (project in file("zio-http-example"))
libraryDependencies += "dev.zio" %% "zio-config" % "4.0.1",
libraryDependencies += "dev.zio" %% "zio-config-typesafe" % "4.0.1",
)
.dependsOn(zioHttpJVM, zioHttpCli)
.dependsOn(zioHttpJVM, zioHttpCli, zioHttpGen)

lazy val zioHttpGen = (project in file("zio-http-gen"))
.settings(stdSettings("zio-http-gen"))
Expand Down
518 changes: 518 additions & 0 deletions docs/dsl/endpoint.md

Large diffs are not rendered by default.

67 changes: 66 additions & 1 deletion docs/dsl/path_codec.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ title: PathCodec

`PathCodec[A]` represents a codec for paths of type `A`, comprising segments where each segment can be a literal, an integer, a long, a string, a UUID, or the trailing path.

The three basic operations that `PathCodec` supports are:

- **decode**: converting a path into a value of type `A`.
- **format**: converting a value of type `A` into a path.
- **++ or /**: combining two `PathCodec` values to create a new `PathCodec` that matches both paths, so the resulting of the decoding operation will be a tuple of the two values.

So we can think of `PathCodec` as the following simplified trait:

```scala
trait PathCodec[A] {
def /[B](that: PathCodec[B]): PathCodec[(A, B)]

def decode(path: Path): Either[String, A]
def format(value: A): : Either[String, Path]
}
```

## Building PathCodecs

The `PathCodec` data type offers several predefined codecs for common types:
Expand All @@ -20,7 +37,7 @@ The `PathCodec` data type offers several predefined codecs for common types:

Complex `PathCodecs` can be constructed by combining them using the `/` operator:

```scala mdoc:compile-only
```scala mdoc:silent
import zio.http.codec.PathCodec
import PathCodec._

Expand All @@ -29,6 +46,54 @@ val pathCodec = empty / "users" / int("user-id") / "posts" / string("post-id")

By combining `PathCodec` values, the resulting `PathCodec` type reflects the types of the path segments it matches. In the provided example, the type of `pathCodec` is `(Int, String)` because it matches a path with two segments of type `Int` and `String`, respectively.

## Decoding and Formatting PathCodecs

To decode a path into a value of type `A`, we can use the `PathCodec#decode` method:

```scala mdoc
import zio.http._

pathCodec.decode(Path("users/123/posts/abc"))
```

To format (encode) a value of type `A` into a path, we can use the `PathCodec#format` method:

```scala mdoc
pathCodec.format((123, "abc"))
```

## Rendering PathCodecs

If we render the previous `PathCodec` to a string using `PathCodec#render` or `PathCodec#toString`, we get the following result:

```scala mdoc
pathCodec.render

pathCodec.toString
```

## Attaching Documentation to PathCodecs

The `PathCodec#??` operator, takes a `Doc` and annotate the `PathCodec` with it. It is useful for generating developer-friendly documentation for the API:

```scala mdoc
import zio.http.codec._

val users = PathCodec.literal("users") ?? (Doc.p("Managing users including CRUD operations"))
```

When generating OpenAPI documentation, these annotations will be used to generate the API documentation.

## Attaching Examples to PathCodecs

Similarly to attaching documentation, we can attach examples to `PathCodec` using the `PathCodec#example` operator:

```scala mdoc
import zio.http.codec._

val userId = PathCodec.int("user-id") ?? (Doc.p("The user id")) example ("user-id", 123)
```

## Using Value Objects with PathCodecs

Other than the common `PathCodec` constructors, it's also possible to transform a `PathCodec` into a more specific data type using the `transform` method.
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/concrete-entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ sidebar_label: "Concrete Entity"
```scala mdoc:passthrough
import utils._

printSource("zio-http-example/src/main/scala/example/CliExamples.scala")
printSource("zio-http-example/src/main/scala/example/endpoint/CliExamples.scala")
```
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const sidebars = {
"dsl/handler",
"dsl/headers",
"dsl/body",
"dsl/endpoint",
"dsl/form",
"dsl/cookies",
"dsl/flash",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package example.endpoint

import zio._

import zio.schema.annotation.description
import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.PathCodec._
import zio.http.codec._
import zio.http.endpoint._
import zio.http.endpoint.openapi._

object BooksEndpointExample extends ZIOAppDefault {
case class Book(
@description("Title of the book")
title: String,
@description("List of the authors of the book")
authors: List[String],
)
object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

object BookRepo {
val book1 = Book("Programming in Scala", List("Martin Odersky", "Lex Spoon", "Bill Venners", "Frank Sommers"))
val book2 = Book("Zionomicon", List("John A. De Goes", "Adam Fraser"))
val book3 = Book("Effect-Oriented Programming", List("Bill Frasure", "Bruce Eckel", "James Ward"))
def find(q: String): List[Book] = {
if (q.toLowerCase == "scala") List(book1, book2, book3)
else if (q.toLowerCase == "zio") List(book2, book3)
else List.empty
}
}

val endpoint =
Endpoint((RoutePattern.GET / "books") ?? Doc.p("Route for querying books"))
.query(
QueryCodec.queryTo[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
"Query parameter for searching books",
),
)
.out[List[Book]](Doc.p("List of books matching the query")) ?? Doc.p(
"Endpoint to query books based on a search query",
)

val booksRoute = endpoint.implement(handler((query: String) => BookRepo.find(query)))
val openAPI = OpenAPIGen.fromEndpoints(title = "Library API", version = "1.0", endpoint)
val swaggerRoutes = SwaggerUI.routes("docs" / "openapi", openAPI)
val routes = Routes(booksRoute) ++ swaggerRoutes

def run = Server.serve(routes.toHttpApp).provide(Server.default)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package example
package example.endpoint

import zio._
import zio.cli._
Expand All @@ -13,9 +13,8 @@ import zio.http.endpoint.cli._
import zio.http.endpoint.{Endpoint, EndpointExecutor}

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

import HttpCodec._
import zio.http.codec.PathCodec._
final case class User(
@description("The unique identifier of the User")
id: Int,
Expand Down Expand Up @@ -82,8 +81,8 @@ object TestCliApp extends zio.cli.ZIOCliDefault with TestCliEndpoints {
object TestCliServer extends zio.ZIOAppDefault with TestCliEndpoints {
val getUserRoute =
getUser.implement {
Handler.fromFunction { case (id, _) =>
User(id, "Juanito", Some("[email protected]"))
Handler.fromFunctionZIO { case (id, _) =>
ZIO.succeed(User(id, "Juanito", Some("[email protected]"))).debug("Hello")
}
}

Expand All @@ -101,7 +100,7 @@ object TestCliServer extends zio.ZIOAppDefault with TestCliEndpoints {
}
}

val routes = Routes(getUserRoute, getUserPostsRoute, createUserRoute)
val routes = Routes(getUserRoute, getUserPostsRoute, createUserRoute) @@ Middleware.debug

val run = Server.serve(routes.toHttpApp).provide(Server.default)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package example.endpoint

import zio._

import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.PathCodec
import zio.http.endpoint.Endpoint
import zio.http.endpoint.EndpointMiddleware.None

object EndpointWithError extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}
case class NotFoundError(error: String, message: String)

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

object BookRepo {
def find(id: Int): ZIO[Any, String, Book] = {
if (id == 1)
ZIO.succeed(Book("Zionomicon", List("John A. De Goes", "Adam Fraser")))
else
ZIO.fail("Not found")
}
}

val endpoint: Endpoint[Int, Int, NotFoundError, Book, None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.out[Book]
.outError[NotFoundError](Status.NotFound)

val getBookHandler: Handler[Any, NotFoundError, Int, Book] =
handler { (id: Int) =>
BookRepo
.find(id)
.mapError(err => NotFoundError(err, "The requested book was not found. Please try using a different ID."))
}

val app = endpoint.implement(getBookHandler).toHttpApp @@ Middleware.debug

def run = Server.serve(app).provide(Server.default)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package example.endpoint

import zio._

import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.{HeaderCodec, PathCodec}
import zio.http.endpoint.Endpoint
import zio.http.endpoint.EndpointMiddleware.None

object EndpointWithMultipleErrorsUsingEither extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

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

case class BookNotFound(message: String, bookId: Int)

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

case class AuthenticationError(message: String, userId: Int)

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

object BookRepo {
def find(id: Int): ZIO[Any, BookNotFound, Book] = {
if (id == 1)
ZIO.succeed(Book("Zionomicon", List("John A. De Goes", "Adam Fraser")))
else
ZIO.fail(BookNotFound("The requested book was not found.", id))
}
}

val endpoint: Endpoint[Int, (Int, Header.Authorization), Either[BookNotFound, AuthenticationError], Book, None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.header(HeaderCodec.authorization)
.out[Book]
.outError[BookNotFound](Status.NotFound)
.outError[AuthenticationError](Status.Unauthorized)

def isUserAuthorized(authHeader: Header.Authorization) = false

val getBookHandler
: Handler[Any, Either[BookNotFound, AuthenticationError], (RuntimeFlags, Header.Authorization), Book] =
handler { (id: Int, authHeader: Header.Authorization) =>
if (isUserAuthorized(authHeader))
BookRepo.find(id).mapError(Left(_))
else
ZIO.fail(Right(AuthenticationError("User is not authenticated", 123)))
}

val app = endpoint.implement(getBookHandler).toHttpApp @@ Middleware.debug

def run = Server.serve(app).provide(Server.default)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package example.endpoint

import zio._

import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.{HeaderCodec, HttpCodec, PathCodec}
import zio.http.endpoint.Endpoint
import zio.http.endpoint.EndpointMiddleware.None

object EndpointWithMultipleUnifiedErrors extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

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

abstract class AppError(message: String)

case class BookNotFound(message: String, bookId: Int) extends AppError(message)

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

case class AuthenticationError(message: String, userId: Int) extends AppError(message)

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

object BookRepo {
def find(id: Int): ZIO[Any, BookNotFound, Book] = {
if (id == 1)
ZIO.succeed(Book("Zionomicon", List("John A. De Goes", "Adam Fraser")))
else
ZIO.fail(BookNotFound("The requested book was not found.", id))
}
}

val endpoint: Endpoint[Int, (Int, Header.Authorization), AppError, Book, None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.header(HeaderCodec.authorization)
.out[Book]
.outErrors[AppError](
HttpCodec.error[BookNotFound](Status.NotFound),
HttpCodec.error[AuthenticationError](Status.Unauthorized),
)

def isUserAuthorized(authHeader: Header.Authorization) = false

val getBookHandler: Handler[Any, AppError, (Int, Header.Authorization), Book] =
handler { (id: Int, authHeader: Header.Authorization) =>
if (isUserAuthorized(authHeader))
BookRepo.find(id)
else
ZIO.fail(AuthenticationError("User is not authenticated", 123))
}

val app = endpoint.implement(getBookHandler).toHttpApp @@ Middleware.debug

def run = Server.serve(app).provide(Server.default)
}
Loading
Loading