Blog
spray-routingのディレクティブを活用しよう
初めまして、Lodeoの土井です。
今回はspray-routingのDirectiveに焦点を絞って、
Lodeoでどんな風に書いているか書きます。
spray-routing
spray-routingではリクエスト・レスポンスの処理を
次のような形式で処理していきます。
本家のMinimal Exampleです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import spray.routing.SimpleRoutingApp object Main extends App with SimpleRoutingApp { implicit val system = ActorSystem("my-system") startServer(interface = "localhost", port = 8080) { path("hello") { get { complete { <h1>Say hello to spray</h1> } } } } } |
http://localhost:8080/helloにGetでアクセスすると、
<h1>Say hello to spray</h1>を返却するというサンプルです。
構文的には比較的分かりやすいのではないでしょうか。
ここで出てくるpath, get, completeがDirectiveと呼ばれ、
個々のリクエストを処理するために使います。
Directive
Directiveは次の4つの機能を提供します
- 内部Routeに渡す前のRequestContext※の変換
- RequestContext※に基いたフィルタリング
- RequestContext※から値を抽出
- リクエストの完了
※ RequestContext: ルーティング構造間で渡される軽量なオブジェクト
先の例で言えば、get, pathは2のフィルタリング、
completeは4のリクエストの完了といったところです。
Directiveを触ってみよう
さて、Directiveを使ってもう少し書き進めてみましょう。
ユーザ登録を想定して次のような処理を行います。
- userクッキーから作成者idを取得
- フォームパラメータからname, ageを取得
- nameが空文字列でなく、ageが0歳以上をバリデーション
- dtoに包んで返却
- ユーザを作成
ソースは次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
object Main extends App with SimpleRoutingApp { ... path("users") { post { cookie("user") { user => formFields('name, 'age.as[Int]) { (name, age) => validate(name.nonEmpty && age > 10, s"Invalid Request") { val request = UserCreateRequest(user.content.toInt, name, age) createUser(request) complete(<h1>User {name} Created</h1>) } } } } } def createUser(request: UserCreateRequest): Unit = ... case class UserCreateRequest(createrId: Int, name: String, age: Int) ... } |
ネストが随分と深くなっていますね…。
ある程度はまとめて書くこともできるのですが、
条件やpathの記述が増えるほどに理解が困難になってしまいます。
Custom Directive登場
そこで、活躍するのがCustom Directiveです。
既存のDirectiveを組み合わせて1つのDirectiveとして使うことができます。
(もちろん、スクラッチから作成することも可能です)
上記の例のリクエストパラメータの抽出を
Custom Directiveに切り出すと次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
object Main extends App with SimpleRoutingApp { ... path("users") { post { extractRequest { request: UserCreateRequest => createUser(request) complete(<h1>User {request.name} Created</h1>) } } } def extractRequest: Directive1[UserCreateRequest] = { import shapeless._ // 各Directiveを繋げる処理. 興味半分で読み飛ばしてください cookie("user").hflatMap { case user :: HNil => formFields('name, 'age.as[Int]).hmap { case name :: age :: HNil => UserCreateRequest(user.content.toInt, name, age) } hfilter { case r :: HNil => r.name.nonEmpty && r.age > 10 } } } ... } |
リクエストの処理は少しスッキリしたでしょうか?
一方でextractRequestの部分がとんでもないことになっていますね。
これでは先程の方がまだましだったようにさえ思えます。
Directive1
そこで活躍するのが1つの値を抽出するようなDirective(aliasに則り以降Directive1)です。
こちらにはmapとflatMapが提供されています。つまりforで書けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
object Main extends App with SimpleRoutingApp { ... def extractRequest: Directive1[UserCreateRequest] = { for { user <- cookie("user") name <- formFields('name) age <- formFields('age.as[Int]) if name.nonEmpty && age > 10 } yield UserCreateRequest(user.content.toInt, name, age) } ... } |
随分スッキリしました。
forのガード条件はそのままですと、
withFilterがないよとwarningがでます。
formFieldsの後にfilterでつなげてしまうか、
withFilterを実装してしまうのが良いかと思います。
Spec
Custom Directiveの部分のテストは下記のように書いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
val creatorId = 1001 val name = "doi" val age = 30 var request: UserCreateRequest = null val cookie = Cookie(HttpCookie("user", creatorId.toString)) Post("/users", FormData(Seq("name" -> name, "age" -> age.toString))).withHeaders(cookie) ~> SprayRouting.extractRequest {req => request = req; complete("") } ~> check { handled must beTrue request.creatorId must_== creatorId request.name must_== name request.age must_== age } |
まさかのvar登場です。
こちらによりマーシャルというResponse用のデータ変換がされる前にデータを抽出しています。
(より良い方法があったらこっそり教えて下さい…)
まとめ
spray-routingのキーコンセプトの1つであるDirectiveについて、
Custom Directiveを使ってすっきりと書く方法を取り上げてみました。
リクエスト情報の展開にはunmarshaller等便利な機能が他にもありますが、
やや理解の難易度があがってしまう印象です。
そのため、Custom Directiveを適切に活用することで
既存の機能を活かしつつ、学習コストを抑えて実装を進めていけるのではないでしょうか。
Directiveについて本ブログを執筆の際に、
Binding Directives in Sprayという記事を紹介いただきました。
Directive1をscalazのMonadで定義しなおしたり、
Directive0をDirective1でリフティングしたりと目から鱗です。
興味のある方は参照してみてください。
それではみなさん、よいscalaライフを!
Copyright about logo
Copyright (c) 2002-2015 EPFL
Author