LoginSignup
18
16

More than 5 years have passed since last update.

Sprayの紹介

Posted at

Sprayとは

HTTP処理をScalaで書けるOSSで、

  • 非同期
  • Actorベース
  • 速くて軽量
  • モジュールで分かれている
  • テストしやすい

という特徴がある。

このようにモジュールが分離されていて、必要なものをsbtの依存に入れて書くようになっている。

主要なものだけ説明。

spray-can

HTTPを受け取ってレスポンスするサーバーとしての機能と、HTTPを送るクライアントとしての機能を、低レベルなAPIで提供している。

サーバーとしての簡単な例を見てみる。

PING/PONGの例
// 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の機能

さっきの例に出てきた、getpathcompleteといった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フレームワークと全然違っておもしろいですねぇ(´-` )

18
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
16