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

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 で書くと - Manipulating Results, Session and Flash scopes, Content negotiation

Scala PlayFramework SkinnyFramework

これは Play framework 2.x Scala Advent Calendar 2013 の 13 日目です。

http://www.adventar.org/calendars/114

なお、この記事で触れる Play は 2.2.1、Skinny は 0.9.20 です。両者とも最新バージョンでは API や仕様が変更になっている場合があります。

Manipulating Results

http://www.playframework.com/documentation/2.2.x/ScalaResults

Changing the default Content-Type

val textResult = Ok("Hello World!")
val xmlResult = Ok(<message>Hello World!</message>)

これは Skinny の場合もコードは同じですが

def hello = Ok("Hello World!")
def hello = Ok(<message>Hello World!</message>)

Content-Type: text/html;charset=UTF-8 として応答します。text/plain や application/xml として応答したい場合は contentType を指定してください。

Play では as で続けて指定することで Content-Type を変えることができますが

val xmlResult = Ok(<message>Hello World!</message>)
val htmlResult2 = Ok(<h1>Hello World!</h1>).as(HTML)

Skinny(Scalatra)では、以下のように setter で指定します。

beforeAtction() { contentType = "text/plain" }

def hello = Ok("Hello World!")
def hello = {
  contentType = "application/xml"
  Ok(<message>Hello World!</message>)
}

Manipulating HTTP headers

val result = Ok("Hello World!").withHeaders(
  CACHE_CONTROL -> "max-age=3600",
  ETAG -> "xx")

上記の Play のコードと同じことは

def hello = {
  response.setHeader("Cache-Control", "max-age=3600")
  response.setHeader("ETag", "xx")
  "Hello World!"
}

のようにします。

Setting and discarding cookies

val result = Ok("Hello world").withCookies(
  Cookie("theme", "blue"))
val result2 = result.discardingCookies(DiscardingCookie("theme"))

Skinny は Scalatra の SweetCookies でスッキリ書けます。

http://www.scalatra.org/2.2/api/index.html#org.scalatra.SweetCookies

cookies += "theme" -> "blue"
cookies -= "theme"

Changing the charset for text based HTTP responses.

object Application extends Controller {
  implicit val myCustomCharset = Codec.javaSupported("iso-8859-1")
  def index = Action {
    Ok(<h1>Hello World!</h1>).as(HTML)
  }
}
def HTML(implicit codec: Codec) = {
  "text/html; charset=" + codec.charset
}

似たようなアプローチもできますが、あまり必要にも見えないので Skinny ではシンプルにやるのがよいのではないでしょうか。beforeAction でデフォルトを指定しておいて、それと異なる時だけ指定すればよいと思います。

object Application extends Controller {
  override def charset = "iso-8859-1"
  def setContentTypeAsHTML() = {
    contentType = s"text/html; charset=${charset}"
  }
  def index = {
    setContentTypeAsHTML()
    Ok(<h1>Hello World!</h1>)
  }
}

Session and Flash scopes

http://www.playframework.com/documentation/2.2.x/ScalaSessionFlash

How it is different in Play

Play はサーバ側にセッションを保持せず Cookie に値を保存しています。

Scalatra は Servlet のセッションを扱うので JSESSIONID にひもづいた HttpServletSession です。Servlet のセッションに状態を持つ場合は複数台の Servlet コンテナを運用する際は Sticky Session にすることになります。この辺は何か別のアプローチのサポートも考えたいのですが、まだ未着手です*1

Reading a Session value

def index = Action { implicit request =>
  session.get("connected").map { user =>
    Ok("Hello " + user)
  }.getOrElse {
    Unauthorized("Oops, you are not connected")
  }
}

Skinny では Scalatra の RichSession を使います。

http://www.scalatra.org/2.2/api/index.html#org.scalatra.servlet.RichSession

def index = {
  session.get("connected").map { user =>
    Ok(s"Hello ${user}")
  }.getOrElse {
    Unauthorized("Oops, you are not connected")
  }
}

これはコードの見た目はほとんど一緒ですね。

Storing data in the Session

Ok("Welcome!").withSession("connected" -> "user@gmail.com")

Skinny は Response に with でつなげる必要はありません。これは connected だけの session になる例なので、このようになります。

session.clear()
session += "connected" -> "user@gmail.com"
Ok("Welcome!")

次に session に値を追加する例ですが

Ok("Hello World!").withSession(session + ("saidHello" -> "yes"))

Skinny では clear がなくなるだけです。

session += "saidHello" -> "yes"
Ok("Hello World!")

最後に attribute を削除するのは

Ok("Theme reset!").withSession(session - "theme")

Skinny では、このようになります。

session -= "theme"
Ok("Theme reset!")

簡単ですね。

Discarding the whole session

Ok("Bye").withNewSession

セッションの中身を掃除するには clear() で attributes を破棄します。

session.clear
Ok("Bye")

Flash scope

def index = Action { implicit request =>
  Ok {
    flash.get("success").getOrElse("Welcome!")
  }
}

def save = Action {
  Redirect("/home").flashing(
    "success" -> "The item has been created")
}

Scalatra の flash は更新可能な Map なのでそのように操作するだけです。Rails でもおなじみの flash.now もあります。

def index = {
  flash("success") = "Welcome!"
  Ok()
}

def save = {
  flash += "success" -> "The item has been created"
}

Body parsers

アーキテクチャが異なるので Skinny(Scalatra)に Body Parser はありません。request から body を取得するには

val body: String = request.body
val is: InputStream = request.inputStream

あたりを操作する感じになります。

Action composition

アーキテクチャが異なるので Skinny(Scalatra)に 合成可能な Action という概念はありません。

Content negotiation

val list = Action { implicit request =>
  val items = Item.findAll
  render {
    case Accepts.Html() => Ok(views.html.list(items))
    case Accepts.Json() => Ok(Json.toJson(items))
  }

Format を implicit parameter で render に渡すとよしなに分岐してレンダリングしてくれます。

  def list()(implicit format: Format = Format.HTML) = withFormat(format) { // respondTo で対応していなかったら 406 応答
    set("items", Item.findAll())
    render(s"/items/list") // HTML, JSON, XML
  }

以上です。明日は @daneko0123 さんです。

*1:Scalatra 側でいいソリューションが出てきたら、自作しなくてすむので嬉しいですが