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 を便利にする」というスタンスで機能拡張しています。
今回は 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 を使っています。ドキュメントはこちら。
このケースだと 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 つを提供しています。
// 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 の連携は以前から実装されていて、実績もあります。
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 操作はすぐに使えるようになります。
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 さんです。その次の日からまた空いてますけどね・・・
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 しかメンテされないんだし、こっちに移行しよう!」と思ったのですが・・・
あれ?
HttpTester の API 改善されてて最高だと思ったら、実行時間がアホみたいに長くなってて悲しみに暮れている。
— Kazuhiro Sera (@seratch) December 6, 2013
実行時間がめちゃくちゃ長くなってしまいました。tester.getResponses(..) のところが遅すぎで普通に 10 秒とかかかっちゃうんですよね。「さすがにそれはおかしいだろう、きっと使い方が悪いに違いない」と思って調べてみたんですが、私が試した限りではどうにもなりませんでした。上記のようなテストであれば 6.x だと一瞬で終わるのですが。
(追記)
という感じで、よくわからないなーと Twitter で日本語で愚痴っていたら Jetty の中の人から SOF で聞いてみたら?とリアクションが。
@joakime Thanks, embedded Jetty is pretty nice. I'm disappointed with ServletTester's slowdown in version 9.x though API is much improved...
— Kazuhiro Sera (@seratch) December 6, 2013
とりあえず stackoverflow にタグ付きで投稿してみました。
http://stackoverflow.com/questions/20428371/why-servlettester-in-jettty-9-x-is-so-slow
早速、こちらの方から回答がもらえました。要はそもそもが私のポカミスだったと・・
@joakime Oh, I got it. Thank you so much!
— Kazuhiro Sera (@seratch) December 6, 2013
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 ヘッダのことがすっかり頭から抜け落ちていました・・。そうか、一定時間で接続切られるまで待っていたのか・・
Jetty の件、解決しました。Jetty6 で動いてたからコードはそのままで動くんだろうと思ったけどそうではなかったと。ともあれ ServletTester はオワコンになってなくてよかったです。
— Kazuhiro Sera (@seratch) December 6, 2013
結論としては、実際は私の確認が甘かっただけで 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:
どうしても困ったら Skinny Framework の宣伝を書きにいきますので、お気軽にご依頼ください(ぇ
— Kazuhiro Sera (@seratch) December 1, 2013
いなかったら本当にやりますよ・・