Last Modified:
Plack based WAF "Slack" #Perl #Slack
- Slack-v0.9.0
Perlで独自CMSを作成するためにCatalystやDancerなどのWAFを使っていたのですが、気に入らない部分が多かったのでWAFを自作しました。
https://github.com/robario/p5-Slack(是非Starしてください)
まだまだ荒削りで仕様も変わったりしますが、どうか生暖かい目でお願いします。
本体は極力薄いWAFを目指していて、モデルやビューには手を出しません。 ぶっちゃけ手を出すのはコントローラーだけで、Slackという名の通りゆるふわなWAFです。 将来的に
Howdy, Slack!
# --- app.psgi --- #
package MyApp;
use Slack qw(App Controller);
action hello => sub {
res->body('Hello, world!');
};
MyApp->new->to_app;
ルーティング
まずSlackの一番の肝であるアクションの定義について。 CatalystとDancerのいいとこ取りなイメージです。
基本
基本の(冗長な)記述方法は以下の通り。
action $name, $pattern, \%methods;
$name
$name
はアクション名で、自由に名前をつけることができます。重複していても構いません。
- アクションに必ず名前が付くので、Sinatra形式には無いセマンティクスな効果があります。
実用上はテンプレートの名前解決に使われます。
これによってDancerで煩わしいテンプレート呼び出しtemplate 'hello' => { number => 42 };
を毎回記述する必要が無くなります。なお、コントローラーにはビューの処理を直接的に記述するべきではないと思ってます。対応するビュー(例えばJSON)が増えた時に困ってしまうからです。
これらはCatalyst inspireです。
$pattern
$pattern
はPATH_INFO
にマッチさせる正規表現または文字列を指定します。省略も可能です。
文字列で指定した場合はqr/$pattern\z/
に変換され、省略した場合はqr/$name\z/
に変換されます。
これは大抵のWAFの挙動と似てますが、\z
が付く点に注意が必要です。余計な文字列が付いたPATH_INFO
は受理しません。
なお後述しますが、全ての$pattern
の前にコントローラー固有のprefix
が前置されます。
Sinatraで使われている:name
形式は用意してませんが、:name
形式は型を指定できないことを踏まえて敢えてできないようにしています。
その代わりnamed captureを利用して(?<username>[[:lower:]][[:lower:]\d]{,7})
のように記述できます。これを便宜上『型を指定できる』と言ってます。
この利点は、早い段階でリクエストを不受理できることと、コントローラーでのバリデーションがほぼ不要になる点です。
型とバリデーションについては長くなるので後述します。
\%methods
\%methods
はREQUEST_METHOD
と実行コードの組または単一の関数を指定します。
{
GET => sub { ... },
POST => sub { ... },
PUT => sub { ... },
DELETE => sub { ... },
}
のように記述します。または単一の関数sub { ... }
を指定すると{ GET => sub { ... } }
に変換されます。
prefix
アプリケーションのパッケージ名がMyApp
で、コントローラーのパッケージ名がMyApp::Foo
の場合、prefix
は/foo/になります。
prefix
を調整するにはsub prefixをオーバーライドするだけです。
例えばいわゆるRootコントローラーには
sub prefix {'/'}
と記述します。Dancer inspireです。
例
package MyApp::Foo;
action bar => sub { ... }; # GET /foo/bar
action bar => { # ditto
GET => sub { ... }
};
action bar => 'bar' => { # ditto
GET => sub { ... }
};
action bar => qr/bar\z/ => { # ditto
GET => sub { ... }
};
action baz => '' => { # PUT /foo/baz and DELETE /foo/baz
PUT => sub { ... },
DELETE => sub { ... },
};
アクション
リクエスト毎に上記ルーティングでマッチしたアクションコードsub { ... }
が呼び出されます。
基本
アクションコードではreq
、res
というキーワードが使え、res
に対して応答をセットします。
action foo => qw{(?<name>[[:alpha:]]\w*)} => sub {
res->body( 'hello, ' . req->args->{name} );
};
アクションコードを実行した結果、res->body
が空の場合はビューが呼び出されます。
ビューの呼び出し時にはres->stash
がパラメータとして渡されます。
action foo => qw{(?<name>[[:alpha:]]\w*)} => sub {
res->stash->{name} = req->args->{name};
};
hello, [% name %]
Catalyst inspireですが、Catalystの場合はコンテキストオブジェクト->stash
ですので注意が必要です。
Slackの場合はレスポンスオブジェクト->stash
となります。
何故ならレスポンスを生成するために使われるから混同しないようにするためです。
req is a Slack::Request
Plack::Request
を継承し、独自のargs
、argv
というアクセサが定義されています。
args
には\%+
(\%LAST_PAREN_MATCH
)、argv
には[$1..$n]
が格納されており、ルーティングでの$pattern
でグルーピングした部分が取得出来ます。
通常のcaptureとnamed captureを混ぜた場合は、おおよそ予想通りの挙動になります。
action foo => qw{(?<name>[[:alpha:]]\w*)/(.+)} => sub {
...;
};
このアクションをGET /foo/robario/hello
と呼び出すと、argv
には[$1,$2]
、args
には{name=>$+{name}}
が格納されます。
argv: [
'robario',
'hello'
]
args: {
name => 'robario'
}
res is a Slack::Response
Plack::Response
を継承し、stash
アクセサが追加されています。
stash
にはビューに渡すパラメータをセットします。
型とバリデーション TODO: 日付の場合どうするか?うるう年とか。それとも、この項目自体消すか?
たまにコントローラーとモデルでのバリデーションの違いが話題に上がりますが、 私は「コントローラー側では型バリデーション」「モデル側では論理バリデーション」と考えています。
- コントローラー側 :: 型バリデーション
- ここでいう型は整数型・文字列型とかではなくて、UserName型・PageNumber型などのようなものです。 おのずと正規表現のみでバリデーションできるはずです。たぶん。
- モデル側 :: 論理バリデーション
- 呼び出し元で既に正しい型に変換できているので、有効範囲や存在チェックなどをする。
# イメージ
function Controller::action(page) {
# ここで字面をチェックする。
pageが/(0|[1-9]\d*)/にマッチしなければエラー
Model::search( PageNumber::new(page) )
}
function Model::search(PageNumber page) {
# 既に正しい型に変換できているわけだから、ここでは字面チェックはしない。nilチェックもしない。
pageがMAX_PAGEを超えていたらエラー
データを取得してみた結果、pageに対応するデータが無かったらエラー
}
あいにくPerlは型付けが弱いのでこの理論は中途半端な実装になってしまうんですが、考え方としての話です。