Unfilteredはじめました

ScalaJPのメーリングリストで話題に上がったので、ScalaWEBフレームワークツールキットのUnfilteredで遊んでみました。

Unfilteredの公式サイトを進めていったメモです。
ちょっと日本語の情報少なすぎませんか!
ゆろよろ先生が0.33のドキュメントを翻訳してた

この記事でやったこと

  • Unfilteredで遊べるように環境を整えた
  • Sinatraのサンプルっぽいのを試した。"/hello/任意のid/"にブラウザでアクセスすると"Hello, #{入力したID}"と表示させるだけ。

間違いの指摘は大歓迎。

導入

学習用ファイルが用意されたリポジトリがあるので、これを使わせてもらうことに。導入は例によってgiter8を利用します。

$ g8 softprops/unfiltered --name=justplayin
$ cd justplayin
$ sbt console

HelloWorldする前に、Unfilteredで作るサーバーの大まかな概要を説明します。

  1. 対応するリクエストと、レスポンス(ただの文字列/HTML/その他のデータ)を定義したモノ(Intent)をつくる
  2. IntentはPartial Functionのように合成ができる。合成したものをintent()メソッドに紐付けたobject(Plan)をつくる。
  3. 開発用サーバーを定義したクラスにPlanをくっつけて(Planも合成可)、サーバーを起動する。

細かいルーティングをたくさん書いて、組み合わせていく、というスタイルなのでしょうか。

ではとりあえずHelloWorldしてみましょう。sbt console した状態で実行して下さい。(ファイルのmainに書いてもできると思いますが)

import unfiltered.request._
import unfiltered.response._

val hello = unfiltered.filter.Planify {
  case _ => ResponseString("Hello, world!")
}

unfiltered.jetty.Http.local(8080).filter(hello).run()

localhost:8080/#{適当なアドレス} にアクセスしてみて下さい。どのパスに行っても、Hello, world! と返すだけの無意味なサーバーの完成です。

で、IntentとPlanというのはどこに行ったんでしょう?実はもう書いてます。
val hello = ... の部分は、これと一緒です。

object Hello extends unfiltered.filter.Plan {
  def intent = {
    case _ => ResponseString("Hello, world!")
  }
}

小文字のクラスを作るのがアレだったのでhを大文字にしましたが、同じものが出来ました。
もちろん、先ほどと同じ方法でサーバーを起動できます。

では次に、/hello にだけ挨拶して、ほかは Not Found! と表示するのを書いてみましょう。
あ、面倒なので、unfiltered.filter.Planifyのほうでやりますね。

val hello = unfiltered.filter.Planify {
  case Path(Seg("hello" :: Nil)) => ResponseString("Hello, world!")
  case _ => ResponseString("Not Found!")
}

Path とか Seg とかは request matcher と呼ばれているようです。(内部的には、ただのExtractor)
Path(Seg("hello" :: Nil)) で /hello に対応しています。

あと、"Not Found!"のところだけど、「404を返す」というオブジェクトを ~> というメソッドで連結したりできます。(でも、またそこまで勉強が進んでいないので勘弁してください)


せっかくなので、分離してから合成してみましょう。

val hello = unfiltered.filter.Planify {
  case Path(Seg("hello" :: Nil)) => ResponseString("Hello, world!")
}

val not_found = unfiltered.filter.Planify {
  case _ => ResponseString("Not Found!")
}

unfiltered.jetty.Http.local(8080).filter(hello).filter(not_found).run()

関数合成によって、ルーティングが設定出来ました。

最後に、"/hello/任意のid"にブラウザでアクセスすると"Hello, #{入力したID}"と表示させるようにしてみましょう。
Extractorを使うことで、文字列をキャプチャーできます。

ついでに、"/hello"にアクセスしたときは"Hello, world!"と返すようにしてみました。

import unfiltered.request._
import unfiltered.response._

val hello = unfiltered.filter.Planify {
  case Path(Seg("hello" :: name :: Nil)) => ResponseString("Hello, %s!" format name)
  case Path(Seg("hello" :: Nil)) => ResponseString("Hello, world!")
}

val not_found = unfiltered.filter.Planify {
  case _ => ResponseString("Not Found!!")
}

unfiltered.jetty.Http.local(8080).filter(hello).filter(not_found).run()