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
