Perlを中心とした技術系の話などをつらつら

2006年03月23日

Sledge::Plugin::FormValidator::Simple - Sledgeのバリデータ

Sledge用のバリデータを作ろうと決めてから結構たってしまいましたが、ようやくそれっぽいものができました。


Sledge::Plugin::FormValidator::Simple-0.01


今のところFormValidator::Simple::Extensionが必要です。

Sledge::Plugin::FormValidator::Simpleを使う場合にはあらかじめこれをインストールしておく必要があります。

このバリデータの特徴は以下のとおりです。

  • プロファイルをYAMLで定義できる
  • 1つのYAMLファイルにすべてのプロファイルを記述でき、バリデーション時には自動で選択される
  • 使用するプロファイルを手動で選択することも可能
  • エラー時の挙動も定義可能
  • エラーメッセージもYAMLで記述可能。テンプレート側でメッセージを選択可能


以下使い方の説明です。


まずPagesクラスでSledge::Plugin::FormValidator::Simpleをuseします。

package Your::Proj::Pages;
use Sledge::Plugin::FormValidator::Simple;

Sledge::Plugin::FormValidator::Simpleをuseすると、validatorメソッドがPagesクラスに

追加されます。

次にコンフィグにバリデーションに関する設定を追加します。

package Your::Config::_common;
$C{VALIDATOR_OPTIONS} = {
    profile  => '/path/to/profiles.yml',
    messages => '/path/to/messages.yml',
    messages_decode_parms => 'utf-8',
    plugins => [
        'FormValidator::Simple::Plugin::Japanese',
        'Your::Original::Validation',
    ],
    loader => 'YAML::Syck',
};
profile
プロファイルを定義したYAMLファイルのパスを指定します。
messages
エラーメッセージを定義したYAMLファイルのパスを指定します。
messages_decode
parms:メッセージを受け取る際に指定の文字コードからデコードします。
plugins
バリデーションのプラグインを指定します。FormValidator::Simple::Pluginと同じ仕様です。
loader
YAMLローダとして使用するモジュール名を指定します。デフォルトはYAMLです。

さらにプロファイル、エラーメッセージを定義したYAMLを用意します。

# profiles.yml
"Your::Pages::Root" :
    contact :
        - name
        - [ [NOT_BLANK], [LENGTH, 0, 30] ]
        - email
        - [ [NOT_BLANK], [EMAIL_LOOSE], [LENGTH, 0, 100] ]
        - tel
        - [ [NOT_BLANK], [NUMBER_PHONE_JP], [LENGTH, 0, 30] ]
        - content
        - [ [NOT_BLANK], [LENGTH, 0, 5000] ]
    login :
        - userid
        - [ [NOT_BLANK], [ASCII] ]
        - password
        - [ [NOT_BLANK] ]
"Your::Pages::Signup" :
    input :
        step1 :
            - userid
            - [ [NOT_BLANK], [ASCII] ]
            - password
            - [ [NOT_BLANK] ]
            - name
            - [ [NOT_BLANK], [LENGTH, 0, 30] ]
            - email
            - [ [NOT_BLANK], [EMAIL_LOOSE], [LENGTH, 0, 100] ]
        step2 :
            - tel
            - [ [NOT_BLANK], [NUMBER_PHONE_JP], [LENGTH, 0, 30] ]
            - address
            - [ [NOT_BLANK], [LENGTH, 0, 200] ]
# messages.yml
DEFAULT:
    name:
        DEFAULT: name is invalid!!
    action1:
        name:
            NOT_BLANK: input name!
            LENGTH: input name(length should be between 0 and 10)!
        email:
            DEFAULT: input correct email address!
    action2:
        name:
            DEFAULT: hoge.

バリデーション実行時、Pagesクラス名とdispatch名に対応したプロファイルが自動で選択されます。

対応したプロファイルがない場合、バリデーションは実行されません。

使用するプロファイルを手動で変更したい場合は、valid_profile_$pageメソッドを定義します。

package Your::Pages::Signup;
use strict;

sub valid_profile_foo { my $self = shift; if ( $self->r->param('step') == 2 ) { return ('step2'); }else { return ('step1'); } } sub dispatch_foo { ... } sub post_dispatch_foo { ... }

valid_profile_$pageメソッドは使用するプロファイルのキー名を返すようにします。

上記の例ではクエリー内のstepが2の場合はYAML内の定義のうち、 Your::Pages::Signup -> foo -> step2 が使用されます。


バリデーションでエラーがみつかると、セッションにvalid_pageとvalid_resultがセットされます。

それぞれの値はの下記のとおりです。

valid_page
dispatchされたページ名($self->page)
valid_result
バリデーションの結果(FormValidator::Simple::Resultsオブジェクト)

バリデーション結果をセットした後、デフォルトでは、current_urlで取得されるURLへのリダイレクトを行います。

もしリダイレクト先を手動で設定したい場合は、valid_redirect_url_$pageを定義します。

sub valid_redirect_url_foo {
    return '/foo/error';
}

また、リダイレクト以外の処理をさせたい場合はvalid_error_$pageを定義します。

sub valid_error_foo {
    my ($self, $result) = @_;
    $self->tmpl->param(result => $result);
    $self->load_template('/bar/error');
    $self->output_content;
}

valid_error_$pageを定義すると、リダイレクトする代わりにこのメソッドが呼び出されます。

上記の例はテンプレートに結果をセットしエラーテンプレートをロードしています。

valid_error_$pageはvalid_redirect_url_$pageより優先されます。


Sledge::Plugin::FormValidator::SimpleがuseされたPagesクラスおよびそのサブクラスでは、

dispatch前にセッションからバリデーションエラーの際にセッションにセットされた

valid_result、valid_pageを取得しようと試みます。(取得の際、セッションからこれらのキーは削除されます。)

valid_resultを取得できた場合、valid_result、valid_pageをテンプレートのパラメータとしてセットし、

さらにfillin_formをloadし、バリデーションに通った値をfillinします。

Posted by horiuchi at 15:47 | Permalink | Comments (0) | TrackBack (0)

2006年03月20日

Template::Plugin::Encode

内部エンコーディングをFlagged UTF-8にしたところ、テンプレート内の日本語を含んだURLにuriフィルタを適用すると、空になるという現象が発生してしまいました。

uriフィルタをハックして、、とも思いましたが、テンプレート内でEncodeモジュールが使えたら便利かなと思い、プラグインを書いてみました。


Template::Plugin::Encode-0.01


使い方はこんな感じ。メソッドの実装は、Template::Plugin::Jcodeを参考に、virtual methodとして扱えるようにしてみました。

[% USE Encode %]
# encode 
<a href="/foo/bar?jword=[% ja_str.encode.encode('utf-8')|html|uri %]">click here</a>
# decode
[% foo.encode.decode('euc-jp') %]
# from_to
[% num = bar.encode.from_to('sjis','euc-jp') %]
[% bar %]

こういうプラグインはすでにCPANにあるだろうと思ったんですが、ミストを感じなかったので、作成してみました。

Posted by horiuchi at 21:06 | Permalink | Comments (0) | TrackBack (0)

2006年03月15日

_blankを使わないで別ウィンドウを開くにはrel="external"を使うのが美しいと思う。

はてなブックマークをみていたら、気になるエントリーを発見。


[戯] target="_blank" を使わないで新しいウィンドウでリンクを開く方法


target="_blank"という書き方がXHTML 1.1 や XHTML Basicに準拠していないので、これらに準拠するようにしつつ、別ウィンドウで開くにはどうすればよいかという話です。

別ウィンドウで開くにはJavaScriptを使えってのが推奨される方法なんですが、onclickを使って定義するのはめんどうということで、この記事では、aタグにclass="popup"という属性を与えておけば、JavaScriptで別ウィンドウを開くということをしています。

この件については友人のHTML、CSSマスターなkawachi君と話したことがあって、そのときは、下のエントリで紹介されているrel="external"という方法を使うのがいいんじゃないかという結論に達しました。


Opening a link in a new window - the valid way


僕は上のエントリを参考に以下のようなjsファイルを用意して使っています。


//external.js
function externalLinks() {
 if (!document.getElementsByTagName) return;
 var anchors = document.getElementsByTagName("a");
 for (var i=0; i<anchors.length; i++) {
   var anchor = anchors[i];
   if (anchor.getAttribute("href") &&
       anchor.getAttribute("rel") == "external")
     anchor.target = "_blank";
 }
}
Event.observe(window,'load', externalLinks, false);

使い方は簡単でこのjsファイルをインクルードするだけです。


<script type="text/javascript" src="/path/to/prototype.js"></script<
<script type="text/javascript" src="/path/to/external.js"></script>

load時にこのファンクションを実行するためにprototype.jsのEvent.observeを使っているため、

prototype.jsもインクルードしています。ここはwindow.onload=externalLinks;に書き換えれば、

prototype.jsは必要ありません。

このファイルをインクルードすると、rel="external"と指定したaタグをクリックすると別ウィンドウで開くようになります。


<a rel="external" href="http://www.google.com/">別ウィンドウで開きます。</a>

rel="external"というこのリンク先は外部のサイトだよという関連性を示す記述に対して、JavaScriptで別ウィンドウで開くんだよと定義してあげるというアプローチが美しいなと思っています。

Posted by horiuchi at 13:02 | Permalink | Comments (16) | TrackBack (1)

2006年03月14日

FormValidator::Simple::Extension - アップロードファイルのバリデーションなど

前回のエントリでFormValidator::Simpleでアップロードされたファイルのバリデーションができるように拡張したモジュールを書いているといいましたが、それっぽいものができました。

FormValidator-Simpe-Extension-0.01.tar.gz

SimpleのExtensionてどっちやねんという感じですが、アップロードファイルのバリデーション以外に、メッセージをFlaggedなUTF-8にデコードした形でgetできるような機能もつけてみたので、名前をなんにするか悩んだ挙句、とりあえずExtensionにしておきました。使い方はこんな感じです。

use FormValidator::Simple::Extension;
## アップロードファイルのバリデーション
FormValiator::Simple::Extension->check( $q, [
    thumbnail => [
        'UPLOAD_NOT_BLANK', # ファイルがアップされたかどうか
        [UPLOAD_FILESIZE,0, 1024], # ファイルサイズは1kBまで
        [UPLOAD_FILENAME_REGEX, 'jpe?g|gif|png$'] #画像っぽいファイル名ならOK
    ],
]);
## メッセージをFlagged UTF-8でうけとる
FormValidator::Simple::Extension->set_messages('messages.yml');
# どんな文字コードからデコードするか文字コードをセット
FormValidator::Simple::Extension->set_message_decode_parms('utf-8');
my $result = FormValidator::Simple::Extension->check($q, $prof);
# メッセージはデコードされる
my $messages = $result->messages;

これらの拡張が本家のFormValidator::Simpleに取り込まれたらうれしいのですが、アップロードファイルを取得する方法がApache::Requestのuploadメソッドしか想定してないので、ちょっとマイナー過ぎるきもします。。

CGI.pmのuploadメソッドも考慮したものにすれば、取り込んでもらえるかなー。

Posted by horiuchi at 23:39 | Permalink | Comments (2) | TrackBack (0)

2006年03月13日

MIME::Lite::TT::Japanese 0.08

TokuLog!な人にSubjectだけじゃなくて、FromもMIMEエンコードしないと、携帯で返信できないyo!というメールをパッチとともにいただいたので、早速適用して、CPANにアップロードしておきました。


MIME::Lite::TT::Japanese 0.08


どうもありがとうございました。

ところで、SledgeのValidatorのほうですが、FormValidator::Simpleだとアップロードされたファイルについてのバリデーションができないことに気づいて、ちょっとなやんでます。とりあえず、アップロードできるように拡張したモジュールを書いてみているんですが、いまいちしっくりきません。。もうちょい時間がかかりそう。

Posted by horiuchi at 22:13 | Permalink | Comments (0) | TrackBack (0)

2006年03月10日

FormValidator::Simple::ProfileManager::YAML

SledgeのValidatorを作るにあたって、FormValidator::Simple用のプロファイルをYAMLで定義するためのモジュールを書いてみました。

FormValidator::Simple::ProfileManager::YAML

まずは以下のようなYAMLファイルを用意します。

# sample yaml profile
group1 :
    - name
    - [ [NOT_BLANK] ]
    - email
    - [ [NOT_BLANK], [EMAIL_LOOSE] ]
    - tel
    - [ [NOT_BLANK], [NUMBER_PHONE_JP] ]
    - content
    - [ [NOT_BLANK] ]
group2 :
   subgroup1 :
       - userid
       - [ [NOT_BLANK]]
       - password
       - [ [NOT_BLANK]]
       - name
       - [ [NOT_BLANK] ]
       - email
       - [ [NOT_BLANK], [EMAIL_LOOSE] ]
    subgroup2 :
       - tel
       - [ [NOT_BLANK], [NUMBER_PHONE_JP] ]
       - { zip : [zip1, zip2] }
       - [ [ZIP_JP] ]
       - address
         - [ [NOT_BLANK] ]

group1とかsubgroup1というのはプロファイルをグルーピングするのに使います。

使い方は簡単です。まずオブジェクトを作成します。

use FormValidator::Simple::ProfileManager::YAML;
my $manager = FormValidator::Simple::ProfileManager::YAML->new('/path/to/profile.yml');

デフォルトではYAMLをロードするのにYAMLモジュールを使いますが以下のようにloaderをオプションとして渡すと、YAML::Syckを使うこともできます。

  my $manager = FormValidator::Simple::ProfileManager::YAML->new(
      '/path/to/profile.yml',
      {
          loader => 'YAML::Syck',
      }
  );

プロファイルを取得するには、get_profileメソッドを使います。パラメータとして、グループ名を渡せば、そのグループに属するプロファイルを取得することができます。

取得したプロファイルはそのままFormValidato::Simpleに渡すせます。

# group1のプロファイルを取得
my $profile1 = $manager->get_profile('group1');
# subgroup2のプロファイルを取得
my $profile2 = $manager->get_profile( 'group2', 'subgroup2' );
#取得したgroup1のプロファイルをそのままわたす。
my  $result = FormValidator::Simple->check($q , $profile1);

Posted by horiuchi at 19:25 | Permalink | Comments (0) | TrackBack (0)

SledgeのValidatorを作ろうと決意するにいたったわけ

Sledgeのバリデータといえば、Sledge::Plugin::Validatorなんですが、このプラグインが出てくる前からSledgeをいじっていたので、バリデータは自前で実装したものを使っていました。

このバリデータの機能はこんな感じのものでした。

  • バリデーションのルールはXMLで定義する。
  • バリデータをuseするだけで、バリデーションが効くようになり、それを削除してもプログラムは問題なく動く(Pagesクラスにバリデーションに関するコードを書く必要がない)
  • バリデーションはPOSTリクエスト時にのみ作動し、BEFORE_DISPATCHのトリガとして動作する
  • エラーを見つけるとテンプレートのパラメータにエラーをセットし、post_dispatch_fooをスキップし、dispatch_fooのみ実行する
  • バリデータをuseするだけで自動的にバリデーションが効くようにするということをやるために、XMLの設定ファイルでは、Pagesクラスとdispatch名とそれごとのルールを記述できるようになっている
  • エラーメッセージはデフォルトのものが用意されており、上記XMLファイルでルールごとに独自のメッセージを定義することもできる。

当時は設定ファイルといえばXMLだ!ってな風潮だったので、それにのっかって設定ファイルをXMLにしたんです。このバリデータ、今も使っているのですが、使っていくうちにいろいろと不満な点も出てきていました。たとえば、

  • XMLの可読性が低い!ルールを定義するときも設定ファイルとにらめっこ。
  • メッセージの切り替えができない(英語版と日本語版のページを用意する場合にこまる)
  • エラーがあった際の動作を独自に定義できない。
  • 1つのdispatch内で条件によってルールを変更できない。(できるけどやり方が変態的)

とまぁ結構あったのですが、実際にどうしてもこのバリデータじゃできないってことに直面しなかったので、ちょこちょこ手を加えながらこれを使いつづけてました。

それが、前回の記事、「SledgeでUTF-8なサイトをつくる」の方法で、UTF-8なサイトを作った時のこと、プログラム内で扱うデータがFlagged UTF-8になったため、エラーメッセージが文字化けしてしまうようになってしまったため、重い腰を上げて新たにバリデータを作ることにしました。

まず、Flagged UTF-8を扱えるようにすること、さらに設定ファイルはXMLから可読性が高くて最近流行のYAMLに変更、バリデーションを実行する部分はFormValidator::Simpleを使うことで、FormValidator::Simple用にたくさん用意されたプラグインの恩恵を受けられるようにというような方針で作ろうと思います。

最初、Sledge::Plugin::Validatorを拡張して使おうかとも思ったのですが、設定ファイルにdispatchごとのルールを定義しておくと、自動でバリデーションを実行してくれるという挙動が気に入っているので、自分で作ることにしました。

Posted by horiuchi at 09:42 | Permalink | Comments (0) | TrackBack (0)

2006年03月07日

SledgeでUTF8なサイトを作る。

Sledgeはプログラム内で扱うデータがEUC-JPにエンコードされたバイナリデータを前提としていて、テンプレートもEUC-JPで作成する必要があります。

これだけが原因ではないのですが、UTF-8なサイトを作ろうとすると、結構いろいろなワナがあります。

このことはSledgeのメーリングリストでも、過去にいろいろ議論されています。

http://lists.sourceforge.jp/mailman/archives/sledge-users/2004-March/000281.html

http://lists.sourceforge.jp/mailman/archives/sledge-users/2004-March/000292.html

http://lists.sourceforge.jp/mailman/archives/sledge-users/2004-March/000294.html

これらの議論は2004年の3月に行われているので、もう2年も前。これだけ時間がたっているので、SledgeでUTF-8をガンガン使っている人もたくさんいると思うのですが、最近ふと、SledgeでUTF8なサイトを作ろうと思い、情報集めのためにググッてみた際、「SledgeでUTF-8扱うにはこうすればいい!」というようなそのものずばりな解決法が見つからなかったので、僕がSledgeでUTF8なサイトを作ったその過程と成果物をさらしてみたいと思います。

* 過程

まず、SledgeでUTF-8を扱うにあたって、以下のような暗黙のルールを定義しました。

  • プログラム内で扱うデータはFlagged UTF-8
  • テンプレートの文字コードはUTF-8
  • RequestクラスでインプットデータをFlagged UTF-8にデコード
  • アウトプット時にUnflagged UTF-8にエンコード
  • mod_perlで運用すること前提。CGIでの動作は無視

次に上記を実現するようなモジュールを書いてみました。

* Sledge::Pages::Apache::I18N

まずは、インプットデータをFlagged UTF-8にデコードするRequestクラスを作ってみました。これはSledgeに標準でついているSledge::Charset::UTF8を使うとできそうですが、Sledge::Charset::UTF8でインプットデータをFlagged UTF-8にデコードしてもApache::RequestがUnflaggedなデータにしてしまうため、うまくいかないようです。

なので、Apache::Requestをどうにかしないといけないのですが、CPANを眺めていたら、Apache::Request::I18Nというまさにそのものずばりなモジュールがあったのでこいつを使ってみました。

こいつを使って書いたSledge::Pages::Apacheの代替モジュールはこんな感じです。

package Sledge::Pages::Apache::I18N;
use strict;
use base qw(Sledge::Pages::Base);
use Apache;
use Apache::Request::I18N;
sub create_request {
    my ($self, $r) = @_;
    my $req = Apache::Request::I18N->new(
        $r || Apache->request,
        DECODE_PARMS => 'utf-8',
    );
    #    $req->param; do parse here
    return $req;
}
1;

* Sledge::Charset::UTF8::I18N

次に、Sledge::Pages::Apache::I18Nでインプットデータのデコードをやってくれるようになったので、

アウトプットの際のエンコードのみを行うSledge::Charsetサブクラスを作りました。

package Sledge::Charset::UTF8::I18N;
use strict;
use base qw(Sledge::Charset::Null);
use vars qw($VERSION);
$VERSION = '0.01';
use Encode;
sub content_type {
    return 'text/html; charset=UTF-8';
}
sub output_filter {
    my($self, $content) = @_;
    return Encode::encode("UTF-8", $content);
}

1;
* Sledge::Template::TT::I18N

最後に、UTF-8なテンプレートを読み込んで内部ではFlagged UTF-8として扱ってくれるSledge::Template::TTの代替モジュールを作成しました。Sledge::Template::TTだとUTF-8なテンプレートを読み込んでも内部では、UnflaggedなUTF-8として扱われるので、Flagged UTF-8な文字列を埋め込むと文字化けを起こしてしまいます。

package Sledge::Template::TT::I18N;
use strict;
use base qw(Sledge::Template::TT);
use vars qw($VERSION);
$VERSION = '0.01';
sub output {
    my $self = shift;
    my %config = %{$self->{_options}};
    my $input  = delete $config{filename};
    $config{LOAD_TEMPLATES} = [Sledge::Template::TT::I18N::Provider->new(\%config)];
    my $template = Template->new(\%config);
    unless (-e $input) {
    Sledge::Exception::TemplateNotFound->throw(
        "No template file detected. Check your template path.",
    );
    }
    $template->process($input, $self->{_params}, \my $output)
    or Sledge::Exception::TemplateParseError->throw($template->error);
    return $output;
}
package Sledge::Template::TT::I18N::Provider;
use strict;
use base qw(Template::Provider);
sub _load {
    my $self = shift;
    my ($data, $error) = $self->SUPER::_load(@_);
    if(defined $data) {
        $data->{text} = utf8_upgrade($data->{text});
    }
    return ($data, $error);
}
sub utf8_upgrade {
    my @list = map pack('U*', unpack 'U0U*', $_), @_;
    return wantarray ? @list : $list[0];
}
1;
* MyProj::Pagesクラスに組み込む

今まで作成したモジュールはこんな感じでPagesクラスに組み込みました。

package YourProj::Pages;
use strict;
use base qw(Sledge::Pages::Apache::I18N);
use Sledge::Template::TT::I18N;
use Sledge::Charset::UTF8::I18N;
....
sub create_charset {
    my $self = shift;
    Sledge::Charset::UTF8::I18N->new($self);
}

これでとりあえず、SledgeでUTF-8を扱うことができました。

* 成果物

今回作成したモジュールたちです。

Posted by horiuchi at 01:13 | Permalink | Comments (1) | TrackBack (0)