Swaggerのファイルを分割して管理、redoclyで結合しよう! [swagger, redocly, GitHubActions]

swaggerを書くときにレスポンスのjsonなど使いまわしたいものをコンポーネント化したいと思ったことありませんか？

今回はredoclyを使ってswaggerのファイル統合を使って、swaggerで記述するものを分割していこうと思います。

# swaggerに関する記事
swaggerはこちらの記事で紹介しています。

https://qiita.com/maooz4426/items/ccbd7d239f8f8b35843a

# redoclyとは
redoclyは、openAPIファイルの生成、管理を行えるCLIツールです。

https://redocly.com/docs/cli

インストールはnpmで行います。

```zsh:zsh
npm install -g @redocly/cli
```

下のコマンドでversionが表示されたらインストール成功です。
```zsh:zsh
redocly --version
```

:::note
dockerイメージを使ってコンテナで動かすこともできますが、後ほど紹介します。
:::


 # swaggerを分割・結合するメリットや必要性

### 機能ごとにファイルを分けられる
サインイン、サインアップ、ユーザーの取得など、機能ごとにファイルを分けることができるので、管理の効率性を上げることができます。

### IDEで確認できる


私はJetbrainsのIDEを使っていますが、IDEにはswaggerのプラグインが整備されていることが多いので、統合したファイルかあらプレビューすることができます。

上記で紹介したコンポーネント毎にファイルを分けてしまうとIDEが読み込むことができないので結合する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/fe34cf3b-ed99-bd41-c4f3-f4fa1f27cc66.png)

### コード生成に必要

近年ではドキュメント駆動設計でswaggerのファイルからjsonやcontroller層で書くコードを生成させて開発させるという方法を採用して開発してる方がかなり多いです。

その場合でもswaggerのファイルにすべての情報が明記されてある必要があるので、結合する必要があります。

# 結合するファイル
今回統合するファイルは、`/docs/swagger`というディレクトリを作り、下のように分けました。

```zsh:/docs/swagger/
.
├── components
│   └── todo.yml
├── paths
│   └── todo.yml
└── root.swagger.yml
```

ディレクトリは以下のように分けています。

|ディレクトリ名|役割
|---|---
|components|jsonのschemaなどの使い回すものを定義
|paths|一つのURIに対してのapi定義を記述
|root.swagger.yml|URI名を設定してpathsに定義したものを割り当て

実際に書いたコードは下のようになっています。

### componentsのファイル

```/docs/swagger/components/todo.yml
TodoID:
  name: "taskId"
  in: path
  description: "詳細を取得したいtodoタスクのID"
  required: true
  schema:
    type: "integer"
    format: "int64"
  example: 1

Todo:
  type: object
  properties:
    id:
      type: integer
      description: タスクのID
      example: 1
    task:
      type: string
      description: タスクの内容
      example: "買い物に行く"
    deadline:
      type: string
      format: date
      description: 締切日
      example: "2024-10-12"
    done:
      type: boolean
      description: タスクの状態
      example: false

  required:
    - id
    - task
    - deadline
    - done

Todos:
  type: object
  properties:
    total:
      type: integer
      description: 全タスクの総数
      example: 2
    tasks:
      type: array
      items:
        type: object
        properties:
          id:
            type: integer
            description: タスクのID
            example: 1
          task:
            type: string
            description: タスクの内容
            example: "買い物に行く"
          deadline:
            type: string
            format: date
            description: 締切日
            example: "2024-10-12"
          done:
            type: boolean
            description: タスクの状態
            example: false
      required:
        - id
        - task
        - deadline
        - done


TodoCreate:
  type: object
  properties:
    id:
      type: integer
      description: タスクのID
      example: 1
    task:
      type: string
      description: タスクの内容
      example: "買い物に行く"
    deadline:
      type: string
      format: date
      description: 締切日
      example: "2024-10-12"
    done:
      type: boolean
      description: タスクの状態
      example: false
  required:
    - task
    - deadline

TodoDetail:
    type: object
    properties:
      task:
        type: string
        description: タスク
        example: "買い物に行く"
      deadline:
        type: string
        description: 締切日
        example: "2024-10-12"
    required:
      - task
      - deadline

TodoDetailUpdate:
  type: object
  properties:
    task:
      type: string
      description: タスクの内容
      example: "買い物に行く"
    deadline:
      type: string
      format: date
      description: 締切日
      example: "2024-10-12"
    done:
      type: boolean
      description: タスクの状態
      example: false
  required:
    - task
    - deadline
    - done
```

### pathsのファイル

```/docs/swagger/paths/todo.yml
todo_create:
    post:
      summary: todo-create
      description: "todoタスクを作成します"
      tags:
        - todo
      requestBody:
        content:
          application/json:
            schema:
              $ref: "../components/todo.yml#/TodoCreate"
      responses:
        201:
          description: success
          content:
            application/json:
              schema:
                $ref: "../components/todo.yml#/Todo"
        400:
          description: Bad Request
        500:
          description: Internal Server Error

todo_read_all:
  get:
    operationId: todolist-findall
    description: "todoタスクの一覧を取得"
    tags:
      - todo
    responses:
      200:
        description: "成功時のレスポンス"
        content:
          application/json:
            schema:
              $ref: "../components/todo.yml#/Todos"
      400:
        description: Bad Request
      500:
        description: Internal Server Error

todo_detail_crud:
  get:
    operationId: todo-detail
    description: "todoタスクの詳細を表示します"
    tags:
      - todo
    parameters:
      - $ref: "../components/todo.yml#/TodoID"
    responses:
      200:
        description: "成功時のレスポンス"
        content:
          application/json:
            schema:
              $ref: "../components/todo.yml#/TodoDetail"
      "400":
        description: Bad Request

  put:
    operationId: todo-update
    description: "タスクの内容を更新します"
    tags:
      - todo
    parameters:
      - $ref: "../components/todo.yml#/TodoID"
    requestBody:
      content:
        application/json:
          schema:
            $ref: "../components/todo.yml#/TodoDetailUpdate"
    responses:
      201:
        description: success
      400:
        description: Bad Request
      500:
        description: Internal Server Error

  delete:
    operationId: todo-delete
    description: "todoタスクを削除します"
    tags:
      - todo
    parameters:
      - $ref: "../components/todo.yml#/TodoID"
    responses:
      204:
        description: success
      400:
        description: Bad Request
      500:
        description: Internal Server Error

```

### 統合ファイル
```/docs/swagger/root.swagger.yml
openapi: 3.0.0
info:
  title: todo
  description: todoAPI
  version: 1.0.0

tags:
  - name: todo
    description: todoタスクのAPI

paths:
  /tasks/create:
    $ref: "./paths/todo.yml#/todo_create"

  /tasks:
    $ref: "./paths/todo.yml#/todo_read_all"

  /tasks/{taskId}:
    $ref: "./paths/todo.yml#/todo_detail_crud"
```

長々していますが、今回はこちらのファイルを使ってみます。

# 結合する

記述したコードを統合したいと思います。
`/docs/swagger`で下のコマンドを打ちます。

```zsh:zsh
redocly bundle ./root.swagger.yml --output=./generated.gen.swagger.yml
```

こうすることで下のようなコードが生成されます。

<details><summary>generated.gen.swagger.yml</summary>

```/docs/swagger/generated.gen.swagger.yml
openapi: 3.0.0
info:
  title: todo
  description: todoAPI
  version: 1.0.0
tags:
  - name: todo
    description: todoタスクのAPI
paths:
  /tasks/create:
    post:
      summary: todo-create
      description: todoタスクを作成します
      tags:
        - todo
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoCreate'
      responses:
        '201':
          description: success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'
        '400':
          description: Bad Request
        '500':
          description: Internal Server Error
  /tasks:
    get:
      operationId: todolist-findall
      description: todoタスクの一覧を取得
      tags:
        - todo
      responses:
        '200':
          description: 成功時のレスポンス
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todos'
        '400':
          description: Bad Request
        '500':
          description: Internal Server Error
  /tasks/{taskId}:
    get:
      operationId: todo-detail
      description: todoタスクの詳細を表示します
      tags:
        - todo
      parameters:
        - $ref: '#/components/parameters/TodoID'
      responses:
        '200':
          description: 成功時のレスポンス
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoDetail'
        '400':
          description: Bad Request
    put:
      operationId: todo-update
      description: タスクの内容を更新します
      tags:
        - todo
      parameters:
        - $ref: '#/components/parameters/TodoID'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoDetailUpdate'
      responses:
        '201':
          description: success
        '400':
          description: Bad Request
        '500':
          description: Internal Server Error
    delete:
      operationId: todo-delete
      description: todoタスクを削除します
      tags:
        - todo
      parameters:
        - $ref: '#/components/parameters/TodoID'
      responses:
        '204':
          description: success
        '400':
          description: Bad Request
        '500':
          description: Internal Server Error
components:
  schemas:
    TodoCreate:
      type: object
      properties:
        id:
          type: integer
          description: タスクのID
          example: 1
        task:
          type: string
          description: タスクの内容
          example: 買い物に行く
        deadline:
          type: string
          format: date
          description: 締切日
          example: '2024-10-12'
        done:
          type: boolean
          description: タスクの状態
          example: false
      required:
        - task
        - deadline
    Todo:
      type: object
      properties:
        id:
          type: integer
          description: タスクのID
          example: 1
        task:
          type: string
          description: タスクの内容
          example: 買い物に行く
        deadline:
          type: string
          format: date
          description: 締切日
          example: '2024-10-12'
        done:
          type: boolean
          description: タスクの状態
          example: false
      required:
        - id
        - task
        - deadline
        - done
    Todos:
      type: object
      properties:
        total:
          type: integer
          description: 全タスクの総数
          example: 2
        tasks:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
                description: タスクのID
                example: 1
              task:
                type: string
                description: タスクの内容
                example: 買い物に行く
              deadline:
                type: string
                format: date
                description: 締切日
                example: '2024-10-12'
              done:
                type: boolean
                description: タスクの状態
                example: false
          required:
            - id
            - task
            - deadline
            - done
    TodoDetail:
      type: object
      properties:
        task:
          type: string
          description: タスク
          example: 買い物に行く
        deadline:
          type: string
          description: 締切日
          example: '2024-10-12'
      required:
        - task
        - deadline
    TodoDetailUpdate:
      type: object
      properties:
        task:
          type: string
          description: タスクの内容
          example: 買い物に行く
        deadline:
          type: string
          format: date
          description: 締切日
          example: '2024-10-12'
        done:
          type: boolean
          description: タスクの状態
          example: false
      required:
        - task
        - deadline
        - done
  parameters:
    TodoID:
      name: taskId
      in: path
      description: 詳細を取得したいtodoタスクのID
      required: true
      schema:
        type: integer
        format: int64
      example: 1

```
</details>
こうすることでIDE上でファイルが見れるので便利になります。

![スクリーンショット 2024-11-14 0.59.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/4e337e5f-e668-9cba-07fd-c7d6bfc7946b.png)
>JetbrainsのIDEで見ています。

# dockerでコマンドを動かす

npmでインストールしたくない場合は、dockerイメージを使うこともできます。

下のコマンドで行うことができます。

```zsh:zsh
docker run -v $(PWD):/spec --rm redocly/cli:latest bundle docs/swagger/root.swagger.yml --output=docs/swagger/generated.gen.swagger.yml
```
> 指定しているファイルを相対パスとして記述しているからか、上のコマンドだとredoclyのversionファイルが出力されませんでした。

-vでボリュームを設定することができ、コンテナとローカルでコードを共有することができます。$(PWD)で現在のディレクトリ位置を表示して、それをコンテナ内の/specディレクトリにマウントします。

--rmでコンテナを実行したらコンテナを消すようにできます。
そこから先述したredoclyコマンドを書きます。

実行すると下のように表示されます。
```zsh
docker run -v /Users/maoz/Documents/application/HubMe:/spec --rm redocly/cli:latest bundle docs/swagger/root.swagger.yml --output=docs/swagger/generated.gen.swagger.yml
(node:1) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
bundling docs/swagger/root.swagger.yml...
📦 Created a bundle for docs/swagger/root.swagger.yml at docs/swagger/generated.gen.swagger.yml 26ms.
```


# GithubActionsでCIを回す。

GithubActionsで結合作業を忘れないよう自動化しましょう。

コマンド長いのと、コマンドを変更した際に変更を適応しやすいので、下のMakefileファイルを書きます。

```Makefile
docker/swagger/bundle:
	docker run -v $(PWD):/spec --rm redocly/cli:latest bundle docs/swagger/root.swagger.yml --output=docs/swagger/generated.gen.swagger.yml
 
```
>こちらはルートディレクトリにおきます。

下のようにCIを書きます。

```.github/workflows/swagger.yml
name: swagger.yml
on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - closed
    branches:
      - develop

jobs:
  swagger:
    name: swagger bundle
    permissions:
      contents: write
      pull-requests: write
    timeout-minutes: 10
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v4.2.2
      - name: swagger bundle
        run: make docker/swagger/bundle
      - name: fix swagger
        uses: dev-hato/actions-diff-pr-management@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          branch-name-prefix: 'fix-swagger'
```

## ymlについて
今回はdevelopブランチに対してpull_requestを作成した際にCIを回すようにしています。
```yml
on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - closed
    branches:
      - develop
```

jobsでactionに関する設定等を記述しています。
`permissions`で、GITHUB_TOKENにcontentの修正やpull_requestの作成を許可しています。
`timeout-minutes`で、タイムアウトを設定しています。
`runs-on`では、実行するOS環境を指定しています。
```yml
permissions:
    contents: write
    pull-requests: write
timeout-minutes: 10
runs-on: ubuntu-latest
```

stepsでjob内で実行する書く処理を記述しています。

checkoutでは、リポジトリの内容をactionsにクローンしています。
```yml
- name: checkout     
  uses: actions/checkout@v4.2.2   
```

swagger bundleで先ほど作成したmakeコマンドを実行しています。
```yml
- name: swagger bundle
  run: make docker/swagger/bundle
```

先ほどのmakeコマンドの結果を`actions-diff-pr-management`でPRとして作成して、適応させます。
```yml
- name: fix swagger
  uses: dev-hato/actions-diff-pr-management@v1
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    branch-name-prefix: 'fix-swagger'
```

`actions-diff-pr-management`に関する記事はこちらになります。

https://qiita.com/maooz4426/items/caf1537e469eff68e3c5


上記のCIを組むことで結合を自動化することができます。

ぜひ参考にしてください！

## GithubActions関連の記事

GithubActionsを使った記事です

https://qiita.com/maooz4426/items/015241f9c97f5aa4f411

https://qiita.com/maooz4426/items/056e0fa08cd0edcee4bf

https://qiita.com/maooz4426/items/e6ee4ed7909f7b2c0fb7

https://qiita.com/maooz4426/items/f6740347eec6df320a1e

https://qiita.com/maooz4426/items/44a71129ce2f0a463817