What’s http4s
RackやWSGIのScala版といったところ。公式サイトの説明も次のように書いてある。
http4s is a minimal, idiomatic Scala interface for HTTP. http4s is Scala’s answer to Ruby’s Rack, Python’s WSGI, Haskell’s WAI, and Java’s Servlets.
まだまだ開発途中でドキュメントなどはあまり整備されていなくて、まともに使おうと思ったらソースコードを読む必要が出てきそう。次期Scalatraのバックエンドになるとかならないとか噂されているけどはてさて?もしかしたらAkkaやFinagleに押されて流行らずに終わる可能性もある。
ちなみにscalaz-streamが使われている。
Install
最小構成で使う場合は http4s-server
と http4s-blaze-server
だけで良い。
resolvers += "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases"
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-server" % "0.10.1",
"org.http4s" %% "http4s-blaze-server" % "0.10.1"
)
Specification
http4sの仕様に準拠したアプリケーション(http4sではサービスという)として最低限必要なことは次の通り。
org.http4s.server.HttpService
型であること
以上!めちゃくちゃシンプルですね。HttpServiceの定義を覗いてみるとこう書いてある。
type Service[A, B] = Kleisli[Task, A, B]
type HttpService = Service[Request, Response]
実際はHttpServiceを作るための関数が HttpService.apply
として用意されているのでこれを使っていく。この関数はRequest
型を受け取ってTask[Response]
型を返すPartialFunction
を引数に取る。
object HttpService {
def apply(pf: PartialFunction[Request, Task[Response]], default: HttpService = empty): HttpService
}
Rackなどを触ったことがある人ならピンと来るはず。
Introduction
アクセスすると画面にHello WorldとQueryStringを表示する簡単なサービスを作ってみる。
// src/main/scala/jp/dakatsuka/http4stest/Bootstrap.scala
package jp.dakatsuka.http4stest
import org.http4s.{Status, Response}
import org.http4s.server.HttpService
import org.http4s.server.blaze.BlazeBuilder
object Bootstrap {
val service = HttpService {
case req =>
Response()
.withStatus(Status.Ok)
.withBody(s"Hello World!! ${req.queryString}")
}
def main(args: Array[String]): Unit =
BlazeBuilder
.bindHttp(8080)
.mountService(service)
.run
.awaitShutdown()
}
sbt run
を実行してブラウザで http://localhost:8080/?foo=bar&fizz=buzz
にアクセス。次のような文字がブラウザ上に表示されるはず。
Hello World!! foo=bar&fizz=buzz
Middleware
ミドルウェアも簡単につくれる。ミドルウェアは既存のHttpServiceに組み込んで(合成して)使う。試しにX-HTTP4S-MESSAGE
というヘッダーを付与するミドルウェアを作ってみる。
// src/main/scala/jp/dakatsuka/http4stest/MessageMiddleware.scala
package jp.dakatsuka.http4stest
import org.http4s.Header
import org.http4s.server.HttpService
object MessageMiddleware {
def apply(service: HttpService, message: String): HttpService = HttpService.lift { req =>
service.map { res =>
res.putHeaders(Header("X-HTTP4S-MESSAGE", message))
}.apply(req)
}
}
組み込み方はこう。
def main(args: Array[String]): Unit =
BlazeBuilder
.bindHttp(8080)
.mountService(MessageMiddleware(service, "Hello!!!!!"))
.run
.awaitShutdown()
ちゃんとヘッダーに追加されていることが分かる。
$ curl --dump-header - http://localhost:8080
HTTP/1.1 200 OK
Content-Length: 14
Content-Type: text/plain; charset=UTF-8
X-HTTP4S-MESSAGE: Hello!!!!!
Date: Sat, 14 Nov 2015 13:30:32 GMT
Hello World!!
http4s-dsl
http4s-dslというパッケージを追加すればSinatraのようにDSLを使ってルーティングを定義できる。
build.sbtに依存関係を追加する。
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-server" % "0.10.1",
"org.http4s" %% "http4s-dsl" % "0.10.1",
"org.http4s" %% "http4s-blaze-server" % "0.10.1"
)
DSLを使うと次のように直感的にルーティングできるようになる。ちょっとしたマイクロサービスを作りたいときにはこれで十分な気がする。
import org.http4s._
import org.http4s.dsl._
import org.http4s.server.HttpService
import jp.dakatsuka.http4stest.model.User
val service = HttpService {
case GET -> Root =>
Ok("Document Root")
case GET -> Root / "users" =>
Ok(User.all)
case GET -> Root / "users" / LongVar(id) =>
User.find(id) match {
case Some(user) => Ok(user)
case _ => NotFound()
}
case GET -> Root / "users" / screen_name =>
User.findOneByScreenName(screen_name) match {
case Some(user) => Ok(user)
case _ => NotFound()
}
case req @ POST -> Root / "users" =>
req.decode[UrlForm] { data =>
val UserParams = for {
email <- data.getFirst("email")
screen_name <- data.getFirst("screen_name")
} yield UserParams(email, screen_name)
???
}
}
JSON レスポンスを返したい
厳密にはcase classをOk
やNotFound
に渡したらJSON文字列に変換してレスポンスを返して欲しい。になる。
http4sは EntityEncoder[T]
を定義しておけばどんな型でもレスポンスとして返せるという仕組みがある(逆のEntityDecoder
もある)JSONに関しては http4s-argonaut という公式パッケージが用意されているのでそれを利用するのが良いだろう。Argonautが嫌な人はJson4s用のパッケージも用意されているのでそちらを。
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-server" % "0.10.1",
"org.http4s" %% "http4s-dsl" % "0.10.1",
"org.http4s" %% "http4s-blaze-server" % "0.10.1",
"org.http4s" %% "http4s-argonaut" % "0.10.1"
)
// src/main/scala/jp/dakatsuka/http4stest/model/User.scala
package jp.dakatsuka.http4s.model
import argonaut.Argonaut._
import argonaut.CodecJson
import org.http4s.argonaut._
import org.http4s.EntityEncoder
case class User(id: Long, email: String, screen_name: String)
object User {
implicit val userCodec: CodecJson[User] = casecodec3(User.apply, User.unapply)("id", "email", "screen_name")
implicit val userEntityEncoder: EntityEncoder[User] = jsonEncoderOf[User]
implicit val usersEntityEncoder: EntityEncoder[List[User]] = jsonEncoderOf[List[User]]
def all(): List[User] = ???
def find(id: Long): Option[User] = ???
def findOneByScreenName(screen_name: String): Option[User] = ???
}
まとめ
駆け足でhttp4sを紹介してみたけど、個人的にはかなり使い勝手が良いと思う。Akka, Playframeworkのような重厚感もないし http4s-dsl を使えば簡単にAPIの実装も出来るだろう。お手軽感って大事だと思う。ルーティングやレスポンスを型安全に書けるのもポイントが高い。
懸念点としては最初にも書いたようにライバルが多くて消えてしまうのでは…ってところ。http4sはServletでも動くので頑張って欲しい。