This page looks best with JavaScript enabled

OpenAPI 3.0からGoのコードを生成する(oapi-codegen)

 ·   ·  ☕ 5 min read

OpenAPI Specification(以降OASとします)はREST APIの仕様を定義する仕組みです。

YAML等のフォーマットで仕様を記述し、ドキュメントや各種言語向けのコードを生成して使います。おかしな仕様にはエラーを出してくれるし、入力値の取得やバリデーションなど自動生成すべきコードをいい感じに用意してくれるので、とても便利です。わかりやすく言うとREST APIのための形式仕様記述(formal specification)1です。

OASで定義したAPI仕様からGoのコードを生成することを考えます。

OpenAPI GeneratorのGo対応は微妙な感じです。ググるとgo-swaggerがよく使われているようですが、OAS 3.0の前身であるSwagger 2.0にしか対応していません。OAS 3.0に対応したものはoapi-codegenがあります。READMEやFuture Tech Blogの記事(本記事がいらないくらい詳しい)がよさげだったので使ってみたところ、普通に使えたので備忘録がてらまとめておきます。

前提

  • oapi-codegen v1.5.0 (Feb 09, 2021)
  • chiルータを利用
    • oapi-codegen自体はEcho, Chi, net/httpに対応している
  • サーバコードの生成だけ
    • 今回はクライアントをGoで実装しなかったので
  • APIキーで認証

※ 以下、一部でGitHubからコードを引用しています。ライセンス等は引用元を参照してください!

コード生成と実装

コード生成

公式のexamplesがめっちゃわかりやすいです。↓に引用します。

このようにgo generateでやるか手動で実行するかですね。

基本形は↓です。

1
oapi-codegen -package hoge -o path/to/package/hoge/hoge.gen.go openapi.yaml

これを実行するとServerInterfaceを含めたコードを生成します。あとはこのインタフェースを満たす実装(後述)をするだけです。生成されるコードが薄くて普通に読めるのが良いですね。

よく使うはずのオプションを入れると↓こうなります。

1
oapi-codegen -package hoge -generate "types,chi-server,spec" -include-tags "Hoge" -o path/to/package/hoge/hoge.gen.go openapi.yml
  • -generate: 何を生成するか
    • types chi-server specあたりですかね?
    • clientはサーバコードの場合不要
    • Echoならchi-serverの代わりにserver
    • specはOASのdump、Validatorに必要
  • -include-tags: OASのタグを見て、指定したタグだけ生成する
    • Fugaは別で実装したいんだよね〜ってときにHogeのコードだけ生成できます
    • ただし、別々で生成したHogeFugaを同じchiルータで動かすのは大変そうです(後述)。

実装

↓の公式の例のように、ServerInterfaceを満たす実装をするだけです。

Goのtipsとして、パッケージ変数にvar _ ServerInterface = (*PetStore)(nil)を定義しておくと何が足りないのかがわかって便利です。

呼び出し側も公式の例がわかりやすいのですが、1)OpenAPIを読み込んで、

2)OapiRequestValidatorに食わせてValidatorのMiddlewareを生成します。

なので、-include-tagsHogeFugaを別々に生成した場合、同じchiルータで動かすのが難しいです。

細かい話なのでクリックで展開

OapiRequestValidatorswaggerごと(HogeFugaで異なる)なので、HogeValidator->FugaValidator->リクエストハンドラという流れになり、Fuga宛のリクエストはHogeValidatorに弾かれます。

ルーティングをがんばって設定すればできるんですかね🤔

CORSとPreflight

Middlewareを使いました。試しにYAMLにOPTIONSメソッドを書いてみたところ、なにも生成されませんでした。

これってAmazon API GatewayやCloud Endpointsではどうするんだろう?PreflightでOPTIONSが飛んでくるのはAPIの仕様ではなくブラウザの仕様だし?詳しい人教えてください。

注意点は、CORSのMiddlewareをOapiRequestValidatorの前に入れる必要があることです(OapiRequestValidatorでOPTIONSが弾かれるため)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
swagger, _ := hoge.GetSwagger()

// Initialize chi router.
r := chi.NewRouter()
// import "github.com/go-chi/cors"
r.Use(cors.Handler(cors.Options{
  AllowedOrigins:   []string{"*"},
  AllowedMethods:   []string{"POST"},
  AllowedHeaders:   []string{"*"},
  ExposedHeaders:   []string{"*"},
  AllowCredentials: false,
  MaxAge:           300,
}))
// import oapimiddleware "github.com/deepmap/oapi-codegen/pkg/chi-middleware"
r.Use(oapimiddleware.OapiRequestValidator(swagger))

Heartbeat

K8sにデプロイする場合などで必要になると思います。これもAPIの仕様じゃないと思うので、Middlewareを使いました。

1
2
3
4
5
6
7
swagger, _ := hoge.GetSwagger()

// Initialize chi router.
r := chi.NewRouter()
// import "github.com/go-chi/chi/middleware"
r.Use(middleware.Heartbeat("/healthz"))
r.Use(oapimiddleware.OapiRequestValidator(swagger))

middleware.Heartbeatを使う代わりに自前で実装してもいいでしょう。

1
2
3
4
5
6
7
8
9
var middlewareHealthz = func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/healthz" {
            w.WriteHeader(http.StatusOK)
            return
        }
        next.ServeHTTP(w, r)
    })
}

認証

これのベストプラクティスがいまいちよくわからないです。↓こんな感じでOptions.AuthenticationFuncをセットしておけばとりあえず動きます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
swagger, _ := hoge.GetSwagger()

// Initialize chi router.
r := chi.NewRouter()

// import oapimiddleware "github.com/deepmap/oapi-codegen/pkg/chi-middleware"
validatorOpts := &oapimiddleware.Options{}
// Inject Authentication function.
validatorOpts.Options.AuthenticationFunc = func(ctx context.Context, input *openapi3filter.AuthenticationInput) error {
  h := input.RequestValidationInput.Request.Header["X-Api-Key"]
  if h == nil {
    return errors.New("X-API-KEY not found")
  }
  if h[0] != "super_strong_password" {
    return errors.New("auth failed")
  }
  return nil
}

r.Use(oapimiddleware.OapiRequestValidatorWithOptions(swagger, validatorOpts))

どうやらOASでメソッドにsecurityが定義されていたらOptions.AuthenticationFuncを見に行くようなので、適切なAuthenticationFuncをセットしておく、でよさそうです。詳細は↓

クリックで展開

↓のcase *openapi3filter.SecurityRequirementsErrorの元をたどっていくと、

↓でOptions.AuthenticationFuncを取り出していて、そのあと実行しています。

おわりに

oapi-codegen、生成されるコードが簡潔だし使い方も簡単なのでオヌヌメです。今回作ったものはそのうち書けたら書きます。


  1. 厳密な定義はよくわからないですが、仕様が破綻していたらエラーを出してくれるので形式仕様記述ですよね?🤔 ↩︎