OpenAPI GeneratorでGoのAPIサーバー/クライアントコードを自動生成する

この記事は[Go 4 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go4) 2日目の記事です。

# この記事について
OpenAPIのyamlファイルから、APIサーバーやクライアントコードを生成するopen-generatorというツールが存在します。
今回は、これを用いてGoのAPIサーバー/クライアントのコードを自動生成させ、それに手を加えて実際に使えるようにするまでの過程を紹介します。

## 使用する環境・バージョン
- go version go1.14 darwin/amd64
- openapi 3.0.1
- openapi-generator 4.3.1

# OpenAPIとは？
OpenAPIとは、REST APIを記述するためのフォーマット規格です。決められた記法でAPI仕様を記述すると、その内容をSwagger UIというきれいな形式にして確認することができるようになります。
![スクリーンショット 2020-11-23 16.13.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/a5abf319-8998-275a-100e-8c4170cf2ba2.png)

"Swagger"というのはOpenAPIの以前の名前です。メジャーバージョン2系まではSwaggerという名前でしたが、3系へアップデートされる際に紆余曲折ありOpenAPIと名前が変わりました。

# ドキュメントの作成
OpenAPIでは、API仕様をyamlファイルかjson形式で書くことになっています。ここではyaml形式で書くことにします。

## yamlファイルを書く
一例として、`openapi.yml`というファイルを用意して簡単なAPI仕様を書いてみます。

```openapi.yml
openapi: 3.0.1

info:
  title: "sample API"
  description: "This is sample."
  version: "1.0.0"

paths:
  /pets/{id}:
    get:
      description: Returns a single pet
      parameters:
        - name: id
          in: path
          description: ID of pet to fetch
          required: true
          schema:
            type: integer
            format: int64
      responses:
        200:
          description: successful pet response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pets'

components:
  schemas:
    Pets:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
          example: doggie
        status:
          type: string
          description: pet status in the store
          enum:
          - available
          - pending
          - sold
```
Goのアドベントカレンダーなので、yamlファイルのフィールドの意味についてここでは詳しくは触れません。
以下、yamlファイルの記法についての参考文献を載せますので、そちらを参照ください。

- [公式Doc OpenAPI Specification](https://swagger.io/specification/)
- [OpenAPI (Swagger) の基本的なあれこれ](https://girigiribauer.com/tech/20190318/)
- [OpenAPI Specification 3.0 チートシート](https://techblog.asahi-net.co.jp/entry/2019/03/04/102734)

## エディタの紹介
[Swagger Editor](https://editor.swagger.io)というウェブ上のエディタを使用すると、Swagger UI表示をすぐに確認することができます。

![スクリーンショット 2020-11-23 16.09.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/83e4e972-d8e0-a616-136c-2e3b19a28321.png)

ウェブ上ではなくて、ローカルでも同様の環境が欲しいという場合は、VSCodeの拡張機能[Swagger Viewer](https://marketplace.visualstudio.com/items?itemName=Arjun.swagger-viewer)を使えば、VSCode上でSwagger UIを確認できるようになります。

# openapi-generaterでのコード自動生成
OpenAPIのメリットは、見栄えのいいAPIドキュメントが作れることだけではなくて、yamlファイルから実際のAPIのコードをある程度自動で作成することができるというところにもあります。

[OpenAPI Generator](https://openapi-generator.tech)を使うことで、yamlファイルからAPIクライアント/サーバーのコードフォーマットを自動で作成することができます。

まずはそれをhomebrewでインストールしてみましょう。

```bash
$ brew install openapi-generator
```
これを実行すれば、openapi-generatorを利用する準備は完了です。

# APIサーバーのコードを自動生成
## コマンドで生成してみる
以下のコマンドを実行します。

```bash
$ openapi-generator generate -i openapi.yml -g go-server -o ./server
```
`openapi-generator`コマンドのオプションの意味は以下の通りです。

- i: ymlファイルの指定
- g: どんなコードを生成するかを指定。go-serverは、Go言語でAPIサーバーのコードを生成するための指定ワード。
- o: 生成ファイルの出力先ディレクトリを指定

すると、現ディレクトリ直下に`server`ディレクトリができ、その中にyamlファイルで規定されたAPIサーバーのコードが生成されています。

```ディレクトリ構造.
/server
├─ .openapi-generator
│    └─ VERSION
├─ api
│   └─ openapi.yaml
├─ go
│   ├─ api_default_server.go
│   ├─ api_default.go
│   ├─ api.go
│   ├─ logger.gp
│   ├─ model_pets.go
│   └─ routers.go
├─ .openapi-generator-ignore
├─ Dockerfile
├─ go.mod
├─ go.sum
├─ main.go
└─ README.md
```

## 生成されたサーバーを動かしてみる
`server`ディレクトリ直下の`main.go`を実行してみます。

```bash
$ cd server # serverディレクトリへ移動
$ go run main.go
```
すると、`http://localhost:8080`でAPIサーバーが稼働します。
実際に別のターミナルで`curl`を叩いてみましょう。

```bash
# 定義されていないパスはちゃんと404が返ってくる
$ curl http://localhost:8080
404 page not found

# 定義されたパスは通ってはいるが、500エラー
$ curl http://localhost:8080/pets/1
$ curl http://localhost:8080/pets/1 -w '%{http_code}\n'
500
```
定義していないパスが404なのはいいとして、定義したパスが500なのは何故でしょうか。
それでは`openapi-generator`コマンドで自動生成されたサーバーコードを詳しくみてみましょう。

## 生成されたコード解説
### service
`api_default_service.go`の中ではserviceの実装が行われています。
ここには「ID等の引数情報を受け取って、そこからAPIで返したいデータを作る」という部分のビジネスロジックを、`DefaultApiService`構造体のメソッドという形で記述します。

```api_default_service.go
func (s *DefaultApiService) PetsIdGet(id int64) (interface{}, error) {
	// TODO - update PetsIdGet with the required logic for this service method.
	return nil, errors.New("service method 'PetsIdGet' not implemented")
}
```
しかしコード生成直後のデフォルト状態では、上記のように「データなし・エラーあり」の返り値になっています。

### controller
`api_default.go`の中にはcontrollerが実装されています。
ここは「特定のパスにhttpリクエストがきたら、所定のserviceからレスポンスのもとになるデータを作り、返す」という部分を担っています。

```api_default.go
func (c *DefaultApiController) PetsIdGet(w http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	id, err := parseIntParameter(params["id"])
	if err != nil {
		w.WriteHeader(500)
		return
	}

	result, err := c.service.PetsIdGet(id)
	if err != nil {
		w.WriteHeader(500)
		return
	}

	EncodeJSONResponse(result, nil, w)
}
```
この`PetsIdGet`の場合は、「パスパラメータからIDを抜き出して、それを先ほどの`PetsIdGet`サービスの引数として渡す→データが帰ってきたらjsonにしてhttpレスポンスを作成、エラーなら500を作成」という処理をしています。

そして、これらの処理は`DefaultApiController`という構造体のメソッドとして定義されています。

```go
type DefaultApiController struct {
	service DefaultApiServicer
}
```

### router
controllerで使用した`DefaultApiController`構造体には、`Router()`というメソッドも存在しています。

```api_default.go
// Routes returns all of the api route for the DefaultApiController
func (c *DefaultApiController) Routes() Routes {
	return Routes{
		{
			"PetsIdGet",
			strings.ToUpper("Get"),
			"/pets/{id}",
			c.PetsIdGet,
		},
	}
}
```
ここで「`/pets/{id}`のパスにGETメソッドがきたら、controllerの`PetsIdGet`を呼び出す」という対応関係を定義しています。

そして、`router.go`の中に、この対応関係から実際の`mux.Router`を生成する関数が定義されています。

```router.go
func NewRouter(routers ...Router) *mux.Router {
	router := mux.NewRouter().StrictSlash(true)
	for _, api := range routers {
		for _, route := range api.Routes() {
			var handler http.Handler
			handler = route.HandlerFunc
			handler = Logger(handler, route.Name)

			router.
				Methods(route.Method).
				Path(route.Pattern).
				Name(route.Name).
				Handler(handler)
		}
	}

	return router
}
```

### main.go
`main.go`では、今まで作ってきたservice,controller,routerといった部品からサーバーを実際に動かす処理が書かれています。

```main.go
func main() {
	log.Printf("Server started")

	DefaultApiService := openapi.NewDefaultApiService()
	DefaultApiController := openapi.NewDefaultApiController(DefaultApiService)

	router := openapi.NewRouter(DefaultApiController)

	log.Fatal(http.ListenAndServe(":8080", router))
}
```

手順としては以下の通りです。

1. service構造体を作る(`DefaultApiService`)
2. serviceからcontrollerを作る(`DefaultApiController`)
3. controllerからrouterを作る
4. routerを`http.ListenAndServe`に渡してサーバーを起動する

## 生成されたコードを元にAPIサーバーを作る
先ほど紹介したような500を返すサーバーではなく、実際にDBにアクセスしてデータを返すようなAPIを作りたければ、`api_default_service.go`に手を加えればOKです。

```diff:api_default_service.go
func (s *DefaultApiService) PetsIdGet(id int64) (interface{}, error) {
	// TODO - update PetsIdGet with the required logic for this service method.
+	// idを手掛かりにDBからデータを入手する
+	data, err := GetDataFromDB(id)
+	
+	return data, err
-	return nil, errors.New("service method 'PetsIdGet' not implemented")
}
```

この状態でそのまま`main.go`を動かせば、きちんとデータが返ってくるAPIサーバーになります。

## モックサーバーを作る
本番環境のAPIサーバーだけでなく、テストや開発用にモックを作りたいという場合もあるかと思います。

そのときは、モック用のserviceを新たに作成します。

```api_mock_service.go
// mock用の新しいservice構造体
type DefaultMockService struct {
}

// mock用サービスのコンストラクタ
func NewDefaultMockService() DefaultApiServicer {
	return &DefaultMockService{}
}

// mock用serviceのメソッド
func (s *DefaultMockService) PetsIdGet(id int64) (interface{}, error) {
	pet := Pets{
		Id:     id,
		Name:   "doggie",
		Status: "available",
	}
	return pet, nil
}
```

そして、`main.go`に以下のように手を加えます。

```diff:main.go
func main() {
	log.Printf("Server started")


-	DefaultApiService := openapi.NewDefaultApiService()
-	DefaultApiController := openapi.NewDefaultApiController(DefaultApiService)


+	DefaultMockService := openapi.NewDefaultMockService()
+	DefaultApiController := openapi.NewDefaultApiController(DefaultMockService)
	
	router := openapi.NewRouter(DefaultApiController)

	log.Fatal(http.ListenAndServe(":8080", router))
}
```
すると、サーバー起動時にmockの方のserviceが使われるようになります。

実際に、サーバーを起動させて`curl`を叩いてみましょう。

```bash
$ curl http://localhost:8080/pets/1
{"id":1,"name":"doggie","status":"available"}
```
きちんとmockで用意したテストデータが返ってきます。

# APIクライアントのコードを自動作成
## コマンドで生成してみる
```bash
$ openapi-generator generate -i test2.yml -g go -o ./client/openapi
```
先ほどのAPIサーバーとの相違点は、オプション`-g`の指定が`go`になっているところです。

このコマンドを実行することで、`client/openapi`直下に以下のようなファイルが生成されます。

```ディレクトリ構造.
/client/openapi
├─ .openapi-generator
│    └─ VERSION
├─ api
│   └─ openapi.yaml
├─ docs
│   ├─ DefaultApi.md
│   └─ Pets.md
│
├─ api_default.go
├─ clinet.go
├─ configuration.go
├─ model_pets.go
├─ response.go
│
├─ .gitignore
├─ git_push.sh
├─ .openapi-generator-ignore
├─ .travis.yml
├─ go.mod
├─ go.sum
└─ README.md
```
このうちAPIクライアント実装部分は、ファイル拡張子が`.go`である5つです。

## 生成されたコード解説
### client.go
`client.go`に、定義したAPIへのクライアントが実装されています。

```client/openapi/client.go
// APIClient manages communication with the sample API API v1.0.0
// In most cases there should be only one, shared, APIClient.
type APIClient struct {
	cfg    *Configuration
	common service // Reuse a single struct instead of allocating one for each service on the heap.

	// API Services
	DefaultApi *DefaultApiService
}
```

この`APIClient`のコンストラクタは以下のように定義されています。見るとわかる通り、`*Configuration`型の引数を必要としています。

```client/openapi/client.go
func NewAPIClient(cfg *Configuration) *APIClient
```

### configuration.go
APIClientのコンストラクタに渡す`Configuration`は何かというと、サーバーURLなどの情報を保持しておく構造体です。
`Configuration`のコンストラクタは、`configuration.go`ファイル内に`NewConfiguration()`として存在します。

```client/openapi/configuration.go
func NewConfiguration() *Configuration {
	cfg := &Configuration{
		BasePath:      "http://localhost:8080",
		DefaultHeader: make(map[string]string),
		UserAgent:     "OpenAPI-Generator/1.0.0/go",
		Debug:         false,
		Servers: []ServerConfiguration{
			{
				Url:         "/",
				Description: "No description provided",
			},
		},
	}
	return cfg
}
```

### api_default.go
実際にAPIを叩くためにはどうすればいいのでしょうか。
もう一度`APIClient`の定義を見てみます。

```client/openapi/client.go
type APIClient struct {
	cfg    *Configuration
	common service
	// API Services
	DefaultApi *DefaultApiService
}
```
`DefaultApi`というフィールドがあるのが分かります。実は、このフィールドの`DefaultApiService`構造体のメソッドを呼び出すことでAPIを叩く仕組みになっています。

`DefaultApiService`のメソッドは`api_default.go`ファイルに記述されています。

```client/openapi/api_default.go
// (例)
// /pets/{id}へGETリクエストを送るメソッド
func (a *DefaultApiService) PetsIdGet(ctx _context.Context, id int64) (Pets, *_nethttp.Response, error)
```
次に、実際にこれらClientの機能を使ってAPIを叩くコードを書いてみましょう。

## APIClientを使ってAPIを叩いてみる
### コード作成
`client`フォルダの中に`main.go`を作り、以下のように記述します。

```client/main.go
package main

import (
	"context"
	"fmt"

	"./openapi"
)

func main() {
	cfg := openapi.NewConfiguration()
	c := openapi.NewAPIClient(cfg)

	ctx := context.Background()
	pets, _, err := c.DefaultApi.PetsIdGet(ctx, 1)

	fmt.Println(pets, err)
}
```
まずは、`import`の部分で`openapi`パッケージを読み込んでいます。これは、今まで紹介した`client.go`や`configuration.go`が`package openapi`で生成されているので、このようにすることでこれらを使用できるようにしています。

そして、main関数の中で以下のように処理を実行しています。
#### 1. configurationを生成
```client/main.go
cfg := openapi.NewConfiguration()
```
#### 2. configurationからAPIClientを生成
```client/main.go
c := openapi.NewAPIClient(cfg)
```
#### 3. 空のContextを作成→それを引数にとりAPIServiceを呼び出す
```client/main.go
pets, _, err := c.DefaultApi.PetsIdGet(ctx, 1)
```

### 実行
実際にClientが使えているかどうか確かめてみましょう。
先ほど作成したmockサーバーを動かした上で、この`main.go`を実行します。

```bash
$ go run main.go
{1 doggie available} <nil>
```
きちんとmockサーバーで書いた内容が取得できました。

# まとめ
以上、openapi-generatorでAPIサーバー/クライアントのコードを生成する手順でした。

感想としては、きちんとインターフェースが定義されていて、テストしやすそうなコードが生成されるなと思いました。
Swagger UIのドキュメントも合わせて簡単に作れることも考えると、yamlの書き方の学習コストを上回るだけの便利さだと思います。
ぜひ、REST APIを作りたいときは、OpenAPIを試してみてください。