はじめに
gRPC + ProtocolBuffers でAPIを作ったり使っている方々がこちらの記事を読んでくれているんだと思いますが、皆さんはAPI仕様をどのように確認しているでしょうか。
protoファイルと別に仕様書を作ると二重管理になってしまってメンテナンスが面倒ですし、protoファイルを直接読むにもファイルが別れている場合、どこに何の記載があるのかをこれまた管理する必要があってやっぱり無駄なものが生まれますよね。
それらを解消する方法として、本記事ではprotoc-gen-docを使ってprotoファイルから直接ドキュメントを生成する方法と、カスタムテンプレートを使って更に読みやすくする方法をまとめたいと思います。
(意外と日本語で取りまとめられてなかったのでアドベントカレンダーなら良い機会かなーって)
protoファイルからドキュメントを生成
protoファイルからドキュメントを生成するには、protoc-gen-docというプラグインを用います。
一応ライブラリとしても公開しているようですが、protocのオプションとしての利用がメインのようです。
protoc-gen-docの使い方
インストールにはgo getを用います。
$ # インストール
$ go get -u github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc
インストールしたら出力を実行します。
下記はカレントディレクトリにprotoディレクトリがあり、その中のprotoファイルのAPI仕様書を作成したい場合に用います。
$ # 出力実行
$ protoc \
--doc_out=./doc \
--doc_opt=markdown,index.md:./ proto/*.proto
なお、大体のAPIでは外部packageをインポートしているので、そのパスも指定しなければなりません。
下記の場合はカレントとgoogleapisへのパスを指定しています。
成功すればカレントディレクトリにindex.mdというファイルが生成されます。
$ # インポートパスを指定して出力実行
$ 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用の自作カスタムテンプレートを作成した場合、コマンドとしては下記のようになります。
$ 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用テンプレートファイルを読んでみます。
デフォルトテンプレートの構造
一部抜粋すると下記のような要素が見当たりました。
## 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
の名前を出力している箇所でした。
そのため、それぞれの構造の定義箇所を見れば何が出力されるのかが分かりそうです。
というわけでプログラムを直接読んで使える構造を調べて下記にまとめました。
ちなみに、呼び出し方はtext/template
に準じているのでこちらを参考にすると良いと思います。
ポイントだけ抜粋すると、スライスはrange
呼び出しでその型を繰り返し処理します。
また、bool型の要素はif
呼び出しで条件式に利用します。
type Template
まず、大本としてTemplate
型があり、フィールドとしてFile
型スライスのFiles
とScalarValue
型スライスのScalars
を持っているとのこと。
カスタムテンプレート内にはこれをベースに埋め込んでいくことになります。
名前 | 型 | 呼び出し方 |
---|---|---|
Files | []*File | {{range .Files}} ~~ {{end}} |
Scalars | []*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 |
Extensions | orderedExtensions | {{range .Extensions}} ~~ {{end}} | 内部的には[]*FileExtension |
Messages | orderedMessages | {{range .Messages}} ~~ {{end}} | 内部的には[]*Message |
Services | orderedServices | {{range .Services}} ~~ {{end}} | 内部的には[]*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 | {{range .Extensions}} ~~ {{end}} |
Fields | []*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 | (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追加
# サンプル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を置けば反映されるらしいです。
<!-- 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/