日向夏特殊応援部隊

俺様向けメモ

Catalyst::Controller::Atompubのdispatch、Slugヘッダとか

Atompubの勉強始めました。

Catalyst::Controller::Atompubについて

こちらはid:teahut*1さん自身がPerl(Catalyst)でAtompubサーバーを作る解説記事を書かれていますので、そちらを見ると大体分かると思います。

で今の所、分かった事とか困ってる事とか書いてみる事にします。

URLが基本的にpackage名で固定されてしまう。

例えば、コレクションFeedを取得したい場合はコレクションリソースURIに対してGETするんですけど、そのCatalystアクションの書き方は、

sub get_feed :Atompub(list) {
  # implements
}

みたいに書くんですが、これが仮にpackage MyApp::Foo::Collectionだとすると/foo/collectionに固定されてしまいます。

と言うのもCatalyst::Controller::Atompub::Collectionにて、

  • create_action()にてAtompub(xxx)みたいなattributeがある場合はxxxに応じたhandlerとしてCODEREFを保持
  • do_xxx(list, read, create, update, delete)を生成。この際、実行される実体は先ほど保持したCODEREF
  • dispatchは基本的にdefault, edit_uriで行い、ここがURIが決めうちになる原因となっている。それぞれHTTP Methodに応じて適切な実体(do_xxx)を呼び出すようになっている

なので、自前のURIとして例えば、/foo//collection みたいな物を提供したいなと思った場合はかなり力技に頼らねばならない。

力技とは、大体こんな感じ。

まず前提としてコレクションリソースURIにアクセスしたのかエントリリソースURIにアクセスしたかで、起動するメソッドが違う

  1. コレクションURI - default()
    1. GET, HEAD - _list()
    2. POST - _create()
  2. リソースURI - edit_uri()
    1. GET, HEAD - read()
    2. POST - _create()
    3. PUT - _update()
    4. DELETE - _delete()

で、default, edit_uriのdispatchが固定されてるんだから上書きしちゃえば良い。

sub default :Regex('^foo/(\w+)/collection') {
  my ($self, $c) = @_;
  $self->NEXT::default($c);
}

sub edit_uri: Regex('^foo/(\w[\w_-]+\w)/collection/([^-?&#][^?&#]*)') {
  my ($self, $c) = @_;
  $self->NEXT::edit_uri($c);
}

そして、その後はdo_xxxを直接実装しちゃうのがいいのかなーと思います。
多分この辺りはたけまるさんやid:ikasam_aが適切な補足をしてくれると思います。

たけまるさんの指摘を元に$c->NEXT::xxxを$self->NEXT::xxxに修正しました。(2008-03-11T23:11:41+09:00)

ところで、

なお,ZIGOROu さんの例では,default メソッドを先に定義していますが, 探索順の関係から edit_uri を先にしてください

これってどういう事でしょう。ちょっと僕には分からなかったです。

追記 (2008-03-11T20:05:55+09:00)

id:ikasam_a反応してくれたです。なるほどChainedアクションですか。
先の例だと、

sub user: Chained PathPart('foo') CaptureArgs(1) {
  my ($self, $c) = @_;
}

sub default: Chained('user') PathPart('collection') {
  my ($self, $c) = @_;
}

sub edit_uri: Chained('user') PathPart('collection') Args(1) {
  my ($self, $c) = @_;

}

みたいな感じかな。

Catalyst::Controller::Resourcesも期待ageですね。

割り当てられるエントリリソースURIについて

この辺りはCatalyst::Controller::Atompub::Collectionのmake_edit_uri()にて実装されています。ソース見た方が早いので拝借。コメントは僕が勝手につけました。

sub make_edit_uri {
    my ( $self, $c, @args ) = @_;

    # XML::Atom::Feedオブジェクト
    my $collection_uri = $self->info->get( $c, $self )->href;

    my $basename;
    if ( my $slug = $c->req->slug ) { # Slugヘッダがある場合
        my $slug = uri_unescape $slug;
        $slug =~ s/^\s+//;
        $slug =~ s/\s+$//;
        $slug =~ s/[.\s]+/_/;
        $basename = uri_escape lc $slug;
    }
    else { # Slugヘッダが無い場合
        my ( $sec, $usec ) = gettimeofday;
        $basename = join '-', strftime( '%Y%m%d-%H%M%S', localtime($sec) ),
            sprintf( '%06d', $usec );
    }

    my @media_types = map { media_type($_) } ( 'entry', @args );

    my @uris;
    for my $media_type (@media_types) {
        my $ext = $media_type->extension || 'bin';
        my $name = join '.', $basename, $ext;
        push @uris, join '/', $collection_uri, $name;
    }

    return wantarray ? @uris : $uris[0];
}

って訳でSlugヘッダがあるか無いかで命名規則が違う実装みたいです。
この辺りはRFC 5023 Atom Publishing Protocol 日本語訳 - Slugヘッダを見ると良く分かります。

Slug は HTTP エンティティヘッダであり、コレクションに POST が行われるときのこのヘッダの存在は、これから作成されるエントリないしメディアリソースを参照するために通常使用される URI の一部としてそのヘッダの値を使ってほしい、というクライアントの要求を示す。 新しく作成されたリソースのメンバ URI を作るとき、サーバは Slug ヘッダの値を使ってもよい(MAY)。たとえば、最後の URI セグメントにその値の中の単語の一部、あるいはすべてを使う。また、atom:id を作成するとき、あるいはメディアリンクエントリのタイトルとしてその値を使ってもよい(MAY)(9.6 節参照)。 サーバは Slug エンティティヘッダを無視することを選択してもよい(MAY)。サーバはそれを使う前に、ヘッダの値を変えるかもしれない(MAY)。たとえば、サーバはある文字を取り除くか、あるいは強調されていない文字を強調された文字に置き換えるか、あるいは下線をスペースで置き換えるかもしれない。

なので、Slugヘッダがあった場合は新しいエントリないしはメディアリソースのURIの一部として使用しても構わないと。

Atompub::Clientを使ってcreateEntryする場合は、

#!/usr/bin/perl

use strict;
use warnings;

use XML::Atom::Entry;
use Atompub::Client;

sub create_entry {
    my ($title, $content) = @_;
    my $entry = XML::Atom::Entry->new;
    $entry->title($title);
    $entry->content($content);
    return $entry;
}

sub create {
    my ($collection_uri, $title, $content) = @_;
    my $client = Atompub::Client->new;
    return $client->createEntry(
        $collection_uri, 
        create_entry($title, $content), 
        $title ### ここがSlugヘッダの値になる
    );
}

print create(@ARGV);

みたいなソースになる。

*1:たけまるさん