underscore-rust作ってみた。あとrust書いてみた動機とか
underscore-rust
underscore-rust 作った。名前から分かる通り、underscore.js の機能を rust にしたもの。
ライブラリ自体のドキュメントは上記に入れたつもりなので、必要であれば参照して欲しい。ポイントとしてはオリジナルの underscore.js のように _
の構造体みたいなものを用意するのではなく、標準の struct(Vec
, TreeMap
, HashMap
)にそのまま underscore 系の関数を追加した点。use すれば普通の API と同じ感覚で使うことができる。
作ったみたが、インターフェイスについては結構後悔している部分が多い。例えば HashMap の拡張は
fn invert(self) -> HashMap<V, K>;
とかにしてしまったけどテストしづらかったとかいう理由で ownership 取ってしまったので(ownership については後述)そこに関しては
fn invert(&'a self) -> HashMap<&'a V, &'a K>;
とかにすべきだったかなと思っている。まぁ、同じ型のものが返るので不都合はなさそうだしこっち方が扱いやすくていいだろと考えたのでこういうインターフェイスにしたのだが、オーバーヘッドが大きくなってしまうのは事実だ。所詮初学者が作ったものなので見る人から見ればおかしい部分が多分に含まれているはずで、その辺りの指摘は甘んじて受けていきたい。
作った動機
特に underscore-rust を作ってみたい実用的な用事があったわけではなく、単純に rust を書いてみたくて、教材として便利だったので作ってみた。underscore系のライブラリは、一体何番煎じか知らないが、とりあえず新しい言語を覚える際には良い教材だと思う。難易度は低いし、割と有益なものを作れる。javascript を引き合いに両者の違いを感じることができる点も良い。
自分は普段 LL しか使わない完全なゆとりエンジニアだが、rust はそういった層により低レイヤーを学ばせる用途に向いている。LL とかそれより上流のコミュニケーション一式が主戦場であっても、パフォーマンスチューニングだとかインフラの冗長化を見積もったりする段階で低レイヤーにまで手を伸ばさなければならない場合は多い。ライブラリの選定で知識が必要な時もある。あるいはもっと単純に、低いレイヤーのことを知るのは物事の道理を学ぶにあたって有益だと思う。 最近はインフラ層の技術がより LL がメインの人たちに近づいてきた印象があり、自分もそういうものと付き合っていかなければないので、この辺りで一度レイヤーが低いっぽい言語に触れておきたかった。
rust に手を出してみたきっかけはこういった気持ちがあったからだけど、これより前は同じような理由で go を触ってみたりしていた。ただ、GC が支配的な世界観でなんとなく違う気がして離れてしまった。rust のメモリ管理は基本人間が頑張るのでそういうのに惹かれたんだと思う。
こう書くと C とか C++ とかやればいいという完全に真当なことを言われそうなのだが、何か一つ知らない言語を選べといわれたらそれなりに新しいものを選びたくなる程度にはミーハーなので正直やる気がおきなかった。あとかなり前に読んだ「ふつうのプログラマのための haskwell〜」とか「7つの言語〜」とかで関数型言語は確かに綺麗だなあという気持ちがチラついたのもある。純粋な興味だけで言ったら関数型言語をガッツリやってみたかったのだが、いくらかでも目の前の課題とか明日のメシと地続きにする知識を得ようとしたら、関数型っぽい風味のある rust に手を出すというのは一つの着地点としてアリだった。
rust のちょっといい話
※この項は特に間違えている可能性が多く含まれるので気をつけて下さい
ポジショントークだけだとさすがにアレなので、具体的に rust のちょっといい話を考えてみた。例えば C でこんなコードを書いたとする。
struct hoge { int num; }; int* pick_num(struct hoge *h) { int *copy = &h->num; free(&h); return copy; } int main(void) { struct hoge *x; x = (struct hoge *) malloc (sizeof(struct hoge)); x->num = 5; int *y = pick_num(x); printf("%i\n", x->num); printf("%i\n", *y); return 0; }
上記のコードだと y
は free された値を参照してしまうダンジリングポインタとなる。これはコンパイルは通るが、実行すると落ちる。このコードを rust に置き換えてみる。
struct Hoge { num: int } fn pick_num(x: Box<Hoge>) -> int { x.num } // rust の場合、ブロックを抜けるときに常にメモリ解放されるので RAII は意識しなくていい fn main() { let x = box Hoge { num: 5i }; let y = pick_num(x); println!("{}", x.num); println!("{}", y); }
これをコンパイルしようとすると、以下のようにエラーを吐いてそもそもコンパイルできない。
hoge.rs:13:20: 13:25 error: use of moved value: `x.num` hoge.rs:13 println!("{}", x.num); ^~~~~ note: in expansion of format_args! <std macros>:2:23: 2:77 note: expansion site <std macros>:1:1: 3:2 note: in expansion of println! hoge.rs:13:5: 13:27 note: expansion site hoge.rs:11:22: 11:23 note: `x` moved here because it has type `Box<Hoge>`, which is non-copyable (perhaps you meant to use clone()?) hoge.rs:11 let y = pick_num(x); ^ error: aborting due to previous error
例に挙げた C のコードが阿呆すぎるような気もするが、とにかく、このようにコンパイルの段階で危険なポインタを教えてくれるのが rust の利点の一つだ。
これを支えているのは ownership,borrow,lifetime という概念で、以下に非常に雑な説明をすると
- 全ての変数に ownership が設けられる
- ownership を持つものだけがその変数へのアクセス権を持つ
- 変数の ownership は borrow することができる
- borrower は ownership を取らずに変数を使用できる
- ただし、borrower はメモリ解放や値の書き込みを行うことができない
- borrow には lifetime が設けられる
- コンパイラが自動で付加してくれるので普段はあまり意識しなくてよいが、参照を返す際などは明示的に lifetime を宣言して返したりする
とかになるが、ownership,borrow,lifetime は変数が mutable だった場合の挙動とかが少しずつ違ったりして一口に説明するのが難しいので、できれば公式ドキュメントの ここ とか ここ を読んでみて欲しい。
今回の例だと、let x
の ownership を pick_num
関数に移譲してしまっている。そのため、再度 x
にアクセスしようとしてもできない。こういうコードの場合、pick_num
の引数は単に borrow してくるのが普通である(はず)。
参考になったもの
rust を学ぶ上で役に立ったものを挙げてみる。
- http://doc.rust-lang.org/
- やはり公式ドキュメント。基礎がしっかりある人なら1日あれば読めるかもしれない。自分はかなり時間がかかった。
- http://rustbyexample.com/
- tour of go みたいなもの。tour of go ほど体系立ってるわけではないが、公式ドキュメントからの概念的な説明と並行しながら見るといいと思う。
- http://doc.rust-lang.org/nightly/std/(https://github.com/rust-lang/rust)
- 実際に rust で何か作っていこうとなったとき、rust way がわからない。そんな時に組み込みAPIの実装を読めて強力。一般に流通している rust ライブラリよりも綺麗に設計されている。
- http://rustforrubyists.com/
- rubyist 向けの rust 導入記事。今思うと大したこと書いてない気がするのだが、導入には良さそう。何より自分のようなゆとりでも門戸が開かれている事実が嬉しかった。
効果の高いテストについて考える
テストエンジニアという奇異な立場にいる。 普通にプロダクトメンバーの一員だが、プロダクト自体のコードはあまり書かず、品質という観点から良かれと思ったことをする。大体グーグルのテスト本に載っているSETをロールモデルとしている。
- 作者: ジェームズ・ウィテカー,ジェーソン・アーボン,ジェフ・キャローロ,長尾高弘
- 出版社/メーカー: 日経BP社
- 発売日: 2013/05/23
- メディア: 単行本
- この商品を含むブログ (8件) を見る
SWTは、例えばエンジニアがテストを書きやすいようにライブラリを作ったり、テストがリリースのネックにならないように高速化したり、手動テストを支援するようなサポートツールを作ったりするのが役割となる。普通に単体テストも書く。(が、それはあまり理想ではなくて、本当はそのコードを書いた人が単体テストも書くべきだ。)
しかし、現実世界であまりそういう人を見なくて、先人の知恵を借りられずやきもきしている。もしかしたら同じことを感じている人がいるかもしれないと思ったので、何かのたしになればと思い、最近考えたことをメモしておく。
効果の高いテストと低いテストがある
テストには効果の高い / 低いがあり、効果の低いテストを書いてしまうと、テスト自体のメンテナンスコストの方が大きくなり、開発を阻害する要因となる。まずはこの点について認識を持つべきだと思う。
効果の低いテスト
例えば、セッターとゲッターしかないクラスにテストを書いたとする。ここにはロジックが含まれていないので、テストする価値はほとんどない。だが実際にCIしていると、意外と落ちたりする。落ちる原因はプロパティ名が変わったとかそんな理由である。そしてプロダクトコードではなくテストの方を修正する。テストコードを修正しなければならないのは、言い換えれば良いテストができていなかったと言えるのではないかと思っている。
少し話は逸れるが、TDD がいまいち浸透せず、アイツは死んだとか言われるのは、こういうトレードオフに対して無自覚だからだと思う。TDD はいつもテストを書くことを強制する。つまり、効果の低いテストも書かなければならない。こういった非効率は現場では許容できず、自然淘汰される。TDD は死んだと言われて久しいが、では次にどうあるべきかというのがなかなか出てこない。それは、「テストには効果が高い / 低いが存在する」というスタート地点が共通認識として持てていないからのように思う。
効果の高いテスト
数値計算や正規表現などを扱う単体テストは効果が高い。例えばゲームだと、レベルアップ・ダメージ計算・エネミーエンカウント・アイテムドロップ等のロジックなどがこれにあたる。こういう部分は手動でテストしづらいので機械からテストしたほうがよっぽど効果が高い。もう少し抽象的な言い方をすると、効果が高いのは、入力に対する出力がわかりづらいロジックのテストだ。そりゃそうだろという話になるが、ではどこからが「数値計算的な部分のテスト」でどこからが「セッターとゲッター的な部分のテスト」なのか、線引きが難しい。この線引きを上手く行えるかどうかが勝負という感じがする。
また、APIの結合テストのようなものも効果が高い。単純に、少ないテストコードで多くの部分をカバーできる。ただし、細かな状態まで結合テストでカバーしようとするのは悪手で、色々やり過ぎるとある時点で管理コストがテストによるメリットを凌駕し、効果の低いテストに転換する。
カバレッジ意味なし
どれだけ効果が高いテストができているかは、カバレッジでは測れない。カバレッジが高いからといって十分な品質が保たれていることにはならないし、世間で思われているよりもそれらの相関はずっと低い。というか、カバレッジ至上主義に陥るとセッターとゲッターのテストを書き始めるのでたちが悪かったりする。もうすっかりカバレッジ90%ですとか言われても感動しない体になった。
モバイル端末操作の自動化しんどい
一昔前から selenium でPCブラウザ操作の自動化を行う流れはあったが、これをさらに発展させ、モバイル端末でも行えるようしようという試みがある。代表的なツールとしては、appium, calabash などが挙げられる。これらのツールは、最近はまずまず安定して動くようになった。しかし、ツールがどうこうというより、モバイルをとりまく環境が複雑すぎて、総合的な安定性に欠ける。その部分について少し書きたい。
第一に、appium や calabash といったツールはサードパーティ製だというのが致命的だ。こうすると、どうしても最新のOSに追従するのが後手になる。最新OSのリリースが終わった後に対応完了というのはザラであるどころか、リリース後一週間以内に対応できれば御の字で、確か iOS7 の時は cocoa 側にバグがありしばらく対応できなかった思い出がある。大体最新OSリリース前にテストを済ませたいはずなので、これでは要件を満たせないのではないだろうか。また、IAPだと、自動化できないようにわざわざ課金のダイアログを押せなくしていたりする。そういうのもサードパーティならではの悩みである。では UIAutomation 使えという話になるがそれはそれそれで、じゃあなんで appium とか calabash って開発されたんでしたっけという話になる。
次に、やれ selenium だ、appium だといっても中で書いているのは所詮スクレイピングである。出来上がるコードが元々辛い。ちょっとしたレイアウト変更でエレメントが取れなくなり、テストが失敗するのはよくある話だ。モバイルの場合、この上にさらにモバイル特有の例外処理を載せていくので最終的に本当に辛いコードになる。例えば端末のスペック差を吸収するため sleep を書きまくったりする。さらにきわまってくると、Android というのは本当にクソ端末多くて、突然 wifi が切れてテストが失敗したりする。ここまでくるともうどうしようもない。
追記
sleepせずにある要素が出るまで待ち続ける処理があるのでそれを使えば良いというコメントを頂きました。
確かにそうなのですが、私が appium を触っていた時はエレメント探索の関数が非常にバギーで使い物にならず、javascript のコード生で渡して実行させる手法を多用していました。この手法を使う限り sleep に頼らざるを得なかったのですが、今は状況が改善しているかもしれません。
そもそも受け入れテスト自動化に本当に向き合うなら、デジタルハーツやポールトゥウィンなんていう会社が世に存在する意味を真剣に考えるべきだ。そういった会社のテスターの練度は高く、例えば格ゲーなら格ゲーに知見のある人がいて、音ゲーなら音ゲーに知見のある人がいる。そういう人はテストの勘所がわかっており、仕様ドキュメントを渡すだけで綿密なテストケースを組んでくれる。コンピュータがそこまで追いつくのは何十年か先の話だろうと思う。また、画面を見てそれが正しい状態なのか間違った状態なのか判断させるのは、正直機械ではなく人間がやった方が手っ取り早い。属人性の極みみたいな所はあるが、現状を鑑みた上で現場でどちらか選べと言われたら正直人間のほうを選びたくなる。
まぁそんな調子なので、今は色々な障壁を乗り越える & メンテする覚悟があるとか、よっぽど何回も繰り返される部分のテストをターゲットにするとかすれば(こういう場合「殺虫剤のパラドックス」という言葉が頭をよぎるが)なんとか元は取れるかなという感覚でいる。もしくは、人間と機械の共存を模索し、完全自動化ではなく人間をサポートする役割にとどめるのが合理的ではないかと思う。
渋川剛気
本当に品質の高いプロダクトを作りたいなら、設計で地雷を踏まないのが一番良い。
ああきたらこう捌く、こうきたらこう捌くなんてというのを考えていたら下の下で、真に護身を身に付けた者であれば、もはや技術は無用であり、そもそも危機に近付くことすらできないのだと思う。
一日の行動をEvernoteにpostするサイト作った
前に gem 作ったんだけど、せっかくなので他の人でも使えるように web アプリにした。いくつかの web サービスのアクティビティをまとめて evernote に上げる。
例えば twitter 認証をすればその日のつぶやきを集めるし、github 認証すればその日のコミットを集めて、それらをまとめて evernote に上げる。
技術的に特筆すべきようなことは特にない普通の rails アプリだけど、強いて何かいうなら こっち でやたら丁寧なコードを書いた反動で雑に作りたくなって、とにかく雑に作ったこととか、タダで済ませたかったので queue サーバーと DB サーバーは別に分けたこととか、moves 認証はスマートフォンなのでレスポンシブデザインにするのが面倒だった、とか。
よかったら使ってみてください
追記:
ちょうど書くのが面倒だった部分に質問もらえたので載せておきます
@r7kamura クロールはしてない。queueサーバーでsidekiq使っててそっちにpostしてる
— 完結編 (@gong023) 2014, 6月 25
@r7kamura そう。例えばツイートとかはリクエストがあってはじめて取得しにいく。けどそういうジョブはqueueにつっこんで非同期にやるって感じ
— 完結編 (@gong023) 2014, 6月 25
クソgemできた
これ
最近、自分が何をしていたのかわからなくなる。 一ヶ月、二ヶ月前ははるか昔のように思うけれど、一年前を思い返してみるとあまり今と変わっていないように思う。 例えるなら水の中を手で掻き分けて進むような時間感覚で、毎日息を止めて自分なりに必死にもがくのだけれど、ふと省みると理想とは程遠い距離しか進めていないことに気づく。
当たり前だが、何かをした分だけ時間というものは失われている。自分が何を選択して時間を過ごしているのか、放っておくと無自覚で、そのくせ現在の自分の責任が過去にある事に気づいてそのたびに辛い思いをする。もっと辛いのは、残り時間で自分ができることも大抵予想がついてしまうことで、これは数ある現実の中でも最も辛い部類に入る。タイムリープするアニメには刺さる名作が多いし、人間時間という概念に強い感情を抱く生き物ではとかいう気さえしてくる。
こういう話は普段あまり思い出さずにいれるが、それこそ時間を追うごとに看過できない問題になってきた実感がある。気づいたら二十代も半分を過ぎてしまっていて、もしこの後真人間っぽいライフプランを描くのならば(別にそんな予定はないが)、やれることが残り少ないのはもう目に見えている。なので毎日の行動をなるべく記録して、後で見返せたほうが良いと思った。
そういうわけでこのスクリプトを書いた。自分のネット上の行動を取得して、最終的にこんな感じのものを Evernote にポストする。ちなみに日記も少し書いてみたが、全く続かなかったし、たまに嘘書いてるので使い物にならなかった。
今対応しているのは上記の通りで、それぞれデイリーのアクティビティを取得している。こういう調子で動かしている
require 'everlog' everlog = Everlog::Daily.new everlog.push(:github, { access_secret: access_secret }) everlog.publish('title', access_secret, 'production')
ライブラリにするほど抽象度が高くないのでこれは紛れも無いクソ gem だが、DDD に則ってコードを書いたおかげで勉強になったり、毎日の出社時間が明らかになって詰められたりとか思い出深い感じだったので記念に gem にした。ちなみに一部 OAuth で token を払い出せないものもあるが、サービスが rails でセッションが cookie だったりするアレだと少しうまくやれば API として使えて便利。
アノテーションとAPIレスポンスをアサーションできるライブラリ作った
概要
アノテーションと API レスポンスをアサーションできるようにするPHPのライブラリを作った。
アノテーションは、swagger のフォーマットを基準にしている。 swagger の説明は別に書いた ので、必要であれば参照してほしい。
インストール
composer からインストールする
"require-dev": { "gong023/swagger-assert": "dev-master" }
なお、PHP5.4 以上必須。
実例
以下の様な API があり、そこに swagger のフォーマットでアノテーションが書かれていたとする。
swagger-assert を使うと、/plain
のレスポンスが、swagger の記述どおり id
と name
キーを持っているかをアサーションできる。
<?php ** * @SWG\Resource( * resourcePath="plain", * @SWG\Api( * path="/plain", * description="plain api structure", * @SWG\Operation( * method="GET",type="SimpleMember",nickname="plain" * ) * ) * ) * * @SWG\Model( * id="SimpleMember", * @SWG\Property(name="id", type="integer", required=true, description="user id"), * @SWG\Property(name="name", type="string", required=true, description="user name") * ) */ $app->get('/plain', function () use ($app) { $response = [ 'id' => 0, 'name' => 'kohsaka' ]; return $app->json($response); });
使うには、まずテスト開始時に、SwaggerAssert::analyze
を呼ぶ。引数には、アノテーションを書いたファイルがあるディレクトリパスを渡す。
<?php // testing bootstrap.php \SwaggerAssert\SwaggerAssert::analyze($targetDir);
続いて、テストクラスで SwaggerAssert::responseHasSwaggerKeys
を呼ぶ。引数に必要な情報は以下である。
- 第一引数:API レスポンスの配列
- 第二引数:リクエストの HTTP メソッド名
- 第三引数:リクエストのエンドポイントの URL 名
PHPUnit を使って先述の API をテストする場合、以下の様なコードになる。
<?php class PlainApiTest extends \PHPUnit_Framework_TestCase { public function testResponseHasSwaggerKeys() { $response = $this->request('get', '/plain'); $result = \SwaggerAssert::responseHasSwaggerKeys(array $response, 'get', '/plain', $onlyRequired = true); $this->assertTrue($result); } }
\SwaggerAssert::responseHasSwaggerKeys
は API レスポンスのキーと swagger に記述されたキーを比較し、一致した場合は true を返す。一致しない場合は以下のようにエラーメッセージを出力する。
SwaggerAssert\Exception\CompareException: Failed asserting that API response and swagger document are equal. --- Response +++ Swagger @@ @@ Array ( - 0 => 'id' - 1 => 'name' + 0 => 'name' )
なお、引数の四つ目はオプションである。 false を渡すと swagger で required=false になっている必須レスポンスでないキーもアサーションの対象にする。デフォルト値は true になっている。
動機
開発に swagger を導入しているプロジェクトがある。 swagger の導入により API の仕様を記述するための統一されたフォーマットが提供され、またアノテーションという形でコードに近い場所で仕様が読めるようになった。これらのおかげでドキュメント事情は wiki で管理するよりも良くなった。
一方で、swagger の内容と実際の API レスポンスが一致しないというトラブルもよく起きた。食い違いがあると、ドキュメントは混乱の種となり逆に開発を阻害してしまう場合すらある。
これを解消するため、API の結合テスト時、レスポンスが swagger の内容と一致しているかどうかをアサーションできるライブラリを作成した。
雑感
すごく雑に思ったことを書く。
swagger-assert と autodoc
autodoc というグレートな gem があり、これはテスト中のリクエスト及びレスポンスをそのまま markdown に吐き出してくれる。 このアプローチを取る限り、ドキュメントとレスポンスに食い違いが生じることはない。 つまり単純に「ドキュメントとレスポンスが食い違う」という問題を解決したいのならば、autodoc のようなアプローチをとるべきであり、swagger-assert のアプローチは非常に筋が悪い。
一方で、swagger に厳しいほど色々なパラメータが用意されているお陰で、アサーションを楽に行えるケースもある。 例えば autodoc のようなアプローチだとレスポンスパラメータの型を知ることはかなり困難だが、swagger からなら簡単に導き出せる。 また、ご存知の通り卓球ハウスには id:r7kamura がいるので色々聞いてみたところ、テストクラスに決まったフォーマットを要求するため既存のプロジェクトに導入への面倒くさい、とかいくつか欠点もあるらしい。 そういった意味ではどこにでもアノテーションさえあれば良い swagger-assert の方が導入は楽ではある。
あとはそもそもドキュメントとして swagger の方が圧倒的に情報量が多いとかそういうメリットもあるが、代わりにアノテーションの仕様について覚える手間もある。 総じて、低いコストで十分なドキュメントを得られる autodoc と、学習コストは高いが詳細なドキュメントを得られる swagger という傾向はあると思う。 まぁ、gem と composer 比べてどうすんだという話はあるが、アプローチの違いが面白いのでまた色々考えてみたい。
PHP
自分は世間で言われているほど PHP が悪い言語だと思っていなくて、むしろゆるく Java っぽく書けるところは割と好きだったりする。 (そもそも Java がそんな嫌いじゃなくて、ああいう言語でドメインオブジェクトが先鋭化されていく時に感じる万能感は ruby が謳う万能感とはまた違う気持ちよさがあると思う。)
ただ、PHP の array はどうしても頂けない。PHPの array はかなりフリーダムな仕様で、collection も勝手に hash にしてしまう。例えば、array(‘a', ‘b', ‘c’) は array(0 => ‘a’, 1 => ‘b’, 2 => ‘c’) にしてしまう。 当然 array の中の型を一律に保証することもできない。string も integer もユーザー定義のクラスも一緒くたに array に混ぜることができる。もっと言えば key にくる型も特に保証できない。 また、全てがオブジェクトという言語でもない。そのせいか知らないが、PHPはとにかくなんでも array にぶち込んで返す習慣が横行してしまっている。少し気を抜くと返ってくるのは大抵このフリーダム array になる。 このため、ことPHPだとプログラミングの労力の多くをこのフリーダム array との闘いに費やさなければならない。 そんな状況を知ってか知らずか、PHPには array 操作のための組み込み関数が異様に多い。
ただ、それらは array を丁寧にクラスにしてあげれば大抵解決できる話なので、なるべく組み込み関数を使わないようにすることを心がけた。 その甲斐あって、30クラス1387行のライブラリで array 系組み込み関数は9回しかでてこない(適当な grep なので間違っているかも)。ちなみに組み込み関数自体の出現は24回。
grep している間に何が言いたいのかよくわからなくなってきたが、要は The ThoughtWorks Anthology にオブジェクト指向強制ギプスというのがあるが、 PHPに限った話で言えば組み込み関数を使わないでプログラミングするとそれっぽい効果が得られるかも、みたいなことが言いたかった。強制ギプスほどキツい制約ではないのでヌルくやるにはオススメかもしれない。
アノテーションからドキュメントを作れるswaggerの使い方
swagger の概要と、基本的な使い方をサンプルを交えて書く。
なお、終始 PHP なので気をつけて下さい。
swagger の概要
開発ドキュメントをどうやって書くかという話題は常々ある。そして一番素直なアプローチを取ると、大抵 wiki みたいなアプリケーションを用意して、そこにドキュメント書こうぜという話になる。しかし、この管理は割とすぐ破綻してしまう。 理由はいろいろあるが、最新のコードに追従できなくなるとか、統一されたフォーマットを保証できず書き手によっては何を書いているのか全くわからないとかそういうケースが多い。
前者について、 swagger はアノテーションでドキュメントを書けるので違いは起きづらくなる。( それでも不足だが自分でライブラリ作って補った )
また後者に関していえば、swagger は API の仕様を記述するためのフォーマットを提供してくれる。
つまり、swagger は API の記述方法について考える手間を肩代わりし、また違反があれば解析の段階でエラーを吐くのでドキュメントの一貫性保証もしてくれる。
swagger のアーキテクチャ
swagger とは厳密には API の仕様を記述するためのフォーマットのことを指す。しかし、実際には以下のドキュメントを生成してくれるライブラリの総称として使われることが多い。
ライブラリの「総称」といったのは、上記ドキュメントを生成する手順は以下の2つのレイヤーからなるため。
1. swagger(-core?)
2. swagger-ui
- 上記で吐き出されたjsonファイルをパースし、ドキュメント化する。
以下にサンプルを通して、アノテーションの記述からドキュメントの生成までの一連の流れを紹介する。
swagger を使ってみる
完成品は以下に上げてあるのでそこだけみてもよいかもしれない。また、各ブロックの説明と commit を対応させるように書いたので、commit だけ追ってもいける。
silex で適当なAPIを作る
とりあえずサンプルとして使う適当な API を作ってみる。API の作成自体はメインの話題ではないのでさらっと流すが、今回はお手軽にやるために silex を使っている。
<?php $app->get('/plain', function () use ($app) { $response = [ 'id' => 0, 'name' => 'kohsaka' ]; return $app->json($response); }); $app->get('/nested', function () use ($app) { $response = [ 'id' => 0, 'name' => 'kohsaka', 'birth' => [ 'month' => 'August', 'day' => '3' ] ]; return $app->json($response); }); $app->get('/collection', function () use ($app) { $response = [ [ 'id' => 0, 'name' => 'kohsaka' ], [ 'id' => 1, 'name' => 'sonoda' ], [ 'id' => 2, 'name' => 'minami' ] ]; return $app->json($response); }); $app->get('/parameter/{id}', function ($id) use ($app) { $index = [ [ 'id' => 0, 'name' => 'kohsaka' ], [ 'id' => 1, 'name' => 'sonoda' ], [ 'id' => 2, 'name' => 'minami' ] ]; return $app->json($index[$id]); });
サンプルプロジェクトのコードの差分 - create sample api
アノテーションを足していく
API ができたので、随時アノテーションを付け足してドキュメントを作っていく。
アノテーションの書き方については、公式ドキュメントや swagger-php の Examples を見るのが良い。
- https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md
- http://zircote.com/swagger-php/
- https://github.com/zircote/swagger-php/tree/master/Examples
パラメータがいっぱいあってげんなりすると思うので注釈する。なお、各々のパラメータは階層構造になっており、この階層構造を外れて記述することはできない。
@SWG\Resource
- 似たエンドポイントはここでひとまとめにする。例えば
GET /users
,GET /users/{id}
のようなもの@SWG\Api
- 一つのエンドポイントごとに書く情報
@SWG\Operations
- 一つのエンドポイントに対して複数の HTTP メソッドが割り当てられている場合必要になる
@SWG\Operation
- 一つの HTTP メソッドごとに書く情報
@SWG\Parameters
- SWG\Parameter のコレクション
@SWG\Parameter
- 想定されるリクエストパラメータに関する情報
@SWG\ResponseMessages
- SWG\ResponseMessage のコレクション
@SWG\ResponseMessage
- レスポンスメッセージに関する情報
@SWG\Model
- レスポンスパラメータに関する情報をまとめるやつ(としか言い様がない)
@SWG\Property
- レスポンスパラメータごとに書く情報
@SWG\Items
- SWG\Property がコレクション構造を取る場合これで表現する
書いといて何だが多分よくわからないと思うので、以下の実例と見比べて見たほうがよい。
シンプルなAPI構造の記述方法
手始めに最も単純な構造の /plain
エンドポイントの API にアノテーションを足してみる。
<?php /** * @SWG\Resource( * resourcePath="plain", * @SWG\Api( * path="/plain", * description="plain api structure", * @SWG\Operation( * method="GET",type="SimpleMember",nickname="plain" * ) * ) * ) * * @SWG\Model( * id="SimpleMember", * @SWG\Property(name="id", type="integer", required=true, description="user id"), * @SWG\Property(name="name", type="string", required=true, description="user name") * ) */ $app->get('/plain', function () use ($app) { $response = [ 'id' => 0, 'name' => 'kohsaka' ]; return $app->json($response); });
レスポンスは、@SWG\Operation
の type と @SWG\Model
の id を対応させることで表現する。
サンプルプロジェクトのコード差分 - add annotation to plain API
ネストするAPI構造の記述方法
レスポンスパラメータがネストする構造をとっている場合は、以下のように model を複数指定する。
<?php /** * @SWG\Resource( * resourcePath="nested", * @SWG\Api( * path="/nested", * description="nested api structure", * @SWG\Operation( * method="GET",type="NestedMember",nickname="nested" * ) * ) * ) * * @SWG\Model( * id="NestedMember", * @SWG\Property(name="id", type="integer", required=true, description="user id"), * @SWG\Property(name="name", type="string", required=true, description="user name"), * @SWG\Property(name="birth", type="Birth", required=true, description="birth day,month") * ) * * @SWG\Model( * id="Birth", * @SWG\Property(name="month", type="string", required=true, description="month"), * @SWG\Property(name="day", type="integer", required=true, description="day") * ) */ $app->get('/nested', function () use ($app) { $response = [ 'id' => 0, 'name' => 'kohsaka', 'birth' => [ 'month' => 'August', 'day' => '3' ] ]; return $app->json($response); });
サンプルプロジェクトのコード差分 - add annotation to nested API
コレクションになっているAPI構造の記述方法
レスポンスがコレクションの構造をとっている場合は、@SWG\Items
でコレクションするモデルの id を指定する。(実はこれ以外にも書き方はけどややこしいので飛ばす。)
<?php /** * @SWG\Resource( * resourcePath="collection", * @SWG\Api( * path="/collection", * description="nested api structure", * @SWG\Operation( * method="GET",type="CollectionMember",nickname="collection" * ) * ) * ) * * @SWG\Model( * id="CollectionMember", * @SWG\Property( * name="member collection", * type="array", * @SWG\Items("SimpleMember"), * required=true, * description="member array" * ) * ) */ $app->get('/collection', function () use ($app) { $response = [ [ 'id' => 0, 'name' => 'kohsaka' ], [ 'id' => 1, 'name' => 'sonoda' ], [ 'id' => 2, 'name' => 'minami' ] ]; return $app->json($response); });
ちなみに、SimpleMember
のように、既に出てきた model は再利用できる。
サンプルプロジェクトのコード差分 - add annotation to collection API
リクエストパラメータを付与の記述方法
リクエストパラメータのドキュメントを書きたい場合、@SWG\Parameters
で表現できる。
<?php /** * @SWG\Resource( * resourcePath="parameter", * @SWG\Api( * path="/parameter/{id}", * description="nested api structure", * @SWG\Operation( * method="GET",type="SimpleMember",nickname="nested", * @SWG\Parameters( * @SWG\Parameter( * name="id", paramType="path",type="string", * required=true, description="specify member id" * ) * ) * ) * ) * ) */ $app->get('/parameter/{id}', function ($id) use ($app) { $index = [ [ 'id' => 0, 'name' => 'kohsaka' ], [ 'id' => 1, 'name' => 'sonoda' ], [ 'id' => 2, 'name' => 'minami' ] ]; return $app->json($index[$id]); });
サンプルプロジェクトのコード差分 - add annotation to parameter API
swagger を実行し json ファイルを得る
ひと通りアノテーションを書いたので、これらのアノテーションから json ファイルを作る。 この手順は swagger-php をインストールしてコマンドを叩けばOK。これを swagger-ui が食う。
というわけで composer から swagger-php を入れる。
], "require": { "php": ">=5.4.0", - "silex/silex": "~1.1" + "silex/silex": "~1.1", + "zircote/swagger-php": "0.9.0" } }
はい
composer update
一応 swagger コマンドが使えるか見る。
~/xxx/swagger-assert-sandbox/ [master*] ./vendor/bin/swagger -h Swagger-PHP 0.9.0 ----------------- Generate Swagger JSON documents for a PHP project. Usage: (略)
大丈夫そうならこんな感じで実行。
~/xxx/swagger-assert-sandbox/ [master*] ./vendor/bin/swagger web --default-base-path 'http://localhost:8080' -o ./web/data/
-o 以下のディレクトリに json ファイルがちゃんと書きだされていたら成功。 -h で確認できるが、最初の引数がアノテーションの解析対象にするディレクトリ、default-base-path は swagger-ui のリクエスト先となるURL、-o は json ファイルの書き出し先になる。
サンプルプロジェクトのコード差分 - create swagger json
swagger-ui に json ファイルを食わせる
最後に、生成した json ファイルを swagger-ui に食わせる。これは結構簡単で、swagger-ui リポジトリを clone して、dist 以下をドキュメントルート以下に配置すればよい。
一点、dist 以下にある index.html の url を自分の json ファイルがあるパスに変える必要がある点だけ注意する。サンプルプロジェクトだとこんな感じになる。
<script type="text/javascript"> $(function () { window.swaggerUi = new SwaggerUi({ - url: "http://petstore.swagger.wordnik.com/api/api-docs", + url: "http://localhost:8080/data/", dom_id: "swagger-ui-container", supportedSubmitMethods: ['get', 'post', 'put', 'delete'], onComplete: function(swaggerApi, swaggerUi){ log("Loaded SwaggerUI");
サンプルプロジェクトのコード差分 - introduce swagger-ui
完成品を見てみる
うまくいくとこんな感じのドキュメントができる。
なお、サンプルプロジェクトは以下の手順で試せると思う。
git clone git@github.com:gong023/swagger-assert-sandbox.git cd swagger-assert-sandbox composer install ./bin/swagger ./bin/server # http://localhost:8080/swagger-ui にアクセス
こんな感じで、何はなくとも swagger の面倒臭さだけは伝わったと思う。 面倒くさいが、代わりに仰々しいまでのドキュメントを得ることができる。API の実行環境が手軽に手に入るのも利点と言えば利点ではある。 swagger はカジュアル用途には向かないが、そうでない場合威力を発揮できる代物だと思う。