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 で書くと - The template engine

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

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

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

The template engine

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

Play2 は独自の Scala Template を持っており、最近では、この実装は Twirl という名前で単独のライブラリとして切り出されています。

https://github.com/spray/twirl

Twirl は Spray のプロジェクトの一部なのですが、最近 Spray も Typesafe のプロダクトになったので、この辺も将来的には統合されていくとかどうとか(あまり動向追ってない)。

Twirl は Play に依存していないので Scalatra など別のフレームワークとも組み合わせて利用することができます。みんな大好き GitBucket は Scalatra + Twirl ですね。

https://github.com/takezoe/gitbucket

一方、Skinny は Scalate を標準のテンプレートエンジンとしてサポートしています。

http://skinny-framework.org/documentation/view-templates.html

http://scalate.fusesource.org/

Scalate も基本的には事前に Scala コードに変換してコンパイルするアプローチですが、Skinny ではなるべくコンパイル待ち・リスタート待ちを減らしたフィードバックの早い開発を実現するために、デフォルトだと開発時はインタプリタ的に実行する設定になっています。

これのデメリットとして、全てのテンプレートに対して検証がされないだけでなく、初回アクセスのパフォーマンスも悪くなるので skinny package で本番向けにビルドする際は事前コンパイルするようになっています。

なお FreeMarker、Thymeleaf のサポートも拡張として存在しますが、最も Scala の表現力を活かせるのはやはり Scalate になります。Java 向けのテンプレートでは Scala メソッド呼び出しでかなり制限があるので、アプリケーションの性質によっては厳しくなりそうです。この辺は用途に合わせて判断されるのがよいかと思います。

それでは早速 Play のドキュメントのサンプルコードを見ていきます。

Overview

@(customer: Customer, orders: List[Order])

<h1>Welcome @customer.name!</h1>
<ul>
@for(order <- orders) {
  <li>@order.title</li>
}
</ul>

上記のような例は Scalate の SSP(Scala Server Pages) だとこのようになります。少し書き方が違うだけですね。

http://scalate.fusesource.org/documentation/ssp-reference.html

<%@ val customer: Customer %>
<%@ val orders: List[Order] %>

<h1>Welcome ${customer.name}!</h1>
<ul>
#for (order <- orders)
  <li>${order.title}</li>
#end
</ul>

Scalate は SSP 以外にも Scaml、Jade、Mustache をサポートしています。私は Jade がお気に入りなので、この記事では Jade のサンプルを多く紹介します。

Jade は Scaml をより簡潔に書けるようにしたものなので Scaml と Jade のリファレンスを併せて見るとほぼやりたいことが網羅されています。ここでは Jade を中心に説明しますが Scaml はタグを %li のように書く以外は Jade とほぼ同じになります。

http://scalate.fusesource.org/documentation/scaml-reference.html

http://scalate.fusesource.org/documentation/jade.html

-@val customer: Customer
-@val orders: List[Order]

h1
  Welcome #{customer.name}!
ul
  -for (order <- orders)
    li 
      =order.title

Play Scala Template(Twirl)だと、コンパイル済のコードを controller から参照しますが*1

val content = views.html.Application.index(c, o)

Skinny では render メソッドに文字列で指定します。

render("/Application/index")

Play では app/views/Application/index.scala.html というファイルが存在する前提ですが Skinny では src/main/webapp/WEB-INF/views/Application/index.html.ssp が存在することが前提となります。

Syntax: the magic ‘@’ character

Play Scala Template(Twirl)では変数や for/if 式の始まりが @ からになりますが Scalate の場合は SSP と Scaml/Jade、Mustache でそれぞれ異なります。この辺は Scalate のドキュメントをご覧ください。

http://scalate.fusesource.org/

Template parameters

Play では、テンプレートが受け取るパラメータをこのように指定しますが

@(customer: Customer, orders: List[Order])

@(title: String = "Home")

@(title: String)(body: Html)

それぞれ Jade だと

-@val customer: Customer
-@val orders: List[Order]

-@val title: String = "Home"

-@val title: String
-@val body: String

のようになります。

Iterating

<ul>
@for(p <- products) {
  <li>@p.name ($@p.price)</li>
}
</ul>

Jade だと以下のようになります。インデントでスッキリと書くことが出来ますね。

ul
  -for (p <- products)
    li  #{p.name} (#{p.price})

If-blocks

@if(items.isEmpty) {
  <h1>Nothing to display</h1>
} else {
  <h1>@items.size items!</h1>
}

Jade だと以下のようになります。こちらもインデントでスッキリと書くことが出来ますね。

- if (items.isEmpty)
  h1 Nothing to display
- else
  h1 #{items.size} items!

Declaring reusable blocks

@display(product: Product) = {
  @product.name ($@product.price)
}
<ul>
@for(product <- products) {
  @display(product)
}
</ul>

@title(text: String) = @{
  text.split(' ').map(_.capitalize).mkString(" ")
}
<h1>@title("hello world")</h1>

JSP に何でも書いちゃうノリで SSP でゴリゴリ実装すれば同じことは出来ますが、わざわざ紹介するようなやり方でもないですね...

<% def display(product: Product) = s"${product.name} (${product.price})" %>
%for (product <- products)
  <%= display(product) %>
%end

ユーティリティの class/object を用意して利用すれば特に問題はなさそうです。

Declaring reusable values

@defining(user.firstName + " " + user.lastName) { fullName =>
  <div>Hello @fullName</div>
}

私の認識が正しければ { .. } 内に限定して値を渡すことはできないですね。そこにこだわらなければ Jade だとこんな感じでやればよいと思います。

-@val fullName: String = user.firstName + " " + user.lastName
div Hello #{fullName}

Import statements

@import utils._

SSP だと

<% import utils._ %>

Jade だと

- import utils._

のようになります。

Play だと build.sbt にあらかじめ

templatesImport += "com.abc.backend._"

とデフォルトの import を設定できますが Skinny では以下のクラスに

src/main/scala/templates/ScalatePackage.scala

  /** Returns the Scala code to add to the top of the generated template method */
   def header(source: TemplateSource, bindings: List[Binding]) = """
import com.abc.backend._
import com.abc.util._
  """

のように指定します。

Comments

@*********************
* This is a comment *
*********************@

Scaml/Jade では

-#
  This is a comment
  Next line is also comment

のように -# のあと、インデントします。

Escaping

<p>
  @Html(article.content)
</p>

デフォルトでエスケープされる点は同じです。エスケープしたくない場合は

p
  != article.content

または

p
  = unescape(article.content)

と指定してください。

Scala templates common use cases

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

Layout

Play では views/main.scala.html で以下のようにしますが

@(title: String)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>@title</title>
  </head>
  <body>
    <section class="content">@content</section>
  </body>
</html>

Skinny は src/main/webapp/WEB-INF/layouts/default.jade で同じことは以下のように記述します。拡張子を変えて ssp/scaml/mustache で記述しても問題ありません。また、このテンプレートを読み込む先と利用するエンジンが異なっていても構いません(views/root/index.html.ssp でこの jade を使用するのも OK)。

-@val title: String
-@val content: String
!!! 5
html
  head
    title #{title}
  body
    section.content #{content}

詳細はこちらをご覧ください。

http://scalate.fusesource.org/documentation/user-guide.html

@(title: String)(sidebar: Html)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>@title</title>
  </head>
  <body>
    <section class="sidebar">@sidebar</section>
    <section class="content">@content</section>
  </body>
</html>

このテンプレートをこんな感じで使うと。

@sidebar = { <h1>Sidebar</h1> }
@main("Home")(sidebar) { <h1>Home page</h1> }

Skinny ではまずテンプレートがこうなり(Jade の場合)

-@val title: String
-@val sidebar: String
-@val content: String
!!! 5
html
  head
    title #{title}
  body
    section.sidebar #{sidebar}
    section.content #{content}

使う側はこのようになります。

-attributes("title") = "Home"
-attributes("sidebar") = capture {
  h1 Sidebar
-}
h1 Home page

Jade だと取っ付きづらく感じられる方もいるかもしれませんので SSP の例もあげるとこのようになります。

<% attributes("title") = "Home" %>
<% attributes("sidebar") = capture { %>
  <h1>Slidebar</h1>
<% } %>
<h1>Home Page</h1>

capture などについては User Guide をご覧ください。

http://scalate.fusesource.org/documentation/user-guide.html

Tags (they are just functions, right?)

views/tags/notice.scala.html のようなファイルに関数を定義しておけば taglib 的に使えるよ、という感じのものですが Scalate では普通に object つくってそれを import して使えばいいという感じのようです。

Includes

テンプレートの中で普通に render を呼べば他のテンプレートを include できます。例えば Jade のファイルと同じ階層に hello.ssp がある場合、以下のように指定するだけです。

= render("hello.ssp")

moreScripts and moreStyles equivalents

Play では @routes.Assets.at のように呼び出すと assets のパスを解決してくれますが Scalate では ContextPath を考慮した形でパスを設定してくれる uri/url というメソッドが使えます。以下は Jade の例です。

script(type="text/javascript" src={uri("/assets/js/jquery-2.0.3.min.js")})
link(rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.1/css/bootstrap.min.css")

以下と同じことは capture でできますね。

@scripts = {
    <script type="text/javascript">alert("hello !");</script>
}

明日・・・

ぶっちゃけ明日の記事は書いてないですし、ちょっとそろそろアレな感じです。明日、誰もいなかったら途切れてしまうかも・・

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

誰かの参加をお待ちしていますね・・って、私は一体何なのだろう。

*1:逆に言えば view がまだなかったら controller のコンパイルが通らない

Play ドキュメントを Skinny で書くと - Manipulating Results, Session and Flash scopes, Content negotiation

これは 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 側でいいソリューションが出てきたら、自作しなくてすむので嬉しいですが

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

Play ドキュメントを Skinny で書くと - Actions, Controllers and Results

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

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

ご存知の方もいらっしゃるかと思いますが、私は Skinny Framework というのをつくっています。これは Servlet ベースのフルスタックな Web アプリ開発フレームワークで Web アプリ部分については「Scalatra を便利にする」というスタンスで機能拡張しています。

http://skinny-framework.org/

今回は Play Framework のドキュメントの内容を Skinny の場合だとどう書くかを説明しながら両者の比較をしてみたいと思います。なお Skinny Framework のバージョンは 0.9.20 です。Skinny はまだ 1.0 がリリースされていないフレームワークなので(1.0 は 2014/3 までにリリース予定)、ここでサポートしていない機能が今後追加されたり、また場合によっては API が変更になる場合があります。

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

なお、明日も明後日もこのアドベントカレンダーはずっと空いているので、もしも誰も入ってこないとなると、毎日こんな感じになりますからね。覚悟してください。

Actions, Controllers and Results

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

What is an Action?

val echo = Action { request =>
  Ok("Got request [" + request + "]")
}

これを Skinny で書くとこのようになります。Action がないだけですね。

def echo = Ok("Got request [" + request + "]")

これは Scalatra の ActionResult を使っています。ドキュメントはこちら。

https://github.com/scalatra/scalatra/blob/2.2.x_2.10/core/src/main/scala/org/scalatra/ActionResult.scala

このケースだと ActionResult を省略して

def echo = "Got request [" + request + "]"

とだけ書いても OK です。

Building an Action

Platy の Action とは違って Scalatra の ActionResult は

case class ActionResult(
  status: ResponseStatus, 
  body: Any, 
  headers: Map[String, String])

という構造なので特に難しいことはないと思います。これを生成する factory として Ok とか NotFound とかがあるだけです。

Controllers are action generators

Play では Controller のコードはこんな感じになりますが

package controllers
import play.api.mvc._
object Application extends Controller {
  def index = Action {
    Ok("It works!")
  }
}

同じ内容が Skinny ではこうなります。単に文字列を返すと 200 OK で指定された文字列を body として応答します。Skinny では object ではなく class になっていますが、これはルーティングのところで説明することになります。

package controller
import skinny._
class Application extends SkinnyController {
  def index = "It works!"
}

Play ではこのようにパラメータをメソッド引数として取得できますが

def hello(name: String) = Action {
  Ok("Hello " + name)
}

0.9.20 時点で Skinny では同じことはできません。params から取得します。

def hello = "Hello " + params.getAs[String]("name").getOrElse("Anonymous")

Simple results

def index = Action {
  SimpleResult(
    header = ResponseHeader(200, Map(CONTENT_TYPE -> "text/plain")),
    body = Enumerator("Hello world!".getBytes())
  )
}

これは Skinny では

def index = {
  status = 200
  contentType = "text/plain"
  "Hello world!"
}

となります。

val ok = Ok("Hello world!")
val notFound = NotFound
val pageNotFound = NotFound(<h1>Page not found</h1>)
val badRequest = BadRequest(views.html.form(formWithErrors))
val oops = InternalServerError("Oops")
val anyStatus = Status(488)("Strange response type")

はそれぞれ、ほぼ同じように書くなら以下のようになります。

// val ok = Ok("Hello world!")
val ok = Ok("Hello world!")

// val notFound = NotFound
val notFound = NotFound()

// val pageNotFound = NotFound(<h1>Page not found</h1>)
val pageNotFound = NotFound(<h1>Page not found</h1>)

// val badRequest = BadRequest(views.html.form(formWithErrors))
set("formWithErrors" -> formWithErrors)
status = 400
render("/form")

// val oops = InternalServerError("Oops")
val oops = InternalServerError("Oops")

// val anyStatus = Status(488)("Strange response type")
status = 488
"Strange response type"

Redirects are simple results too

ダイレクトを意味する API のデフォルトが Play では 303 ですが Scalatra では 302 という違いがあります*1

// 303 redirect
def index = Action {
  Redirect("/user/home")
}
// 301 redirect
def index = Action {
  Redirect("/user/home", MOVED_PERMANENTLY)
}

これを Skinny でやると以下のようになります。ScalatraBase にある redirect メソッドは 302 でリダイレクトします。Scalatra は redirect と ActionResult を提供していて Skinny が redirect301 のようなメソッド 3 つを提供しています。

https://github.com/skinny-framework/skinny-framework/blob/develop/framework/src/main/scala/skinny/controller/feature/ExplicitRedirectFeature.scala

// 301 redirect
def index = redirect301("/user/home")
def index = MovedPermanently("/user/home")
// 302 redirect
def index = redirect("/user/home")
def index = redirect302("/user/home")
def index = Found("/user/home")
// 303 redirect
def index = redirect303("/user/home")
def index = SeeOther("/user/home")

“TODO” dummy page

def index(name:String) = TODO

これは存在しないですが

def index = ???

とでもしておけばいいのではないでしょうか。

明日は?

以上、「Actions, Controllers and Results」のページでした。Scalatra をご存知の方はお分かりかと思いますが、半分以上は Scalatra の機能です。Skinny は Scalatra をより便利にするというスタンスなのでこのような形になります。

明日も担当者が現れなかったら・・・続きをやります。

Advent Calendar で公開しなかった場合も続きは普通の記事として公開しますので、割り込みをお待ちしております。

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

*1:指定がないときは 302 が妥当な気がしますが

3 分でできる Play2 で Skinny ORM を使う手順 #play_ja

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

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

ご存知の方もいるかと思いますが、私は Skinny Framework というフレームワークをつくっています。これのコンポーネントは基本的に Skinny Framework 以外でも使えるようにつくられていて、その一つである Skinny ORM がある程度使える ORM として育ってきました。まだドキュメントはそれほど充実していませんが、こちらをご覧ください。

http://skinny-framework.org/documentation/orm.html

この Skinny ORM は ScalikeJDBC という DB ライブラリをより ORM 的に使えるようにするために、ScalikeJDBC を土台につくられています。ScalikeJDBC と Play2 の連携は以前から実装されていて、実績もあります。

http://scalikejdbc.org/

Skinny ORM を Play ユーザの皆さんにもぜひ使っていただきたいので、今回は導入までの手順を紹介します。

Play アプリをつくる

この時点では Play 2.2.1 が最新です。私のように最近 play コマンド使ってないなーという方は brew upgrade play しておきましょう。

play new play-with-skinny-orm
cd play-with-skinny-orm

build.sbt を書き換える

build.sbt をこのように書き換えてください。

name := "play-with-skinny-orm"

version := "1.0-SNAPSHOT"

libraryDependencies ++= Seq(
  "org.skinny-framework" %% "skinny-orm"              % "0.9.29",
  "org.scalikejdbc"      %% "scalikejdbc-play-plugin" % "1.7.1",
  "com.h2database"       %  "h2"                      % "1.3.174"
)

play.Project.playScalaSettings

conf/play.plugins

Play に ScalikeJDBC ベースのコネクションマネージメントを伝えるために、ScalikeJDBC の Play プラグインを追加します。

10000:scalikejdbc.PlayPlugin

conf/application.conf

Play の DB 設定を更新します。今回の説明の都合上、H2 をファイルベースの DB に変えてください。

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:file:play"
db.default.user=sa
db.default.password=""

conf/db/migration/V1__Create_companies.sql

DB マイグレーション用ファイルをつくってください。V1__ のアンダースコアは二つなので気をつけてください。Flyway のファイルです。今回は手動でマイグレーションを実行します。

create table company (
  id bigserial not null primary key,
  name varchar(64) not null,
  url varchar(128),
  created_at timestamp not null,
  updated_at timestamp,
  deleted_at timestamp
);

app/models/Company.scala

Skinny ORM では SkinnyCRUDMapper という trait を継承すると基本的な CRUD 操作はすぐに使えるようになります。

https://github.com/skinny-framework/skinny-framework/blob/develop/orm/src/main/scala/skinny/orm/feature/CRUDFeature.scala

package models

import scalikejdbc._, SQLInterpolation._
import skinny.orm._, feature._
import org.joda.time.DateTime

case class Company(
  id: Long,
  name: String,
  url: Option[String] = None,
  createdAt: DateTime,
  updatedAt: Option[DateTime] = None,
  deletedAt: Option[DateTime] = None)

object Company extends SkinnyCRUDMapper[Company]
    with TimestampsFeature[Company]
    with SoftDeleteWithTimestampFeature[Company] {

  override val defaultAlias = createAlias("c")

  override def extract(rs: WrappedResultSet, c: ResultName[Company]): Company = new Company(
    id = rs.long(c.id),
    name = rs.string(c.name),
    url = rs.stringOpt(c.url),
    createdAt = rs.dateTime(c.createdAt),
    updatedAt = rs.dateTimeOpt(c.updatedAt)
  )
}

app/controllers/Application.scala

説明を簡略化するためにただ toString しています。

package controllers

import play.api._
import play.api.mvc._
import models.Company

object Application extends Controller {

  def index = Action {
    Ok(Company.findAll().toString)
  }
}

手動で DB マイグレーション

今回はさらっと試すだけなので play console でマイグレートしてしまいましょう。

skinny.DBSettings.initialize()
skinny.dbmigration.DBMigration.migrate()

例外がでなければ成功です。こんな感じで model が使えるようになりました。console から試してみてください。

skinny.DBSettings.initialize()
import models._
Company.count()
Company.createWithAttributes('name -> "Typesafe")
Company.findAll()

console で実行した結果を貼付けておきますね。

$ sbt console

[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.10.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_25).
Type in expressions to have them evaluated.
Type :help for more information.

scala> skinny.DBSettings.initialize()

scala> import models._
import models._

scala> Company.count()
res1: Long = 0

scala> Company.createWithAttributes('name -> "Typesafe")
res2: Long = 1

scala> Company.findAll()
res3: List[models.Company] = List(Company(1,Typesafe,None,2013-12-08T20:03:36.771+09:00,None,None))

play run

最後に play run で Play アプリにちゃんと組み込まれたか確認します。

play run

ブラウザから http://localhost:9000/ にアクセスして正常に表示されれば OK です。

最終的にはこのようなファイル構成となりました。

.
├── README
├── app
│   ├── controllers
│   │   └── Application.scala
│   ├── models
│   │   └── Company.scala
│   └── views
│       ├── index.scala.html
│       └── main.scala.html
├── build.sbt
├── conf
│   ├── application.conf
│   ├── db
│   │   └── migration
│   │       └── V1__Create_companies.sql
│   ├── play.plugins
│   └── routes
├── logs
│   └── application.log
├── play.h2.db
├── project
│   ├── build.properties
│   └── plugins.sbt
├── public
│   ├── images
│   │   └── favicon.png
│   ├── javascripts
│   │   └── jquery-1.9.0.min.js
│   └── stylesheets
│       └── main.css
└── test
    ├── ApplicationSpec.scala
    └── IntegrationSpec.scala

14 directories, 19 files

明日は unokazuhiko さんです。その次の日からまた空いてますけどね・・・

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

Jetty の ServletTester の挙動がおかしいと思ったら自分がおかしかったでござる

Jetty に ServletTester というのがあってですね、生 Servlet のテストをしたいときなんかに結構重宝してたりしたんですよ。

<dependency>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>jetty-servlet-tester</artifactId>
    <version>6.1.26</version>
    <scope>test</scope>
</dependency>

こんな感じにテストが書けます。

package example;

import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.mortbay.jetty.testing.HttpTester;
import org.mortbay.jetty.testing.ServletTester;

public class SampleServletTest {

    @Test
    public void testDoGet() throws Exception {
        ServletTester tester = new ServletTester();
        tester.addServlet(SampleServlet.class, "/index");
        tester.start();

        HttpTester request = new HttpTester();
        request.setMethod("GET");
        request.setHeader("Host", "tester"); // should be "tester"
        request.setURI("/index");
        request.setVersion("HTTP/1.1");
        request.setContent("");

        String responses = tester.getResponses(request.generate());
        HttpTester response = new HttpTester();
        response.parse(responses);

        assertThat(response.getStatus(), is(equalTo(200)));
    }

}

ただ、この API、パッと見ておわかりいただけると思うんですが、すごく・・変ですよね・・・。なんというか無理矢理感がすごいというか。

ただ、現実問題として生 Servlet と戦うときにこのテストツールは強力な武器になってくれるので、そこはまあ目をつぶるわけですよ。

org.eclipse になって

で、Jetty が 8,9 で org.eclipse に移管されて、パッケージも org.mortbay.jetty から org.eclipse.jetty になった段階で書き換えられたようです。今回は Servlet 3.1 ではなく Servlet 3.0 をやっていたので 9.0.x を示しています。

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-servlet</artifactId>
    <version>9.0.7.v20131107</version>
    <scope>test</scope>
</dependency>

で、こんな感じに書けるようになったと。

package example;

import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.servlet.ServletTester;

public class SampleServletTest {

    @Test
    public void testDoGet() throws Exception {
        ServletTester tester = new ServletTester();
        tester.addServlet(SampleServlet.class, "/index");
        tester.start();

        HttpTester.Request request = HttpTester.newRequest();
        request.setMethod("GET");
        request.setHeader("Host", "tester"); // should be "tester"
        request.setURI("/index");
        request.setVersion("HTTP/1.1");
        request.setContent("");

        HttpTester.Response response = HttpTester.parseResponse(tester.getResponses(request.generate()));

        assertThat(response.getStatus(), is(equalTo(200)));
    }

}

まあ、これでも冗長さはあるわけですけど Request/Response の無理矢理感はなくなって、だいぶマシになりましたね。パッケージやメソッド名なんかも妥当な感じに改善されているんです。「これはいいな、今後は org.eclipse しかメンテされないんだし、こっちに移行しよう!」と思ったのですが・・・

あれ?

実行時間がめちゃくちゃ長くなってしまいました。tester.getResponses(..) のところが遅すぎで普通に 10 秒とかかかっちゃうんですよね。「さすがにそれはおかしいだろう、きっと使い方が悪いに違いない」と思って調べてみたんですが、私が試した限りではどうにもなりませんでした。上記のようなテストであれば 6.x だと一瞬で終わるのですが。

(追記)

という感じで、よくわからないなーと Twitter で日本語で愚痴っていたら Jetty の中の人から SOF で聞いてみたら?とリアクションが。

とりあえず stackoverflow にタグ付きで投稿してみました。

http://stackoverflow.com/questions/20428371/why-servlettester-in-jettty-9-x-is-so-slow

早速、こちらの方から回答がもらえました。要はそもそもが私のポカミスだったと・・

Jetty 9 のサーバは HTTP/1.1 なんだけど

Jetty 9's HttpTester also defaults to HTTP/1.0.

とのことで(HttpTester はリクエストをつくってるやつですね)私のサンプルは HTTP/1.1 指定してるんだから

request.setHeader("Connection", "close");

つけるか HTTP/1.0 でリクエストしないとダメということでした。Connection ヘッダのことがすっかり頭から抜け落ちていました・・。そうか、一定時間で接続切られるまで待っていたのか・・

結論としては、実際は私の確認が甘かっただけで ServletTester はオワコンにはなってなかったです。まずは自分のコードを疑えってことですね。

Typesafe Activator について #scalajp #play_ja

Advent Calendar!

この記事は Play framework 2.x Scala Advent Calendar 2013 の 4 日目です。

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

Activator については前に書いたことがある

7 月に Typesafe Activator のテンプレートをつくってみたというブログ記事を書きました。使い方はこの記事の内容で十分かと思います。

http://seratch.hatenablog.jp/entry/2013/07/02/005454

それから半年くらい経ち、色々と変化がありましたね。

Typesafe Activator が OSS になった

8 月に Typesafe Activator の実装が公開されました。

https://www.typesafe.com/blog/typesafe-activator-is-now-open-source

https://github.com/typesafehub/activator

これにより issue を登録してフィードバックしたり、実装を修正して pull request を送ったりといったことが可能になりました。

Typesafe Activator 1.0 リリース

9 月に vesrion 1.0 がリリースされました。この時点で 29 個のテンプレートが登録されていたようです。

http://typesafe.com/blog/announcing-activator-10-create-reactive-apps-in-minutes

ちなみに今日時点での最新バージョンは 1.0.8、登録テンプレートは 41 個ありました。

Typesafe Activator をつくりたい人へ

7 月のブログ記事には「どうやって Typesafe Activator のテンプレートつくるか」が書かかれていませんでした。というのも、こちらのページにある内容だけなので

http://typesafe.com/activator/template/contribute

あえて日本語で説明しなくても、という感じだったのですが、簡単に触れておきます。特に難しいことはないのですが、いくつか注意点を。

activator.properties の name を適当につけない

activator.properties というファイルにメタデータを指定します(なぜ Typesafe Config じゃないの・・とちょっと思った)。前にも書きましたが、この name の文字列がそのまま unique key にされてしまうので、適当につけるのはやめましょう。私はあとから hello-scalikejdbc にしたくなったけど、面倒で scalikejdbc-activator-template のままになっています。。

ライセンス表記が必要

何らかの OSS ライセンスを明記する必要があります。プロジェクトのルートディレクトリに LICENSE または LICENSE.md というファイル名で配置してください。

https://github.com/scalikejdbc/hello-scalikejdbc/pull/1

現時点でリポジトリの移動に対応していない

Typesafe Activator のテンプレートは上記の contribute ページから GitHub リポジトリの URL を指定して追加/更新するのですが、12/4 時点でリポジトリの owner が変わった場合に対応できていません。例えば、私が作っている

https://github.com/scalikejdbc/hello-scalikejdbc

は、以前は https://github.com/seratch/hello-scalikejdbc だったのですが owner が seratch として登録されているので scalikejdbc が owner の URL で更新しようとすると permission がねえぞゴラッみたいなエラーになります。回避策としては古い URL でリクエストすると受け付けてくれるようです。

恒久的には手動対応しかないんですかねぇということで、他に問い合わせ先がなかったので issue にしてみました。(追記)対応してくれました。

https://github.com/typesafehub/activator/issues/180

まとめ

感覚として Typesafe Activator のテンプレートを作ったことによってユーザが増えたとかそういう印象はあまりないのですが zip をダウンロードして初回起動までは特に知識がなくてもできるので Scala 初心者の方に体験してもらうにはよいツールではないでしょうか。ぜひ試してみてください。

明日が空いていますけど・・

誰かいませんか?

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

FYI:

いなかったら本当にやりますよ・・