Play で Scalate を使う
play-scalate っていう名前のプラグインはいろんな人が書き散らかしてて github 検索するとボロボロ出てくるんですが、ついうっかり仲間に加わってしまいました。
https://github.com/tototoshi/play-scalate
build.sbt に設定すれば
libraryDependencies ++= Seq( "com.github.tototoshi" %% "play-scalate" % "0.1.0-SNAPSHOT", "org.scalatra.scalate" %% "scalate-core" % "1.7.0", "org.scala-lang" % "scala-compiler" % scalaVersion.value ) unmanagedResourceDirectories in Compile += baseDirectory.value / "app" / "views"
こんな感じで使えます。
package controllers import play.api._ import play.api.mvc._ import com.github.tototoshi.play2.scalate._ object Application extends Controller { def index = Action { implicit request => Ok(Scalate.render("index.jade", Map("message" -> "hello"))) } }
ライブラリという形にして、SNAPSHOT を publish はしたけれど、そんな大したものではないのでいじりたければプロジェクト内にコピペして好きにいじってもらえばいいかなと思います。
Play で Scalate を使うときに気をつけることは、Dev モードで普通に動いてるのに Prod モードだと動かん!ってことにならないように、テンプレートファイルをファイルとしてではなくリソースとして読み込めってこどでしょうか。つまり、Play.api.current.getFile
じゃなくて Play.api.current.resourceAsStream
とか使おうってことです。Scalate はファイルを読み込むのを想定しているので、リソースを読み込むようにするのに一手間必要です。
Play ではリソースファイルは conf/
に置くことになってる(ちょっと気持ちわるい)ために
app/views
のファイルはリソースとして扱われないので、デプロイしたときにリソースが見つからん!というエラーも起きます。これを防ぐためにはテンプレートファイルは、app/views
ではなく conf/views
に置くか、conf
以下から app/views
にシンボリックリンクを...っていやまさか、
build.sbt で
unmanagedResourceDirectories in Compile += baseDirectory.value / "app" / "views"
とすれば良いです。
RFC 的には text/* のデフォルトエンコーディングは ISO-8859-1
以下のような、日本語のデータを text/csv という Content-Type で返す Play アプリケーションで、
GET /test controllers.Application.test
package controllers import play.api._ import play.api.mvc._ import play.api.libs.iteratee.Enumerator import scala.concurrent.ExecutionContext.Implicits.global object Application extends Controller { def test = Action { implicit request => { Result( header = ResponseHeader(200, Map(CONTENT_TYPE -> "text/csv", CONTENT_DISPOSITION -> ("attachment; filename=test.csv"))), body = Enumerator.outputStream { out => try { // CSVデータを出力 out.write("テスト".getBytes("UTF-8")) } finally { out.close() } } ) } } }
次のようなテストを書くとエラーになるんだけどなんで?と聞かれました。
import org.scalatest._ import play.api.test._ import play.api.test.Helpers._ import play.api.libs.ws.WS import play.api.Play.current class ApplicationSpec extends FlatSpec with Matchers { it should "work" in { running(TestServer(3333, FakeApplication())) { val testUrl = "http://localhost:3333/test" val body = await(WS.url(testUrl).get).body body should be("テスト") } } }
[info] Compiling 1 Scala source to /Users/toshi/work/scala/play-hello/target/scala-2.11/test-classes... [info] ApplicationSpec: [info] - should works *** FAILED *** [info] "[ãã¹ã]" was not equal to "[テスト]" (ApplicationSpec.scala:13) [info] ScalaTest [info] Run completed in 3 seconds, 921 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0 [info] *** 1 TEST FAILED *** [error] Failed: Total 1, Failed 1, Errors 0, Passed 0 [error] Failed tests: [error] ApplicationSpec [error] (test:test) sbt.TestsFailedException: Tests unsuccessful [error] Total time: 7 s, completed 2014/10/18 23:44:56
見ての通り、エンコードがおかしくなってアサーションが通りません。
これ不思議だなあと思って調べたんですが、Play の WS(クライアント側) のソースコードの該当部分を見てみるとコメントがついていました。
lazy val body: String = { // RFC-2616#3.7.1 states that any text/* mime type should default to ISO-8859-1 charset if not // explicitly set, while Plays default encoding is UTF-8. So, use UTF-8 if charset is not explicitly // set and content type is not text/*, otherwise default to ISO-8859-1 val contentType = Option(ahcResponse.getContentType).getOrElse("application/octet-stream") val charset = Option(AsyncHttpProviderUtils.parseCharset(contentType)).getOrElse { if (contentType.startsWith("text/")) AsyncHttpProviderUtils.DEFAULT_CHARSET else "utf-8" } ahcResponse.getResponseBody(charset) }
Play のデフォルトのエンコーディングは UTF-8 なのですが、 RFC-2616 的には Content-Type が text/* のデフォルトエンコーディングは ISO-8859-1 なので Content-Type が text/* なデータはあえて ISO-8859-1 にしてあります。
つまりサーバーの実装のほうが、
CONTENT_TYPE -> "text/csv; charset=utf-8;"
とちゃんと UTF-8 と書けって話でした。
なるほどなとは思いましたが、サーバー側のほうは Play のデフォルトである UTF-8 で返すのにクライアントだけ ISO-8859-1 で受け取るってのはハマるよなあと思いました。ただサーバーのほうでもエンコーディングが指定されていなければ ISO-8859-1 に変えるとするのは今の Play の実装では無理があるし、そんなに望まれている動作でもないかもなとも思います。微妙ですね。
なので、とりあえず RFC-2616 的には Content-Type が text/* のデフォルトエンコーディングは ISO-8859-1 という事実だけ覚えておくことにしました。
The "charset" parameter is used with some media types to define the character set (section 3.4) of the data. When no explicit charset parameter is provided by the sender, media subtypes of the "text" type are defined to have a default charset value of "ISO-8859-1" when received via HTTP. Data in character sets other than "ISO-8859-1" or its subsets MUST be labeled with an appropriate charset value. See section 3.4.1 for compatibility problems.
テンプレートエンジン Night で Scala のテンプレートエンジンの紹介をしました
テンプレートエンジン Night という不穏なイベントで Twirl を中心に Scala のテンプレートエンジンの紹介をしました。 Scala を知らない or そんなにやってない人向けの発表です。 スライドにはあまり内容詰め込んでなくて、キーワードだけ見てもらえればいいかなという感じです。 広義の?テンプレートエンジンとして String Interpolation とか XML リテラルも紹介しました。 ほかに紹介したかったけどやめたものとして quasiquote とかがあります。
テンプレートエンジン Night はいいかんじに荒れてて面白いイベントでした。
いくつか思ったこと。
Haml とかが dis られ気味だった件
Jade とか Haml みたいなインデントがシンタックスに入っているテンプレートが「コピペすると壊れる」として dis られていて、せやなって感じではあるけれど、見通しが良いのはやっぱり良いこと。 コピペができても見通しの悪いシンタックスだと、実はすでにタグの対応がとれてないけれど見通しが悪すぎてどこがどうぶっ壊れてるのかわからない、とりあえず壊れてるっぽいことだけはわかる、みたいなことがあって、そのほうがずっとつらい気がする。
mixer2 とか Lift とか
思想としては好きだし、デザイナーと共同作業するには良さそうなんだけど、 view と密結合すぎるところが辛い。懇親会で話に出たんだけど wicket とかもそうですね。
静的解析、コンパイルによるチェック
あまり先入観を与えたくないなと思って Twirl について意図的にあまりデメリットを話さなかったけれど、 思ったよりコンパイルするというアプローチが好意的に受け止められていました。
ただしコンパイル時間はたしかに伸びるし、開発時にいちいち全部コンパイルされても...ということは実際あります。 開発時にいちいちコンパイルするよりは、jsp だとか、Scalate とか、デプロイするときに、プリコンパイルをするっていうほうが現実には合ってるんじゃないかと思いました。
デザイナーとの共同作業問題
あまり触れられてなかったけれど、シンタックスどうこう以前に、コンパイルする言語だとデザイナー向けに環境を用意するのがめんどう、とかそういう問題があります。
PHP
メール送信とかできて便利です。
Xslate
すごい
Smarty
やばい
次は バリデーション Night、 フォーリンキー Night らしいです。荒れるな。
Scala 2.11.3 が生まれる前に死んでしまった話
↓に関して、https://gitter.im/scalajp/public で盛り上がってた内容をまとめました。
なぜこんなことになってしまうのだろう(未調査) https://github.com/skinny-framework/skinny-framework/issues/193
これ Skinny 側で何か work around できないですかね https://github.com/skinny-framework/skinny-framework/issues/193#issuecomment-58773130
2.11.3 を指定して build した Scalatra を scalaVersion := "2.11.0" なアプリから利用するのは問題なさそうだった。となると Scalatra に 2.11.3 でビルドした 2.3.1 を出してもらうようお願いするのが手っ取り早いのかな。こういう問題への対処、それでいいのかという気はするけど、ユーザが困ってしまうので。
Scalatra に issue 立てるのはまだ早いのでまずは scala-lang の JIRA へ...
2.11.3 の scala.collection.TraversableLike
の呼び出すメソッドが変わったってこと,,?
bincompat-backward.whitelist.conf に
+ // see github.com/scala/scala/pull/3925, SI-8627, SI-6440 + { + matchName="scala.collection.TraversableLike.filterImpl" + problemName=MissingMethodProblem + },
というのが増えてるな
これは影響範囲が大きすぎるような。
これが該当の変更かな? https://github.com/scala/scala/pull/3949 なぜ例外が起きるのかわかってないけど。
privateからprivate[scala]にすると、どちらにしろソース上はprivateだから問題なさそうに見えるけど、内部表現結構変わるとかそういう...
scalatraが標準ライブラリのコレクションを継承してることも一因だろうか。まぁ本当にあの変更が原因なら、scala本体側が明らかに悪そうだけど
2.11.3 はまだ様子見しよう...
これ、confに書いてるから確信犯だよなー。そんなに重要な修正だった and/or 他に修正方法なかったのだろうか。dbuildは全部同じversionでビルドやり直すから、こういう問題は多分検知されないし
キタコレ! play2でも trait Cookies extends Traversable[Cookie]
というのあるから、Scala 2.11.3 使って、request.cookies.filter
とかするとAbstractMethodError
発生する! https://github.com/playframework/playframework/blob/2.3.5/framework/src/play/src/main/scala/play/api/mvc/Http.scala#L683
@xuwei-k 2.11.3 はお蔵入り決定ですね
キタ━━━━(゜∀゜)━━━━ッ!!
キタ━━━━(゜∀゜)━━━━ッ!!
2.11.3 生まれる前に死んでしまったか…
R.I.P. 2.11.3
scala-internalsのメーリングリストで SI-8899 の話が始まらないのだけど、(時差とかあるし)まだ気づいてないか問題の重要性認識してる人が少ないのか、いやこの程度なら予定通り出すのか、それともclosedなところでtypesafeの人たちは既に話を始めているのか。もうしばらく待って(待たなくても?) 「えっ、明らかにバイナリ互換崩れてるのに、予定通り 2.11.3 公式アナウンスするつもりなんですか?」って scala-internals のメーリングリストに誰か投稿したほうが?
とりあえず知らせといた https://groups.google.com/d/msg/scala-internals/ZSx-ZW69BJA/bFF8PtjdzpsJ
お、はやい。ありがとうございます :+1:
:+1:
:+1:
おっ、このtweet https://twitter.com/xuwei_k/status/521272044663894016 がScalatra作者にretweetされたり、Scalazコミッターからmention来たりして広まってきた > /dev/null
お、「別の重要な問題も見つかったんだ」的な返信きてる・・・
"[SI-8900] Assertion failure in isAnonymousOrLocalClass - Scala" https://issues.scala-lang.org/browse/SI-8900
元のやつも、バグのPriorityをコミッターの人が1番上か2番目?に上げてるし、まぁ重要性は伝わってるみたいなので、あとは見守るしかないな
:pray:
BlockerでFix versionが2.11.4ってことは2.11.3はこのままお蔵入りか
2.11.3のtagできててcentralにもあがってるから、2.11.3がtarget versionのままではおかしいから、とりあえずtarget versionを2.11.4にあげただけで、お蔵入りかどうかなどの詳細はまだ決まってなさそう(というあくまでも予想)
アルパカの人より自分のほうがdbuildの仕組みとか把握してるっぽい https://twitter.com/extempore2/status/521379266789797888 し、とりあえずrevetして https://github.com/scala/scala/pull/4048 2.11.4 をはやめに出す方向になりそう?だし、大体予想通りなので言うこと無い > /dev/null
https://github.com/scalaz/scalaz/commit/6a1354d74#commitcomment-8130530 a few daysらしい
Also note 2.11.3 wasn't fully released (pushing binaries to Sonatype isn't the last step in the process).
アナウンスするまでがリリースってことかな
とりあえず、ぎりぎり公式アナウンス前に地雷踏んでくれた瀬良さんに感謝ですね・・・
ですね。アナウンス前なのでさほど混乱はなさそう。
Slick でテーブル定義のコードを自動生成する
Slick 2系では experimental 扱いではありますが、コード生成の機能が含まれていて、 めんどうなテーブル定義のコードを自動生成することができます。ちょっとやってみましょう。
テスト用のテーブル、こんな感じです。
slickcodegenexample=# CREATE TABLE users ( id serial PRIMARY KEY, first_name varchar(100) NOT NULL, last_name varchar(100) NOT NULL, birth_date timestamp NOT NULL );
コード生成機能は slick 本体の jar ではなく、slick-codegen と言う jar に含まれています。使用しているデータベースに合わせた jdbc driver も必要です。それぞれ build.sbt の libraryDependencies に加えて下さい。
name := """slick-codegen-example""" version := "1.0" scalaVersion := "2.11.1" libraryDependencies ++= Seq( "com.typesafe.slick" %% "slick" % "2.1.0", "com.typesafe.slick" %% "slick-codegen" % "2.1.0", "org.postgresql" % "postgresql" % "9.3-1102-jdbc41" )
コード生成のためのコードは以下のような単純なもので、SourceCodeGenerator.main
というメソッドを呼ぶだけです。
気をつけることは jdbc driver と slick のほうの driver を合わせることくらいでしょうか。今回は PostgreSQL を使っているので scala.slick.driver.PostgresDriver
を選びます。
package com.example import scala.slick.codegen.SourceCodeGenerator object SlickCodegen { def main(args: Array[String]): Unit = { val slickDriver = "scala.slick.driver.PostgresDriver" val jdbcDriver = "org.postgresql.Driver" val url = "jdbc:postgresql://localhost/slickcodegenexample" val outputFolder = "src/main/scala" val pkg = "com.example.models" SourceCodeGenerator.main( Array( slickDriver, jdbcDriver, url, outputFolder, pkg ) ) } }
実行すると、outPutFolder
で指定した場所にコードが生成されます。
見てみるといつも書いてるようなコードですね。
package com.example.models object Tables extends { val profile = scala.slick.driver.PostgresDriver } with Tables trait Tables { val profile: scala.slick.driver.JdbcProfile import profile.simple._ import scala.slick.model.ForeignKeyAction import scala.slick.jdbc.{GetResult => GR} lazy val ddl = Users.ddl case class UsersRow(id: Int, firstName: String, lastName: String, birthDate: java.sql.Timestamp) implicit def GetResultUsersRow(implicit e0: GR[Int], e1: GR[String], e2: GR[java.sql.Timestamp]): GR[UsersRow] = GR{ prs => import prs._ UsersRow.tupled((<<[Int], <<[String], <<[String], <<[java.sql.Timestamp])) } class Users(_tableTag: Tag) extends Table[UsersRow](_tableTag, "users") { def * = (id, firstName, lastName, birthDate) <> (UsersRow.tupled, UsersRow.unapply) def ? = (id.?, firstName.?, lastName.?, birthDate.?).shaped.<>({r=>import r._; _1.map(_=> UsersRow.tupled((_1.get, _2.get, _3.get, _4.get)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported.")) val id: Column[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey) val firstName: Column[String] = column[String]("first_name", O.Length(100,varying=true)) val lastName: Column[String] = column[String]("last_name", O.Length(100,varying=true)) val birthDate: Column[java.sql.Timestamp] = column[java.sql.Timestamp]("birth_date") } lazy val Users = new TableQuery(tag => new Users(tag)) }
データベースの timestamp 型が java.sql.Timestamp
型にマッピングされています。java.sql.Timestamp
型では少々使いにくい。Java 8 日時 API or joda-time が良さそうと思いますが、そういえば私は slick-joda-mapper というのを作っていました。これで timestamp 型を org.joda.time.DateTime
にマッピングさせましょう。
https://github.com/tototoshi/slick-joda-mapper
slick-codegen でのコード生成はこういう時のためにカスタマイズが可能になっています。まず先ほどの build.sbt に joda-time と slick-joda-mapper の依存性も加えます。
name := """slick-codegen-example""" version := "1.0" scalaVersion := "2.11.1" libraryDependencies ++= Seq( "com.typesafe.slick" %% "slick" % "2.1.0", "com.typesafe.slick" %% "slick-codegen" % "2.1.0", "org.postgresql" % "postgresql" % "9.3-1102-jdbc41", "joda-time" % "joda-time" % "2.4", "com.github.tototoshi" %% "slick-joda-mapper" % "1.2.0" )
コード生成のためのコードは次のようになります。
SourceCodeGenerater
を継承して、"java.sql.Timestamp"
を "DateTime"
に変更しています。
import scala.slick.{model => m} import scala.slick.codegen.SourceCodeGenerator import scala.slick.driver.JdbcProfile class CustomSourceCodeGenerator(model: m.Model) extends SourceCodeGenerator(model) { override def code = "import com.github.tototoshi.slick.PostgresJodaSupport._\n" + "import org.joda.time.DateTime\n" + super.code override def Table = new Table(_) { override def Column = new Column(_) { override def rawType = model.tpe match { case "java.sql.Timestamp" => "DateTime" case _ => { super.rawType } } } } } object CodeGen extends App { val slickDriver = "scala.slick.driver.PostgresDriver" val jdbcDriver = "org.postgresql.Driver" val url = "jdbc:postgresql://127.0.0.1/slickcodegenexample" val outputFolder = "src/main/scala" val pkg = "com.example.models" val driver: JdbcProfile = scala.slick.driver.PostgresDriver val db = { driver.simple.Database.forURL(url, driver = jdbcDriver) } db.withSession { implicit session => new CustomSourceCodeGenerator(driver.createModel()).writeToFile(slickDriver, outputFolder, pkg) } }
生成されたコードを見ると、さっき java.sql.Timestamp
だったところが DateTime
に変わっていることがわかります。
package com.example.models object Tables extends { val profile = scala.slick.driver.PostgresDriver } with Tables trait Tables { val profile: scala.slick.driver.JdbcProfile import profile.simple._ import com.github.tototoshi.slick.PostgresJodaSupport._ import org.joda.time.DateTime import scala.slick.model.ForeignKeyAction import scala.slick.jdbc.{GetResult => GR} lazy val ddl = Users.ddl case class UsersRow(id: Int, firstName: String, lastName: String, birthDate: DateTime) implicit def GetResultUsersRow(implicit e0: GR[Int], e1: GR[String], e2: GR[DateTime]): GR[UsersRow] = GR{ prs => import prs._ UsersRow.tupled((<<[Int], <<[String], <<[String], <<[DateTime])) } class Users(_tableTag: Tag) extends Table[UsersRow](_tableTag, "users") { def * = (id, firstName, lastName, birthDate) <> (UsersRow.tupled, UsersRow.unapply) def ? = (id.?, firstName.?, lastName.?, birthDate.?).shaped.<>({r=>import r._; _1.map(_=> UsersRow.tupled((_1.get, _2.get, _3.get, _4.get)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported.")) val id: Column[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey) val firstName: Column[String] = column[String]("first_name", O.Length(100,varying=true)) val lastName: Column[String] = column[String]("last_name", O.Length(100,varying=true)) val birthDate: Column[DateTime] = column[DateTime]("birth_date") } lazy val Users = new TableQuery(tag => new Users(tag)) }
slick-codegen は sbt に組み込んでコンパイル時にコード生成を行う、ということもでき、そのサンプルが https://github.com/slick/slick-codegen-example にあります。
まだ experimental なのでそこまでやるべきかは微妙なところですが、少なくともコード生成自体は普通に使えそうだなあと思いました。
ScalaJP の Gitter がオープンしました
ScalaJP のメーリングリストでも流れていますが、 Gitter という Github と連携して使えるチャットサービスを利用して、Scala の日本語チャットルームができました。(kawachi さんが作ったのかな?)すでに 60 人くらい参加しているようです。
https://gitter.im/scalajp/public
Gitter → Twitter
Gitter に常駐するのがだるい人用に Twitter bot を作りました。
押そう→
Gitter で発言すると
Twitter に流れます。
yuka2py: とりあえずブログに書きましたですー。みなさんありがとうございますー。 http://t.co/HJfaELXstX
— scalajp_gitter (@scalajp_gitter) October 4, 2014
Gitter の無料プランは 2 週間しか履歴が保存されないのですが、 Twitter に全部垂れ流すことで解決されました。Twitter はストレージサービスだったんだ!すごい!!
Twitter → Gitter
bot を作ったら Gitter のメッセージじゃなくてこの bot に返信してしまいそうになったので、 いっそのことこの bot にリプライすることで、Twitter から Gitter に投稿できるようにしてしまえ!となりました。
@scalajp_gitter IDEAでScalaコードをステップ実行するとコード上の特定箇所にマウスカーソル当ててもインスペクション発動しないですよね。あれ、何とかならないかなー Javaのステップ実行だとすぐ見れて良いのに
— Shuya Tsukamoto (@s_tsuka) 2014, 10月 3
↑ のツイートは
↑ こんな感じで Gitter に流れます。
これで Gitter ⇄ Twitter の双方向のやりとりが可能になりましたが、 もしかして Gitter いらなくて全部 Twitter で良かったのではって感じがしてきました!
ともあれ、なにか質問を投げるとどこからか人が湧いてきて回答してくれる便利サービスになっているのでご活用ください。無料です。
ScalaMatsuri の運営に参加した感想
ScalaMatsuri の運営やってみての感想を書いておきます。そろそろ振り返りのミーティングもあるし。 ScalaMatsuri は体調のせいでほぼ参加できなかったので感想ありません(悲)。
ScalaMatsuri の意義について
今年 400 人も集まってしまったのは、オダスキーパワーですが、とはいえもう Scala の認知度はそれなりに上がってきていて、あとは使いたい人は勝手に使い始める段階に来ています。普及活動を目的とした ScalaMatsuri はもう意味がないのかなと思いました。実際会場に来ていた人の中でも業務で使っている人が多かったようです。
お堅い SIer でも普及させたいとか「ラムダがわからないおっさんに使ってもらうには?」みたいなのは会社や個人個人の都合がでかすぎるし、ScalaMatsuri としてそこに切り込んでいく必要はない、というか無理があると思いました。
...というのは割とこじつけかもしれない。一番の問題は自分に「Scala を普及させたい!」という気持ちがさほどない...
個人的には ScalaMatsuri の意義は、とにかく年1回カンファレンスを開いて盛り上がってる感を演出したり、コミュニティに刺激を与えること。Scala の普及とかは刺激を受けた人が自発的に勉強会始める、とかに任せたいです。(なぜならめんどうだから)
英語について
ScalaMatsuri は海外との交流を目的の1つとしてはいますが、それは英語をしゃべることを意味しないと思っています。 ScalaMatsuri の運営の中では英語を推奨する声が多数ですけど、自分は英語には興味はなくもっと Scala のほうを楽しみたいです。 英語の壁を取り去りたいという思惑が強すぎて、逆に英語を意識するようなイベントとなっていることは残念です。
イベントとしては同時通訳(できれば日本語英語双方向の)があれば解決する話。くよくよするな金で解決しろ。
リモートからの運営参加
準備ミーティングは体調の都合上リモート参加が多かったのですが、それほど困ることはありませんでした。 「来れない人はリモート」ではなく「基本リモートで、来れる人は現地に」という建前にしてもっと地方から協力してくれる方を増やしたいです。
その他
カンファレンスでのユニバーサル・アクセスへ向けて | eed3si9n Scala祭と日本のScalaコミュニティ - scalaとか・・・
だいたい同意です。
全体として
運営として、どのようなイベントにしたいか、というのはあるにしても、それよりもカンファレンスを楽しんでもらうことを優先したいです。
あと健康は大事です!