swagger-merger + ReDoc + DockerでAPIドキュメントを作る


# やりたいこと
- 最近の開発でSwaggerを使ってAPI定義を書くことが多いが、APIが増えてくるとSwaggerが肥大化して辛いのでファイルを分割管理したい！
- 複数のAPIを1ファイルに定義した場合にswagger-uiのGUIだとちょっと見にくいなーと感じたので
イカした見た目で確認できるようにしたい！

# やらないこと
- CI
  - ここはとりあえずdockerで確認できるところまで

# 登場人物の概要
## OpenAPI
- 正式名称はOpenAPI-Specification（OAS）
- OpenAPI Initiative（OAI）というコミュニティによって定義されているRESTful APIの標準仕様
- JSON/YAML形式で作成可能

## Swagger
- OpenAPI仕様に基づいて構築されたオープンソースツールのセット
- 主要ツール
  - Swagger Editor
  - Swgger UI
  - Swagger Codegen

## [swagger-merger](https://github.com/WindomZ/swagger-merger)
- 複数のswaggerファイルを一つのswaggerにまとめて出力してくれるスゴイやつ
- JSON/YAMLのどちらでもOK
- OpenAPI 3.0にも対応してる

## [ReDoc](https://github.com/Redocly/redoc)
- swaggerをイカした見た目で表示できるスゴイやつ
- OpenAPI 3.0にも対応してる



# OpenAPIドキュメントを作る
~~１から自分で作るのが面倒なので~~ 今回はOAIにある以下のサンプルを使って分割・ReDocを試してみます。
https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/uspto.yaml

エンドポイントは3つですが1ファイルで200行を超えています。

# swaggerファイルを分割する
実際のプロジェクトで使うときはもう少し細かく分けたほうが良いと思いますが、
とりあえず今回は以下の構成にしてみます。


```shell
.
├── components
│   ├── index.yml
│   └── schemas
│       └── index.yml
├── index.yml
└── paths
    ├── index.yml
    ├── metadata
    │   ├── fields.yml
    │   └── index.yml
    └── search
        └── records.yml
```

## ルートファイルの記述
`index.yml`がルートファイルになります。
ここでは`$ref`を使用してpathsとcomponentsの各ディレクトリの`index.yml`の読み込みを行っています。

```yaml
openapi: 3.0.1
servers:
  略
info:
  略
tags:
  略
paths:
  $ref: "./paths/index.yml"
components:
  $ref: "./components/index.yml"
```

※pathsとcomponents以外はexampleの内容のままなので省略しています。

## pathsの記述
`paths/index.yml`では各エンドポイントごとにyamlファイルを読み込みを行います。

```yaml
/:
  $ref: "./metadata/index.yml"
/{dataset}/{version}/fields:
  $ref: "./metadata/fields.yml"
/{dataset}/{version}/records:
  $ref: "./search/records.yml"
```

`paths/metadata/index.yml`の内容は以下。（サンプルのコピペ）

```yaml
/:
  get:
    tags:
      - metadata
    operationId: list-data-sets
    summary: List available data sets
    responses:
      '200':
        description: Returns a list of data sets
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/dataSetList'
            example:
              {
                "total": 2,
                "apis": [
                  {
                    "apiKey": "oa_citations",
                    "apiVersionNumber": "v1",
                    "apiUrl": "https://developer.uspto.gov/ds-api/oa_citations/v1/fields",
                    "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/oa_citations.json"
                  },
                  {
                    "apiKey": "cancer_moonshot",
                    "apiVersionNumber": "v1",
                    "apiUrl": "https://developer.uspto.gov/ds-api/cancer_moonshot/v1/fields",
                    "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/cancer_moonshot.json"
                  }
                ]
              }
```

`paths/metadata/fields.yml`の内容は以下。（サンプルのコピペ）

```
/{dataset}/{version}/fields:
  get:
    tags:
      - metadata
    summary: >-
      Provides the general information about the API and the list of fields
      that can be used to query the dataset.
    description: >-
      This GET API returns the list of all the searchable field names that are
      in the oa_citations. Please see the 'fields' attribute which returns an
      array of field names. Each field or a combination of fields can be
      searched using the syntax options shown below.
    operationId: list-searchable-fields
    parameters:
      - name: dataset
        in: path
        description: 'Name of the dataset.'
        required: true
        example: "oa_citations"
        schema:
          type: string
      - name: version
        in: path
        description: Version of the dataset.
        required: true
        example: "v1"
        schema:
          type: string
    responses:
      '200':
        description: >-
          The dataset API for the given version is found and it is accessible
          to consume.
        content:
          application/json:
            schema:
              type: string
      '404':
        description: >-
          The combination of dataset name and version is not found in the
          system or it is not published yet to be consumed by public.
        content:
          application/json:
            schema:
              type: string
```

`paths/search/index.yml`の内容は以下。（サンプルのコピペ）

```yaml
/{dataset}/{version}/records:
  post:
    tags:
      - search
    summary: >-
      Provides search capability for the data set with the given search
      criteria.
    description: >-
      This API is based on Solr/Lucense Search. The data is indexed using
      SOLR. This GET API returns the list of all the searchable field names
      that are in the Solr Index. Please see the 'fields' attribute which
      returns an array of field names. Each field or a combination of fields
      can be searched using the Solr/Lucene Syntax. Please refer
      https://lucene.apache.org/core/3_6_2/queryparsersyntax.html#Overview for
      the query syntax. List of field names that are searchable can be
      determined using above GET api.
    operationId: perform-search
    parameters:
      - name: version
        in: path
        description: Version of the dataset.
        required: true
        schema:
          type: string
          default: v1
      - name: dataset
        in: path
        description: 'Name of the dataset. In this case, the default value is oa_citations'
        required: true
        schema:
          type: string
          default: oa_citations
    responses:
      '200':
        description: successful operation
        content:
          application/json:
            schema:
              type: array
              items:
                type: object
                additionalProperties:
                  type: object
      '404':
        description: No matching record found for the given criteria.
    requestBody:
      content:
        application/x-www-form-urlencoded:
          schema:
            type: object
            properties:
              criteria:
                description: >-
                  Uses Lucene Query Syntax in the format of
                  propertyName:value, propertyName:[num1 TO num2] and date
                  range format: propertyName:[yyyyMMdd TO yyyyMMdd]. In the
                  response please see the 'docs' element which has the list of
                  record objects. Each record structure would consist of all
                  the fields and their corresponding values.
                type: string
                default: '*:*'
              start:
                description: Starting record number. Default value is 0.
                type: integer
                default: 0
              rows:
                description: >-
                  Specify number of rows to be returned. If you run the search
                  with default values, in the response you will see 'numFound'
                  attribute which will tell the number of records available in
                  the dataset.
                type: integer
                default: 100
            required:
              - criteria
```

## componentsの記述
`components/index.yml`はpathsと同様にサブディレクトリの`index.yml`を読み込んでいます。

```yaml
schemas:
  $ref: "./schemas/index.yml"
```

`components/schemas/index.yml`の内容は以下。（サンプルのコピペ）

```
dataSetList:
  type: object
  properties:
    total:
      type: integer
    apis:
      type: array
      items:
        type: object
        properties:
          apiKey:
            type: string
            description: To be used as a dataset parameter value
          apiVersionNumber:
            type: string
            description: To be used as a version parameter value
          apiUrl:
            type: string
            format: uriref
            description: "The URL describing the dataset's fields"
          apiDocumentationUrl:
            type: string
            format: uriref
            description: A URL to the API console for each API
```

# swaggerファイルをマージする

## swagger-mergerをインストール
ファイルの分割が終わったので`swagger-merger`の出番です!!
npmで簡単にインストールできます！

```shell
$ npm install --dev swagger-merger
```

## マージ！
今回はグローバルにインストールしていないので相対パスでファイル指定して実行します。
npm scriptsに設定するとかすると楽だと思います。

```
$ ./node_modules/.bin/swagger-merger -i index.yml -o swagger.yml
```

これで分割て作成したYAMLが`swagger.yml`にマージされて出力されます！

## swagger-uiで確認する
dockerで簡単にswagger-uiの環境を構築できるので、サクッと動かします。

`docker-compose.yml`を以下のようにします

```yaml
version: "3"

services:
  swagger-ui:
    image: swaggerapi/swagger-ui
    ports:
      - 8080:8080
    volumes:
      - ./swagger.yml:/usr/share/nginx/html/swagger.yml
    environment:
      API_URL: swagger.yml
```

起動＆確認

```
$ docker-compose up -d --build
$ open http://localhost:8080
```

よく見るswagger-uiの画面が確認できます！

![localhost_8080_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206915/b90e147c-2b5a-6d9f-64a1-578b22a03fc4.png "localhost_8080_.png")


# ReDoc
APIが数本だとswagger-uiでも特に困らないのですが、APIが増えた時にswagger-uiだとちょっと見にくいなーと感じました。
そこでReDocというSwaggerをイカした見た目にできるツールを使ってみたいと思います。

dockerで動かすだけなら非常に簡単で、以下を`docker-compose.yml`に追記するだけです。

```yaml
  redoc:
    image: redocly/redoc
    ports:
      - 8081:80
    volumes:
      - ./swagger.yml:/usr/share/nginx/html/swagger.yml
    environment:
      SPEC_URL: swagger.yml

```

動作確認

```
$ docker-compose up -d --build
$ open http://localhost:8081
```

![localhost_8081_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206915/f2ac3803-ff33-7e36-a20e-b3fd64757696.png "localhost_8081_ (1).png")

なんかかっこよくなったぞ！！！！！！

# おわり
今回はdockerを使ってローカルPCで確認するまでにしておきましたが
実際使うならCIに組み込んで公開まで行うとより捗るかと思います。