暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Scala: Http4s Introduction

楠爸自习室 2020-02-12
694

What is http4s?

http4s is a typeful, functional, streaming HTTP for Scala.

The current status of each version

This introduction is based on the stable version 0.20

How to install?

Add following configuration into your build.sbt

val Http4sVersion = "0.20.17"
libraryDependencies += Seq(
"org.http4s" %% "http4s-blaze-server" % Http4sVersion, // http server implementation
"org.http4s" %% "http4s-blaze-client" % Http4sVersion, // http client implementation
"org.http4s" %% "http4s-circe" % Http4sVersion, // supply some utility methods to convert the Encoder/Decoder of circe to the EntityEncoder/EntityDecoder of http4s
"org.http4s" %% "http4s-dsl" % Http4sVersion // supply lots of syntax to parse request
)

Add following package into you import header

import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.circe._

Core Concept

Request

sealed abstract case class Request[F[_]](
method: Method = Method.GET,
uri: Uri = Uri(path = "/"
),

httpVersion: HttpVersion = HttpVersion.`HTTP/1.1`,
headers: Headers = Headers.empty,
body: EntityBody[F] = EmptyBody,
attributes: Vault = Vault.empty
) extends Message[F]

The Request
model contains all the information needed by a http request
. The body of http request
is EntityBody
which is a fs2.Stream[F, Byte]
.

Obviously, when the server receives a request, it's not an easy work to convert Stream
to a data model. So there is a type class called EntityDecoder
to help us to do this work.

trait EntityDecoder[F[_], T] {
def decode(msg: Message[F], strict: Boolean): DecodeResult[F, T]
}

its consumer is defined in Message
which is the parent of Request

trait Message[F[_]] {
def attemptAs[T](implicit decoder: EntityDecoder[F, T]): DecodeResult[F, T] =
decoder.decode(this, strict = false)

def as[A](implicit F: MonadError[F, Throwable], decoder: EntityDecoder[F, A]): F[A] =
attemptAs.leftWiden[Throwable].rethrowT
}

Actually, the EntityDecoder
didn't help us too much, because the decode
method still get a Message
which contains Stream
body. In term of the widely usage of JSON and circe in the RestfulAPI, http4s supply jsonOf
to let us convert the Decoder
of circe to EntityDecoder

import org.http4s.circe._

implicit val personDecoder: Decoder[Person] = new Decoder[Person] {
override def apply(c: HCursor): Decoder.Result[Person] =
for {
name <- c.get[String]("name")
age <- c.get[Option[Int]]("age")
phoneNumbers <- c.get[List[String]]("phone")
} yield Person(name, age, phoneNumbers)
}
implicit val personEntityDecoder: EntityDecoder[IO, Person] = jsonOf[IO, Person]

But when you want to send a http request
by a http client
, you need to convert a data model to Stream
. http4s supply another type class called EntityEncoder
to do this work and also supply jsonOfEncoder
to let you use JSON and circe Encoder
.

final case class Entity[+F[_]](body: EntityBody[F], length: Option[Long] = None)
trait EntityEncoder[F[_], A] {
def toEntity(a: A): Entity[F]
}

trait Message[F[_]] {
def withEntity[T](b: T)(implicit w: EntityEncoder[F, T]): Self
}

import org.http4s.circe._

implicit val personEncoder: Encoder[Person] = new Encoder[Person] {
override def apply(a: Person): Json = Json.obj(
"name" -> a.name.asJson,
"age" -> a.age.asJson,
"phone" -> a.phoneNumbers.asJson
)
}
implicit val personEntityEncoder: EntityEncoder[IO, Person] = jsonEncoderOf[IO, Person]

Response

final case class Response[F[_]](
status: Status = Status.Ok,
httpVersion: HttpVersion = HttpVersion.`HTTP/1.1`,
headers: Headers = Headers.empty,
body: EntityBody[F] = EmptyBody,
attributes: Vault = Vault.empty
) extends Message[F]


Except the status
attribute, the usage of EntityEncoder
and EntityDecoder
, most parts of Response
are same as Request
.

When a server wants to return a http response
, the EntityEncoder
will be used.

case class Person(name: String, age: Option[Int], phoneNumbers: List[String])
implicit val personEntityEncoder: EntityEncoder[IO, Person] = jsonEncoderOf[IO, Person]

Ok(Person("Job", Some(18), List("111111"))) // send back Ok status with body

When a client want to parse a http response
, the EntityDecoder
will be used.

trait Client[F[_]] {
def expect[A](req: F[Request[F]])(implicit d: EntityDecoder[F, A]): F[A]
def expect[A](uri: Uri)(implicit d: EntityDecoder[F, A]): F[A]
def fetchAs[A](req: Request[F])(implicit d: EntityDecoder[F, A]): F[A]
def fetchAs[A](req: F[Request[F]])(implicit d: EntityDecoder[F, A]): F[A]
}

Routes

  type Http[F[_], G[_]] = Kleisli[F, Request[G], Response[G]]
type HttpApp[F[_]] = Http[F, F]
type HttpRoutes[F[_]] = Http[OptionT[F, ?], F]

A HttpRoutes
is just a function which takes Request
as input and produce Response
as output.

Usually, we will build different Routes
for different business logic, so the HttpRoutes
won't return Response
for every Request
, that's why it returns Option[Response]
.

But the http server
should be able to handle all the Request
, so we need to give a default route, then the HttpRoutes
will become HttpApp
which is used in Server

Middleware

A middleware is just a function HttpApp[F[_], G[_]] => HttpApp[F[_], G[_]]

Usage

How to start a server?

object Main extends IOApp {
def helloWorldRoutes: HttpRoutes[IO] = HttpRoutes
.of[IO]({
case GET -> Root / "hello" => Ok("Hello!!")
})
override def run(args: List[String]): IO[ExitCode] = {
val resource = for {
server <- BlazeServerBuilder[IO]
.bindHttp(8888, "0.0.0.0")
.withHttpApp(helloWorldRoutes.orNotFound)
.resource
} yield server
resource.use(_ => IO.never)
}
}

How to create a route?

HttpRoutes
is a type alias of Kleisli[OptionT[F, ?], Request[F], Response[F]]
which is complicated, http4s supply HttpRoutes.of
to help us convert a partial function Request[F] => Response[F]
to HttpRoutes

def helloWorldRoutes: HttpRoutes[IO] = HttpRoutes
.of[IO]({
case GET -> Root / "hello" => Ok("Hello!!")
})

HttpRoutes
is the main place of business logic, in which we will parse Request
, do computation then construct the Response
.

How to parse the Uri of Request?

To parse the Uri, http4s supply a set of dsl to help us and it already gives the dsl implementation of IO
.

Here we will use IO
as our effect, so we need to import its dsl when we parse the Uri

import org.http4s.dsl.io._

  • Method

      case (GET|POST) -> Root / "hello" =>
    // ^
    // Method

  • Path parameter

      case GET -> Root / "hello" / name / IntVar(id) / LongVar(hash) =>
    // ^ ^ ^
    // String Int Long

  • Query Parameter

    • Required query parameter

        // url is /hello?user_name=john

      object UserName extends QueryParamDecoderMatcher[String]("user_name")
      case GET -> Root / "hello" :? UserName(name) =>
      // ^
      // Required query parameter(String)

    • Optional query parameter

        // url is /hello?user_name=john
      // or /hello

      object UserName extends OptionalQueryParamDecoderMatcher[String]("user_name")
      case GET -> Root / "hello" :? UserName(name) =>
      // ^
      // Optional query parameter(Option[String])

    • Multiple query parameter

        // url is /hello?user_name=john&age=18

      object UserName extends QueryParamDecoderMatcher[String]("user_name")
      object Age extends OptionalQueryParamDecoderMatcher[Int]("age")
      case GET -> Root / "hello" :? UserName(name) +& Age(age) =>
      // ^
      // Multiple query parameter

    • Optional query parameter which occurs multiple times

        // url is /hello?user_name=john&user_name=lili
      // or /hello

      object UserNames extends OptionalMultiQueryParamDecoderMatcher[String]("user_name")
      case GET -> Root / "hello" :? UserNames(names) =>
      // ^
      // Occurs multiple times query parameter(List[String])

How to get the header of Request?

  case request@GET -> Root / "hello" =>
println(request.headers)
Ok()

How to get the body of Request?

Assume we have a model Person
in our program

case class Person(name: String, age: Option[Int], phoneNumbers: List[String])

And we already defined the EntityDecoder
of Person

object Person {
implicit val personDecoder: Decoder[Person] = new Decoder[Person] {
override def apply(c: HCursor): Decoder.Result[Person] =
for {
name <- c.get[String]("name")
age <- c.get[Option[Int]]("age")
phoneNumbers <- c.get[List[String]]("phone")
} yield Person(name, age, phoneNumbers)
}

implicit val personEntityDecoder: EntityDecoder[IO, Person] = jsonOf[IO, Person]
}

Then if the body of Request
is a JSON

{
"name": "Job",
"age": 18,
"phone": ["1", "2"]
}

We can get the body of Request
like this

  case request@POST -> Root / "hello" =>
println(request.as[Person])
Ok()

How to construct a Response?

http4s already defined the Status
model for each status code, it's a simple entry of Response
. we need to choose the status code first, then pass the header or body to it.

Here we will use 200
to give an example.

  • Header

    Ok(Header("Vary", "Origin,Access-Control-Request-Methods"))

  • Body

    Assume we already defined the EntityEncoder
    of Person

    object Person {
    implicit val personEncoder: Encoder[Person] = new Encoder[Person] {
    override def apply(a: Person): Json = Json.obj(
    "name" -> a.name.asJson,
    "age" -> a.age.asJson,
    "phone" -> a.phoneNumbers.asJson
    )
    }

    implicit val personEntityEncoder: EntityEncoder[IO, Person] = jsonEncoderOf[IO, Person]
    }

    Then we can return Person
    as body like this

    Ok(Person("Job", Some(18), List("1111")))

How to add middleware?

How to support CORS?

def helloWorldRoutes: HttpRoutes[IO] = CORS(HttpRoutes
.of[IO]({
case GET -> Root / "hello" => Ok("Hello!!")
}))

How to create a client?

val resource = for {
client <- BlazeClientBuilder[IO](scala.concurrent.ExecutionContext.Implicits.global).resource
} yield client

resource.use(client => client.expect[Person](Uri.unsafefromString("http://localhost:8888/hello"))).unsafeRunSync()

How to send a GET request?

val response: IO[Person] = client.expect[Person](Uri.unsafefromString("http://localhost:8888/hello"))

How to send a POST request?

val request: Request[IO] = Request[IO](Method.POST, Uri.unsafefromString("http://localhost:8888/hello")).withEntity(Person("Jon", Some(18), List("1111")))
val response: IO[Response[IO]] = client.expect[String](IO(request))

Summary

Now you should be able to start a simple http server and send http request to remote server. if you want to try more features, please clone the following repo and have fun.

git clone git@github.com:sjmyuan/http4s-intro.git


文章转载自楠爸自习室,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论