dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

kintoneのAPIスキーマ情報からREST APIドキュメントを自動生成する

この記事は 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/ を見てみるのが良いかと思います。

f:id:dackdive:20191209021924p:plain

成果物

やってみる

以下、記事中のコードはすべてJavaScriptです。(↑のリポジトリではTypeScriptで書いています)


1. APIスキーマ情報を取得する

はじめに、APIスキーマ情報を取得するAPIを実行し、結果をJSONで保存します。
流れとしては

  1. API 一覧の取得API (/k/v1/apis.json) を叩き、全APIスキーマ情報取得用のlinkを得る
  2. 取得した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 にアクセスすると...

f:id:dackdive:20191206220248p:plain

無事APIドキュメントが表示できました。

なお余談ですが、VSCode というエディタを使っている場合は Swagger Viewer というextensionを入れると、エディタ上でこのようなAPIドキュメントを表示できます。

OpenAPI Specファイルを開いているときにコマンドパレットで

> Preview Swagger

というコマンドを実行してみてください。

f:id:dackdive:20191206220513p:plain

ドキュメントをどこかのサイトにホスティングする必要がないのであれば、これでも十分ですね。


4. [未完成😤] 認証機能を追加し、ドキュメントからAPIを実行できるようにする

先ほど生成したAPIドキュメントには「Try it out」というボタンが表示されています。
ここから自社のkintone環境にリクエストを送り、実際のデータのレスポンスを確認できたら便利ですね。

というわけでトライしてみます。

実際のkintone環境にアクセスするには認証が必要ですが、調べてみるとOpenAPI Specファイルにいくつかの項目を追加すればいいことがわかります。

これによると、必要なのは以下の2点です。

  1. servers > url にkintone環境のURLを指定する (例: https://hoge.cybozu.com)
  2. 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というボタンが追加されています。

f:id:dackdive:20191206233220p:plain:w320

kintoneで発行したAPIトークンを入力してAPIを実行してみましたが、エラーになってしまいました。

f:id:dackdive:20191206233232p:plain

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ファイルに変換しようとしたところ、盛大にエラーが出ます。

f:id:dackdive:20191209013154p:plain
生成したSpecファイルを https://editor.swagger.io/ に貼り付けたら大量にエラーが出た様子

エラーメッセージを少し見た様子だと、OpenAPIとして期待している型が違う(integerを期待しているところが文字列)だったり、patternPropertiesなどOpenAPIがサポートしていないJSON Schemaのキーワードを使用しているのが問題のようです。

前者はまだいいとして、後者にちゃんと対応するのはなかなか大変そうですね。。。(そもそも回避できるのかもわかってない)


所感

JSON SchemaもOpenAPIも今回初めてまともに使用したのでうまくいくか全くわかりませんでしたが、それっぽいものを作ることはできました。
とはいえ、TODOに書いたように完全なREST APIドキュメントを生成できるようにするのはだいぶハードルが高そうです。

また、調べていて苦労した点は、どのツールがデファクトスタンダードなのかあまりよくわからなかったというところです。
今回はGitHubのStar数やコミットの最終更新日時などを判断材料に選定しましたが、適切な選択になっていたかはわかりません。

また、そもそもツールを探すのも一苦労でした。
一応、今回の知見としてOpenAPI.Toolsというツール一覧サイトがあるようです。

OpenAPI関連でやりたいことがあれば、まずはこのサイトを見るのがいいのかなーと思いました。
このサイトの信頼性もよくわかりませんが。。。