Router
, RequestReader
and ResponseBuilder
.Router
io.finch.route
package implements the route combinators API, which allows you to build an infinite number of routers, combining them from primitive routers accessible from the box. Parser Combinators and scodec use the same approach.Router[A]
is a function of Route => Option[(Route, A)]
. Router
takes the abstract Route route and returns Option
from the remaining route and the extracted value of type A
In other words, Router
returns Some(...)
in case of success (if the request was able to route it).int
, long
, string
and boolean
. In addition, there are routers that do not extract the value from the route, but simply associate it with a sample (for example, routers for HTTP methods: Get
, Post
).router
routes the GET /(users|user)/:id
requests and extracts the integer id
value from the route. Pay attention to the operator /
(or andThen
), with the help of which we sequentially compose two routers, and also on the operator |
(or orElse
), which allows you to compose two routers in terms of logical or
. val router: Router[Int] => Get / ("users" | "user") / int("id")
/
. case class Ticket(userId: Int, ticketId: Int) val r0: Router[Int / Int] = Get / "users" / int / "tickets" / int val r1: Router[Ticket] = r0 map { case a / b => Ticket(a, b) }
Service
) from the route. Such routers are called endpoints (in fact, Endpoint[Req, Rep]
is just a type alias on Router[Service[Req, Rep]]
). Endpoint
s can be implicitly converted to Finagle services ( Service
), which allows them to be used transparently with the Finagle HTTP API. val users: Endpoint[HttpRequest, HttpResponse] = (Get / "users" / long /> GetUser) | (Post / "users" /> PostUser) | (Get / "users" /> GetAllUsers) Httpx.serve(":8081", users)
RequestReader
io.finch.request.RequestReader
is key in Finch. Obviously, most of the REST API (without taking into account business logic) is reading and validating the request parameters. This is RequestReader
. Like everything else in Finch, RequestReader[A]
is a HttpRequest => Future[A]
function. Thus, RequestReader[A]
accepts an HTTP request and reads some value of type A
from it. The result is placed in the Future
, primarily in order to represent the stage of reading the parameters as an additional Future
transformation (as a rule, the first) in the data-flow of the service. Therefore, if RequestReader
returns Future.exception
, no further transformations will be performed. This behavior is extremely convenient in 99% of cases when the service should not do any real work if one of its parameters is invalid.RequestReader
title
reads the required query-string “title” parameter or returns a NotPresent
exception if the parameter is missing in the request. val title: RequestReader[String] = RequiredParam("title") def hello(name: String) = new Service[HttpRequest, HttpResponse] { def apply(req: HttpRequest) = for { t <- title(req) } yield Ok(s"Hello, $t $name!") }
io.finch.request
package provides a rich set of built-in RequestReader
s for reading various information from an HTTP request: starting from query-string parameters and ending with cookies. All available RequestReader
-y are divided into two groups - required (required) and optional (optional). Required readers read the value or exception NotPresent
, optional - Option[A]
. val firstName: RequestReader[String] = RequiredParam("fname") val secondName: RequestReader[Option[String]] = OptionalParam("sname")
RequestReader
provides an API with which you can compose two readers into one. There are two APIs: monadic (using flatMap
) and applicative (using ~
). While monadic syntax looks familiar, it is highly recommended to use applicative syntax, which allows you to accumulate errors, while the fail-fast nature of monads returns only the first one. The example below shows both ways of composing a reader. case class User(firstName: String, secondName: String) // the monadic style val monadicUser: RequestReader[User] = for { firstName <- RequiredParam("fname") secondName <- OptionalParam("sname") } yield User(firstName, secondName.getOrElse("")) // the applicate style val applicativeUser: RequestReader[User] = RequiredParam("fname") ~ OptionalParam("sname") map { case fname ~ sname => User(fname, sname.getOrElse("")) }
RequestReader
allows RequestReader
to read from the request values of types other than String. You can convert the readable value using the RequestReader.as[A]
method. case class User(name: String, age: Int) val user: RequestReader[User] = RequiredParam("name") ~ OptionalParam("age").as[Int] map { case name ~ age => User(fname, age.getOrElse(100)) }
as[A]
method is an implicit DecodeRequest[A]
type parameter. Type-class DecodeRequest[A]
carries information about how type A
can be obtained from a String
. In case of conversion error, RequestReader
will read NotParsed
exception. Out of the box, conversions to Int
, Long
, Float
, Double
and Boolean
types are supported.RequestReader
implemented in the same way: we can use the as[Json]
method if for Json
there is an implementation of DecodeRequest[Json]
in the current scope. In the example below, RequestReader
user
reads a user that is serialized in JSON format to the body of the HTTP request. val user: RequestReader[Json] = RequiredBody.as[Json]
RequestReader
greatly simplified. import io.finch.jackson._ case class User(name: String, age: Int) val user: RequestReader[User] = RequiredBody.as[User]
RequestReader.should
and RequestReader.shouldNot
. There are two ways of validation: using inline rules and using ready-made ValidationRule
. In the example below, the age
reader reads the “age” parameter, provided it is greater than 0 and less than 120. Otherwise, the reader will read the NotValid
exception. val age: RequestReader[Int] = RequiredParam("age").as[Int] should("be > than 0") { _ > 0 } should("be < than 120") { _ < 120 }
io.finch.request
package and the io.finch.request
composers and
from the ValidationRule
. val age: RequestReader[Int] = RequiredParam("age").as[Int] should (beGreaterThan(0) and beLessThan(120))
ResponseBuilder
io.finch.response
package provides a simple API for building HTTP responses. It is considered common practice to use a specific ResponseBuilder
corresponding to the response status code, for example, Ok
or Created
. val response: HttpResponse = Created("User 1 has been created") // plain/text response
io.finch.response
package is type-class EncodeResponse[A]
. ResponseBuilder
is able to build HTTP responses from any type A
, if for it there is an implicit value EncodeResponse[A]
in the current scope. This is how JSON support is implemented in ResponseBuilder
: for each supported library there is an implementation of EncodeResponse[A]
. The following code shows integration with the standard JSON implementation from the finch-json
module. import io.finch.json._ val response = Ok(Json.obj("name" -> "John", "id" -> 0)) // application/json response
ResponseBuilder
adding implicit EncodeResponse[A]
values for the required type to the current scope. For example, for type User
. case class User(id: Int, name: String) implicit val encodeUser = EncodeResponse[User]("applciation/json") { u => s"{\"name\" : ${u.name}, \"id\" : ${u.id}}" } val response = Ok(User(10, "Bob")) // application/json response
Source: https://habr.com/ru/post/250591/
All Articles