ぼくのかんがえたさいきょうのOpenAPI(Swagger) 仕様の策定環境　その1 redocly(yamlのLINT)の導入

OpenAPI(Swagger)を使ってRestAPIの仕様を策定し、Lint , Mockserver , Swagger UI機能を使えるようにしたので、解説します。

当記事では、まず redoclyを導入し、lintを実行するまでの記事としています。
続きは別記事にて紹介できればと思います。

# 記事の概要
## この記事でわかること
- OpenAPIのyamlファイルのlint方法
- OpenAPIのyamlファイルへ独自lint(カスタムルール)の追加方法
- ~~docker-composeを使って swagger uiの構築方法と複数ファイルAPI定義があった場合のdocker-composeの実装方法~~ 別記事にします
- ~~docker-composeを使って OpenAPIの定義からmockserverを起動する方法と ホットリロードの実装方法~~ 別記事にします

## この記事で触れないこと
- OpenAPIのyamlの書き方

--- 
皆さんはじめまして株式会社クライドでオフショア開発部門の事業部長とフィリピン支社のCEOをしている Matsuo です。

今回、私が取り組んでいるプロジェクトにて、FrontendはNextJS , BackendはJavaで多くのAPIを開発する必要があったため、開発をよりスムーズにし、Frontendはバックエンドの実装を待たずにAPIをコールできるようにするのと、FrontendとBackendでAPIの形式の認識齟齬と実装上の違いが起きないようにするために、OpenAPIの定義を使ってFrontendとBackendで共通のAPI仕様を使って開発できるようにする実装方針を選定しました。
そこで、Swagger UIや LINT、Mockserverなどいくつか構築に手間取った箇所があったため、共有します。

---
# OpenAPI定義のLINT
各OpenAPI定義のyamlのLINTには、[Redocly CLI](https://redocly.com/) を選定しました。
redoclyでは yaml定義のLINTの他に、APIドキュメントの生成などの機能を持っています。
今回は、ドキュメント機能は Swagger UIを使うため、 LINT機能のみにフォーカスして紹介します。

早速ですが、LINTの実行は npm での実行と Dockerでの実行が行なえますが、今回はJavaの開発メンバーもいるため、簡単に導入できるよう Dockerでの実行と、CI用でもチェックを行うため、npmを使った実行の両方を行っています。

## ルールの定義
Redocly CLIでのLINTチェックのルールは、 /redocly.yamlにて定義することができます。
私は redocly.yaml へは以下のように設定しています
``` yaml: redocly.yaml
apis:
  hogeApiGroup@v1: 
    root: openapi/hoge-api.yaml
  fugaApiGroup@v1: 
    root: openapi/fuga-api.yaml
extends:
  - recommended
plugins:
  - './plugins/custom-lint-rules.js'
rules:
  info-license: off
  no-unused-components: error
  my-local-plugin/query-param-snake-case: error
  my-local-plugin/schema-property-snake-case: error
  my-local-plugin/operation-id-camel-case: error
  my-local-plugin/tags-kebab-case: error
  my-local-plugin/enum-snake-case: error
theme:
  openapi:
    htmlTemplate: ./docs/index.html
    generateCodeSamples:
      languages:  # Array of language config objects; indicates in which languages to generate code samples.
        - lang: curl
        - lang: Node.js
        - lang: JavaScript
```
APIの定義を openapi/配下に格納し、 ルールの拡張を ./plugins/配下に定義しています。
ここではクエリパラメーターや、スキーマのプロパティの命名規則、タグの命名規則、operationIdの命名規則、enum値の命名規則を定義しています。
デフォルトの機能でもできそうだったのですが、実装方法を見つけるまで至れなかったため、独自で拡張しています。

プラグインのルールの読み込みは
``` yaml:redocly.yaml
plugins:
  - './plugins/custom-lint-rules.js'
```
この箇所で読み込んでいます。

plugins/custom-lint-rules.js では以下のようにカスタムルールの読み込みを実装しています
``` javascript: plugins/custom-lint-rules.js
const QueryParamSnakeCase = require('./rules/query-param-snake-case')
const SchemaPropertySnakeCase = require('./rules/schema-property-snake-case')
const OperationIdCamelCase = require('./rules/operation-id-camel-case')
const TagsKebabCase = require('./rules/tags-kebab-case')
const EnumSnakeCase = require('./rules/enum-snake-case')

module.exports = {
  id: 'my-local-plugin',
  rules: {
    oas3: {
      'query-param-snake-case': QueryParamSnakeCase,
      'schema-property-snake-case': SchemaPropertySnakeCase,
      'operation-id-camel-case' : OperationIdCamelCase,
      'tags-kebab-case' : TagsKebabCase,
      'enum-snake-case': EnumSnakeCase
    },
  },
}
```
例えば、plugins/rules/query-param-snake-case.js では以下のような実装になっています

``` javascript: plugins/rules/query-param-snake-case.js
module.exports = QueryParamSnakeCase;

function QueryParamSnakeCase() {
  return {
    Parameter: {
      enter(parameter, ctx) {
        if (parameter.in === 'query') {
          // $refの解決
          if ('$ref' in parameter) {
            const resolved = ctx.resolve(parameter.$ref);
            if (resolved.result) {
              parameter = resolved.result;
            }
          }

          // スネークケースの正規表現
          const snakeCaseRegex = /^[a-z0-9]+(?:_[a-z0-9]+)*$/;

          // パラメータ名のチェック
          if (!snakeCaseRegex.test(parameter.name)) {
            ctx.report({
              message: `The query parameter "${parameter.name}" must be a snake case.`,
              location: ctx.location.child('name'),
            });
          }
        }
      },
    },
  };
}

```

当プロジェクトでは、 openapiの定義yamlから外部yamlファイルにて定義した schemaや queryParameterのyamlをロードしており、ロードしている外部ファイルであってもLintのルールを適応するため、 `ctx.resolve` にて $ref を解決する処理をいれています。

ですが、階層が深いとうまく読み込めないケースもあり、バージョンアップに期待したいところです。
カスタムルールの定義は、[公式のページ](https://redocly.com/docs/cli/custom-plugins/custom-rules/)を参考に実装することができます。

また、カスタムルール設定時に、どのNodeTypeに対して実行するかによって $refの値を読み込めたり、読み込めなかったりということが発生しました。
その際のこちらの [node type tree](https://redocly.com/docs/openapi-visual-reference/openapi-node-types/) のページが参考になりましたので紹介いたします。

尚、以下のようなopenapiの定義としております。
```yaml:openapi/hoge-api.yaml
paths:
  /login:
    post: 
      security: []
      tags:
        - auth
      operationId: login
      summary: login
      description: login
      requestBody:
        $ref: './common/request-bodies.yaml#/LoginRequest'
      responses:
        '200':
          description: succeeded
          content:
            application/json:
              schema:
                $ref: './common/schema.yaml#/AuthenticationToken'
        '400':
          $ref: './common/error-response.yaml#/BadRequest'
        '401':
          $ref: './common/error-response.yaml#/Unauthorized'
```

# Lint(readocly lint)の実行
実行は npm run ~~~ と、 Dockerでの実行、GithubでのCIの実行をできるようにしています。

## NPMでの実行
### インストール
```
npm install @redocly/cli
```

```json:package.json
{
  "scripts": {
    "lint": "redocly lint"
  },
  "dependencies": {
    "@redocly/cli": "^1.4.1"
  }
}
```

### 実行
```
npm run lint
```

## Dockerでの実行
```sh
docker run --rm -v $PWD:/spec redocly/openapi-cli lint
```
/spec配下にcurrent directoryをマウントすることによって、redocly.yamlの定義をそのまま使えるようにしています。

## GithubのCIでの実行
```yaml:.github/workflows/test.yml
name: Test

on:
  pull_request:
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  test-and-build:
    timeout-minutes: 30
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: 'npm'
          cache-dependency-path: ./package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Run LINT
        run: npm run lint
```
github actionsでは npmにて定義したlintコマンドを実行するようにしています。
---
以上で、OpenAPIのyamlを書いた後に、変更したyamlがルール通りに書くことが出来ているかを
ローカルとCIでチェックすることで、所定のフォーマット通りに設計されていることを担保することがでるようになりました。
これで人によって命名規則の違いや、パラメーターの不足、入力誤りなどを防ぐことができ、安心して変更・追加していくことができます。

# その他
VScodeをお使いの場合は、[Redocly OpenAPI](https://marketplace.visualstudio.com/items?itemName=Redocly.openapi-vs-code) を入れておくことで、openapiのyaml定義時にsuggestしてくれたりしますのでおすすめです。

次回は、docker-composeを使って[Swagger UI](https://swagger.io/tools/swagger-ui/) にて、定義したopenapiのyamlをローカルで表示するまでを記事にしたいと思います。