【OSS】api_gen 公開

# はじめに
- 決められたフォーマットで記述したGoの構造体からを自動生成するジェネレータがv1.0.0迎えたからアウトプットする回(12/31時点でv1.6.1)
- コントローラーを生成するserver_generator、API疎通するためのTypeScriptを生成するclient_generatorの2種類がある
- 基本的には構造体を書くだけ
- (beta) 生成後のコントローラーにある程度swaggoに対応したgodocも挿入されている為、調整すればSwaggerコードも自動生成できる
- [リポジトリ(go-generalize/api_gen)](https://github.com/go-generalize/api_gen)

---

# こんな方向け
- **Goコード正義**でAPIコントローラー/クライアントを自動生成したい
- エンジニア間でバックエンドとフロントエンドのI/Fを合わせるのがめんどくさい
- バックエンドでルーティング、フロントエンドでAPIリクエストを実装ごとに書くのが辛い

こんな人達に向いている

---

# とりあえず試す
###### 前提: [こちら](https://github.com/go-generalize/api_gen/tree/master/templates)のディレクトリに居てほしい~~(雑)~~

各バイナリを落とす

```terminal
$ make bootstrap
```

コード生成

```terminal
$ make generate
```

生成に関してはこれで終わり
backend/とfrontend/以下に様々なコードが生成されていることが確認できれば成功
詳細は後述

---

# サーバージェネレータ
###### [server_generator](https://github.com/go-generalize/api_gen/tree/master/server_generator) に含まれる内容

##### 構造体名ルール
- `{HTTP_METHOD}Name{Request|Response}`
  + 必ずRequestとResponseがそれぞれ対になるようにする

##### 対応HTTPメソッド
- `GET`, `POST`, `PUT`, `DELETE`, `PATCH`

##### パスルーティングをする場合
- ディレクトリは `_` 始まり
  + 例: `/service/:serviceID/hogehoge` にしたい場合は `/service/_serviceID/*.go` のようにする
- ファイルは `0_` 始まり
  + 例: `/service/:serviceID` にしたい場合は `/service/0_service_id.go` のようにする

---

実行

```terminal
$ server_generator ./hoge/
```

mockオプション(後述)

```terminal
$ server_generator -mock ./hoge/
```

---

## サーバージェネレータ使用例
#####[今回使用するソースコード](https://github.com/54m/api_gen-example/tree/main/backend)

##### 今回の構成

```terminal
$ tree backend/interfaces
backend/interfaces
├── api
│   ├── user
│   │   ├── 0_user_id.go
│   │   ├── _userID
│   │   │   └── age_increment.go
│   │   └── search.go
│   └── user.go
└── health_check.go

3 directories, 5 files
```

---

GET `/health_check`

```go:interfaces/health_check.go
type GetHealthCheckRequest struct{}

type GetHealthCheckResponse struct {
	Status string `json:"status"`
}
```

---

PUT `/api/user`

```go:interfaces/api/user.go
type PutUserRequest struct {
	Name   string       `json:"name" validate:"required,min=3,max=10,excludesall=!()#@{}"`
	Age    int          `json:"age" validate:"required,gt=0,lte=150"`
	Gender model.Gender `json:"gender" validate:"required,oneof=1 2 3"`
}

type PutUserResponse struct {
	Status int         `json:"status"`
	User   *model.User `json:"payload,omitempty"`
}
```

---

GET `/api/user/search`

```go:interfaces/api/user/search.go
type GetSearchRequest struct {
	Name   string       `json:"name" query:"name"`
	Age    int          `json:"age" query:"age"`
	Gender model.Gender `json:"gender" query:"gender"`
}

type GetSearchResponse struct {
	Status   int                   `json:"status"`
	User     []*model.User         `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}
```

---

GET/PATCH/DELETE `/api/user/:userID`

```go:interfaces/api/user/0_user_id.go
type GetRequest struct {
	ID string `json:"userID" param:"userID" validate:"required"`
}

type GetResponse struct {
	Status   int                   `json:"status"`
	User     *model.User           `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}

type PatchRequest struct {
	ID     string       `json:"userID" param:"userID" validate:"required"`
	Name   string       `json:"name,omitempty" validate:"min=5,max=10,excludesall=!()#@{}"`
	Age    int          `json:"age,omitempty" validate:"gt=0,lte=150"`
	Gender model.Gender `json:"gender,omitempty" validate:"oneof=1 2 3"`
}

type PatchResponse struct {
	Status   int                   `json:"status"`
	User     *model.User           `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}

type DeleteRequest struct {
	ID string `json:"userID" param:"userID" validate:"required"`
}

type DeleteResponse struct {
	Status   int                   `json:"status"`
	Messages []werror.FailedReason `json:"messages"`
}
```

---

POST `/api/user/:userID/age_increment`

```go:interfaces/api/user/_userID/age_increment.go
type PostAgeIncrementRequest struct {
	ID string `json:"userID" param:"userID" validate:"required"`
}

type PostAgeIncrementResponse struct {
	Status   int                   `json:"status"`
	User     *model.User           `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}
```

---

### 生成
```terminal
$ make server_generate
```

<details><summary>`生成後tree`</summary><div>

```terminal
$ tree backend/interfaces
backend/interfaces
├── api
│   ├── put_user_controller_gen.go
│   ├── routes_gen.go
│   ├── user
│   │   ├── 0_user_id.go
│   │   ├── _userID
│   │   │   ├── age_increment.go
│   │   │   ├── post_age_increment_controller_gen.go
│   │   │   └── routes_gen.go
│   │   ├── delete_controller_gen.go
│   │   ├── get_controller_gen.go
│   │   ├── get_search_controller_gen.go
│   │   ├── patch_controller_gen.go
│   │   ├── routes_gen.go
│   │   └── search.go
│   └── user.go
├── bootstrap_gen.go
├── get_health_check_controller_gen.go
├── health_check.go
├── props
│   └── controller_props.go
├── routes_gen.go
└── wrapper
    ├── internal
    │   └── fmt.go
    └── wrapper.go

6 directories, 20 files
```
</div></details>

---

### 生成コントローラ(一部)

```go:interfaces/api/user/_userID/post_age_increment_controller_gen.go
// Package _userID ...
// generated version: 1.6.1
package _userID

import (
	"github.com/54m/api_gen-example/backend/interfaces/props"
	"github.com/labstack/echo/v4"
)

// PostAgeIncrementController ...
type PostAgeIncrementController struct {
	*props.ControllerProps
}

// NewPostAgeIncrementController ...
func NewPostAgeIncrementController(cp *props.ControllerProps) *PostAgeIncrementController {
	p := &PostAgeIncrementController{
		ControllerProps: cp,
	}
	return p
}

// PostAgeIncrement ...
// @Summary WIP
// @Description WIP
// @Accept json
// @Produce json
// @Param userID path string true "user id"
// @Success 200 {object} PostAgeIncrementResponse
// @Failure 400 {object} wrapper.APIError
// @Failure 500 {object} wrapper.APIError
// @Router /api/user/{userID}/age_increment [POST]
func (p *PostAgeIncrementController) PostAgeIncrement(
	c echo.Context, req *PostAgeIncrementRequest,
) (res *PostAgeIncrementResponse, err error) {
	// API Error Usage: github.com/54m/api_gen-example/backend/interfaces/wrapper
	//
	// return nil, wrapper.NewAPIError(http.StatusBadRequest)
	//
	// return nil, wrapper.NewAPIError(http.StatusBadRequest).SetError(err)
	//
	// body := map[string]interface{}{
	// 	"code": http.StatusBadRequest,
	// 	"message": "invalid request parameter.",
	// }
	// return nil, wrapper.NewAPIError(http.StatusBadRequest, body).SetError(err)
	panic("require implements.") // FIXME require implements.
}
```

### 最終的には [こうなりました](https://github.com/54m/api_gen-example/blob/main/backend/interfaces/api/user/_userID/post_age_increment_controller_gen.go)

---

# クライアントジェネレータ
###### [client_generator](https://github.com/go-generalize/api_gen/tree/master/client_generator) に含まれる内容

- Typescript + fetchを利用したライブラリが生成される
- server_generator向けの用意したフォルダに対して実行する
  + ライブラリは今いるディレクトリに対して生成される

---

生成後

```terminal
$ tree frontend/client_generated 
frontend/client_generated
├── api_client.ts
└── classes
    ├── api
    │   ├── types.ts
    │   └── user
    │       ├── _userID
    │       │   └── types.ts
    │       └── types.ts
    └── types.ts

4 directories, 5 files
```

---

#### クライアントジェネレータ使用例
```ts
import { APIClient } from "./client_generated/api_client";

// the simplest
(async () => {
    const client = new APIClient();

    const resp = await client.getHealthCheck(/* param */ {});

    console.log(resp);
})();

// with options
(async () => {
    const client = new APIClient(
        /* token */
        "pass", // [optional] token for Authorization: Bearer
        /* commonHeaders */
        {
            "X-Foobar": "foobar", // [optional] custom headers
        },
        /* baseURL */
        "http://localhost:8888",  // [optional] custom endpoint
        /* commonOptions */
        {
            cache: "no-cache", // [optional] options for fetch API
        },
    );

    const resp = await client.api.putUser(
        /* param */ 
        {
            name: "54m",
            age: 100,
            gender: 1,
        },
        /* header */
        {
            "X-Foobar": "hoge", // [optional] custom headers
        },
        /* options */
        {
            mode: "cors" // [optional] options for fetch API
        },
    );

    console.log(resp);
})();
```

---

# よくある質問
##### Q. とりあえずReq/Resの構造体が出来てサーバー↔クライアント間で疎通出来るようになったけどバックエンド実装がない時、フロント側の開発は待たないとダメじゃね？
A. そんなことは無い 
   api_genにはモックサーバが用意されており正しく使えばバックエンドとフロントエンドが並行で開発出来る

今回用意したサンプルだと次のように扱う
<details><summary>`サーバー`</summary><div>

```terminal
$ make server_generate_with_mock
```

何をしたのか

```terminal
$ server_generator -mock ./interfaces
```
このように引数に `-mock` を付ければ良い

```
mock_jsons/
mock_routes_gen.go
mock_bootstrap_gen.go
backend/interfaces/cmd/mock/main.go
```
これらのファイルが追加で自動生成される

##### モックサーバー実行方法
```terminal
$ make run_mock
```

モックサーバは生成対象としたディレクトリの直下に
`cmd/mock/main.go` として生成される
モックサーバを起動するためにはビルドオプションで
`-tags mock` を付ける必要がある

```terminal
go run -tags mock backend/interfaces/cmd/mock/main.go -port 8888
```

</div></details>

<details><summary>`クライアント`</summary><div>

```ts
import { APIClient, MockOption } from "./client_generated/api_client";

// mock mode
(async () => {
    const client = new APIClient();
    const mockOption: MockOption = {
        wait_ms: 10,
        target_file: 'default.json'
    }

    const resp = await client.api.user.getSearch(/* param */ {
        name: "54m",
        age: 100,
        gender: 1,
    }, /* header */ undefined, /* options */ {
        'mock_option': mockOption,
    });

    console.log(resp);
})();
```
</div></details>

[モックサーバに関するドキュメント(server)](https://github.com/go-generalize/api_gen/blob/master/docs/server_ja.md) / [モックサーバに関するドキュメント(client)](https://github.com/go-generalize/api_gen/blob/master/docs/client_ja.md)

---

##### Q. Cookieを送信したい
A. `{credentials: "include"}` をfetch APIのオプションに設定 ([FYI](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API))

---

# まとめ
APIのI/Fが自動生成されるだけで幸せになれました(?)
まだまだ開発途中なので、[Issue](https://github.com/go-generalize/api_gen/issues) / [PR](https://github.com/go-generalize/api_gen/pulls) お待ちしております！