「東京メトロオープンデータ活用コンテスト」②APIの仕様設計を理解し、ドキュメント化する

  • 168
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

成果物:apiary.ioによる、APIドキュメント
とりあえず見て貰えるとイメージつかみやすいです↑

あらすじ

  • 100万円欲しいのでAPIを利用したアプリ開発を学ぶ絶好の機会なので、東京メトロオープンデータ活用コンテストに挑戦しようとしている。
  • 前回、とりあえずざっと触ってみてAPIのイメージは掴んだ
  • 今回は、公式ドキュメントと実際のレスポンスを咀嚼して、仕様設計を汎用ドキュメント化しつつ理解していくことを目的とする
    • せっかくドキュメント化するからには、汎用性と実用性が高いフォーマットとしたい(オレオレにはしたくない)

成果物

API Blueprintで記述されたテキストをApiary.ioでホスティングし、以下を作った

やってみてわかった、ApiaryによるAPIドキュメントのいいところ

いいところ 理由
メンテが容易 API BlueprintというAPI Documentation専用のmarkdown風言語で記述し、それを元に生成されている。単一のテキストファイルなので、バージョン管理もしやすい
そこそこの見栄え APIを説明するのに最適化されたドキュメントを生成してくれる。
デバッグに便利 ドキュメントから簡易モックサーバを生成してくれるので、ドキュメントを書いたらクライアントの開発が進められる
フリーミアム 個人レベルの利用ならタダ
スニペット生成 11の形式・言語によるコードスニペットをドキュメントが自動生成してくれる

オレオレAPIドキュメントと比べると、圧倒的なメリットがあるように感じる

API Blueprintとは何なのか


引用: API Blueprint - API Documentation with powerful tooling

WebAPI専用のmarkdown風言語。機械が読める構造を維持しつつ、人にも易しい設計。

apiary.ioとは何なのか


引用: Apiary — Home

API Blueprintで記述したテキストファイルから、以下を生成できるREST API Platform。

  • apiblueprintで記述したファイルから、APIドキュメントの生成
  • ドキュメントを元にした、簡易API mockサーバ生成
  • 12の形式、言語によるcode samplesの生成
  • デバッグ機能と自動テスト(syntax validation)
  • githubとの連携
  • フリーミアムモデル

apiaryによるドキュメント例

apiary docの使い方

  1. 確認したい、APIのタイトル(上の図で言う、「運賃」)を選択
  2. Responseをクリック
  3. 生成するコードスニペットの言語を選択
  4. 環境を選択。「Production」を選ぶと本番URLにリクエストを飛ばす。「MockServer」を選択すれば、example responseを返す
  5. Exampleを押すと、リクエストパラメータの入力画面になる。東京メトロAPIの場合、ConsumerKeyの入力がmustなので、少なくともここは書き換える
  6. Call Resourceを押すと、レスポンスが帰ってくる
  7. CodeSnippetを押すと、リクエストするソースコードを表示する

Traffic Inspector


引用: Apiary — Home

簡易的なトラフィックモニターもあるので、モックサーバ向けのリクエストをデバッグするのに役立つ

おまけ(メトロの運行情報を取得するソースコード)

以下、現在の運行情報を取得するソースコードを11の形式、言語で表現する。
xxxxxxxxxとなっている箇所は、自身で取得したアクセストークンと差し替える

cURL

cURL_Request
curl --include \
https://private-1e992-tokyometroopendataapi.apiary-mock.com/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey=xxxxxxxxxxxxxxxx

JavaScript

JavaScript_Request
var Request = new XMLHttpRequest();

Request.open('GET', 'https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx');

Request.onreadystatechange = function () {
  if (this.readyState === 4) {
    console.log('Status:', this.status);
    console.log('Headers:', this.getAllResponseHeaders());
    console.log('Body:', this.responseText);
  }
};

Request.send(JSON.stringify(body));

Node.js

Node.js_Request
var request = require('request');

request('https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx', function (error, response, body) {
  console.log('Status:', response.statusCode);
  console.log('Headers:', JSON.stringify(response.headers));
  console.log('Response:', body);
});

Python

Python_Request
from urllib2 import Request, urlopen

request = Request('https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx')

response_body = urlopen(request).read()
print response_body

PHP

PHP_Request
<?php
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, "https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&#38;acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);

$response = curl_exec($ch);
curl_close($ch);

var_dump($response);

Ruby

Ruby_Request
require 'rubygems' if RUBY_VERSION < '1.9'
require 'rest_client'

response = RestClient.get 'https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx'
puts response

C#

C#_Request
//Common testing requirement. If you are consuming an API in a sandbox/test region, uncomment this line of code ONLY for non production uses.
//System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

var request = System.Net.WebRequest.Create("https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&#38;acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx") as System.Net.HttpWebRequest;
request.KeepAlive = true;

request.Method = "GET";
request.ContentLength = 0;
string responseContent=null;
using (var response = request.GetResponse() as System.Net.HttpWebResponse) {
  using (var reader = new System.IO.StreamReader(response.GetResponseStream())) {
    responseContent = reader.ReadToEnd();
  }
}

Vasual Basic

VasualBasic_Request
Dim request = TryCast(System.Net.WebRequest.Create("https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&#38;acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx"), System.Net.HttpWebRequest)

request.Method = "GET"

request.ContentLength = 0
Dim responseContent As String
Using response = TryCast(request.GetResponse(), System.Net.HttpWebResponse)
  Using reader = New System.IO.StreamReader(response.GetResponseStream())
    responseContent = reader.ReadToEnd()
  End Using
End Using

Groovy

Groovy_Request
import groovyx.net.http.RESTClient
import static groovyx.net.http.ContentType.JSON
import groovy.json.JsonSlurper
import groovy.json.JsonOutput

@Grab (group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.5.0')
def client = new RESTClient("https://api.tokyometroapp.jp/api/v2")

response = client.get( path : "/datapoints?rdf:type=odpt:TrainInformation&#38;acl%3AconsumerKey={acl%3AconsumerKey}")

println("Status:" + response.status)
if (response.data) {
  println("Content Type: " + response.contentType)
  println("Body:\n" + JsonOutput.prettyPrint(JsonOutput.toJson(response.data)))
}

Objective-C

Objective-C_Request
NSURL *URL = [NSURL URLWithString:@"https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&#38;acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx"];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
[request setHTTPMethod:@"GET"];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
                                        completionHandler:
                              ^(NSData *data, NSURLResponse *response, NSError *error) {

                                  if (error) {
                                      // Handle error...
                                      return;
                                  }

                                  if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                                      NSLog(@"Response HTTP Status code: %ld\n", (long)[(NSHTTPURLResponse *)response statusCode]);
                                      NSLog(@"Response HTTP Headers:\n%@\n", [(NSHTTPURLResponse *)response allHeaderFields]);
                                  }

                                  NSString* body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                                  NSLog(@"Response Body:\n%@\n", body);
                              }];
[task resume];

Swift

Swift_Request
// NOTE: Uncommment following two lines for use in a Playground
// import XCPlayground
// XCPSetExecutionShouldContinueIndefinitely()

var url = NSURL(string: "https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&#38;acl:consumerKey=xxxxxxxxxxxxxxxxxxxxxxxxxx")
var request = NSMutableURLRequest(URL: url)

request.HTTPMethod = "GET"

var session = NSURLSession.sharedSession()
var task = session.dataTaskWithRequest(request) { (data: NSData!, response: NSURLResponse!, error: NSError!) in

    if (error) {
        // Handle error...
        return
    }

    println(error)
    println(response)
    println(NSString(data: data, encoding: NSUTF8StringEncoding))
}

task.resume()

Response(JSON)

帰ってくるレスポンスは、以下となる

JSON_Response
[
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE9",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Tozai",
    "odpt:timeOfOrigin": "2014-09-16T12:36:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE7",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Marunouchi",
    "odpt:timeOfOrigin": "2014-09-19T20:30:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE8",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Namboku",
    "odpt:timeOfOrigin": "2014-09-18T17:30:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE6",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Hibiya",
    "odpt:timeOfOrigin": "2014-08-30T23:30:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE3",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Fukutoshin",
    "odpt:timeOfOrigin": "2014-09-21T17:30:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE5",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Hanzomon",
    "odpt:timeOfOrigin": "2014-09-17T22:56:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE4",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Ginza",
    "odpt:timeOfOrigin": "2014-09-16T09:15:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BEA",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Yurakucho",
    "odpt:timeOfOrigin": "2014-09-16T23:00:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  },
  {
    "@context": "http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
    "@id": "urn:ucode:_00001C000000000000010000030C3BE2",
    "dc:date": "2014-09-22T00:35:03+09:00",
    "dct:valid": "2014-09-22T00:40:03+09:00",
    "odpt:operator": "odpt.Operator:TokyoMetro",
    "odpt:railway": "odpt.Railway:TokyoMetro.Chiyoda",
    "odpt:timeOfOrigin": "2014-09-21T13:00:00+09:00",
    "odpt:trainInformationText": "現在、平常どおり運転しています。",
    "@type": "odpt:TrainInformation"
  }
]    

まとめ

  • やはりドキュメントがあると、他のメンバーとの情報共有がしやすい
  • なにより、ドキュメントを作っている本人が一番理解がすすむ
  • apiary上でリアルタイムでAPI blueprintのシンタックスチェックが入るので、構文エラーをすぐに発見できる。
  • ドキュメントソースはgithubにあるので、気軽にプルリクエストしてもらえそうでよい
  • 書き慣れたmarkdownでかけるので、作成工数は高くない。モックサーバも作れることもあり、精神衛生上もよい(無駄な作業をしている感には襲われない)

APIのドキュメントは、身の回りでは割と軽視されている感あるけど、円滑な開発の為にすごく重要だと思う。apiblueprintでなくてもいいので、これだ!という標準フォーマットは、はやく決まって欲しい

メトロAPIの全体像がわかったところで、次は各APIの詳細なパラメータの内容について検証していきたい