アノテーションからドキュメントを作れるswaggerの使い方

swagger の概要と、基本的な使い方をサンプルを交えて書く。

なお、終始 PHP なので気をつけて下さい。

swagger の概要

開発ドキュメントをどうやって書くかという話題は常々ある。そして一番素直なアプローチを取ると、大抵 wiki みたいなアプリケーションを用意して、そこにドキュメント書こうぜという話になる。しかし、この管理は割とすぐ破綻してしまう。 理由はいろいろあるが、最新のコードに追従できなくなるとか、統一されたフォーマットを保証できず書き手によっては何を書いているのか全くわからないとかそういうケースが多い。

前者について、 swagger はアノテーションでドキュメントを書けるので違いは起きづらくなる。( それでも不足だが自分でライブラリ作って補った

また後者に関していえば、swagger は API の仕様を記述するためのフォーマットを提供してくれる。

つまり、swagger は API の記述方法について考える手間を肩代わりし、また違反があれば解析の段階でエラーを吐くのでドキュメントの一貫性保証もしてくれる。

swagger のアーキテクチャ

swagger とは厳密には API の仕様を記述するためのフォーマットのことを指す。しかし、実際には以下のドキュメントを生成してくれるライブラリの総称として使われることが多い。

ライブラリの「総称」といったのは、上記ドキュメントを生成する手順は以下の2つのレイヤーからなるため。

1. swagger(-core?)

  • アノテーションを解析し、jsonファイルを生成する。

  • このレイヤーは色々な言語で行える

  • ちなみに各々の言語で名前がバラバラで、一貫した呼び名がわからないので(-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 を見るのが良い。

パラメータがいっぱいあってげんなりすると思うので注釈する。なお、各々のパラメータは階層構造になっており、この階層構造を外れて記述することはできない。

  • @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 はカジュアル用途には向かないが、そうでない場合威力を発揮できる代物だと思う。

下目黒でシェアハウス始めた

パーティを11時で終わらせるな

という言葉があって、確かソーシャルネットワークだったと思うが、僕は割とこの言葉が好きです。

ところで、この7月から新しいシェアハウスに移ってみた。

ここの人たちに声かけて住み始める前、僕はギークハウス高円寺とギークハウス水道橋というところにいて、それぞれ思い出深いところではあったのだけれど、色々あって今の形に落ち着いた。

ギークハウス高円寺という場所は、個室が一切ないシェアハウスで、一つの部屋に二段ベッドが2つで計4人、それが3部屋ある古い一軒家だった。住民間の距離が(物理的に)とても近く、かえってそれが良い方向に働いてとてもアットホームな場所だった。距離が近いとそれなりにたまるストレスもあるのだけれど、それ以上に住民間で色々話してゲラゲラしてた思い出が多くて、思い返してみると楽しかったなぁと素直に思う。ここに住んではじめて僕は、家族でもなければ恋人・友人とも違う「住人」という特殊な対人カテゴリを得た。

シェアハウスの住人というのはとても不思議な人間関係で、所詮は他人なので大切な部分はお互いなかなか話さない。けれど、一緒に住んでいるうちに積み重なって共有される時間とか物事がなんとなく意味を持ってくる。どうもうまくは言えないのだけれど、とにかく僕はそうやって安易に得られる人間関係に居心地の良さを感じた。一人でいるのが好きなくせして寂しがり屋な人はシェアハウス向いていると思う。

高円寺には一年以上いたのだけれど、社会人になってからは個室ナシの空間はキツイなーと思い、個室があるギークハウス水道橋というところに移った。水道橋は、当時ギークハウス文化の中心地だったと言っていいと思う。地理的にも東京の中心に位置していて、色々な場所から色々な人が集まってきた。もはやギークという概念はその文化の一部でしかなく、リビングにはプログラマ・ニート・就活生・スナック勤務のおねーさん・DJ・芸術家の卵・平田ガールズ・その他よくわからん人が曜日とか関係なく集まってきた。いや。ほとんど平田ガールズだった気がしてきた。どうだろう。あとそういえば漫画の「シマシマ」でしかみたことない添い寝屋という人を始めてみたのもここだ。 水道橋はとても人が「濃い」場所で、僕は毎夜毎夜「東京はいろんな人いるなぁ」と思っていた。リビングは人の流れのうねりのような吹き溜まりのような場所で、とにかくそこに人が集まっていることが奇跡のような感じがしたし、僕はそれを眺めているのが好きだった。

話は全然違うのだが、秋葉原にMOGRAというクラブミュージックの有名な箱があって、気分がいい時にいくととても楽しい。僕が行くのはだいたいアニマトかボカマニで、特にアニマトなんかはオールナイトでやってるから、イベントの時間が深くなるにつれて、どんどん日常生活から遠ざかる感じがする。日をまたいだ頃に終電がなくなって、2時ぐらいに酒回ってハイになって、3時過ぎた頃に足が痛くなって、それでもどっかの原住民みたいに踊ったり跳ねたり叫んだりしてると、だんだんよくわからん感じになってるけど爆音は爆音で、とにかく気分だけが気持ちよく吹っ飛んでいく。 こんな激しくはないけど、僕が水道橋で感じた感覚は割とこれに近いんじゃないかなと思う。なんかよくわからんフロアの一体感みたいな空気に酔って、あーもう隅っこのほうで溶けてるわみたいな気持ちになってくる。

そういう空間において、そこにそういう音楽とか映像が流れているということに実はそれほど強い意味はなくて、とにかくそこにそういう空気が成り立っているのが最も大事なことだと思う。夏祭りもだいたい同じ原理だ。あれが祭りとして成り立っているのは祭事が行われるからではなくて、夏休みに行われるからだし、浴衣を着ていいことになっているからだし、まぁとにかくそういう様々な要因を組み合わせて奇跡的なバランスでそうなっているのであって、それを大事にしないことには祭りは祭りたりえない。

水道橋もそういうバランスの上に成り立っていたのだけれど、残念ながらある日それは失われてしまった。 早い話、そういう空気が許せない人たちの希望によって、内部的から崩れた。夜中はゲスト来訪禁止というルールができたし、他にも善意で行われていたことがルールになって、保たれていた曖昧さは崩れた。一応断っておくが、そういう人たちを批判する気持ちは一切ない。バランス感覚が違えばそれまでだと思う。部屋キレイになったし。 ただ、ゆるい連帯の中で曖昧に生まれたものでしか本質的ではないと思う。

水道橋の出来事は、僕にはパーティーが11時で閉まってしまった感じがした。終電前のイベントには意味がない。 だから、新しく声かけてシェアハウス作ってみることにした。 流れに身を任せるけど、ここが二次会会場になればいいなとぼんやり考えている。

しかし他の住人はエンジニアとして全員が全員で僕なんかより10倍も20倍も優れている人たちなので、結構死にたい。 家の設備とかについては他の住人がもう書いてたしなるべくスピリチュアルな事書いてみた。 声かけたらなんかすごい人来ちゃった感あって死ぬ。 それにしてもブログ塩漬けにしてたし、元々プライベートなこと書かなかったのでなんか恥ずかしい。振り返ったら長くなった。

「長い。三行で。」と言われそうなので三行でまとめます。一行、いや二行でいいです。

http://www.amazon.co.jp/registry/wishlist/31WJYTS73D19K
http://www.amazon.co.jp/registry/wishlist/2DFLW2BAK8HT/ref=cm_sw_r_tw_ws_Bj.4rb0JQ70B6

ひっそりとあげ

ニコニコ動画のランキングから動画を落とす&mp3にするスクリプト作った

作りました。

https://github.com/gong023/nicoaudio

 

もちろん全部の動画落としてくるわけじゃなくて「歌ってみた」とかそのへんのmp3にする価値があるやつを狙って変換します。あと、処理が終わったらtwitterのDMで処理にかかった時間と成功or失敗をお知らせしてくれます。

2,3ヶ月前からcronで毎日回して運用していますがそこそこ動作が安定してきた。これのおかげでだいぶ楽になりました。新しい曲を何もしなくても毎日聞けるのはいい。

READMEに簡単な使い方書いてあります。もし使いたい場合はどうぞ。(といってもcloneするだけじゃ動きません。READMEもいつかはちゃんと最後まで書きます)

 

作っていて気になっていたことを書きます

rubyのマルチスレッド

このスクリプトはruby1.9.3のマルチスレッドを使って動画のDLを行なっています。ベンチマークを取ってみたら処理速度が5倍になっていてビビりました。

マルチスレッドなし --  

D, [2012-08-23T04:39:06.525075 #16552] DEBUG -- : get / all /  3.560000   5.090000   8.650000 (1638.229236)

 マルチスレッドあり --

D, [2012-08-23T04:08:10.576479 #16269] DEBUG -- : get / all /  4.200000   6.380000  10.580000 (319.074013)

rubyは1.9.1からカーネルスレッドになったらしいのですが、特別な事情がない限りスレッドセーフになっていて、なんちゃってマルチスレッドみたいになっています。ただIOが絡む場合は別で、今回はそのケースに当てはまるのでちゃんと効果が出たみたいです。

その辺りを検証してくれているブログもありました

http://www.kaeruspoon.net/articles/726

MySQL

無駄にmysql使ってます。videoのidが一意であることを保証するためです。それだけです。ええ。

完全に大げさ。

yamlでいいじゃんといえばそれまでなんですが

insert ignore楽じゃん。indexもあるしさ

その他

  • もっとrubyの黒魔術使いたかった
  • なんか書こうかと思ったけど忘れた

 

せっかくmp3がとれたので今はブラウザでプレイヤー作ってます

iPhoneSafariだとバックグラウンドいってもスリープモードでもaudioタグ効くんだよ。助かる。

ニコニコ動画のランキングからmp3を自動抽出

タイトルの通りですが、ニコニコ動画の総合ランキングから適当な単語を抽出してそれをmp3に自動変換してくれるスクリプトを作りました。

ただしあんまり完成度は高くないので、しばらく個人で運用して使い勝手とか高めていきたいなーと思っています。

糞糞糞うんこコードですが一応githubにもさらしました

https://github.com/gong023/nicoaudio

処理の手順としては

  1. ニコニコ動画のランキングをとってくる
  2. そのなかで「歌ってみた」とかを含むタイトルの動画を日時と一緒にMongoDBに保存
  3. mongoDBの情報をもとにニコニコAPIで動画(mp4)を取得
  4. mp4をffmpegでmp3に変換
これをcronで実行します。
毎日ランキングの内容をmp3に変換してくれるのでなかなか便利です。糞コードですが。
 

すごく有り合わせのものでいい加減に作ったのでこのスクリプトのいい所なんてぎりぎり動く所ぐらいなのですが、リファクタ&機能追加はしていきたいなーと思います

とりあえず

  1. スクリプトをrubyで統一(getflv...)
  2. webインターフェースの作成
  3. ブラウザからDLできるようにしたい
  4. 検索するキーワード指定できるようにしたい
  5. カテゴリ選べるようにしたい
  6. ある程度ランキングの期間指定できるようにしたい

これぐらいは考えてます。まぁいつできるか知りません。

 

昔はいろいろブラウジングしながら好きな曲とか見つけてたんですが、まぁさすがに色々忙しくなってきてなかなかそういう時間をとるのが難しくなってきました。で、ある程度自動化できることはできたらいいなぁと思って今回のようなスクリプトを書いてみました。こういうのエンジニアリングで解決できるのは楽しいです

PHPUnitが意外と入らない

pearPHPUnitって意外とすんなり入ってくれないですよね。

という訳で必要な最小限のコマンドだけメモ。

sudo pear upgrade -Z pear
pear channel-discover pear.phpunit.de
pear channel-discover components.ez.no
pear channel-discover pear.symfony-project.com
sudo pear install -Z --alldeps phpunit/PHPUnit

-Zがいい感じのはまりポイントでした。