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

More than 3 years have passed since last update.

成果物: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の詳細なパラメータの内容について検証していきたい