こんにちは、Development Team の水谷です
本記事は AI Shift Advent Calendar 2021 の11日目の記事です
今回は、OpenAPI Specification 3のYAML
からGoのサーバーを生成してみます
OpenAPI Specification とは
パスパラメーターやリクエストボディー、レスポンスのフォーマットなどのAPI仕様を、JSON
やYAML
で記載することで、クライアントやサーバーのテンプレートを生成でき、クライアント/サーバー間で、同じ型や仕様を共有しながら開発が可能になる仕組みです
例えば、APIの仕様変更が生じた際に、API仕様が記載されるJSON
かYAML
の変更から始めることで、職域間で共通認識をもちながら、開発を進めることが可能になります
具体的な流れ
- OpenAPI Specification を
YAML
で記述 YAML
からGoのAPIテンプレートを生成
OpenAPI Specification をYAMLで記述
OpenAPI Tools に、petstore.yamlというサンプルがありますが、今回は以下2点のシンプルなAPI仕様とします
- ユーザのリスト取得
- ユーザの登録
openapi: "3.0.0"
info:
version: 1.0.0
title: User API
paths:
/users:
get:
tags: [Users]
operationId: ListUsers
parameters:
- name: limit
in: query
description: limit
required: true
schema:
type: integer
format: int32
- name: page
in: query
description: page
required: true
schema:
type: integer
format: int32
responses:
"200":
description: user response
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/user"
post:
tags: [Users]
operationId: CreateUsers
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewUser"
responses:
"200":
description: user response
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/user"
components:
schemas:
user:
allOf:
- $ref: "#/components/schemas/NewUser"
- required:
- id
properties:
id:
type: integer
format: int64
NewUser:
required:
- name
- age
properties:
name:
type: string
age:
type: number
YAMLからGoのAPIテンプレートを生成する
今回、テンプレート生成するために、deepmap/oapi-codegenというライブラリを利用します
Overviewを参考にし、インストールしていきましょう
go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen
インストールしたら、oapi-codegen -h
でオプションを確認できます、今回利用するオプションを以下で確認していきましょう。ちなみに、オプション説明はUsing oapi-codegenにもあります
Usage of oapi-codegen:
-generate string
Comma-separated list of code to generate; default "types,client,server,spec"
-o string
Where to output generated code, stdout is default
-package string
The package name for generated code
1. サーバーのテンプレートを生成する(server
) 2. パラメータ各種の構造体を生成する(type
)
今回は上記2点を行うので、1.はserver
オプションで用いて、 2.のパラメータ各種、例えばリクエストパラメータやレスポンスボディーの型生成するオプションはtypes
で可能です
結果的に以下2つのコマンドで生成が可能になります
oapi-codegen -generate "types" -package oas oas.yaml > ./oas/types.gen.go
oapi-codegen -generate "server" -package oas oas.yaml > ./oas/server.gen.go
types
オプションについて
oas.yaml
から生成したコード./oas/type.gen.go
から一部抜粋します。
これはoas.yaml
に記載した期待するパラメータを構造体にマッピングしたものです
EchoのBind
機能で、クライアントから送られてきたリクエストボディーの値を生成した構造体にマッピングできたり、
逆にレスポンス時のフォーマットとして、活用することができます
// Package oas provides primitives to interact with the openapi HTTP API.
//
// Code generated by unknown module path version unknown version DO NOT EDIT.
package oas
// NewUser defines model for NewUser.
type NewUser struct {
Age float32 `json:"age"`
Name string `json:"name"`
}
// User defines model for user.
type User struct {
// Embedded struct due to allOf(#/components/schemas/NewUser)
NewUser `yaml:",inline"`
// Embedded fields due to inline allOf schema
Id int64 `json:"id"`
}
// ListUsersParams defines parameters for ListUsers.
type ListUsersParams struct {
// limit
Limit int32 `json:"limit"`
// page
Page int32 `json:"page"`
}
// CreateUsersJSONBody defines parameters for CreateUsers.
type CreateUsersJSONBody NewUser
// CreateUsersJSONRequestBody defines body for CreateUsers for application/json ContentType.
type CreateUsersJSONRequestBody CreateUsersJSONBody
server
オプションについて
server
オプションでは、oas.yaml
で定義した仕様に基づいて、インタフェースが定義されます
そのインタフェース定義を、実装で満たした構造体を用意することで利用ができます
今回、生成されたインタフェースは、ListUsers
とCreateUsers
が定義されたServerInterface
となります
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /users)
ListUsers(ctx echo.Context, params ListUsersParams) error
// (POST /users)
CreateUsers(ctx echo.Context) error
}
このServerInterface
を満たす構造体(handler
)を用意し、README.mdのRegistering Handlersを参考にしつつ、サーバを起動してみましょう
package main
// ServerInterface の定義を満たす構造体
type server struct{}
// ListUsers は ServerInterface に定義される ListUsers の実装
* func (h server) ListUsers(ctx echo.Context, params oas.ListUsersParams) error {os.Stdout
return ctx.JSON(200, map[string]interface{}{
"msg": "200 success",
"limit": params.Limit,
"page": params.Page,
})
}
// CreateUsers は ServerInterface に定義される CreateUsers の実装
func (h server) CreateUsers(ctx echo.Context) error {
b := &oas.CreateUsersJSONBody{}
if err := ctx.Bind(b); err != nil {
return err
}
return ctx.JSON(
200,
map[string]interface{}{
"msg": "200 success",
"reqBody": b,
})
}
// ServerInterface の定義を満たす server をルーティングする
// https://github.com/deepmap/oapi-codegen#registering-handlers の Echo の箇所を参考にします
func setHandler() *echo.Echo {
s := server{}
e := echo.New()
// RegisterHandlers の第2引数の型は ServerInterface なので、
// ServerInterface を満たした構造体を代入する
oas.RegisterHandlers(e, s)
return e
}
func main() {
e := setHandler()
e.Logger.Fatal(e.Start("0.0.0.0:8080"))
}
$ go run main.go
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.6.1
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:8080
リクエストを送ってみる
ブラウザからhttp://localhost:8080/users
にアクセスすると、required
なクエリパラメータが不足しているので、400
が返ります
{
"message": "Invalid format for parameter page: query parameter 'page' is required"
}
今回必要なクエリパラメーターは,page
とlimit
なので、それを付与したURL(http://localhost:8080/users?page=1&limit=1
)にアクセスすると、期待通り200
が返ります
{
"msg": "200 success",
"limit": 1,
"page": 10
}
一方で、POSTのリクエストについては、NewUser
の構造体を参考にJSONボディーを用意しPOSTすると、これも正しく200
が返ってくるかと思います。
curl -X POST 'localhost:8080/users' \
-H 'Content-Type: application/json' \
-d '{
"age": 20,
"name": "taro"
}'
{
"msg":"200 success",
"reqBody":{
"age":20,
"name":"taro"
}
}
例えば、age
の値を20
から"20"
にすると、期待しているageの型がstring
ではなく数値である旨のエラーメッセージと共に400
が返ります
このようにして型安全なAPIを開発することが可能になります
{
"message": "Unmarshal type error: expected=float32, got=string, field=age, offset=15"
}
終わりに
今回はcurlで疎通を試みましたが、 APIクライアントの生成ができる点も、OpenAPI Specificationのメリットです
OpenAPITools/openapi-generatorというライブラリを使えば、TypescriptやGo、その他の言語でのAPIクライアント生成が可能なので、気になる方は是非使ってみてください!