play2でフィルター処理を実現する

前回の記事 をベースに考えて、なんとなく動くフィルター処理を作ってみました。

https://github.com/rabitarochan/play2-filter-sandbox

フィルターの書き方についてはだいたいイメージ通りに行きました。

object Application extends Controller with FilterController {
  
  def index = LoggingFilter >> WithAction { req =>
    Ok(views.html.index("Your new application is ready."))
  }
  
  case class User(name: String, password: String)
  
  case object UserKey extends AttributeKey[User]

  /**
   * 認証もどきのフィルター。
   * 
   * QueryString に user と password が設定されていて、
   * その値が同じであれば User を作成して次の処理へ進む。
   * それ以外の場合は認証エラー (Unauthorized) を返す。 
   */
  def AuthFilter = Filter { req: AttributedRequest[AnyContent] =>
    val userOpt = req.getQueryString("user")
    val passwordOpt = req.getQueryString("password")
    
    (userOpt, passwordOpt) match {
      case (Some(user), Some(password)) if user == password => {
        continueWith(UserKey -> User(user, password))
      }
      case _ => Unauthorized("user is not equal password.")
    }
  }
  
  /**
   * ログを出力するだけのフィルター
   */
  def LoggingFilter = Filter { req: AttributedRequest[AnyContent] =>
    Logger.debug(s"[${req.method}] ${req.path}")
    continue
  }
  
  
  def test = 
    LoggingFilter >>
    AuthFilter >>
    WithAction { implicit request =>
      val user = UserKey.get
      Ok(s"Action: ${user}")
    }
  
}

次のフィルター、アクションにパラメータを渡すあたりの実装は、t2v/stackable-controllerをかなり参考にさせていただきました。

なぜこれを作ったのか

参考にさせていただいた stackable-controller は、StackableControllerを継承したトレイトをControllerにmixinしていくことで、同じように共通処理を実行できます。

ただ、このフィルターのように、実際どのような処理が行われているのかが見えるほうが分かりやすいのではないかと思い、作ってみることにしました。

ふと思いついた、という理由が大きかったり。

課題

テスト等もまだまだ不十分ですし、これを使って何かを作ったわけではないです。それはこれからとして、今の時点でどうにかならないか考えていることが何個かあります。

1. Filterのリクエストの型を明示しない方法を作りたい

上記のコードの通り、全てのFilter処理のリクエストに型を明示しています。これを書かないとコンパイルが通らないため書いていますが、どうにか明示しない方法がないかを模索しています。

完全に書かないわけにはいかないと思うので、フィルターの型パラメータにするなどで、省略出来ればいいかなーと考えています。

// Filterの型パラメータとして
def HogeFilter = Filter[java.io.File] { req => 
  continue
}

// 省略したら AnyContent とみなす

def HogeFilter = Filter { req =>
  continue
}

2. 内部処理はなるべくPlay2標準のActionを利用するようにしたい

現在はFilter処理を保持するためにFilterActionというクラスを作っていますが、独自のものよりは標準で用意されているものを使ったほうがいいのかなと思っています。

3. 英語・・・

scaladocを英語で書きたかったのですが、読めるけど書けないというなんとも残念な感じなので、どうにかしたいところです。


とりあえず動くところまでは行ったので、あとは使いながら修正していこうかなと思います。もう少ししっかり作れば、最低限のフィルター処理的なものに使えると思うので、実用出来るところまで持って行きたいです。

play2でフィルター処理っぽく書きたい

ソースも全然できてませんが、play2でActionを実行する前に色々とフィルター処理がかけたらいいなーと思い、色々と試行錯誤してます。

イメージ的には、こんな感じ。(フィルター処理は適当)

def index =
  Filter { req =>
    req.getQueryString("key1") match {
      case Some(_) => continue // continueで次のフィルターに処理が移る。
      case None    => BadRequest // continue以外のResultの場合はそれが返る。
    }
  } >> Filter { req =>
    req.getQueryString("key2") match {
      case Some(value) => continueWith(value) // continueWithで渡した値は次のFilter/Actionに渡る(出来ればタプルで)
      case None        => BadRequest
    }
  } >> Action { (value2) => request =>
    Ok("key2 is %s" format value2)
  }

実現できないかなぁ・・・。

Play2のevolutionスクリプトのコメントを表示するプラグインを書いた

この記事は Play or Scala Advent Calendar 2012 12/8 分の記事です。

前は @lyrical_logical さんの View/Context Bounds の制約 です。
次は @gakuzzzz さんです。


Play2には「evolutions」という、DBスクリプトを管理してくれる機能があります。他の仕組みでDDL等を管理していないのであれば、これを利用しているのではないでしょうか。

スクリプトには、スキーマの変更情報をSQL直で記述できるので、RoRのmigrationよりもいいなーと思っているのですが、ファイル名が気になっていました。

migrationでは、ファイル名を201212080149_create_users_schema.rbのように自由に決めることができ、ファイル名からある程度どのようなスクリプトかが判断できます。

しかし、evolutionsでは、ファイル名が1.sql, 2.sqlというように固定となっており、ファイル名からスクリプトの中身を判断できません。

そのため、Playプラグイン(sbtプラグイン)の勉強をかねて、evolutionsスクリプトに記載されているコメントを一覧で表示するプラグインを書いてみました。

rabitarochan/play2-evolist-plugin

どんなもの?

playコンソールでevolistというコマンドを実行すると、スクリプトのコメント部分を列挙するプラグインです。

スクリプトには#で始めるとコメント行として認識されるため、スクリプトからその行を取得し、一覧として表示しています。

例として、以下のようなスクリプトがあったとします。

(1.sql)
# Person schema

# !Ups
create sequence person_id_seq;
create table person (
  id integer not null default nextval('person_id_seq'),
  name varchar(40)
);

# !Downs
drop table person;
drop sequence person_id_seq;
(2.sql)
# Group schema with add column to User schema

# !Ups
create sequence group_id_seq;
create table group (
  id integer not null default nextval('group_id_seq'),
  name varchar(40)
);
alter table user add column group_id integer;

# !Downs
alter table user drop column group_id;
drop table person;
drop sequence person_id_seq;

この場合、スクリプトを実行すると以下のような出力が得られます。

[testapp] $ evolist
database: default
1: Person schema
2: Group schema with add column to User schema

もしコメントが長い場合、次の行に行った場合に適切なインデントを入れてあげるようにしています。( 現在はコンソールが 80 文字と決め打ちをしています。この部分はちゃんと実装します・・ )

[testapp] $ evolist
database: default
1: Person schema
2: Group schema with add column to User schema
3: This script is fix to the user join to some groups. Define relation table for
    user and group.

あ、一文字ズレてる!!

また、以下のようにDB名を指定することもできます。

[testapp] $ evolist -db test
database: test
...

まとめ

なにもまとまりませんが・・・。

ネタを探しつつ、ふと前から不便に思っていた部分を実装してみました。実装を初めてまだ時間が経ってなく、絶賛実装中です。ドキュメントもテストも整備されていません。

evolutionスクリプトを使ってらっしゃる方で同じようなことを思っていた方は、一度試していただき、いろいろとツッコミを入れていただければと思います。

sbtプラグインについても少しずつ分かってきたので、ネタが見つかったらプラグインの作り方を書こうと思います。

evolutionsスクリプト、ファイル名じゃ何がなんだか分からないからプラグインを書いてみたよー。というお話でした。

ScalaでSPI(サービスプロバイダインターフェース)を使う

Scalaでも、JavaのSPI(サービスプロバイダインターフェース)はもちろん使えます。

SPIは、AsakusaFrameworkで知って、Javaではいろいろ遊んでみたのですが、Scalaでは試してなかったので、試してみました。

ソースはこちら。
https://github.com/rabitarochan/scala-spi-test.git

インターフェースは?

Javaでは、インターフェースの実装クラスを [ META-INF/services/${インターフェースのFQCN} ] というファイルに書きます。

Scalaでは、インターフェース名の部分をトレイト名にしてあげることで、同じように使えました。

呼び出し方

ほぼJavaと同様ですが、Service.providers で得られるIteratorジェネリクスでないため、キャストが必要なようです。

package com.rabitarochan.parent

import java.util.Iterator
import scala.collection.JavaConversions._
import sun.misc.Service

object Main extends App {
  val services = Service.providers(classOf[PrintTrait]).asInstanceOf[Iterator[PrintTrait]]

  val message = "Hello world"

  services.foreach(p => p.print(message))

}

Scala版SPI?

github内を[ META-INF/services ]で検索してみたところ、apache/activemq-apolloというプロジェクトで[ ClassFinder ]というソースを見つけました。

https://github.com/apache/activemq-apollo/blob/5b8879cd8a681bcd91c5b661d26ccd51783bd06b/apollo-util/src/main/scala/org/apache/activemq/apollo/util/ClassFinder.scala

SPIの内部もこのような実装になってるのかなと思いつつ、他にも同じようなものがないかをいろいろ探しています。

パーサコンビネータの処理が終わってくれない!!

パーサコンビネータの勉強も兼ねて、ScalaでWikiエンジンを作ろうと思ってます。

パーサコンビネータでいろいろ試していますが、以下のコードは、私の環境では処理が終わってくれません。
なんでだろう・・・。

import scala.util.parsing.combinator.RegexParsers

case class Indent(i: Int)
case class Text(s: String)
case class Line(i: Indent, t: Text)

object Parser extends RegexParsers {

  def indent: Parser[Indent] = """^\s*""".r ^^ {in => Indent(in.length)}
  def text: Parser[Text] = """[^\n\r]*""".r ^^ {s => Text(s)}
  def eol = """\n?\r?""".r

  def line: Parser[Line] = indent ~ (text <~ eol) ^^ {case in ~ te => Line(in, te)}

  def parse(s: String) = parseAll(rep(line), s)
 
}

object Main extends App {
  val s = """  hoge\n  fuga"""
  
  // 自分の環境では、この処理は終わらない。
  // 戻り値は List(Line(Indent(2), Text(hoge)), Line(Indent(2), Text(fuga))) を想定しているが・・・
  Parser.parse(s)
}

とりあえずやりたいことは、

  • indent で行頭のスペースの数を取得したい
  • text で本文を取得したい
  • eof で行を区切る

なのですが、うまく処理が終わってくれません。

Scalaはコンストラクタもカリー化できる!の続き

前回記事の続きです。

scalaはコンストラクタもカリー化できる! - rabitarochanの日記

ブログへのコメントや、Twitterのリプライでいろいろ教えていただきました。
@さん、@さん、ありがとうございました!

一部の引数を指定したコンストラクタ

一部の引数を指定したコンストラクタについては、前回失敗した書き方を少し変えることで可能でした。

scala> class Person(val name: String)(val age: Int)
defined class Person


// 失敗・・・
scala> val p1 = new Person("rabitarochan") _
<console>:8: error: missing arguments for constructor Person in class Person
       val p1 = new Person("rabitarochan") _
                ^

// 成功!!!
scala> val p2 = new Person("rabitarochan")(_)
p2: Int => Person = <function1>

_ と (_) って何が違うの!?という疑問については前回記事のコメントか、@kmizuさんのブログ、

_ と _ の違い - ((プログラミング | 形式) 言語) について書く日記

にかかれてます。

case class の場合

例では普通のクラスを利用していましたが、case class を利用した場合は、以下のとおりにインスタンス化できます。

scala> case class Person(name: String)(age: Int)
defined class Person

scala> val p1 = Person("rabitarochan") _
p1: Int => Person = <function1>

case class を定義すると、コンパニオンオブジェクトも自動的に定義されます。
この場合、コンパニオンオブジェクトの apply メソッドに対して部分適用された関数が返ります。

手動でコンパニオンオブジェクトを定義した場合でも、同じ動きを確認できました。

// コンパニオンオブジェクトの定義は :paste してから。
scala> :paste
// Entering paste mode (ctrl-D to finish)

class Person(val name: String)(val age: Int)
object Person {
  def apply(name: String)(age: Int) = new Person(name)(age)
}

// Exiting paste mode, now interpreting.

defined class Person
defined module Person

scala> val p1 = Person("rabitarochan") _
p1: Int => Person = <function1>

まとめ

new Hoge("hoge")(_) と記述することで、カリー化したコンストラクタについても、部分適用(というの??)した関数を取得することができました。カリー化した場合の制限だと思っていたものも解決しました。

あとは、@kmizuさんのブログを読んで理解するだけです。
言語仕様も読まないとなぁ・・・。