LoginSignup
7
3

More than 3 years have passed since last update.

【OSS】api_gen 公開

Last updated at Posted at 2020-12-31

はじめに

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

こんな方向け

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

こんな人達に向いている


とりあえず試す

前提: こちらのディレクトリに居てほしい(雑)

各バイナリを落とす

$ make bootstrap

コード生成

$ make generate

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


サーバージェネレータ

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 のようにする

実行

$ server_generator ./hoge/

mockオプション(後述)

$ server_generator -mock ./hoge/

サーバージェネレータ使用例

今回使用するソースコード
今回の構成
$ 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

interfaces/health_check.go
type GetHealthCheckRequest struct{}

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

PUT /api/user

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

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

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

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"`
}

生成

$ make server_generate

生成後tree
$ 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


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

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.
}

最終的には こうなりました


クライアントジェネレータ

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

生成後

$ 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

クライアントジェネレータ使用例

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にはモックサーバが用意されており正しく使えばバックエンドとフロントエンドが並行で開発出来る

今回用意したサンプルだと次のように扱う

サーバー
$ make server_generate_with_mock

何をしたのか

$ server_generator -mock ./interfaces

このように引数に -mock を付ければ良い

mock_jsons/
mock_routes_gen.go
mock_bootstrap_gen.go
backend/interfaces/cmd/mock/main.go

これらのファイルが追加で自動生成される

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

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

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

クライアント
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);
})();

モックサーバに関するドキュメント(server) / モックサーバに関するドキュメント(client)


Q. Cookieを送信したい

A. {credentials: "include"} をfetch APIのオプションに設定 (FYI)


まとめ

APIのI/Fが自動生成されるだけで幸せになれました(?)
まだまだ開発途中なので、Issue / PR お待ちしております!

7
3
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
7
3