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

のらねこの気まま暮らし

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

Spicaのドキュメントの下書き

開発二部と布教用の為に下書きしてみた。

Spicaについては以下。

https://github.com/rymizuki/p5-Spica

めざしたもの

WEB APIにまつわる困ったこと

  • 構造や命名規則やフォーマットが異なる
  • リクエスト方法が異なる
  • 受け取ったデータを加工しないと再利用できない

構造や命名規則やフォーマットが異なる

  • CameCaseだったりsnake\_caseだったり、入り交じってたり
  • dateという名前が提供もとによって時間を含んだり含まなかったり
  • ミリ秒単位まであったりepochだったり
  • そもそもパラメータ名が意味不明だったり
  • JSONだったりXMLだったり


=> 自分のルールに合わせたい

  • 時間を含むなら`_at`、含まないなら`_on`
  • DateTimeモジュールで利用可能な形式で使いたい
  • パラメータ名は仕様書読まなくてもなんとなくわかるようにしたい


リクエスト方法が異なる

  • データ取得するだけなのにPOSTリクエスト
  • データをXMLシリアライズ
  • URLにパラメータ埋め込む
  • ヘッダーにはコレ入れてね


=> どれが何を要求してるのかわからなくなる

  • APIや提供元毎に同じ書式で記述したい

受け取ったデータを加工しないと再利用できない

  • XMLを解析
  • 正規表現で不要な文字列を削除
  • ミリ秒に0.001掛けて秒に直す
  • base64をデコードする


=> Controllerとかビジネスロジックにそういうのは書きたくない

  • APIのクライアント側で利用可能なデータに整形して欲しい
  • DB系モジュールのinflateの機能が欲しい
  • RowClassで拡張して複雑な処理も柔軟に対応できるようにしたい

いままでの自作APIクライアント達

  • FurlやLWPでフェッチしてくる
  • XML::SimpleやJSONモジュールでパース
  • HashRefやArrayRefをそれぞれそのままモデルやControllerへ受け渡し
  • 必要に応じて受け取り側で解析
  • どこで何やってるのかわからなくなる
  • 何がどんなデータ持ってるのかわからなくなる
  • 複数のAPIの仕様が混線して仕様書から漁ってくる
  • APIってなんだ(ゲシュタルト崩壊)

Spica

異なるリクエスト方法をまとめて定義


Spica::SpecでAPIのリクエスト方法や受け取り方を指定できる

package Servers::Spec;
use Spica::Spec::Declare;
 
client {
    name 'service';
    endpoint 'list' => '/servers', [];
    endpoint 'single' => '/service/{service_id}' => ['service_id'];
    endpoint 'update' => +{
        method   => 'PUT',
        path     => '/service/{service_id}',
        requires => ['service_id'],
    };
    columns qw(id ip hostname name environment port description roles config updateed_at created_at);
};


Spicaからclientやendpoinを指定してAPIを叩く

my $spica = Spica->new(host => 'api.servers.dev.ry-m.com');
$spica->spec('Servers::Spec');
$spica->parser('Spica::Parser::JSON');
 
my $services = $spica->fetch(service => 'list', +{}); # iterator
 
my $serivce = $spica->fetch(service => 'single', +{service_id => '0xEE763766EC2C11E2B20E9F176D21D1C2'})->next;
 
$service = $spica->fetch(service => 'update', +{
    service_id  => $serivce->id,
    description => 'Jenkinsのサービスだよ',
});

受け取ったデータのinflate


Tengと同様のI/Fでinflateを定義できる

package Servers::Spec;
use Spica::Spec::Declare;
 
use DateTimeX::Factory;
 
client {
    name 'service';
    endpoint 'single' => '/servers', [];
    columns qw(id ip hostname name environment port description roles config updateed_at created_at);
    inflate qr{_at$} => sub {
        my $value  = shift;
        return if !$value;
        return if $value eq '0000-00-00 00:00:00';
        return DateTimeX::Factory->new(time_zone => 'Asia/Tokyo')->strptime($value => '%F');
    };
};


columnの末尾に`_at`がつくものをDateTimeのオブジェクトに変換している。

my $service = $spica->fetch(service => 'list', +{})->next;
 
say $service->created_at->ymd('/');

受け取ったデータがSpicaの想定しないない構造をしていた


初期値のSpicaはデータの受取をSpica::Receiver::Iteratorが行う。
Spica::Receiver::Iteratorは受け取るデータとして`ArrayRef`を期待するが、
以下のような構造が返ってくることが在る

+{
    data => \@data,
    pager => \%pager_args,
}


そのような場合、以下の二点で対策が可能

  • Spica::Receiver::Iteratorのnewメソッドを拡張する
  • Spica::Clientにfilterを登録する

Spica::Receiver::Iteratorのnewメソッドを拡張する


下記の例では、pagerのデータを捨てているが、もちろんIterator側にpagerのメソッドを用意してもよい。

package Servers::Spec;
use Spica::Spec::Declare;
 
client {
    name 'service';
    endpoint 'list' => '/servers', [];
    columns qw(id ip hostname name environment port description roles config updateed_at created_at);
    receiver 'Servers::Receiver::Iterator';
};
 
package Servers::Receiver::Iterator;
use parent qw(Spica::Receiver::Iterator);
 
use Class::Method::Modifieres;
 
around new => sub {
    my $origin = shift;
    my $class  = shift;
    my %args   = @_;
 
    $args{data} = $args{data}{data};
 
    return $class->$origin(%args);
};
 
1;

Spica::Clientにfilterを登録する


filterは第一引数にフックポイント名、第二引数に実行するCodeRefを期待する。
フックポイントで呼び出されたCodeRefはreceiverクラスに渡すデータを返却しなければならない。
この場合はpagerを切り捨てるしか無い。

package Servers::Spec;
use Spica::Spec::Declare;
 
client {
    name 'service';
    endpoint 'list' => '/servers', [];
    columns qw(id ip hostname name environment port description roles config updateed_at created_at);
    filter before_receive => sub {
        my ($spica, $data) = @_;
        return $data->{data};
    };
};


※ フックポイント名は今後変更する可能性があります。

APIステータスコードがエラーだったら死にたい


Spica::Clientにtrigger、もしくはfilterを登録する。
triggerの登録はfilterと同様に、第一引数にフックポイント名、第二引数にCodeRefを期待する。
triggerは返り値を考慮しない点がfilterと異なる。

package Server::Spec;
use Spica::Spec::Declare;
 
client {
    name 'service';
    endpoint 'list' => '/servers', [];
    columns qw(id ip hostname name environment port description roles config updateed_at created_at);
    trigger before_receive => sub {
        my ($spica, $data) = @_;
 
        if ($data->{status} ne 'success') {
            Carp::croak('API retruned error. reason is '. $data->{reason});
        }
    };
};


※ フックポイント名は今後変更する可能性があります。