Sprayとは
HTTP処理をScalaで書けるOSSで、
- 非同期
- Actorベース
- 速くて軽量
- モジュールで分かれている
- テストしやすい
という特徴がある。
このようにモジュールが分離されていて、必要なものをsbtの依存に入れて書くようになっている。
主要なものだけ説明。
spray-can
HTTPを受け取ってレスポンスするサーバーとしての機能と、HTTPを送るクライアントとしての機能を、低レベルなAPIで提供している。
サーバーとしての簡単な例を見てみる。
// Actorのメッセージで、HTTPリクエストの情報かが詰まったオブジェクトを受け取る。
// そしてHTTPレスポンスのオブジェクトを、メッセージで返す。
class FooActor extends Actor {
def receive = {
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
sender ! HttpResponse(entity = "PONG")
}
}
// Listenする。
val fooActor: ActorRef = ...
IO(Http) ! Http.Bind(fooActor, interface = "localhost", port = 8080)
Actorのメッセージでやり取りするので、今までのwebフレームワークと違って新しい感じだ。
ただ、このままだと書きづらいので、書きやすいDSLを提供するのがspray-routingだ。
spray-routing
spray-canを、より簡単に扱える高レベルAPIを提供するもの。
さっきのPING/PONGをspray-routingで書いてみると、
object Main extends App with SimpleRoutingApp {
startServer(interface = "localhost", port = 8080) {
get { // GETのリクエストで
path("ping") { // /pingへのリクエストで
complete("PONG") // PONGをレスポンスする
}
}
}
}
とてもシンプルに書けるようになる。
様々なspray-routingの機能
さっきの例に出てきた、get
やpath
、complete
といったDSLは、Directiveと呼ばれている。多種多様なDirectiveが用意されていて、うまく組み合わせることでシンプルで直感的な記述が可能になる。
Directiveの使用例
TODOアプリの例
get {
path("todos" / IntNumber) { id =>
complete {
findById(id).map("todo is " + _) // 文字列で返す。Noneなら404。
}
}
}
def findById(id: Int): Option[Todo] = ...
Optionをcompleteに渡すとうまいこと扱ってくれるのは、spray-httpx
のmarshallingという機能があるからだ。他にもFutureやTry、Eitherなどうまいこと扱ってくれる。
POSTだとこんな感じ。
post {
path("todos") {
formFields('content) { content =>
complete(service.create(content))
}
}
}
Jsonでやり取りする簡単なTODOアプリを作ったので参考にどうぞ。
もう一つ例を。
get {
path("check") {
cookie("uid") { uid => // クッキーから値を取り
validate(uid.length > 0, "uidが不正") { // バリデーションして
onSuccess(existsByUid(uid)) { // Futureを剥がして
case true => complete("OK")
case false => complete("NG")
}
}
}
}
}
def existsByUid(uid: String): Future[Boolean] = ...
Directiveはたくさん用意されているので、一通り目を通しておこう。
もちろん自分で定義することもできる。
カスタムDirective
例えば検索条件を取得するdirectiveを定義してみる。
case class SearchParams(id: Int, name: Option[String], isDebug: Boolean)
// 値を取得してcase classに詰める。
// cookieにisDebug=1があればtrueに
val searchParams: Directive1[SearchParams] = {
parameter(('id.as[Int], 'name.?.as[String])) &
optionalCookie("isDebug")
.hmap { case id :: name :: isDebugCookie :: HNil =>
SearchParams(id, name, isDebugCookie.exists(_.content == "1"))
}
}
Directive1は、値を1つ渡すからDirective1
だ。
使うときはいつも通り。
get {
path("search") {
searchParams { sp: SearchParams =>
findByParam(sp)
...
}
}
}
ちなみに、Directive1はforでも書ける。
さっきのをforで書いてみると
val searchParams: Directive1[SearchParams] = {
for {
id <- parameter('id.as[Int])
name <- parameter('name.as[String])
isDebugCookie <- optionalCookie("isDebug")
} yield {
SearchParams(id, name, isDebugCookie.exists(_.content == "1"))
}
}
こっちのほうが分かりやすいかな?
AND&
やOR|
でも繋げれるので工夫次第で色んな書き方がある。
今までのwebフレームワークと全然違っておもしろいですねぇ(´-` )