ATOM開発チームの上野です。 普段はGo言語を使ってAPIサーバやバッチ処理機構の実装などを担当しています。
今回はATOMのプロジェクトでAPIサーバのコード生成に新しく導入した
ogen というパッケージをご紹介します。
GoでOpenAPI定義からAPIハンドラなどの基盤コードを自動生成するには様々なツールがありますが、
その中でも当時は go-swagger を使用していました。
go-swaggerの使用感自体は悪くなかったのですが、
go-swaggerは OpenAPI2.0(Swagger2.0)にしか対応しておらず、
OpenAPI3.0に移行するために乗り換え先を探したところ、ogen-go/ogen に出会いました。
ogenには以下の特徴があります
- OpenAPI3.0に対応している
- 生成コードがシンプルで、(HTTPサーバを実装する上では)無駄がなく、生成も早い
- 開発が盛んで、ドキュメントも見やすい
ogen を使用したサンプルプロジェクトは以下で公開されています。
https://github.com/ogen-go/example
自動生成
生成コマンド
$ ogen -target ./pkg/openapi/ -package openapi -clean ./openapi.yml
出力コード (Quick startの内容を抜粋しています)
API定義
openapi: 3.0.2 servers: - url: /v3 info: version: 1.0.0 title: Pet store schema tags: - name: pet description: Everything about your Pets paths: /pet: post: tags: - pet summary: Add a new pet to the store description: Add a new pet to the store operationId: addPet responses: '200': description: Successful operation content: application/json: schema: $ref: '#/components/schemas/Pet' requestBody: description: Create a new pet in the store required: true content: application/json: schema: $ref: '#/components/schemas/Pet' '/pet/{petId}': get: tags: - pet summary: Find pet by ID description: Returns a single pet operationId: getPetById parameters: - name: petId in: path description: ID of pet to return required: true schema: type: integer format: int64 responses: '200': description: successful operation content: application/json: schema: $ref: '#/components/schemas/Pet' '404': description: Pet not found responses: '200': description: successful operation components: schemas: PetStatus: type: string description: pet status in the store enum: - available - pending - sold Pet: required: - name properties: id: type: integer format: int64 example: 10 name: type: string example: doggie photoUrls: type: array items: type: string status: $ref: '#/components/schemas/PetStatus'
スキーマ
// Ref: #/components/schemas/Pet type Pet struct { ID OptInt64 `json:"id"` Name string `json:"name"` PhotoUrls []string `json:"photoUrls"` Status OptPetStatus `json:"status"` } // GetID returns the value of ID. func (s *Pet) GetID() OptInt64 { return s.ID } ...
Handler interface
type Handler interface { // AddPet implements addPet operation. // // Add a new pet to the store. // // POST /pet AddPet(ctx context.Context, req *Pet) (*Pet, error) // GetPetById implements getPetById operation. // // Returns a single pet. // // GET /pet/{petId} GetPetById(ctx context.Context, params GetPetByIdParams) (GetPetByIdRes, error) }
実装
type Handler struct { oas.UnimplementedHandler // automatically implement all methods } func (h Handler) GetPetById(ctx context.Context, params oas.GetPetByIdParams) (oas.GetPetByIdRes, error) { return &oas.Pet{ ID: oas.NewOptInt64(params.PetId), Name: fmt.Sprintf("Pet %d", params.PetId), Status: oas.NewOptPetStatus(oas.PetStatusAvailable), }, nil } func main() { oasServer, err := oas.NewServer(Handler{}) if err != nil { // } httpServer := http.Server{ Addr: arg.Addr, Handler: oasServer, } if err := httpServer.ListenAndServe(); err != nil { // } }
このように生成コードは単純で、生成されたinterfaceに従って実装するだけになっています。
リクエストパラメータ、レスポンス、enumなど全て型が与えられており、実装に迷うことはなさそうです。
また、これに加えて自作のミドルウェアを差し込むことも可能になっています。
フィールドの生成型
Pet.ID
が OptInt64
という型になっていますが、こちらも自動生成された型になります。
ogenでは、OpenAPI定義の required
nullable
によって以下のように生成型が変わります。
required |
nullable |
型 |
---|---|---|
true | false | string |
false | false | OptString |
true | true | NilString |
false | true | OptNilString |
OptXXX
型には IsSet()
、 NilXXX
型には IsNull()
の関数が生成されるので
クライアントからのリクエストを厳密に扱うことになります。
例えばリクエスト {}
と {"data": null}
どちらも "data"
というフィールドに値が存在しない場合ですが、
OptXXX
の型では {"data": null}
を受け取ることができず、必ずAPI定義で nullable:true
を設定し、
OptNilXXX
の型で受け取らなければなりません。
この厳密さは時折不便と感じるポイントかもしれません。
実際にフロントアプリケーション側で nullable
を設定していないのに
{"data": null}
のように明示的にnullを送ってしまうことはあると思いますが、
その度にバリデーションエラーが発生してしまうのはどうなんだろう、という感想です。
まとめ
ogenを導入しましたが、個人的には良かったかなと思っています。
OpenAPIから自動生成するツールの中で、必要なものが揃っている上で特に減点ポイントがない印象でした。
特にOpenAPI3.0を使う場合はぜひ参考にしてみてください!