OpenAPI3.0のチュートリアルをやってみた

# 1. はじめに

本題に入る前に、前提となることを記述しておきます。

この記事は、徐々に仕様書ができていく様を表示しています。
そのため、行数が多くなっています。


## 1.1. 結論
この記事の結論を先に書いてしまいます。
結論は以下です。

- 以下のステップでOpenAPIが書ける。
  - メタ情報を書く
  - パス情報を書く
  - コンポーネントで整形する

## 1.2. 目的
目的は以下です。

- OpenAPI3.0を書けるようにする

## 1.3. 読者の対象
読者の対象は以下です。

- APIの仕様書を軽く理解したい方
- プログラミング初心者
- OpenAPI3.0の書き方をググったけど、英語ドキュメントばかりでわからない方
- 以下の基本知識がある方(参考にしているチュートリアルに記載されています)
  - REST API
  - YAML形式

## 1.4. 参考

- [(SwaggerHub)OpenAPI 3.0 Tutorial](https://app.swaggerhub.com/help/tutorials/openapi-3-tutorial)
- [SwaggerのDocumentation](https://swagger.io/specification/)

# 2. 本題

基本的に、SwaggerHubに書かれているOpenAPI3.0チュートリアル(上記「参照」のチュートリアル)に沿って記載しています。

なお、ここで説明するパラメーターは、チュートリアルに登場するパラメーターのみです。
すべてのパラメーターについては公式Documentation(上記「参照」のDocumenatation)を参照してください。

## 2.1. OpenAPIとは

- オープンソースのフォーマットで、APIの仕様を記述するもの。
- REST APIの仕様を記述するデファクトスタンダード。
- JSON形式とYAML形式のどちらでも記述できる。
  - SwaggerHubとしてはYAML形式を推奨している(容易に読み書きできるため)。

ここでは、タイトルにあるように、OpenAPI 3.0の説明をしていきます。
OpenAPIの最新バージョンは、「[OpenAPI-Specificationリリースノート](https://github.com/OAI/OpenAPI-Specification/releases)」を参照してください。

## 2.2. APIを作ってみる

では早速作ってみましょう。
まずは、OpenAPI 3.0で記述される仕様書がどのようなものなのか、説明します。

OpenAPI 3.0では、定義ファイルが以下3つのセクションに分かれます。

- メタ情報
- パス情報(エンドポイント)
  - パラメーター
  - リクエストボディ
  - レスポンス
- コンポーネント
  - スキーマ(データモデル)
  - パラメーター
  - レスポンス
  - その他のコンポーネント

それぞれの項目について、以下で説明します。
なお、以降で記載するサンプルYAMLは、チュートリアル記載のファイルとは異なる、自作のものです。
何に関するアプリケーションを作っているか、ご想像にお任せします。

### 2.2.1. メタ情報

作成するAPIがどういうものなのか、つまり情報についての情報(=メタデータ)を記述するセクションです。

#### サンプルコード

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise
servers:
  - url: http://localhost/training/v1
```

各パラメーターについて説明します。

パラメーター|説明
---|---
`openapi`|使用するOpenAPIのバージョンを記載する。ここでは3.0.0を使っている。<br>バージョンは、OpenAPIの[リリースノート](https://github.com/OAI/OpenAPI-Specification/releases)を参照してください。
`info`|OpenAPIで記述したい内容を記載するオブジェクト。主なパラメーターは以下。<br><br>　・必須パラメーター：`version`、 `title`<br>　・任意パラメーター：`description`
`title`|必須パラメーター。このAPI仕様書を用いて作成するアプリケーションのタイトル。
`version`|必須パラメーター。アプリケーションのバージョン(**OpenAPIのバージョンとは関係ない。アプリケーションのバージョンである。**)
`description`|任意パラメーター。アプリケーションについての説明
`servers`|APIターゲットホストサーバー情報を記載するオブジェクト。必須パラメーターとして`url`がある。
`url`|必須パラメーター。APIターゲットホストのURL。

他にもセキュアな認証認可を実現するAPIパラメーターもありますが、ここでは割愛します(ローカルホスト内エンドポイントを想定しているため)。

### 2.2.2. パス情報

エンドポイントやメソッドを記述するところです。

#### サンプルコード

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise
servers:
  - url: http://localhost/training/v1

#---ここから追加---
paths:
  /plans:
    get:
      description: Return a list of training plans
#---ここまで追加---
```

各パラメーター／値について説明します。

パラメーター／値|説明
---|---
`paths`|エンドポイントを記載するオブジェクト。<br>パスとして`/plans`を定義している。<br>また、パス`/plans`に対してGETメソッドを定義する。
`/plans`|サンプルYAMLではパスパラメーターに値`/plans`を定義している。
`get`|REST APIのメソッド。ここではパス`/plans` に対し、 `GET`メソッドを定義している。
`desctiption`|任意パラメーター。パス`/plans`に対し定義した`GET`メソッドについての説明。

ここまででわかることは、

「クライアントはトレーニングプラン一覧を取得するために、`GET http://localhost/training/v1/plans`を実行する」ということです。

#### (1) パラメーター
上記GETメソッドを実行するときに、パラメーターを設定できます。
設定できる主なパラメーターは以下です。

- クエリパラメーター
- リクエストボディ
- パスパラメーター

##### (1.1) クエリパラメーター
APIでよく見られるのが、このクエリパラメーターだと思います。URLの`?`のあとに見られるのがクエリパラメーターです。

###### サンプルコード

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise
servers:
  - url: http://localhost/training/v1
paths:
  /plans:
    get:
      description: Return a list of training plans

#---ここから追加---
      parameters:
        - name: limit
          in: query
          description: Limit the number of plans on a page
          schema:
            type: integer
        - name: offset
          in: query
          description: Specifies the page number of the plans to be displayed
          schema:
            type: integer
#---ここまで追加---
```

各パラメーターについて説明します。

パラメーター|説明
---|---
`parameters`|リクエストパラメーターを記述するオブジェクト
`name`|パラメーターの名前。ここでは`limit`、`offset`というパラメーターを用いている。
`in`|パラメーターの種類。ここでは`query`(クエリパラメーター)を用いている。
`description`|パラメーターの説明。
`schema`|パラメーター構造を記述するオブジェクト。
`type`|パラメーターの型の説明。ここでは`integer`型を用いている。

ここまででわかることは、「クライアントはトレーニングプラン一覧に対し、1ページあたり10項目、全3ページを取得するために、`GET http://localhost/training/v1/plans?limit=10&offset=3`を実行する。」ということです。


##### (1.2) リクエストボディ

`POST`、`PUT`、`PATCH` リクエストの場合、リクエストボディを含むことが多いです。

リクエストボディは、`requestBody`オブジェクトによって定義されます。

###### サンプルコード

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise
servers:
  - url: http://localhost/training/v1
paths:
  /plans:
    get:
      description: Return a list of training plans
      parameters:
        - name: limit
          in: query
          description: Limit the number of plans on a page
          schema:
            type: integer
        - name: offset
          in: query
          description: Specifies the page number of the plans to be displayed
          schema:
            type: integer
#---ここから追加---
    post:
      description: Register a new plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan_id
              properties:
                plan_id:
                  type: integer
                planner_name:
                  type: string
                plan_name:
                  type: string
                duration:
                  type: integer
                difficulty:
                  type: integer
#---ここまで追加---
```

各パラメーター／値について説明します。

パラメーター／値|説明
---|---
`requestBody`|リクエストボディを中身を設定するオブジェクト。
`content`|リクエストボディの内容を示すオブジェクト。
`application/json`|リクエストボディの構成がJSONであることを示す。

ここまででわかることは、「クライアントはトレーニングプラン一覧に対し、1ページあたり10項目、全3ページを取得するために、`GET http://localhost/training/v1/plans?limit=10&offset=3`を実行する。また、`plan_id`を必須パラメーターとして、`planner_name`、`plan_name`、`duration`、`difficulty`を選択することで新しいトレーニングプランを登録できる」ということです。


##### (1.3) パスパラメーター

パスパラメーターは、クライアントが操作しているデータの特定のコンポーネントを分離するために使われます。

パスパラメーターは必ず`parameters`のところで定義されます。

###### サンプルコード

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise
servers:
  - url: http://localhost/training/v1
paths:
  /plans:
    get:
      description: Return a list of training plans
      parameters:
        - name: limit
          in: query
          description: Limit the number of plans on a page
          schema:
            type: integer
        - name: offset
          in: query
          description: Specifies the page number of the plans to be displayed
          schema:
            type: integer
    post:
      description: Register a new plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan_id
              properties:
                plan_id:
                  type: integer
                planner_name:
                  type: string
                plan_name:
                  type: string
                duration:
                  type: integer
                difficulty:
                  type: integer

#---ここから追加---
  /plans/{planner_name}:
    get:
      description: Get information about the plan registered by the described planner
      parameters:
        - name: planner_name
          in: path
          required: true
          schema:
            type: string
#---ここまで追加---
```

ここまででわかることは、「クライアントはトレーニングプラン一覧に対し、1ページあたり10項目、全3ページを取得するために、`GET http://localhost/training/v1/plans?limit=10&offset=3`を実行する。また、`plan_id`を必須パラメーターとして、`planner_name`、`plan_name`、`duration`、`difficulty`を選択することで新しいトレーニングプランを登録できる。さらに、特定の`planner_name`をパスパラメーターに設定することができる(設定してリクエストを送信した後のレスポンスをどうするかは後述)。」ということです。


#### (2) レスポンス

各メソッドに対してレスポンスを返すのが、このレスポンスセクションです。

どのレスポンスも、最低1つ以上のHTTPステータスコードを記載する必要があります。ステータスコードを`200`にすれば`正常`、`400`にすれば`異常`としておけばよいでしょう。
レスポンスを記載したサンプルコードを掲載します。

##### サンプルコード

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise
servers:
  - url: http://localhost/training/v1
paths:
  /plans:
    get:
      description: Return a list of training plans
      parameters:
        - name: limit
          in: query
          description: Limit the number of plans on a page
          schema:
            type: integer
        - name: offset
          in: query
          description: Specifies the page number of the plans to be displayed
          schema:
            type: integer

#---ここから追加---
      responses:
        '200':
          description: Successfully returned a list of training plans
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - plan_id
                  properties:
                    plan_id:
                      type: integer
                    plan_name:
                      type: string
                    planner_name:
                      type: string
                    duration:
                      type: integer
                    difficulty:
                      type: integer
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
#---ここまで追加---

    post:
      description: Register a new plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan_id
              properties:
                plan_id:
                  type: integer
                planner_name:
                  type: string
                plan_name:
                  type: string
                duration:
                  type: integer
                difficulty:
                  type: integer

#---ここから追加---
      responses:
        '200':
          description: Successfully returned a list of training plans
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
#---ここまで追加---

  /plans/{planner_name}:
    get:
      description: Get information about the plan registered by the described planner
      parameters:
        - name: planner_name
          in: path
          required: true
          schema:
            type: string

#---ここから追加---
      responses:
        '200':
          description: Successfully returned a list of training plans
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - plan_id
                  properties:
                    plan_id:
                      type: integer
                    plan_name:
                      type: string
                    planner_name:
                      type: string
                    duration:
                      type: integer
                    difficulty:
                      type: integer
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
#---ここまで追加---
```

ここまででわかることは、「クライアントはトレーニングプラン一覧に対し、1ページあたり10項目、全3ページを取得するために、`GET http://localhost/training/v1/plans?limit=10&offset=3`を実行する。また、`plan_id`を必須パラメーターとして、`planner_name`、`plan_name`、`duration`、`difficulty`を選択することで新しいトレーニングプランを登録できる。さらに、特定の`planner_name`をパスパラメーターに設定することで、その`planner_name`に紐づいたトレーニングプランを取得できる。」ということです。

### 2.2.3. コンポーネント

上記に記載したサンプルYAMLを見ると、同じ用語がたくさん含まれているのがわかります(例：`plan_name`、`duration`など)。これらを何度も書くのはよくないです。情報は1箇所にまとまっているべきです。それを解決するのが、コンポーネントセクションです。

というわけで、ここではそれらの用語を再利用していくことを説明します。。

コンポーネントでは、以下のような型を定義できます。

- スキーマ
- パラメーター
- リクエストボディ
- レスポンス
- レスポンスヘッダ
- 例
- リンク
- コールバック

チュートリアルでは、スキーマ、パラメーターとレスポンスが掲載されていますので、今回もそれらを説明します。

#### (1) スキーマ

`schema`では、全く同じ構造体をグループにまとめることができます。

例えば、サンプルYAMLでは以下の部分が全く同じなので、まとめることができます。

```
                  type: object
                  required:
                    - plan_id
                  properties:
                    plan_id:
                      type: integer
                    plan_name:
                      type: string
                    planner_name:
                      type: string
                    duration:
                      type: integer
                    difficulty:
                      type: integer
```
これらを`components`にしてまとめましょう。
まとめた結果、サンプルYAMLは以下のようになります。

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise

servers:
  - url: http://localhost/training/v1

paths:
  /plans:
    get:
      description: Return a list of training plans
      parameters:
        - name: limit
          in: query
          description: Limit the number of plans on a page
          schema:
            type: integer
        - name: offset
          in: query
          description: Specifies the page number of the plans to be displayed
          schema:
            type: integer
      responses:
        '200':
          description: Successfully returned a list of training plans
          content:
            application/json:
              schema:
                type: array
                items:

#---ここから変更---
                  $ref: '#/components/schemas/plan'
#---ここまで変更---

        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
    post:
      description: Register a new plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan_id
              properties:
                plan_id:
                  type: integer
                planner_name:
                  type: string
                plan_name:
                  type: string
                duration:
                  type: integer
                difficulty:
                  type: integer
      responses:
        '200':
          description: Successfully returned a list of training plans
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
  /plans/{planner_name}:
    get:
      description: Get information about the plan registered by the described planner
      parameters:
        - name: planner_name
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Successfully returned a list of training plans
          content:
            application/json:
              schema:
                type: array
                items:

#---ここから変更---
                  $ref: '#/components/schemas/plan'
#---ここまで変更---

        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string

#---ここから変更---
components:
  schemas:
    plan:
      type: object
      required:
        - plan_id
      properties:
        plan_id:
          type: integer
        plan_name:
          type: string
        planner_name:
          type: string
        duration:
          type: integer
        difficulty:
          type: integer
#---ここまで変更---
```

行数が短くなるのはもちろんですが、同じスキーマを使った新しくエンドポイントを追加するのも手短にできるようになります。

#### (2) パラメーターとレスポンス

他にも、同じパラメーターやレスポンスを一部まとめてみましょう。

```training.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Training Menu API
  description: A simple menu for exercise
servers:
  - url: http://localhost/training/v1
paths:
  /plans:
    get:
      description: Return a list of training plans
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageOffset'
      responses:
        '200':
          description: Successfully returned a list of training plans
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/plan'
        '400':

#---ここから変更---
          $ref: '#/components/responses/400Error'
#---ここまで変更---

    post:
      description: Register a new plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan_id
              properties:
                plan_id:
                  type: integer
                planner_name:
                  type: string
                plan_name:
                  type: string
                duration:
                  type: integer
                difficulty:
                  type: integer
      responses:
        '200':
          description: Successfully returned a list of training plans
        '400':

#---ここから変更---
          $ref: '#/components/responses/400Error'
#---ここまで変更---

  /plans/{planner_name}:
    get:
      description: Get information about the plan registered by the described planner
      parameters:
        - name: planner_name
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Successfully returned a list of training plans
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/plan'
        '400':

#---ここから変更---
          $ref: '#/components/responses/400Error'
#---ここまで変更---

components:
  schemas:
    plan:
      type: object
      required:
        - plan_id
      properties:
        plan_id:
          type: integer
        plan_name:
          type: string
        planner_name:
          type: string
        duration:
          type: integer
        difficulty:
          type: integer

#---ここから変更---
  parameters:
    PageLimit:
        name: limit
        in: query
        description: Limit the number of plans on a page
        schema:
          type: integer
    PageOffset:
        name: offset
        in: query
        description: Specifies the page number of the plans to be displayed
        schema:
          type: integer
  responses:
    400Error:
      description: Invalid request
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
#---ここまで変更---
```

## 2.3. まとめ

これでREST APIの仕様書が作成できました。
このチュートリアルでやった流れは、

1. メタ情報を書く
1. パス情報を書く
1. コンポーネントにまとめて整形する

です。