protoc-gen-openapiv2を使用したprotoから自動生成されるswaggerのカスタマイズ

# 前提環境
- go version1.17.10 linux/amd64
- [grpc-ecosystem/grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) version2.3.0

# 概要
前提としてスキーマ駆動開発、コード自動生成、golangの静的型付けによる安全性といった恩恵を受けるためにProtocol Buffersを使用してAPIの定義を行なっています。
このAPIの仕様を他開発者も読むため、定義したprotoファイルからswaggerを自動で生成できるようにProtocol Buffersのコマンドを設定しています。
しかしこのswaggerに関して、細かい仕様定義ができずに意思疎通ができないという問題がありました。
`grpc-ecosystem/grpc-gateway`に内包されている**protoc-gen-openapiv2プラグイン**を使用して自動生成されるswaggerをカスタマイズすることができました。
日本語記事があまりなく、grpc-gatewayのドキュメントなどにも詳しく載っていないため備忘録として残そうと思います。

# 発生した問題
デフォルトのswagger自動生成で発生した意思疎通の齟齬では下記のような問題が発生していました。
- 各APIにおいて発生しうるステータスコードがわからない
- リクエストの必須項目やどんなバリデーションがなされるのかがわからない

# 前提

### ディレクトリ構造
- 作業の前下記のようなディレクトリ構造にしています。
- 実務ではもっとファイル数が多いのですがqiitaのために省略しています。
- `proto`: プロジェクトを構成するために用意したprotoファイルを格納しています。
  - `/third_party`: googleなどが提供しているprotoファイルを格納しています。
    - `/google/api`: [こちら](https://github.com/grpc-ecosystem/grpc-gateway)のREADMEの通りにprotoファイルを配置しています。
    - `/google/protobuf`: [こちら](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/any.proto)と[こちら](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto)を配置しています。
    - `/grpc/health/v1`: [こちら](https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto)で提供されているprotoファイルを配置しています。
- `proto_generated`: protoファイルから生成されたファイルを格納しています。今回はswaggerだけなのでopenapiディレクトリだけを記載しています。

```
project
│
├── 色々省略....
├── proto
│   ├── third_party
│   │   ├── google
│   │   │   ├── api
│   │   │   │   ├── annotations.proto
│   │   │   │   ├── field_behavior.proto
│   │   │   │   ├── http.proto
│   │   │   │   └── httpbody.proto
│   │   │   └── protobuf
│   │   │       ├── any.proto
│   │   │       ├── descriptor.proto
│   │   │       └── field_mask.proto
│   │   └── grpc
│   │       └── health
│   │           └── v1
│   │               └── health.proto
│   └── user_service.proto
├── proto_generated
│   └── openapi
│       └── api_definition.swagger.json
```

### protoファイル
- ひとまずわかりやすくするためにuser_service.protoとhealth.protoのみです。
- user_service.protoは下記になります。

user_service.proto
```proto
syntax = "proto3";

package qiita.service;
option go_package = "github.com/qiita/proto";

import "third_party/google/api/annotations.proto";

service UserService {
  rpc Create(CreateRequest) returns (CreateResponse) {
    option (google.api.http) = {
      post: "/v1/users/create"
    };
  }
  rpc Read(ReadRequest) returns (ReadResponse) {
    option (google.api.http) = {
      get: "/v1/users"
    };
  }
}

message CreateRequest {
  string email = 1;
  string name = 2;
}

message CreateResponse {
}

message ReadRequest {}

message ReadResponse {
  repeated User users = 1;
}

message User {
  string user_id = 1;
  string email = 2;
  string name = 3;
}
```

### 生成コマンド
- makefileで実行しています。

```makefile
PROTO_FILES = $(shell find ./proto -name "*.proto" -type f)

.PHONY: protoc
protoc:
	rm -rf ./proto_generated/*
	mkdir ./proto_generated/openapi
	protoc -I ./proto --openapiv2_out proto_generated/openapi --openapiv2_opt logtostderr=true \
	--openapiv2_opt disable_default_errors=true \
	--openapiv2_opt allow_merge=true \
	--openapiv2_opt merge_file_name="api_definition.yml" ./proto/*.proto $(PROTO_FILES)

```

### 生成されるswagger

```json-doc
{
  "swagger": "2.0",
  "info": {
    "title": "user_service.proto",
    "version": "version not set"
  },
  "tags": [
    {
      "name": "UserService"
    },
    {
      "name": "Health"
    }
  ],
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "Health_Check",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/v1HealthCheckResponse"
            }
          }
        },
        "parameters": [
          {
            "name": "service",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "Health"
        ]
      }
    },
    "/v1/users": {
      "get": {
        "operationId": "UserService_Read",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/serviceReadResponse"
            }
          }
        },
        "tags": [
          "UserService"
        ]
      }
    },
    "/v1/users/create": {
      "post": {
        "operationId": "UserService_Create",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/serviceCreateResponse"
            }
          }
        },
        "tags": [
          "UserService"
        ]
      }
    }
  },
  "definitions": {
    "HealthCheckResponseServingStatus": {
      "type": "string",
      "enum": [
        "UNKNOWN",
        "SERVING",
        "NOT_SERVING",
        "SERVICE_UNKNOWN"
      ],
      "default": "UNKNOWN"
    },
    "serviceCreateResponse": {
      "type": "object"
    },
    "serviceReadResponse": {
      "type": "object",
      "properties": {
        "users": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/serviceUser"
          }
        }
      }
    },
    "serviceUser": {
      "type": "object",
      "properties": {
        "userId": {
          "type": "string"
        },
        "email": {
          "type": "string"
        },
        "name": {
          "type": "string"
        }
      }
    },
    "v1HealthCheckResponse": {
      "type": "object",
      "properties": {
        "status": {
          "$ref": "#/definitions/HealthCheckResponseServingStatus"
        }
      }
    }
  }
}

```


# エラーレスポンスがわからない
- protoファイルから生成されたswaggerをみるとエラーレスポンスが定義されていません。
- そのため別開発者が各APIで発生しうるステータスコードを考慮できないという問題がありました。
- そこで冒頭で記載した**protoc-gen-openapiv2**プラグインを使用します。

## 使用方法
### protoファイルのDLと配置
- [protoc-gen-openapiv2](https://github.com/grpc-ecosystem/grpc-gateway/tree/master/protoc-gen-openapiv2/options)のoptionsディレクトリから、`annotations.proto`と`openapiv2.proto`をダウンロードします。
- 今回は下記のようなディレクトリになるように配置しました。

```
├── proto
│   ├── third_party
│   │   ├── google
│   │   │   ├── api
│   │   │   │   ├── annotations.proto
│   │   │   │   ├── field_behavior.proto
│   │   │   │   ├── http.proto
│   │   │   │   └── httpbody.proto
│   │   │   └── protobuf
│   │   │       ├── any.proto
│   │   │       ├── descriptor.proto
│   │   │       └── field_mask.proto
│   │   └── grpc
│   │       ├── health
│   │       │   └── v1
│   │       │       └── health.proto
│   │       └── openapiv2
│   │           └── options
│   │               ├── annotations.proto ※ここ
│   │               └── openapiv2.proto ※ここ
│   └── user_service.proto
├── proto_generated
   └── openapi
      └── api_definition.swagger.json
```

- ちなみに配置した`annotations.proto`と `openapiv2.proto`のコメントに使用方法が記載されています。

### protoファイルのインポートと変更
- まず全てのAPIで500エラーは返り得るので、全てのAPIに500エラーを追加するところから始めます。
- 実施する作業は下記
    - protoファイルに`import "third_party/grpc/openapiv2/options/annotations.proto"`を記述して、先ほどDLしたprotoファイルを読み込めるようにする
    - `grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger`アノテーションを使用してカスタマイズする

### 実際にカスタマイズしたファイル
user_service.proto

```proto
syntax = "proto3";

package qiita.service;
option go_package = "github.com/qiita/proto";

import "third_party/google/api/annotations.proto";
import "third_party/grpc/openapiv2/options/annotations.proto";

option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
  responses: {
    key: "500";
    value: {
      description: "Internal Server Error";
    }
  }
};

service UserService {
以下略...
```

これにより、swagger上の全てのAPIに500エラーの定義が追加されることがわかるかと思います。

### レスポンスの定義を使用する
- 今はレスポンス自体の定義を行なっていないため、値がdescriptionのみになっています。
- そんな時にはschema機能を使用します。
- 下記のように書き換えることで、レスポンスの定義を使用できるかと思います。

```proto
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
  responses: {
    key: "500";
    value: {
      description: "サーバーエラー";
      schema: {
        json_schema: {
          ref: ".ErrorResponse";
        }
      }
    }
  }
};

message ErrorResponse {
  int32 status = 1;
  string title = 2;
  string detail = 3;
}

```

### exampleがわからない
- 今回は500エラーだけですが、レスポンスの中のエラーコードなどをexampleで表現したい場合もあるかと思います。
- そんな時はexamplesを使用することで解決できます。
- examplesはstringで認識されるため、面倒ではあるのですがJSON構造をstringで構築する必要があります。

```proto
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
  responses: {
    key: "500";
    value: {
      description: "サーバーエラー"
      schema: {
        json_schema: {
          ref: ".ErrorResponse";
        }
      }
      examples: {
        key: "application/json"
        value: "{\"status:\": 1, \"title\": \"titleExample\", \"detail\": \"detailExample\"}"
      }
    }
  }
};
```
### API単位でカスタマイズしたい
- 今までの例は全てのAPIに関連するようなカスタマイズ設定でした。
- もちろんですが、API固有の設定をすることもできます。
- その際は`grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation`アノテーションで実現できます。
- 例えば、Createを実行した場合にユーザーの重複により409 Conflictを返却したい場合は下記のように記述します。

```proto
service UserService {
  rpc Create(CreateRequest) returns (CreateResponse) {
    option (google.api.http) = {
      post: "/v1/users/create"
    };
    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
      responses: {
        key: "409"
        value: {
          description: "ユーザの重複時に返却"
          schema: {
            json_schema: {
              ref: ".ErrorResponse";
            }
          }
          examples: {
            key: "application/json"
            value: "{\"status:\": 1, \"title\": \"titleExample\", \"detail\": \"detailExample\"}"
          }
        }
      }
    };
  }
  rpc Read(ReadRequest) returns (ReadResponse) {
    option (google.api.http) = {
      get: "/v1/users"
    };
  }
}
```

# まとめ
- 今回は`openapiv2_swagger`と`openapiv2_operation`に絞って紹介しましたが、`openapiv2_schema `, `openapiv2_tag`, `openapiv2_field`など他にもカスタマイズ機能が用意されています。
- protoファイルからのgoコードの自動生成、swaggerの自動生成は非常に便利だと感じています。
- ただその反面、痒いところに手が届かなかったりということは起きてしまうのかなと感じました。
- このカスタマイズが個人的には少し手間だと感じているので、もう少しいいやり方があればいいなぁと思っています。


# 参考
- https://techblog.cartaholdings.co.jp/entry/protoc-gen-swagger
- https://rotational.io/blog/documenting-grpc-with-openapi/

# さいごに

トレタでは一緒に開発する仲間を募集しています。<br>
興味がある方は是非カジュアル面談へお越しください！

https://www.wantedly.com/companies/toreta/projects