北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

Router::Simpleのソースを読む

最近モダンPerlのソースを読んでなかったので、ぼちぼちソースを読んでみようかと。ってことで、Router::Simpleの0.09です。

まともに使ったこと無いのにソースだけ読んでるので、間違ってたらごめんなさい(これから使うつもりだけど)。

概要

Router::Simple がAPIの口。実際のマッチングをやってるのはRouter::Simple::Route で、このクラスが一番重要。Router::Simple::SubMapper は階層的なマッピングを実現するクラスだけど、こいつの中身は単なるシンタックスシュガー。Router::Simple::Declare はDSL的なインタフェースの提供。

Router::Simple

connect する度に Router::Simple::Route を生成して保持する。マッチング処理もそのまま Router::Simple::Route に委譲しているだけ。ただし、matchの引数が$envでなくPATH文字列でも動くってのは、このクラスが違いを吸収している。

submapper も Router::Simple::SubMapper に処理を委譲しているだけ。

Router::Simple::Route

メインのロジック。コンストラクタでパターン($pattern)をコンパイルかける。{year:\d{4}}などの表現をs///gex; で置換かける際に置換ブロックにがっつりコードを書いて、正規表現に変更しながら必要な名前をキャプチャしていくのは面白い。

do {
    ...snip...
    $pattern =~ s!
        \{((?:\{[0-9,]+\}|[^{}]+)+)\} | # /blog/{year:\d{4}}
        :([A-Za-z0-9_]+)              | # /blog/:year
        (\*)                          | # /blog/*/*
        ([^{:*]+)                       # normal string
    !
        if ($1) {
            my ($name, $pattern) = split /:/, $1, 2;
            push @capture, $name;
            $pattern ? "($pattern)" : "([^/]+)";
        } elsif ($2) {
            push @capture, $2;
            "([^/]+)";
        } elsif (...) {
        ...snip...
        }
    !gex;
    qr{^$pattern$};
};

0.09 の実装では、$pattern内の*をキャプチャするために '__splat__' という文字列を内部的に使っているので、'/:__splat__/*' のような$patternを使うと直感に反する動作になる。

Router::Simple::SubMapper

どうやって実現してるのかなーと思ったら、親のconnectを呼んでるだけ。submapper呼ばれた段階ではSubMapper に引数を全部保存しておいて、その後SubMapper もconnectが呼ばれたら保存しておいた引数と渡された引数をmixして親のrouterへconnectしている。

0.09 の実装ではsubmapperの入れ子は考慮されてない。また、親のconnectを呼ぶ際にname引数を渡す仕組みがないので、名前付きのRouteは作れない。

Router::Simple::Declare

DSL提供している。routerブロック内でだけ $Router::Simple::Declare::_ROUTER に値が入っているようにしておいて、そのブロック内で connect と submapper を呼んだときは全部 $_ROUTER に対して呼び出されるというよくある仕組み。