protoファイルからいい感じのAPI仕様書を作りたい

# はじめに

gRPC + ProtocolBuffers でAPIを作ったり使っている方々がこちらの記事を読んでくれているんだと思いますが、皆さんはAPI仕様をどのように確認しているでしょうか。

protoファイルと別に仕様書を作ると二重管理になってしまってメンテナンスが面倒ですし、protoファイルを直接読むにもファイルが別れている場合、どこに何の記載があるのかをこれまた管理する必要があってやっぱり無駄なものが生まれますよね。

それらを解消する方法として、本記事ではprotoc-gen-docを使ってprotoファイルから直接ドキュメントを生成する方法と、カスタムテンプレートを使って更に読みやすくする方法をまとめたいと思います。

（意外と日本語で取りまとめられてなかったのでアドベントカレンダーなら良い機会かなーって）

# protoファイルからドキュメントを生成

protoファイルからドキュメントを生成するには、protoc-gen-docというプラグインを用います。
一応[ライブラリとしても公開](https://godoc.org/github.com/pseudomuto/protoc-gen-doc)しているようですが、protocのオプションとしての利用がメインのようです。

## protoc-gen-docの使い方

インストールにはgo getを用います。

```console:console
$ # インストール
$ go get -u github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc
```

インストールしたら出力を実行します。
下記はカレントディレクトリにprotoディレクトリがあり、その中のprotoファイルのAPI仕様書を作成したい場合に用います。

```console:console
$ # 出力実行
$ protoc \
--doc_out=./doc \
--doc_opt=markdown,index.md:./ proto/*.proto
```

なお、大体のAPIでは外部packageをインポートしているので、そのパスも指定しなければなりません。
下記の場合はカレントとgoogleapisへのパスを指定しています。
成功すればカレントディレクトリにindex.mdというファイルが生成されます。

```console:console
$ # インポートパスを指定して出力実行
$ protoc \
-I. \
-I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--doc_out=markdown,index.md:./ proto/*.proto
```

例ではMarkdown形式を指定していますが、下記の出力形式を選択可能です。

- markdown
- html
- docbook
- json

## 惜しいポイント

protoc-gen-doc でprotoファイルからドキュメント作れるの良いんですけど微妙に利用しづらい点が個人的にありました。

- タイトルやらが英語  
ド頭にProtocol Documentationドーンじゃなくてプロジェクト名とか日本語のタイトルとか付けたい
- 一覧が見づらい  
API仕様書として読みたいのに、一覧に型(Message)まで載せるのは情報過多
- 地味にScalarValueTypeにGoの型が載っていない

あとはHTMLに出力したときの見た目がダサい。。。（個人的な感想です）

このあたりを解消するための方法にカスタムテンプレートの利用があるので次節で説明します。

# カスタムテンプレートの利用

まず前提として、protoc-gen-docはテンプレート出力処理として、text/templateを使用しているとのことです。
上記で説明した出力処理では`--doc_out=markdown,index.md`と記載しましたが、この設定の場合はデフォルトのMarkdown用テンプレートファイルを元に出力するのですが、これを自作テンプレートに変更することが可能です。

仮にカレントディレクトリに`resource`ディレクトリを設置し、Markdown用の自作カスタムテンプレートを作成した場合、コマンドとしては下記のようになります。

```console
$ protoc \
-I. \
-I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--doc_out=resource/custom_markdown.tmpl,index.md:./ \
proto/*.proto
```

## 利用可能な構造

何のテンプレートライブラリを使っているのかが分かったとしてもどのような構造を使えるのかが分からないとテンプレート書きようがないですよね。
ということでまずは直接[デフォルトのMarkdown用テンプレートファイル](https://github.com/pseudomuto/protoc-gen-doc/blob/master/resources/markdown.tmpl)を読んでみます。

### デフォルトテンプレートの構造

一部抜粋すると下記のような要素が見当たりました。

```markdown:markdown.tmpl
## Table of Contents
{{range .Files}}
{{$file_name := .Name}}- [{{.Name}}](#{{.Name}})
  {{range .Messages}}  - [{{.LongName}}](#{{.FullName}})
  {{end}}
  {{range .Enums}}  - [{{.LongName}}](#{{.FullName}})
  {{end}}
  {{range .Extensions}}  - [File-level Extensions](#{{$file_name}}-extensions)
  {{end}}
  {{range .Services}}  - [{{.Name}}](#{{.FullName}})
  {{end}}
{{end}}
```

これは一覧部分の描画で、protoファイル順に`Messages`、`Enums`、`Extensions`、`Services`の名前を出力している箇所でした。
そのため、それぞれの構造の定義箇所を見れば何が出力されるのかが分かりそうです。

というわけで[プログラム](https://github.com/pseudomuto/protoc-gen-doc/blob/master/template.go)を直接読んで使える構造を調べて下記にまとめました。

ちなみに、呼び出し方は`text/template`に準じているので[こちら](https://golang.org/pkg/text/template/)を参考にすると良いと思います。
ポイントだけ抜粋すると、スライスは`range`呼び出しでその型を繰り返し処理します。
また、bool型の要素は`if`呼び出しで条件式に利用します。

### type Template

まず、大本として`Template`型があり、フィールドとして`File`型スライスの`Files`と`ScalarValue`型スライスの`Scalars`を持っているとのこと。
カスタムテンプレート内にはこれをベースに埋め込んでいくことになります。

|名前  |型  |呼び出し方  |
|---|---|---|
|Files  |[]*[File](#type-file)  |{{range .Files}} ~~ {{end}} |
|Scalars  |[]*[ScalarValue](#type-scalarvalue)  |{{range .Scalars}} ~~ {{end}}|

#### type File

protoファイルに定義した情報は全て`File`型に内包されています。
API仕様書に記載する`Message`や`Service`の情報は`File`経由で読み出します。

|名前  |型  |呼び出し方  |備考  |
|---|---|---|---|
| Name  | string  |{{.Name}} |   |
| Description  | string  |{{.Description}} |   |
| Package  | string  |{{.Package}} |   |
| HasEnums  | bool  |{{if .HasEnums}} ~~ {{end}} |   |
| HasExtensions  | bool  |{{if .HasExtensions}} ~~ {{end}} |   |
| HasMessages  | bool  |{{if .HasMessages}} ~~ {{end}} |   |
| HasServices  | bool  |{{if .HasServices}} ~~ {{end}} |   |
| Enums  | orderedEnums  |{{range .Enums}} ~~ {{end}} | 内部的には[]*[Enum](#type-enum)|
| Extensions  | orderedExtensions  |{{range .Extensions}} ~~ {{end}} |  内部的には[]*[FileExtension](#type-fileextension) |
| Messages  | orderedMessages  |{{range .Messages}} ~~ {{end}} |  内部的には[]*[Message](#type-message) |
| Services  | orderedServices  |{{range .Services}} ~~ {{end}} |  内部的には[]*[Service](#type-service) |
| Options  |map[string]interface{}  | {{range \$k, $v := .Options}} ~~ {{end}} |

##### type FileExtension

|名前  |型  |呼び出し方  |
|---|---|---|
| Name  | string  |{{.Name}} |
| LongName  | string  |{{.LongName}} |
| FullName  | string  |{{.FullName}} |
| Description  | string  |{{.Description}} |
| Label  | string  |{{.Label}} |
| Type  | string  |{{.Type}} |
| LongType  | string  |{{.LongType}} |
| FullType  | string  |{{.FullType}} |
| Number  | string  |{{.Number}} |
| DefaultValue  | string  |{{.DefaultValue}} |
| ContainingType  | string  |{{.ContainingType}} |
| ContainingLongType  | string  |{{.ContainingLongType}} |
| ContainingFullType  | string  |{{.ContainingFullType}} |


##### type Message

|名前  |型  |呼び出し方  |
|---|---|---|
| Name  | string  |{{.Name}} |
| LongName  | string  |{{.LongName}} |
| FullName  | string  |{{.FullName}} |
| Description  | string  |{{.Description}} |
| HasExtensions  | bool  |{{if .HasExtensions}} ~~ {{end}} |
| HasFields  | bool  |{{if .HasFields}} ~~ {{end}} |
| Extensions  |[]*[MessageExtension](#type-messageextension)  |{{range .Extensions}} ~~ {{end}} |
| Fields  |[]*[MessageField](#type-messagefield)  |{{range .Fields}} ~~ {{end}} |
| Options  |map[string]interface{}  | {{range \$k, $v := .Options}} ~~ {{end}} |


###### type MessageField

|名前  |型  |呼び出し方  |
|---|---|---|
| Name  | string  |{{.Name}} |
| Description  | string  |{{.Description}} |
| Label  | string  |{{.Label}} |
| Type  | string  |{{.Type}} |
| LongType  | string  |{{.LongType}} |
| FullType  | string  |{{.FullType}} |
| IsMap  | bool  |{{.IsMap}} |
| DefaultValue  | string  |{{.DefaultValue}} |
| Options  |map[string]interface{}  | {{range \$k, $v := .Options}} ~~ {{end}} |


###### type MessageExtension

|名前  |型  |呼び出し方  |
|---|---|---|
| (埋め込み) | [FileExtension](#type-fileextension)  | （[FileExtension](#type-fileextension)参照） |
| ScopeType  | string  |{{.ScopeType}} |
| ScopeLongType  | string  |{{.ScopeLongType}} |
| ScopeFullType  | string  |{{.ScopeFullType}} |


##### type Enum

|名前  |型  |呼び出し方  |
|---|---|---|
| Name  | string  |{{.Name}} |
| LongName  | string  |{{.LongName}} |
| FullName  | string  |{{.FullName}} |
| Description  | string  |{{.Description}} |
| Values  |[]*EnumValue  |{{range .Values}} ~~ {{end}} |
| Options  |map[string]interface{}  | {{range \$k, $v := .Options}} ~~ {{end}} |

###### type EnumValue

|名前  |型  |呼び出し方  |
|---|---|---|
| Name  | string  |{{.Name}} |
| Number  | string  |{{.Number}} |
| Description  | string  |{{.Description}} |
| Options  |map[string]interface{}  | {{range \$k, $v := .Options}} ~~ {{end}} |

##### type Service

|名前  |型  |呼び出し方  |
|---|---|---|
| Name  | string  |{{.Name}} |
| LongName  | string  |{{.LongName}} |
| FullName  | string  |{{.FullName}} |
| Description  | string  |{{.Description}} |
| Methods  | []*ServiceMethod  |{{range .Methods}} ~~ {{end}} |
| Options  |map[string]interface{}  | {{range \$k, $v := .Options}} ~~ {{end}} |

###### type ServiceMethod

|名前  |型  |呼び出し方  |
|---|---|---|
| Name  | string  |{{.Name}} |
| Description  | string  |{{.Description}} |
| RequestType  | string  |{{.RequestType}} |
| RequestLongType  | string  |{{.RequestLongType}} |
| RequestFullType  | string  |{{.RequestFullType}} |
| RequestStreaming  | bool  |{{if .RequestStreaming}} ~~ {{end}} |
| ResponseType  | string  |{{.ResponseType}} |
| ResponseLongType  | string  |{{.ResponseLongType}} |
| ResponseFullType  | string  |{{.ResponseFullType}} |
| ResponseStreaming  | bool  |{{if .ResponseStreaming}} ~~ {{end}} |
| Options  |map[string]interface{}  | {{range \$k, $v := .Options}} ~~ {{end}} |


#### type ScalarValue

protobufの`scalar value type`（スカラ値型）についての情報の型です。
デフォルトではGoTypeが指定されていないので、必要な人は追加すると良いです。

|名前  |型  |呼び出し方  |
|---|---|---|
| ProtoType  |string  |{{.ProtoType}} |
| Notes  |string  |{{.Notes}} |
| CppType  |string  |{{.CppType}}  |
| CSharp  |string  |{{.CSharp}}  |
| GoType  |string  |{{.GoType}}  |
| JavaType  |string  |{{.JavaType}}  |
| PhpType  |string  |{{.PhpType}}  |
| PythonType  |string  |{{.PythonType}}  |
| RubyType  |string  |{{.RubyType}}  |

## サンプル

デフォルトテンプレートを少しだけ変更したMarkdown用のテンプレートを置いておきますので、良かったら使ってください。
- タイトル日本語化
- インデックス調整
- Go用のScalarValueType追加

```md:custom_markdown.tmpl
# サンプルAPI仕様書
<a name="top"></a>

## インデックス
- [API仕様](#API仕様)
{{range .Files}}
{{$file_name := .Name}}  - [{{.Name}}](#{{.Name}})
  {{range .Messages}}    - [{{.LongName}}](#{{.FullName}})
  {{end}}
  {{range .Enums}}    - [{{.LongName}}](#{{.FullName}})
  {{end}}
  {{range .Extensions}}    - [File-level Extensions](#{{$file_name}}-extensions)
  {{end}}
  {{range .Services}}    - [{{.Name}}](#{{.FullName}})
  {{end}}
{{end}}
- [スカラー値型](#スカラー値型)

## API仕様
{{range .Files}}
{{$file_name := .Name}}
<a name="{{.Name}}"></a>
<p align="right"><a href="#top">Top</a></p>

### {{.Name}}
{{.Description}}

{{range .Messages}}
<a name="{{.FullName}}"></a>

#### {{.LongName}}
{{.Description}}

{{if .HasFields}}
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
{{range .Fields -}}
  | {{.Name}} | [{{.LongType}}](#{{.FullType}}) | {{.Label}} | {{nobr .Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}} |
{{end}}
{{end}}

{{if .HasExtensions}}
| Extension | Type | Base | Number | Description |
| --------- | ---- | ---- | ------ | ----------- |
{{range .Extensions -}}
  | {{.Name}} | {{.LongType}} | {{.ContainingLongType}} | {{.Number}} | {{nobr .Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}} |
{{end}}
{{end}}

{{end}} <!-- end messages -->

{{range .Enums}}
<a name="{{.FullName}}"></a>

#### {{.LongName}}
{{.Description}}

| Name | Number | Description |
| ---- | ------ | ----------- |
{{range .Values -}}
  | {{.Name}} | {{.Number}} | {{nobr .Description}} |
{{end}}

{{end}} <!-- end enums -->

{{if .HasExtensions}}
<a name="{{$file_name}}-extensions"></a>

#### File-level Extensions
| Extension | Type | Base | Number | Description |
| --------- | ---- | ---- | ------ | ----------- |
{{range .Extensions -}}
  | {{.Name}} | {{.LongType}} | {{.ContainingLongType}} | {{.Number}} | {{nobr .Description}}{{if .DefaultValue}} Default: `{{.DefaultValue}}`{{end}} |
{{end}}
{{end}} <!-- end HasExtensions -->

{{range .Services}}
<a name="{{.FullName}}"></a>

#### {{.Name}}
{{.Description}}

| Method Name | Request Type | Response Type | Description |
| ----------- | ------------ | ------------- | ------------|
{{range .Methods -}}
  | {{.Name}} | [{{.RequestLongType}}](#{{.RequestFullType}}){{if .RequestStreaming}} stream{{end}} | [{{.ResponseLongType}}](#{{.ResponseFullType}}){{if .ResponseStreaming}} stream{{end}} | {{nobr .Description}} |
{{end}}
{{end}} <!-- end services -->

{{end}}

## スカラー値型

| .proto Type | Notes | Go Type | C++ Type | Java Type | Python Type |
| ----------- | ----- | -------- | -------- | --------- | ----------- |
{{range .Scalars -}}
  | <a name="{{.ProtoType}}" /> {{.ProtoType}} | {{.Notes}} | {{.GoType}} | {{.CppType}} | {{.JavaType}} | {{.PythonType}} |
{{end}}
```

## おまけ HTML出力時のスタイルシート

今回はMarkdown出力を例にしましたが、HTML出力の場合、HTMLファイル（今回の例だとindex.html）の隣にstylesheet.cssを置けば反映されるらしいです。

```html:HTML用デフォルトテンプレートより抜粋
    <!-- User custom CSS -->
    <link rel="stylesheet" type="text/css" href="stylesheet.css"/>
```

# 終わりに

自分の整理のために書いてみたら思いの外長くなりました。
カスタムテンプレートを利用すれば整理された見やすいAPI仕様書ができるんじゃないかと思います。
あとはCI等利用して自動生成してREADME.mdあたりからリンクを貼っておけば、継続的にメンテされた仕様書の完成となるのかなと。

どこかの誰かのために少しでも参考になれば嬉しいです。

# 参考

protoc-gen-doc公式ドキュメント
https://github.com/pseudomuto/protoc-gen-doc/blob/master/examples/doc/example.md#com.example.Vehicle.Category

package template
https://golang.org/pkg/text/template/