http4sのHTTP Serverを使ってみる
はじめに
FS2に依存するライブラリはいくつかありますが、http4sというHTTPサーバとクライアントを用意してくれているライブラリがあり、ちょっと興味が湧いたので触ってみようかと思います。
http4sはFS2(と、もちろんCats Effects)をベースにしているので、FunctinnalでStreamingでFunctionalというのが特徴みたいです。
やってみる
環境
$ scala --version Scala code runner version 2.13.6 -- Copyright 2002-2021, LAMP/EPFL and Lightbend, Inc. $ sbt --version [info] 1.2.7 sbt script version: 1.5.2
プロジェクトの作成
まずはベースとなるプロジェクトを作成します。
$ sbt new sbt/scala-seed.g8 [info] welcome to sbt 1.5.2 (Oracle Corporation Java 1.8.0_292) [info] loading global plugins from /home/yuya-hirooka/.sbt/1.0/plugins [info] set current project to new (in build file:/tmp/sbt_adb8af4/new/) A minimal Scala project. name [Scala Seed Project]: http4s-example
作成されたプロジェクトのbuild.sbt
に以下の依存を追加します。
libraryDependencies += "org.http4s" %% "http4s-dsl" % "0.21.23", libraryDependencies += "org.http4s" %% "http4s-blaze-server" % "0.21.23", libraryDependencies += "org.http4s" %% "http4s-blaze-client" % "0.21.23",
今回はhttp4sの現在のstableバージョンである0.21.x
系を利用しようと思います。
名前を受け取って挨拶を返すサーバを作成する
最初にhttp4s-blaze-server
を使って、サーバーの方を作成します。
最終的には名前をJsonで受け取って挨拶を返すサーバを作成しようと思います。
helloの文字列を返す
まずは、単純なGETリクエストに対して"hello"という文字列を返すだけのサーバを作ります。
そのためにはRouter
とBlazeServerBuilder
を使います。
Router
を使ってルートの定義を作成します。
import cats.effect._ import org.http4s._ import org.http4s.dsl.io._ import org.http4s.implicits.http4sKleisliResponseSyntaxOptionT import org.http4s.server.Router object SampleRouters { def helloHandler() = IO("hello") val helloRouter = HttpRoutes.of[IO] { case GET -> Root / "hello" => helloHandler().flatMap(Ok(_)) } def createApp = Router("/" -> helloRouter).orNotFound }
HttpRoutes
と http4s-dsl,を使ってルータを定義します。
みてすぐわかるかも知れませんが、パターンマッチングでGETと/hello
のパスにマッチした場合helloHandr()
を呼び出し、その戻り値であるIO("")
をflatMapして取り出しOk()
レスポンスで返すRouterを定義しています。
1番最後の行ではRouter objectを作成策定していますが、ここでは定義したルータのルートに当たるパスを指定することができます。
Routeの作成はできたので、次にアプリケーションのエントリーポイントとなるobjectを作ります。
import cats.effect.{ExitCode, IO, IOApp} import org.http4s.server.blaze.BlazeServerBuilder import scala.concurrent.ExecutionContext.Implicits.global object AppStarter extends IOApp { override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO](global) .bindHttp(8081, "localhost") .withHttpApp(SampleRouters.createApp) .serve .compile .drain .as(ExitCode.Success) }
http4sは様々なバックエンドをサポートしているようですが(サポートはここを確認してください。
BlazeServerBuilder
を使って、
もし、IOApp以外の場所でBuilderを使いたい場合は以下のimplicit
を定義する必要があります。
implicit val cs: ContextShift[IO] = IO.contextShift(global) implicit val timer: Timer[IO] = IO.timer(global)
アプリケーションを実行するし、cURLでアプリケーションにリクエストを送ってみます。
$ curl localhost:8081/greeting -v * Trying 127.0.0.1:8081... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) > GET /greeting HTTP/1.1 > Host: localhost:8081 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 < Date: Fri, 11 Jun 2021 14:20:49 GMT < Content-Length: 5 < * Connection #0 to host localhost left intact hello
うまく起動できてるみたいですね。
パスパラーメータで値を受け取る
それでは次に、パスパラメータ名前を受け取ってその名前に対して挨拶を返す用にしてみます。
新しく、Routerを作成します。
object SampleRouters { def helloHandler() = IO("hello") val helloRouter = HttpRoutes.of[IO] { case GET -> Root / "hello" => helloHandler().flatMap(Ok(_)) } def greetingSomeone(name: String) = IO(s"Hello, $name") val greetingRouter = HttpRoutes.of[IO] { case GET -> Root / "greeting" / name => helloHandler().flatMap(Ok(_)) } def createApp = Router("/" -> helloRouter, "/v1" -> greetingRouter).orNotFound }
ここではgreetingRouter
を新たに定義してます。
DSLのなかでパスの位置い変数を置くことで、パスパラメータを束縛することができます。
また、上記のようにRouter object作成時には複数のRouterを登録することができます。
$ curl localhost:8081/v1/greeting/Moheji -v * Trying 127.0.0.1:8081... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) > GET /v1/greeting/Moheji HTTP/1.1 > Host: localhost:8081 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 < Date: Fri, 11 Jun 2021 14:46:01 GMT < Content-Length: 13 < * Connection #0 to host localhost left intact Hello, Moheji
Jsonで値を受け取る。Jsonの値を返す
それでは最後にJsonの値を扱うようなPathを追加してみようと思います。
まずですが、http4sでJsonを扱うためには以下の依存を追加する必要があります。
libraryDependencies += "org.http4s" %% "http4s-circe" % "0.21.23" libraryDependencies += "io.circe" %% "circe-generic" % "0.13.0"
circe-generic
の依存はオプショナルっぽいですが、JsonとScalaのクラスとの変換を知れてくれるみたいなので追加しておきます。
それでは追加したhttp4s-circe
とcirce-generic
を使ってgreetingRouter
に新たなRouteを書き加えてみます。
import io.circe.generic.auto._ import io.circe.syntax.EncoderOps import org.http4s._ import org.http4s.circe.CirceEntityCodec._ //他のImportは省略 object SampleRouters { // 他の実証は諸略 case class Name(name: String) case class Greeting(hello: String) val greetingRouter = HttpRoutes.of[IO] { case req@POST -> Root / "greeting" => for { n <- req.as[Name] resp <- Created(Greeting(n.name).asJson) } yield resp case GET -> Root / "greeting" / name => greetingSomeone(name).flatMap(Ok(_)) } def createApp = Router("/" -> helloRouter, "/v1" -> greetingRouter).orNotFound }
POSTリクエストで名前を受け取るためにDSLを使います。
req@
のように記述刷ることでreq
にリクエストの情報をバインドできるみたいです。
リクエストを受け取るためのcase
// リクエスト用のcase class case class Name(name: String) // レスポンス用のcase class case class Greeting(hello: String)
本来はこれらに対してエンコーダーとデコーダーを作ってimplicitとして定義刷る必要があるようですが。
以下の2つをインポートしていればそれぞれが自動でimplicit
されるみたいです。
import io.circe.generic.auto._ import org.http4s.circe.CirceEntityCodec._
それではリクエストを送ってみましょう。
$ curl localhost:8081/v1/greeting -d '{"name":"Moheji"}' -v * Trying 127.0.0.1:8081... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) > POST /v1/greeting HTTP/1.1 > Host: localhost:8081 > User-Agent: curl/7.68.0 > Accept: */* > Content-Length: 17 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 17 out of 17 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 201 Created < Content-Type: application/json < Date: Fri, 11 Jun 2021 15:51:56 GMT < Content-Length: 18 < * Connection #0 to host localhost left intact {"hello":"Moheji"}
これでちょっと触った程度ですが一応、サーバができましたね。