読者です 読者をやめる 読者になる 読者になる

seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

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"))