この記事は kintone Advent Calendar 2019 6日目の記事です。
kintone のアドベントカレンダーは初参加です、よろしくお願いします。
はじめに
kintone にはさまざまなAPIがありますが、その中でもちょっと特殊なのがAPIのスキーマ情報を取得するためのAPIです。
https://developer.cybozu.io/hc/ja/articles/201941924
これは、各APIのリクエスト・レスポンスがどういったパラメータで構成されているかを JSON Schema というフォーマットで返してくれるAPIです。
このリクエスト・レスポンスのデータ構造の情報を応用するといろんなツールに使えそうだと思い、今回はここからOpenAPI(旧Swagger)という規格のファイルを生成することで、REST APIドキュメントをいい感じに作成できないかな〜というのを試してみた話です。
JSON SchemaとOpenAPIとは何か
JSON Schema とは、ざっくりいうとJSONのデータ構造をJSONで定義するための言語です。
たとえば、firstName, lastName, age
というプロパティを持つ Person
というJSONデータをJSON Schemaで表現すると以下のようになります。
{ "$id": "https://example.com/person.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Person", "type": "object", "properties": { "firstName": { "type": "string", "description": "The person's first name." }, "lastName": { "type": "string", "description": "The person's last name." }, "age": { "description": "Age in years which must be equal to or greater than zero.", "type": "integer", "minimum": 0 } } }
(https://json-schema.org/learn/miscellaneous-examples.html より引用)
一方のOpenAPIですが、こちらはREST APIの仕様を記述するためのフォーマットです。
Swagger という言葉をご存知の方は多いかもしれません。2015年にOpenAPI Initiativeという団体が発足したのに伴い、Swaggerは現在OpenAPIに名前が変わっているようです。
OpenAPIやSwaggerでREST APIの仕様を記述するメリットとして、その仕様を利用した便利なツールが充実していることが挙げられます。
今回使用するAPIドキュメント生成のためのツールもその1つです。
Swagger ◯◯と名のつくツール群については、以下の記事がわかりやすいです。
【連載】Swagger入門 - 初めてのAPI仕様管理講座 [1] Swaggerとは|開発ソフトウェア|IT製品の事例・解説記事
また、OpenAPIがどのようなフォーマットなのか、またそこからどのようなAPIドキュメントができるのかについては、Swagger Editorというオンラインエディタのデモサイト https://editor.swagger.io/ を見てみるのが良いかと思います。
成果物
やってみる
以下、記事中のコードはすべてJavaScriptです。(↑のリポジトリではTypeScriptで書いています)
1. APIスキーマ情報を取得する
はじめに、APIのスキーマ情報を取得するAPIを実行し、結果をJSONで保存します。
流れとしては
- API 一覧の取得API (
/k/v1/apis.json
) を叩き、全APIのスキーマ情報取得用のlinkを得る - 取得したlinkからスキーマ情報の取得API (
/k/v1/apis/*.json
) を順番に実行し、各APIのスキーマ情報を得る
という2ステップです。
import fetch from "node-fetch"; import fs from "fs"; import path from "path"; import prettier from "prettier"; const subdomain = process.env.KINTONE_SUBDOMAIN; export async function fetchKintoneAPISchemas() { const baseUrl = `https://${subdomain}.cybozu.com/k/v1`; // TODO: fetch all of kintone REST apis // const resp = await fetch(`${baseUrl}/apis.json`); // const apis: Apis = (await resp.json()).apis; // const fetchSchemasPromises = Object.values(apis).map(async api => { // const resp: any = await fetch(`${baseUrl}/${api.link}`); // return resp.json(); // }); // // const schemas = await Promise.all(fetchSchemasPromises); const schemas = [await (await fetch(`${baseUrl}/apis/app/acl/get.json`)).json()]; // (*) fs.writeFileSync( path.resolve(__dirname, "generated", "kintone-api-schemas.json"), prettier.format(JSON.stringify(schemas), { parser: "json" }) ); }
ただし、今回はとりあえず適当なAPI1つを例にうまくいくか試したかったので、アプリのアクセス権の取得API (/k/v1/app/acl.json
)だけにしています。
全APIのスキーマ情報を取得したい場合は、(*)
行のかわりにコメントアウトしている部分を使うとうまくいきます。
生成した generated/kintone-api-schemas.json
はこんな中身になっています。
[ { "id": "app/acl/get", "baseUrl": "https://zaki-yama.cybozu.com/k/v1/", "path": "app/acl.json", "httpMethod": "GET", "request": { "properties": { "app": { "format": "long", "type": "string" } }, "required": ["app"], "type": "object" }, "response": { "properties": { "rights": { "items": { "$ref": "Right" }, "type": "array" }, "revision": { "format": "long", "type": "string" } }, "type": "object" }, "schemas": { "Right": { "properties": { "recordImportable": { "type": "boolean" }, "appEditable": { "type": "boolean" }, "recordExportable": { "type": "boolean" }, "recordAddable": { "type": "boolean" }, "recordViewable": { "type": "boolean" }, "recordEditable": { "type": "boolean" }, "includeSubs": { "type": "boolean" }, "recordDeletable": { "type": "boolean" }, "entity": { "properties": { "code": { "type": "string" }, "type": { "enum": ["USER", "ORGANIZATION", "GROUP", "CREATOR"], "type": "string" } }, "type": "object" } }, "type": "object" } } } ]
2. APIスキーマ情報をOpenAPI Specificationのファイルに変換する
続いて、先ほど保存したJSONファイルからOpenAPI Specificationという仕様に準拠したファイル(以下、OpenAPI Specファイルと呼びます)に変換します。
OpenAPIのドキュメントは、以下のようなフォーマットで記述します。
(YAMLでもJSONでもOKですが、読みやすさからYAMLで記述しています)
openapi: 3.0.1 info: description: Kintone REST API version: 1.0.0 title: Kintone REST API paths: /app/acl.json: get: parameters: - in: query name: app schema: type: string required: true responses: '200': description: OK content: application/json: schema: properties: rights: items: $ref: '#/components/schemas/Right' type: array revision: format: long type: string type: object components: schemas: Right: properties: recordImportable: type: boolean appEditable: type: boolean recordExportable: type: boolean ...(略)...
paths
の下に各APIエンドポイントのパスを書き、リクエストを parameters
に、レスポンスを responses
に記述します。
一方、kintoneのAPIスキーマ情報のレスポンスとしては、request
, response
にそれぞれリクエスト、レスポンスのパラメータ情報が含まれているので、これらをうまくマッピングしてあげれば良さそうです。
import fs from "fs"; import path from "path"; import yaml from "js-yaml"; import { convertRequestToParameters } from "./request-converter"; export function generateOpenAPISchema() { const kintoneAPISchemas = JSON.parse( fs.readFileSync( path.resolve(__dirname, "generated", "kintone-api-schemas.json"), "utf8" ), (key, value) => { if (key === "$ref") { return `#/components/schemas/${value}`; } return value; } ); const json = { openapi: "3.0.1", info: { description: "Kintone REST API", version: "1.0.0", title: "Kintone REST API" } }; const paths = generatePaths(kintoneAPISchemas); const components = generateComponents(kintoneAPISchemas); json.paths = paths; json.components = components; fs.writeFileSync( path.resolve(__dirname, "generated", "openapi.yaml"), yaml.safeDump(json) ); } function generatePaths(kintoneAPISchemas) { const paths = {}; kintoneAPISchemas.forEach(schema => { const key = `/${schema.path}`; paths[key] = { [schema.httpMethod.toLowerCase()]: { parameters: convertRequestToParameters(schema.request), responses: { "200": { description: "OK", content: { "application/json": { schema: schema.response } } } } } }; }); return paths; } function generateComponents(kintoneAPISchemas) { let schemas = {}; kintoneAPISchemas.forEach((schema) => { schemas = { ...schema.schemas, ...schemas }; }); return { schemas }; }
// request-converter.js export function convertRequestToParameters(request) { const parameters = []; Object.keys(request.properties).forEach(fieldCode => { const required = request.required.includes(fieldCode); parameters.push({ in: "query", name: fieldCode, schema: { type: request.properties[fieldCode].type }, required }); }); return parameters; }
https://github.com/zaki-yama/kintone-openapi-generator/blob/master/src/generate-openapi-schema.ts
3. OpenAPI SpecファイルからREST APIドキュメントを生成する
スキーマ情報からOpenAPI Specのファイルが生成できたら、ここから周辺ツールを使ってREST APIドキュメントを生成します。
Swaggerのツール群でいうと Swagger UI がこれに該当します。
関連しそうなライブラリがいくつもあって迷ったんですが、今回は swagger-ui-express を使いました。
先ほど生成した OpenAPI Spec ファイルを読み込んで、以下のように渡してあげます。
// doc-server.js import fs from "fs"; import path from "path"; import jsyaml from "js-yaml"; import swaggerUi from "swagger-ui-express"; import express from "express"; const spec = fs.readFileSync( path.resolve(__dirname, "generated", "openapi.yaml"), "utf8" ); const doc = jsyaml.safeLoad(spec); const app = express(); app.use("/", swaggerUi.serve, swaggerUi.setup(doc)); app.listen(3000, () => { console.log("Listen on port 3000"); });
あとは、ターミナルで
$ node doc-server.js
でローカルサーバーを起動し、 http://localhost:3000 にアクセスすると...
無事APIドキュメントが表示できました。
なお余談ですが、VSCode というエディタを使っている場合は Swagger Viewer というextensionを入れると、エディタ上でこのようなAPIドキュメントを表示できます。
OpenAPI Specファイルを開いているときにコマンドパレットで
> Preview Swagger
というコマンドを実行してみてください。
ドキュメントをどこかのサイトにホスティングする必要がないのであれば、これでも十分ですね。
4. [未完成😤] 認証機能を追加し、ドキュメントからAPIを実行できるようにする
先ほど生成したAPIドキュメントには「Try it out」というボタンが表示されています。
ここから自社のkintone環境にリクエストを送り、実際のデータのレスポンスを確認できたら便利ですね。
というわけでトライしてみます。
実際のkintone環境にアクセスするには認証が必要ですが、調べてみるとOpenAPI Specファイルにいくつかの項目を追加すればいいことがわかります。
これによると、必要なのは以下の2点です。
servers > url
にkintone環境のURLを指定する (例: https://hoge.cybozu.com)components > securitySchemes
を指定する
後者について、OpenAPIではベーシック認証やAPIキー(トークン)による認証、OAuth2といった認証認可のスキームをサポートしています。
一方kintone REST APIがサポートしている認証方式は kintone REST APIの共通仕様 > ユーザー認証 の項に記載があります。
残念ながらユーザー名・パスワードによる認証は一般的なベーシック認証とは異なるため、ここではAPIトークン認証を使います。
APIトークン認証はトークンを X-Cybozu-Authorization
というヘッダーに乗せて送ります。これはOpenAPIでは以下のように書きます。
components: securitySchemes: ApiTokenAuth: type: apiKey in: header name: X-Cybozu-API-Token ... # 最後にこれも必要っぽい security: - ApiTokenAuth: []
servers > url
と合わせて、ステップ2で作成したスクリプトに処理を追加しておきます。
詳しくはリポジトリの generate-openapi-schema.ts を参照ください。
結果
さて、この状態で再度APIドキュメントを立ち上げると、期待通りAuthorizeというボタンが追加されています。
kintoneで発行したAPIトークンを入力してAPIを実行してみましたが、エラーになってしまいました。
localhostだからいけない(httpsならいける?)のか、それとも結局同一ドメインじゃないとうまくいかない...?
というのを調査しようと思いましたが、残念ながら時間切れです。
余談: モックサーバーを立てる
OpenAPI を利用したツールは今回紹介したAPIドキュメント生成以外にもさまざまなものがあります。
別の活用例として、たとえば prism というツールを使うと、モックサーバーを生成することもできます。
$ npm install @stoplight/prism-cli
でインストールし、
$ ./node_modules/.bin/prism mock <OpenAPI Specファイルのパス>
というコマンドを実行するだけです。
# モックサーバー起動 $ ./node_modules/.bin/prism mock <OpenAPI Specファイルのパス> [01:13:41] › [CLI] … awaiting Starting Prism… [01:13:41] › [HTTP SERVER] ℹ info Server listening at http://127.0.0.1:4010 [01:13:41] › [CLI] ℹ info GET http://127.0.0.1:4010/app/acl.json?app=magni [01:13:41] › [CLI] ▶ start Prism is listening on http://127.0.0.1:4010 # 別ウィンドウで $ curl "http://localhost:4010/app/acl.json?app=1" {"rights":[{"recordImportable":true,"appEditable":true,"recordExportable":true,"recordAddable":true,"recordViewable":true,"recordEditable":true,"includeSubs":true,"recordDeletable":true,"entity":{"code":"string","type":"USER"}}],"revision":"string"}%
TODO
ここまでやって、一通り完成したように見えましたが、このまま全APIのスキーマをOpenAPI Specファイルに変換しようとしたところ、盛大にエラーが出ます。
エラーメッセージを少し見た様子だと、OpenAPIとして期待している型が違う(integerを期待しているところが文字列)だったり、patternProperties
などOpenAPIがサポートしていないJSON Schemaのキーワードを使用しているのが問題のようです。
前者はまだいいとして、後者にちゃんと対応するのはなかなか大変そうですね。。。(そもそも回避できるのかもわかってない)
所感
JSON SchemaもOpenAPIも今回初めてまともに使用したのでうまくいくか全くわかりませんでしたが、それっぽいものを作ることはできました。
とはいえ、TODOに書いたように完全なREST APIドキュメントを生成できるようにするのはだいぶハードルが高そうです。
また、調べていて苦労した点は、どのツールがデファクトスタンダードなのかあまりよくわからなかったというところです。
今回はGitHubのStar数やコミットの最終更新日時などを判断材料に選定しましたが、適切な選択になっていたかはわかりません。
また、そもそもツールを探すのも一苦労でした。
一応、今回の知見としてOpenAPI.Toolsというツール一覧サイトがあるようです。
OpenAPI関連でやりたいことがあれば、まずはこのサイトを見るのがいいのかなーと思いました。
このサイトの信頼性もよくわかりませんが。。。