読者です 読者をやめる 読者になる 読者になる

のらねこの気まま暮らし

Perlについてとか、創作についてとか、発展途上の自分と向き合う記録。

Amon2::Plugin::Web::ValidatorというPluginを書いた

Perl Amon2

ソースは以下。まだ全然書き途中
https://github.com/rymizuki/p5-Amon2-Plugin-Web-Validator

長めの前置き

以前からポツポツとリクエストのValidation系のModuleがどうにも肌に合わず、去年の秋ごろにData::Validatorを弄る感じで自作Moduleを使っていた。

別にあれがダメこれがだめで、というよりは社内で使うのにいろいろ制約があったことと、どうも冗長に感じること、Data::Validatorをメソッドのバリデーションに使っているのにリクエストはRuleの書式が違うことなど、どうにもまとまりがなかった。

受け付けるデータのチェックを行うのであれば、ModelもContollerも同じフォーマットであるにこしたことはないし、Mouse::Typesとか型チェック系のModuleも使っているのだから、ある程度ルールも共通化できる。
nameパラメータは何文字以上何文字以内、とか。Webインターフェースだけが受け付けるわけじゃなく、CUIインターフェースでも利用する可能性があるいじょう、どちらかに依存してしまうのはよくないし、かといってModelでValidationすれば他でしなくてもいい、ということではないと僕は考えている。


Data::ValidatorはRuleごとインスタンスをキャッシュできたり、Roleで挙動を変えられたり、TypeConstraintsで型を定義できたりしていて使い勝手が良い。
メソッドなんかで引数のバリデーションに使うのであれば、メソッドの先頭に持ってきて、明示的にどういう引数を期待しているのかを記述できる。これはメンテナンスのときに非常にありがたい。


そんなこんなもあって、最初はData::Validatorでリクエストをバリデーションしていたんだけれど、だんだんその冗長性に嫌気がさしてきた。余談だけれど、僕は一月半で一つのサービス作るくらいの勢いでコードを書いていたから、そりゃもうValidationしまくりなのです。
要求としては以下が追加された。

  • なるべく短くValidationを行いたい
  • バリデーションを行ったパラメータだけを受け取りたい
  • エラーメッセージとパラメーターを紐付けて受け取りたい
  • Fileのアップロードもバリデーションしたい(サイズとかContentTypeとか)


そこまで高機能なValidatorに思い当たらず(探せばあったかもだけど、入れるのがかなり手間の予感)、僕だけの要求が強そうなので、勉強もかねてとりあえず書いてみた。
ついでにそこからAmon2::Pluginにしてみた。


だいたいそんなところが、書いたよって話のコンテキスト。いや長い。

READMEの練習みたいなの

SYNOPSIS

package Project::Web::App;
use Amon2::Lite;

get '/list' => sub {
    my $c = shift;
    my $data = $c->validator(rule => +{
        page => {isa => 'Int', default => 1},
    })->valid_data;
    my $result = $c->model('any')->run(%$data);
    $c->render('list.tx', {result => $result});
};

post '/add' => sub {
    my $c = shift;
    my $validator = $c->validator(rule +{
        name  => 'Str',
        profile => 'Str',
    });
    if ($validator->is_success) {
      ... things ...
    } else {
        return $c->render('error.tx', {errors => $validator->get_errors)};
    }
};

__PACKAGE__->load_plugins(
    'Web::Validator' => {
        module     => 'Data::Validator',
        message  => {str => 'input type is strings!'},
);

1;

がいよー

初期化時に"module"を読み込んでいるのは、Data::Validator以外も使えるようにしたいとか思いそうと思ったから。

messageはdecamelizeしたRuleのISAをキーにしたHashRef。なんでdecamelizeしたのかは忘れたけどたぶんそういう仕様なんだ。

$c->validatorでruleをHashRefで読み込んでいるけれど、外部ファイルにまとめて初期化時に渡すことも可能。

__PACKAGE__->load_plugin('Web::Validator' => \%rule);

もともとは、こっちで使ってたんだけど、@kfly8 氏に突っ込まれて$c->validatorからRuleを呼べるようにした。

Data::ValidatorはNoThrow,AllowExtraのRoleを使っており、Validationしない余計なパラメータがあっても大丈夫。失敗した時に死んだりしない。まだ頑張れる。

メソッドチェイン厨としては、いまのインターフェースは結構きにいってて、最短一行3メソッド呼び出しでバリデーションデータを受け取れるのは嬉しい。

一応テストもちょっとずつ追加して、週末にはMakefile.PLあたりをもうちょいちゃんとしたいとおもっているのと、Podもちゃんと書こうと思ってる。

is_successとかvalid_dataとかget_errorsとか以外にもいろいろメソッドは追加しているので、その辺もちゃんとやりたいのです。



2年間くらいPerl書いてきたけど、低機能なValidatorでもみんな気にせず使ってて、僕だけなのかこんなことに悩むのは、と長らく思っていた。
単純に慣れの問題とか、優先順位の問題とか、そもそもそんなにValidator拘るようなコード書いてへんってのもあるのかなーとは思う。管理画面クリエイターだもの、僕は。


ともあれ、2年目にしてようやく実用的な(?)モジュールを公開できそうなのはちょっとした成長だと思う。
ちなみに最近週一で新しいPlugin生やしてるので、使えそうなのあったら社内外かかわらず出して行きたいと思ってる。とりあえず次は一応Githubには上がってる。ニヤリ

先輩のコードを大きくパクったという、ね・・・
ま、週末にでも書きましょう。

翌朝追記

早速コメントを頂いた。

id:kfly8

opts でon_error と on_success 受け取れるようにして欲しいなーw

わがままさんめっ

こんなふうにすると、Validation成功時・失敗時にコールバックを実行できる。

__PACKAGE__->load_plugin('Web::Validator', {
    module => 'Data::Validator',
    message => \%message,
    on_success => sub {
        my ($c, $validator) = @_;
        debugf 'yappie! %s', ddf $validator->valid_data;
    },
    on_error => sub {
        my ($c, $validator) = @_;
        return $c->render('error.tx', {errors => $validator->get_errors});
    },
});