17日目: Web サービス

Jobeet のフィード追加に関して、求職者はリアルタイムで新しい求人情報が知らされます。

一方では、求人を投稿するとき、できる限りもっとも注目されたいと願います。たくさんの小さな Web サイトで求人情報が配信される場合、適任者を見つける機会が増えます。 これがロングテールの力です。今日開発するWebサービスのおかげでアフィリエイトは自身の Web サイトに投稿された最新の求人情報を公開できます。

アフィリエイト

2日目の要件です:

"ストーリー F7: アフィリエイトは現在アクティブな求人リストを読み取る"

新たなモジュール

今回は、JSON::Anyが新たに必要になります。cpanmで入れておきましょう。Makefile.PLへの追記も忘れずに。

フィクスチャ

アフィリエイト用にフィクスチャデータを作りましょう:

# sql/fixtures/default.pl
{
    my $affiliate = models('Schema::Affiliate')->create({
        url       => 'http://www.sensio-labs.com/',
        email     => 'fabien.potencier@example.com',
        is_active => 1,
    });
    $affiliate->add_to_category_affiliate({
        category_id => models('Schema::Category')->find({ name => 'Programming' })->id,
    });
}

{
    my $affiliate = models('Schema::Affiliate')->create({
        url       => 'http://www.symfony-project.org/',
        email     => 'fabien.potencier@example.org',
        is_active => 1,
    });
    $affiliate->add_to_category_affiliate({
        category_id => models('Schema::Category')->find({ name => 'Design' })->id,
    });
    $affiliate->add_to_category_affiliate({
        category_id => models('Schema::Category')->find({ name => 'Programming' })->id,
    });
}

実際のユーザーがアカウントを申し込むとき、トークンの生成が必要になります:

# Jobeet/Schema/Result/Affiliate.pm
use Digest::SHA1 qw/sha1_hex/;
use Data::UUID;

sub insert {
    my $self = shift;

    $self->token(sha1_hex(Data::UUID->new->create))
        unless $self->token;

    $self->next::method(@_);
}

デフォルトデータを作り直しておきましょう:

$ rm database.db
$ perl ./script/dev/upgrade_database.pl
$ perl ./script/dev/insert_default_data.pl

求人情報のWebサービス

新しいリソースを作成するとき、最初に URL を定義するのはよい習慣です。API 用の新しいコントローラ Jobeet::Controller::API を作成します:

package Jobeet::Controller::API;
use Ark 'Controller';

use Jobeet::Models;

sub token :Chained('/') :PathPart('api') :CaptureArgs(1) {
    my ($self, $c, $token) = @_;

    $c->detach('/default') unless length $token == 40;

    my $affiliate = models('Schema::Affiliate')->single({ token => $token })
        or $c->detach('/default');

    $c->stash->{affiliate} = $affiliate;
}

sub jobs :Chained('token') :Args(0) {
    my ($self, $c) = @_;
}

1;

このコントローラでは /api/{token}/jobs という URL を定義しています。 まず、token アクションが実行され、token 文字列が40文字でない場合とデータベースを検索して該当データがない場合、404ハンドラーへ処理を飛ばしています。

tokenアクション内で detach されなかった場合(404でない場合)はそのあとに jobs アクションが実行されます。

今回のウェブサービスは JSON 形式でデータを返すようにしてみましょう。 JSON 形式のデータを出力するためには Ark::View::JSON というビュークラスが用意されていますので、これを継承するだけでそのビューの機能を利用できます。以下のような Jobeet::View::JSON を作成します:

package Jobeet::View::JSON;
use Ark 'View::JSON';

has '+expose_stash' => (
    default => 'json',
);

1;

継承し、expose_stash という設定を変更しています。これはこのビューはデフォルトでは stash にはいっているデータすべてを JSON 形式で返すビューなので、それ以外のものを返したい場合ここでキー名を指定するようにします。ここでは json という名前を指定していますので、$c->stash->{json} の値がレンダリングされることになります。

というわけでこの json データをアクション内で埋めておきます。

sub jobs :Chained('token') :Args(0) {
    my ($self, $c) = @_;

    my $json = [];
    my $affiliate = $c->stash->{affiliate};
    my $max_rows  = models('conf')->{max_jobs_on_homepage};

    for my $category ($affiliate->categories) {
        for my $job ($category->get_active_jobs({ rows => $max_rows })->all) {
            push @$json, {
                category     => $category->name,
                type         => $job->type,
                company      => $job->company,
                url          => $job->url,
                position     => $job->position,
                location     => $job->location,
                description  => $job->description,
                how_to_apply => $job->how_to_apply,
                expires_at   => $job->expires_at->epoch,
            };
        }
    }

    $c->stash->{json} = $json;
}

このままでは '/end' に処理が渡され、そこでは View::MT に処理が飛んでしまうため、この API 階層で再度 end を定義し上書きして View::JSON に処理が飛ぶようにします。

sub end :Private {
    my ($self, $c) = @_;

    if ($c->stash->{json}) {
        $c->forward( $c->view('JSON') );
    }
    else {
        $c->forward('/end');
    }
}

Web サービスのテスト

Webサービスをテストを書いておきましょう:

use strict;
use warnings;

use Test::More;
use Jobeet::Test;
use Jobeet::Models;
use JSON;

my $datafile = models('home')->subdir(qw/sql fixtures/)->file('default.pl');
do $datafile or die $!;

{
    my $res = request( GET => '/api/foo/jobs' );
    is $res->code, 404, '404 ok';
}

{
    my $data = models('Schema::Affiliate')->single({});

    my $res = request( GET => '/api/' . $data->token . '/jobs' );
    is $res->code, 200, '200 ok';
    like $res->content_type, qr!application/json!, 'content_type ok';

    my $json = from_json($res->content);
    is $json->[0]{category}, $data->categories->first->name, 'category ok';
}

done_testing;

テストを書いたらちゃんと動かしておきましょう。

また明日

今日の宿題はこのアフィリエイトの登録フォームです。 Ark::Form によるフォームの作成の復習をしておきましょう。

明日は、JobeetのWebサイトに欠けている最後の機能である検索エンジンを実装します。