ScalaでANSI Color 文字列の表示を少し楽にする

ツールを使った時によく「success」は緑色、「error」は赤色とかで表示されていることがありますよね。

それは、Scala標準でscala.ConsoleにANSI Colorがあるのですぐにでも使えます。
参考:ScalaのREPLをカラフルにして遊ぶ - tototoshiの日記
   Scala REPL を無駄に Colourful にする方法@xuwei_kさんのgist

ですが、Console.RESETで終わらないと色を指定した以降文字列がすべて指定した色になってしまったり面倒ですね。

ということで、もう少し簡単になるように作ってみました。(まぁ、誰得ってかんじです)

https://github.com/sassunt/scalor

これで、

import com.github.sassunt.scalor._
import Colors._, Tones._

をインポートすると、

"あおいろ!!" :# blue
"あかるいあかいろ" :# red :@ bright
"太字" :@ bold
"背景黄色" :~ yellow

または、

"あおいろ!!".:#[Blue]
"hoge".:#[Red].:@[Bright]
"太字".:@[Bold]
"背景黄色".:~[Yellow]

で指定できます。

"背景マゼンタの太字でアンダーラインで明るい緑色" :~magenta :@bold :@underline :#green :@bright

色調:@brightは、:#で色指定していないとコンパイルエラーになります。(色指定してないのに色調指定とか・・・)

"compile error" :@bright

先に色を指定していれば コンパイルは通ります。

"compile success" :#red :@bright

circumflexでyabeを作成 - Day1 (A first iteration of the data model)

Day1は、Circumflex ORMを使用し,
Play Frameworkのチュートリアルの「A first iteration of the data model」を元に作成していきます。
日本語:http://playscalaja.appspot.com/documentation/0.9.1/guide2
英語:http://www.playframework.org/modules/scala-0.9.1/guide2

出来る限り、似せて作成しています。

Userモデル

circumflexのORMは、SQLに近い形でモデルを書くことができます。

import ru.circumflex.orm._

class User extends Record[Long, User] with IdentityGenerator[Long, User] {
  val id = "id".BIGINT.NOT_NULL.AUTO_INCREMENT
  val email = "email".VARCHAR(255).NOT_NULL
  val password = "password".VARCHAR(255).NOT_NULL
  val fullname = "fullname".VARCHAR(255).NOT_NULL
  val isAdmin = "isAdmin".BOOLEAN.NOT_NULL

  def PRIMARY_KEY = id
  def relation = User
}

Recordクラスを継承して、テーブル定義を記述します。

また、コンパニオンオブジェクトを用意しておきます。

object User extends User with Table[Long, User] {
}

コンパニオンオブジェクトでは、主にSQLクエリや外部キーの設定を記述することになります。

モデルのテスト

circumflex-ormでは、テストにspecsを使用しており、play frameworkではscalatestを使用しています。
(play frameworkはsclaatestをバンドルしています。)

今回は、scalatestでテストしていきましょう。*1

import ru.circumflex.orm._

import org.scalatest._
import org.scalatest.junit._
import org.scalatest.matchers._

class BasicTests extends Spec with ShouldMatchers with BeforeAndAfterEach {

  it ("should create and retriece a User") {
    val user = new User
    user.email := "test@test.com"
    user.password := "secret"
    user.fullname := "Tarou"
    user.isAdmin := false
    user.INSERT()

    val tarou = (User AS "u").map{ u => SELECT(u.*).FROM(u).WHERE(u.email EQ "test@test.com").unique }

    tarou should not be (None)
    tarou.get.fullname() should be ("Tarou")
  }

}

circumflex-ormには、他のORMに存在するようなfindメソッドのような機能などは基本的に存在しません。*2
代わりに、SQLを書く感覚で記述することができます。

ここで注意してほしい点は、tarou.get.fullname()のです。
fullname()の括弧を省略することができません。
値を取り出すには必ず括弧をつけて呼び出す必要があります。

では、テストを実行してみましょう。

テストの実行前にテーブルを作成しておく必要がありますが、
テストのためにテーブルを用意するのは大変です。

それに関しても問題ありません。
circumflex-ormでは、テーブルを作成・削除するための機能があります。

では、テスト前には、テーブルのCREATEを行い、テスト後にはDROPすることをテストコードに記載しましょう。

class BasicTests extends Spec with ShouldMatchers with BeforeAndAfterEach {

  override def beforeEach() {
    new DDLUnit(User).CREATE()
  }

  override def afterEach() {
    new DDLUnit(User).DROP()
  }

  ...
}

DDLUnitクラスを使用して、コンストラクタに指定したモデルのテーブルを作成および削除が可能になります

また、Day0でも用意したcx.propertiesをテストDB用に準備する必要があります。
src/test/resources/cx.properties

orm.connection.driver=org.h2.Driver
orm.connection.url=jdbc:h2:mem:yabe
orm.connection.username=sa
orm.connection.password=

これで準備が整ったので、テストを開始しましょう。

sbt test

オールグリーンになったところで、Play frameworkのチュートリアルと同じように
ユーザとパスワードの存在チェックを行うconnectメソッドをUserに追加してみましょう。

object User extends User with Table[Long, User] {

  def connect(email: String, password: String): Option[User] = {
    (this AS "q").map{ q => SELECT(q.*).FROM(q).WHERE((q.email EQ email) AND (q.password EQ password)).unique }
  }
}

同じようにテストメソッドも追加しましょう。

it ("should connect a User") {
  val user = new User
  user.email := "test@test.com"
  user.password := "secret"
  user.fullname := "Tarou"
  user.isAdmin := false
  user.INSERT()

  User.connect("test@test.com", "secret") should not be (None)
  User.connect("test@test.com", "badpassword") should be (None)
  User.connect("bad@test.com", "secret") should be (None)
}

Postモデル

class Post extends Record[Long, Post] with IdentityGenerator[Long, Post] {
  val id = "id".BIGINT.NOT_NULL.AUTO_INCREMENT
  val title = "title".VARCHAR(255).NOT_NULL
  val content = "content".TEXT.NOT_NULL
  val postedAt = "postedAt".DATE.NOT_NULL
  val author_id = "author_id".BIGINT.NOT_NULL

  def PRIMARY_KEY = id
  def relation = Post
}

object Post extends Post with Table[Long, Post] {
  val fkey = CONSTRAINT("user_id_fkey").FOREIGN_KEY(User, this.author_id -> User.id)
}

外部キーは、コンパニオンオブジェクトに設定します。

テストコードも書きましょう。

it ("should create a Post") {
  val user = new User
  user.email := "test@test.com"
  user.password := "secret"
  user.fullname := "Tarou"
  user.isAdmin := false
  user.INSERT()

  val post = new Post
  post.title := "My first post"
  post.content := "Hello!"
  post.postedAt := new Date
  post.author_id := 1
  post.INSERT()

  val postCount = (Post AS "p").map{ p => SELECT(COUNT(p.id)).FROM(p).unique }.get
  postCount should be (1)

  val posts = (Post AS "p").map{ p => SELECT(p.*).FROM(p).WHERE(p.author_id EQ 1).list }
  posts.length should be (1)

  val firstPost = posts.headOption

  firstPost should not be (None)
  firstPost.get.author_id() should be (1)
  firstPost.get.content() should be ("Hello!")
}

PostとUserを結合してPostに紐付くUserをセットにするクエリを追加しましょう。

def allWithAuthor(): Seq[(Post, User)] = {
  val p = Post AS "p"
  val u = User AS "u"
  SELECT(p .* -> u.*).FROM(p JOIN u).WHERE(p.author_id EQ u.id).ORDER_BY(p.postedAt DESC).list.collect{ case (Some(po), Some(us)) => (po, us) }
}

2テーブルの結合はサポートされているため、戻り値の型がSeq[(Option[Post], Option[User])]になります。


テストコードも追加しましょう。

it ("should retrieve Posts with author") {
  val user = new User
  user.email := "test@test.com"
  user.password := "secret"
  user.fullname := "Tarou"
  user.isAdmin := false
  user.INSERT()

  val post = new Post
  post.title := "My 1st post"
  post.content := "Hello!"
  post.postedAt := new Date
  post.author_id := 1
  post.INSERT()

  val posts = Post.allWithAuthor
  posts.length should be (1)

  val (p, author) = posts.head

  p.title() should be ("My 1st post")
  author.fullname() should be ("Tarou")
}

Commentモデル

class Comment extends Record[Long, Comment] with IdentityGenerator[Long, Comment] {

  def this(post_id: Post, author: String, content: String) = {
   this()
   this.post_id := post_id
   this.author := author
   this.content := content
   this.postedAt := new java.util.Date()
  }
  val id = "id".BIGINT.NOT_NULL.AUTO_INCREMENT
  val author = "author".VARCHAR(255).NOT_NULL
  val content = "content".TEXT.NOT_NULL
  val postedAt = "postedAt".DATE.NOT_NULL
  val post_id = "post_id".BIGINT.NOT_NULL.REFERENCES(Post).ON_DELETE(CASCADE)

  def PRIMARY_KEY = id
  def relation = Comment
}

object Comment extends Comment with Table[Long, Comment] {
  val fkey = CONSTRAINT("post_id_fkey").FOREIGN_KEY(Post, this.post_id -> Post.id)
}

Postに紐付くコメントリストを取得するクエリを追加します。

def allWithAuthorAndComments(): Seq[(Post, User, Seq[Comment])] = {
  val p = Post AS "p"
  val u = User AS "u"
  val c = Comment AS "c"
  val ls = SELECT(p.* AS "post", u.* AS "user", c.* AS "comment").
           FROM((p JOIN u).ON(p.author_id EQ u.id).
           LEFT_JOIN(c).ON(c.post_id EQ p.id)).
           ORDER_BY(p.postedAt DESC).list

  (for {
      ((Some(p), Some(u)), xs) <- ls.groupBy{m => (m.get("post"), m.get("user"))}
  } yield (p.asInstanceOf[Post],
           u.asInstanceOf[User],
           xs.flatMap(_.get("comment")).map(_.asInstanceOf[Comment])
  )).toSeq
}

ここで注目してほしいところは、3テーブル以上を結合する場合、2テーブルの結合と違って、
戻り値の型がSeq[Map[String, Any]]になる点です。
そのため、ASで別名を付け、asInstanceOfでキャストしています。

もう一つ追加しておきましょう。

def byIdWithAuthorAndComments(id: Long): Option[(Post, User, Seq[Comment])] = {
  val p = Post AS "p"
  val u = User AS "u"
  val c = Comment AS "c"
  val ls = SELECT(p.* AS "post", u.* AS "user", c.* AS "comment").
           FROM((p JOIN u).
           ON(p.author_id EQ u.id).
           LEFT_JOIN(c).ON(c.post_id EQ p.id)).
           WHERE(p.id EQ id).list

  (for {
      ((Some(p), Some(u)), xs) <- ls.groupBy{m => (m.get("post"), m.get("user"))}
  } yield (p.asInstanceOf[Post],
           u.asInstanceOf[User],
           xs.flatMap(_.get("comment")).map(_.asInstanceOf[Comment])
  )).headOption
}

同じようにテストコードを追加する前にCommentモデルにコンストラクタを追加しておきます。

class Comment extends Record[Long, Comment] with IdentityGenerator[Long, Comment] {

  def this(post_id: Post, author: String, content: String) = {
   this()
   this.post_id := post_id
   this.author := author
   this.content := content
   this.postedAt := new java.util.Date()
  }
  ...

コンストラクタを追加することで、データの登録を1行で書けるようになります。


それでは、さっそくテストコードで見てみましょう。

it ("should support Comments") {
  val user = new User
  user.email := "test@test.com"
  user.password := "secret"
  user.fullname := "Tarou"
  user.isAdmin := false
  user.INSERT()

  val post = new Post
  post.title := "My 1st post"
  post.content := "Hello!"
  post.postedAt := new Date
  post.author_id := 1
  post.INSERT()

  new Comment(post, "Suzuki", "Nice post").INSERT()
  //val comment1 = new Comment
  //comment1.author := "Suzuki"
  //comment1.content := "Nice post"
  //comment1.postedAt := new Date
  //comment1.post_id := post
  //comment1.INSERT()

  new Comment(post, "Satou", "I knew that !").INSERT()
  //val comment2 = new Comment
  //comment2.author := "Satou"
  //comment2.content := "I knew that !"
  //comment2.postedAt := new Date
  //comment2.post_id := post
  //comment2.INSERT()

  val userCount = (User AS "u").map{ u => SELECT(COUNT(u.id)).FROM(u).unique }.get
  userCount should be (1)
  val postCount = (Post AS "p").map{ p => SELECT(COUNT(p.id)).FROM(p).unique }.get
  postCount should be (1)
  val commentCount = (Comment AS "c").map{ c => SELECT(COUNT(c.id)).FROM(c).unique }.get
  commentCount should be (2)

  val Some((p, author, comments)) = Post.byIdWithAuthorAndComments(1)

  p.title() should be ("My 1st post")
  author.fullname() should be ("Tarou")
  comments.length should be (2)
  comments(0).author() should be ("Suzuki")
  comments(1).author() should be ("Satou")

}

ここでも注目してほしい点があります。
Commentのコンストラクタのpost_idがPOST型になっていることです。
他のORMならばInt型になると思いますが、circumflexでは紐付く値は紐付くべき型の値を指定する必要があります。

複雑なテスト

Play Frameworkでは、YAMLを読み込んでテストを行っていますが、
Circumflex-ormでは、YAMLの代わりにXMLを使って同じ様なことが実現できます。

実際のデータはgithubにあるのでそちらを見てください。

では、テストデータをロードする部分を見てみましょう。

Deployment.readAll(XML.load(getClass.getResourceAsStream("/test.data.xml"))).foreach(_.process)

test.data.xmlはsrc/test/resources/にあります。

少し長いですが、テストコードを追加してみましょう。

it ("should load a complex graph from XML") {
  Deployment.readAll(XML.load(getClass.getResourceAsStream("/test.data.xml"))).foreach(_.process)

  val userCount = (User AS "u").map{ u => SELECT(COUNT(u.id)).FROM(u).unique }.get
  userCount should be (2)
  val postCount = (Post AS "p").map{ p => SELECT(COUNT(p.id)).FROM(p).unique }.get
  postCount should be (3)
  val commentCount = (Comment AS "c").map{ c => SELECT(COUNT(c.id)).FROM(c).unique }.get
  commentCount should be (3)

  User.connect("tarou@test.com", "secret") should not be (None)
  User.connect("suzuki@test.com", "secret") should not be (None)
  User.connect("suzuki@test.com", "badpassword") should be (None)
  User.connect("tanaka@test.com", "secret") should be (None)

  val allPostsWithAuthorAndComments = Post.allWithAuthorAndComments

  allPostsWithAuthorAndComments.length should be (3)

  val (post, author, comments) = allPostsWithAuthorAndComments(2)
  post.title() should be ("Scala Programming Language")
  author.fullname() should be ("Tarou")
  comments.length should be (2)

  // We have a reference integrity error
  intercept[org.h2.jdbc.JdbcSQLException] {
    (User AS "u").map{ u => DELETE(u).WHERE(u.email EQ "tarou@test.com").execute }
  }

  val deletePostRowsNum =  (Post AS "p").map{ p => DELETE(p).WHERE(p.author_id EQ 1).execute }
  deletePostRowsNum should be (2)

  val deleteUserRowsNum = (User AS "u").map{ u => DELETE(u).WHERE(u.email EQ "tarou@test.com").execute }
  deleteUserRowsNum should be (1)

  val userCountAfterDelete = (User AS "u").map{ u => SELECT(COUNT(u.id)).FROM(u).unique }.get
  userCountAfterDelete should be (1)
  val postCountAfterDelete = (Post AS "p").map{ p => SELECT(COUNT(p.id)).FROM(p).unique }.get
  postCountAfterDelete should be (1)
  val commentCountAfterDelete = (Comment AS "c").map{ c => SELECT(COUNT(c.id)).FROM(c).unique }.get
  commentCountAfterDelete should be (0)

}

以上でDay1は終了になります。

次回は画面開発になります。

*1:使いたかっただけで特に意味はありません。

*2:探した限り見つからなかったので、知らないだけで存在するかもしれません。

circumflexでyabeを作成 - Day0 (Starting up the project)

プロジェクトの作成

git clone git@github.com:sassunt/circumflex-sbt-quickstart.git

giter8を使用したい場合は、

g8 sassunt/circumflex

アプリケーションの起動

sbt container:start

ブラウザからhttp://localhost:8080を指定しましょう。
it works! と表示されれば成功です。

では、ルーティングを行っている部分を見てみましょう。
ルーティングを行うクラスはどのクラスであるかは、src/main/resources/cx.propertiesファイルに記載されています。

cx.router=com.example.MyRouter

それでは、指定されているクラスを見てみましょう。

class MyRouter extends Router {
  get("/") = "<h1>it works!</h1>"
  ...

ここでは、"/"ルートに対してGETリクエストを受け取った場合に、起こす処理を記述しています。
このようにRouterクラスを継承したクラスでは、リクエストの種類とパスによって、
行う処理を振り分けることになります。

さきほどのルーティングでは、HTMLを返していましたが、
circumflexは、FreeMarkerとscalateをサポートしており、テンプレートエンジンを使用することは可能です。

ただし、scalateのサポートはcircumflex2.1までしか行っておらず、2.2以降でscalateを使用する場合は、
ServletRenderContext.viewなどを使用する必要があります。*1
(参照:https://github.com/inca/circumflex/blob/release-2.2/CHANGELOG.md)

DBの設定

yabeを作り始める前に、DBの設定を行っておきましょう。

今回はH2DBを使用します。
まずは、build.sbtに下記の内容を追加しましょう。

libraryDependencies += "com.h2database" % "h2" % "1.3.164"

そして、さきほどルーティングを行うクラスを指定したcx.propertiesにDBの情報を記載する必要があります。

orm.connection.driver=org.h2.Driver
orm.connection.url=jdbc:h2:mem:yabe
orm.connection.username=sa
orm.connection.password=

以上で必要な設定は終わりです。

次回はモデルについて説明します。

*1:yabe Day2では、サポートされたものを使用せず、scalateを個別で導入し使用します。

少し寄り道してcircumflex web framework使ってみない?

あなたがWeb frameworkを使う時、おそらくPlay!(1.x系、2.x系) , Lift, unfiltered, scalatraを使っていると思うが
あなたはcircumflexというweb frameworkをご存知だろうか?

私は以前Twitterでこのような発言をしていたが、(綴り間違ってる)

これまで使ったことはなかった。

とりあえずcircumflexが何なのかの前に読み方・意味がわからなかったため調べてみたが、以下のようだった。

circumflex:
[日本語訳] 曲折アクセント記号、アクサンシルコンフレックス(フランス語の・・?)
[読み] サーカムフレックス 

そしてこれについても、私は過去にTwitterで呟いていたようだ。

とにかく、過去の自分は何か言っていたようだ・・・・。
とりあえず、今後は「しるこん!」と呼ぼうと思う。

そして先日、Scala勉強会第71回 in 秋葉原に参加したときの話だ。

ORMの比較の時に、circumflexの話が出た(というか自分が聞いた。)。

いや、cirumflexの話が出たことが問題ではなく、その話題のすぐ後に、
勉強会参加者が以下の(若干違うと思うが)発言をしてくれた。(どなたが発言してくれたかは、覚えてないです。)

(おそらくurl(http://circumflex.ru/)を見て)ロシアだ・・・

「.ru」ドメイン。 そうロシアだ。

私はこの時思った・・・・勉強すればいずれ・・・

(DARKER THAN BLACK -流星の双子-の「蘇芳・パブリチェンコ」もロシアだ・・・・)
もしや、美少女ロシアっ子とペアプロできるんじゃね!?

(嘘です。思ってません。というか銀のほうが好きです。どうでもいい)

ということで、circumflex web frameworkはロシア製なのだろう。(たぶん・・・)

ドキュメントに関してだが、割と揃っているようだ。(http://circumflex.ru/)

githubでもandreyshikov/circumflex-sbt-quickstartというものがあり、すぐにでも始めれるようになっている。
しかしsbtが0.7.7だったので、とりあえずforkして0.11.x系にしておいた。

git clone git@github.com:sassunt/circumflex-sbt-quickstart.git

そして、このブログは見ている方はこれからcircumflexを試すと思う。

そういうことも考慮してgiter8テンプレートも用意しておいた。ぬかりはない。(ドヤッ

> g8 sassunt/circumflex

ただ、気をつけてほしい点がある。
circumflex web frameworkのドキュメントの至る所に、
RequestRouterを継承して作成している点だ。(Webページの方は古い?)

そんなモノはなくなっている。(結構前に名称が変更されていました。)
https://github.com/inca/circumflex/commit/ac86c8ec9492e7b7c93c20a83c3c1d080fb6433d

そのためRequestRouterではなく、Routerを継承する必要がある。

というか、この口調は疲れるし、うざいのでやめます。

その他にも変わってるっぽいので、気を付けたほうがいいです。

あと、version2.2がリリースされてるみたいですが、2.2ってどこにあるんでしょうかね。
(mavenにはなかった。giter8のテンプレートで作成したものはデフォルト2.1です。)


で、circumflexがどんなものかはまだわからないです。(ドキュメントも何も読んでないし、全然使ってないです。)

ただサンプルアプリとしては、
blogアプリのhttps://github.com/inca/sandbox-blog
https://github.com/inca/circumflex-crud
https://github.com/RyuuGan/scalaqaがあるみたいなので、

時間があれば、playのyabeやLiftのyabeのように
circumflexでyabeを作っていきたいです。

ということで、みなさんPlay!とかLiftもいいですけど、
circumflexとかどうですかね。

Unfilteredが0.6.0になったよ!

0.5.4になったと思ったら,
0.6.0になってた・・・

なんか新しい機能が増えた?みたいです。
何が増えたのかはimplicit.ly(http://implicit.ly/unfiltered-060)を見ればだいたい分かると思います。

というか、unfiltered.kit.Routesが増えて、正規表現でマッチできるようになった。
(0.5.3ってできませんでしたよ・・ね? 間違ってたら指摘してください。。。。)

下記、3つのメソッドを使ってマッチさせることができます。

  • startsWith
  • regex
  • specify

使い方は紹介するほどもなく、テストコード見れば一発です。
https://github.com/unfiltered/unfiltered/blob/master/library/src/test/scala/RoutesStartsWithSpec.scala
https://github.com/unfiltered/unfiltered/blob/master/library/src/test/scala/RoutesRegexSpec.scala
https://github.com/unfiltered/unfiltered/blob/master/library/src/test/scala/RoutesSpec.scala

しかし、startsWithを例にあげても↓みたいに冗長になりそうだ・・・

こんな感じにした方がいいんだろうか・・・?

unfilteredとdispatchに入門しつつ何か作らなイカ? その1

ということで、入門しましょう!!

とりあえず、dispatchの基本的な使い方から

次のgistの内容dispatchを使って取得します。

では、次にdispatchのjsonを使いましょう。(lift-jsonとほとんど同じはずです。。たしか)
サンプルでは、github apiから取得します。

次はunfilteredです。
今回はunfilteredをHeroku上で動かすので、前に作ったgiter8のテンプレートを使います。

> g8 sassunt/unfiltered-heroku


以上。(unfilteredはこれだけです。・・・)

ここまで出来れば、簡単です。

そう、unfiltereddispatchならね。
というわけでこんな感じの物ができます。

https://github.com/sassunt/gistalker
Heroku:http://gistalker.herokuapp.com/
gistに記載されているgithubアカウントを元にその人のgistを監視できます。(監視ってほどでもない。。。)

unfilteredの機能全然つかってなくね?って事になりますが・・・ご勘弁・・・(今後、追加できればいいな・・・

ちょっと便利だったり、短めでちょっぴり勉強になるやつ

1.unfilteredのutilで空いてるポートを探してくれます
  https://github.com/unfiltered/unfiltered/blob/0.5.3/util/src/main/scala/utils.scala#L3-11

2.同じくunfilteredからブラウザを開いてくれるやつ
  https://github.com/unfiltered/unfiltered/blob/0.5.3/util/src/main/scala/utils.scala#L13-24
  これはunfiltered使ったことある人なら見たことあると思いますが、かなり便利ですよね。

3.conscriptのClean
  https://github.com/n8han/conscript/blob/0.3.4/src/main/scala/clean.scala
  なんか再帰使ってますね。いまだに/:がわからなくなるときがある・・・

4.picture-showのFile
  https://github.com/softprops/picture-show/blob/master/core/src/main/scala/Files.scala
  はじめに見る分にはわかりやすくて勉強になるのではないでしょうかね。

あと一つ何かあったはずなのに・・・忘れてしまった・・・思い出したら更新します。

ちなみに、githubのURL貼るときは・・・・githubのURLを貼るときに気をつけていることを参考にして貼りましょう。

※3がgiter8になっていましたが、conscriptの間違いです・・・・修正。