Spicaのドキュメントの下書き
開発二部と布教用の為に下書きしてみた。
Spicaについては以下。
https://github.com/rymizuki/p5-Spica
WEB APIにまつわる困ったこと
- 構造や命名規則やフォーマットが異なる
- リクエスト方法が異なる
- 受け取ったデータを加工しないと再利用できない
構造や命名規則やフォーマットが異なる
- CameCaseだったりsnake\_caseだったり、入り交じってたり
- dateという名前が提供もとによって時間を含んだり含まなかったり
- ミリ秒単位まであったりepochだったり
- そもそもパラメータ名が意味不明だったり
- JSONだったりXMLだったり
=> 自分のルールに合わせたい
- 時間を含むなら`_at`、含まないなら`_on`
- DateTimeモジュールで利用可能な形式で使いたい
- パラメータ名は仕様書読まなくてもなんとなくわかるようにしたい
いままでの自作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}); } }; };
※ フックポイント名は今後変更する可能性があります。