32
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

and factoryAdvent Calendar 2019

Day 15

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

Last updated at Posted at 2019-12-14

はじめに

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

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

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

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

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

protoファイルからドキュメントを生成するには、protoc-gen-docというプラグインを用います。
一応ライブラリとしても公開しているようですが、protocのオプションとしての利用がメインのようです。

protoc-gen-docの使い方

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

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

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

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

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

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用の自作カスタムテンプレートを作成した場合、コマンドとしては下記のようになります。

$ 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用テンプレートファイルを読んでみます。

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

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

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ファイル順にMessagesEnumsExtensionsServicesの名前を出力している箇所でした。
そのため、それぞれの構造の定義箇所を見れば何が出力されるのかが分かりそうです。

というわけでプログラムを直接読んで使える構造を調べて下記にまとめました。

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

type Template

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

名前 呼び出し方
Files []*File {{range .Files}} ~~ {{end}}
Scalars []*ScalarValue {{range .Scalars}} ~~ {{end}}

type File

protoファイルに定義した情報は全てFile型に内包されています。
API仕様書に記載するMessageServiceの情報は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追加
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用デフォルトテンプレートより抜粋
    <!-- 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/

32
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?