この記事は 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で書いています)
はじめに、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
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 } ;
}
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 ファイルを読み込んで、以下のように渡してあげます。
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ファイルに変換しようとしたところ、盛大にエラーが出ます。
生成したSpecファイルを https://editor.swagger.io/ に貼り付けたら大量にエラーが出た様子
エラーメッセージを少し見た様子だと、OpenAPIとして期待している型が違う(integerを期待しているところが文字列)だったり、patternProperties
などOpenAPIがサポートしていないJSON Schemaのキーワードを使用しているのが問題のようです。
前者はまだいいとして、後者にちゃんと対応するのはなかなか大変そうですね。。。(そもそも回避できるのかもわかってない)
所感
JSON SchemaもOpenAPIも今回初めてまともに使用したのでうまくいくか全くわかりませんでしたが、それっぽいものを作ることはできました。
とはいえ、TODOに書いたように完全なREST API ドキュメントを生成できるようにするのはだいぶハードルが高そうです。
また、調べていて苦労した点は、どのツールがデファクトスタンダード なのかあまりよくわからなかったというところです。
今回はGitHub のStar数やコミットの最終更新日時などを判断材料に選定しましたが、適切な選択になっていたかはわかりません。
また、そもそもツールを探すのも一苦労でした。
一応、今回の知見としてOpenAPI.Toolsというツール一覧サイトがあるようです。
OpenAPI関連でやりたいことがあれば、まずはこのサイトを見るのがいいのかなーと思いました。
このサイトの信頼性もよくわかりませんが。。。