ScalaのHTTPインターフェース http4s 超入門

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-serverhttp4s-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をOkNotFoundに渡したら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でも動くので頑張って欲しい。