Play ドキュメントを Skinny で書くと - HTTP routing
これは Play framework 2.x Scala Advent Calendar 2013 の 11 日目です。
http://www.adventar.org/calendars/114
毎日こんな感じになりますけど、本当にそれでいいんでしょうか?
なお、この記事で触れる Play は 2.2.1、Skinny は 0.9.20 です。両者とも最新バージョンでは API や仕様が変更になっている場合があります。
HTTP routing
http://www.playframework.com/documentation/2.2.x/ScalaRouting
The built-in HTTP router
Play は conf/routes に以下のように記述しますが
GET /clients/:id controllers.Clients.show(id: Long)
Skinny では、以下のようにします。前のエントリで Controller を object にせずに class にしていましたが Skinny では routing とひもづけるまでは object にはしません(あくまで推奨するスタイル)。
// src/main/scala/controller/Controllers.scala object Controllers { object clients extends new Clients with Routes { val showUrl = get("/clients/:id")(show).as('show) } } // src/main/scala/ScalatraBootstrap.scala class ScalatraBootstrap exntends SkinnyLifeCycle { override def initSkinnyApp(ctx: ServletContext) { Controllers.clients.mount(ctx) } }
Controllers.scala をつくるのは任意で、別の名前のものや違ったやり方で管理しても構いません。テスタビリティを考慮しながら自由にルールを決めてください。Path パラメータの id は params.getAs[String]("id") のようにして show メソッドの中で取得します。
「showUrl って何?」と思われた方もいるかと思いますが、これは Scalatra の Reverse Routes です。あとで説明します。
http://scalatra.org/2.2/guides/http/reverse-routes.html
The routes file syntax
Skinny は Scala コードで記述するので省略。
The HTTP method
Skinny は Scalatra をそのまま使用します。
https://github.com/scalatra/scalatra/blob/2.2.x_2.10/core/src/main/scala/org/scalatra/CoreDsl.scala
The URI pattern
Static path
GET /clients/all controllers.Clients.list()
Scala のコードで記述します。
object Controllers { object clients extends new Clients with Routes { val listUrl = get("/clients/all")(list).as('list) } }
Dynamic parts
GET /clients/:id controllers.Clients.show(id: Long)
Scala のコードで記述します。Path パラメータはメソッド引数ではなく params 経由で取得します。
object Controllers { object clients extends new Clients with Routes { val showUrl = get("/clients/:id")(show).as('show) } }
Dynamic parts spanning several /
Play のこのコードは "name" というパラメータに /files/ 配下のパスをバインドするのですが(例: "images/logo.png")
GET /files/*name controllers.Application.download(name)
Skinny の場合・・というか Scalatra の仕様ですが、以下のようにすると "splat" という名前(固定)で取得できます。複数ある場合は multiParams で受け取ると Seq で全て取得できます。
def download = { val name = params.getAs[String]("splat") ... } get("/files/*")(download)
Dynamic parts with custom regular expressions
Play の以下のサンプルは「[0-9]+」という正規表現にマッチしたらそのキャプチャを id として引き渡しますが
GET /items/$id<[0-9]+> controllers.Items.show(id: Long)
Skinny(Scalatra)では "captures"(固定)というパラメータで取得できます。複数ある場合は multiParams で受け取ると Seq で全て取得できます。
def show = { val id = params.getAs[Long]("captures") Item.findById(id).map { ... } } get("/items/([0-9]+)".r)(show)
Call to the Action generator method
Play の Path パラメータとクエリストリングの場合の conf/routes 例で
# Extract the page parameter from the path. GET /:page controllers.Application.show(page) # Extract the page parameter from the query string. GET / controllers.Application.show(page)
処理メソッドはともにこうなりますが
def show(page: String) = Action { loadContentFromDatabase(page).map { htmlContent => Ok(htmlContent).as("text/html") }.getOrElse(NotFound) }
Skinny の場合はこのようになります。
object Controllers { object application extends new Application with Routes { val showPathParamUrl = get("/:page")(show).as('show1) val showQueryParamUrl = get("/")(show).as('show2) } } def show = params.getAs[Int]("page").map { page => loadContentFromDatabase(page).getOrElse haltWithBody(404) }.getOrElse haltWithBody(404)
Parameter types
以下のような型にバインドする場合に Play は型にマッチしなかった場合、Bad Request で応答しますが
GET /clients/:id controllers.Clients.show(id: Long)
Skinny ではそもそも params から取得するので、利用者の実装によって挙動は制御されます。
def show = params.getAs[Long]("id").map { id => // 正常系 }.getOrElse haltWithBody(404) // 400 でも 404 でも自由
Parameters with fixed values
# Extract the page parameter from the path, or fix the value for / GET / controllers.Application.show(page = "home") GET /:page controllers.Application.show(page)
Skinny では Scalatra の params の API を使います。params.getAs は Option 型を返すので getOrElse などで代用可能です。
Parameters with default values
# Pagination links, like /clients?page=3 GET /clients controllers.Clients.list(page: Int ?= 1)
こちらも Skinny では Scalatra の params の API を使って制御します。
Optional parameters
# The version parameter is optional. E.g. /api/list-all?version=3.0 GET /api/list-all controllers.Api.list(version: Option[String])
こちらも(ry
Routing priority
Play では先に定義されたルーティングルールが優先されますが Skinny(Scalatra)では逆に後から定義されたものが優先されます。
Reverse routing
Play で以下のような hello という処理メソッドがあり conf/routes で何らかのルーティング情報とひもづいている場合
object Application extends Controller { def hello(name: String) = Action { Ok("Hello " + name + "!") } }
このように reverse route を解決できます。
// Redirect to /hello/Bob def helloBob = Action { Redirect(routes.Application.hello("Bob")) }
Skinny(Scalatra)では、このようになります。Controller だけでなく Scalate の view template でも同様に呼び出すことが出来ます。
class Application extends Controller { def hello(name: String) = Action { Ok("Hello " + name + "!") } } object Controllers { object app extends Application with Routes { val helloUrl = get("/hello/:name")(hello).as('hello) } } def helloBob = redirect(url(Controllers.app.helloUrl, "name" -> "Bob"))