Slide 1

Slide 1 text

@akeno_0810 2022.06.25 oapi-codegenを使ってみた Umeda.go 2022 Spring

Slide 2

Slide 2 text

自己紹介 About me akeno (@akeno_0810) Webエンジニア歴2年くらい Rust, API/コード設計, DevOps/開発の効率化 触っている技術 最近興味のある分野

Slide 3

Slide 3 text

背景

Slide 4

Slide 4 text

構成 oapi-codegen Material UI + Theme openapi-generator sql-migrate sqlboiler OpenAPI 3.0スキーマから Goのコードを生成する • Stoplight Studi† • echo/v4 (CIでコード生成してテスト・ビルド)

Slide 5

Slide 5 text

スキーマ駆動開発の動機 F API定義の認識を合わせるのが面2 F API仕様書を作る必要があっ" F 同じ定義を参照してコードを手で書いていくのが辛かっ" F 楽をして品質を上げたい gRPCやGraphQLという選択肢 F 納期があるので大きなリスクはとれなk F インフラが自由に触れなk F 既存のやり方に戻れる程度の改‡ F コード生成部分さえ整えてしまえば広げるのが楽

Slide 6

Slide 6 text

oapi-codegen

Slide 7

Slide 7 text

ライブラリ概要 oapi-codegen (https://github.com/deepmap/oapi-codegen) OpenAPI 3.0 スキーマからGoのコードを生成する 使った理# ! 薄めの生成コー2 ! いざとなれば書き直せる程度の依存 ! echoとの親和性 試し ! openapi-generatow ! go-swagger

Slide 8

Slide 8 text

生成ファイル https://github.com/Tim0401/oapi-codegen-demo echo v4.7.2 oapi-codegen v1.11.0 右がschemaをswaggar editorで見たもの CRUDと一部に認証を付けている server,types,specの3つに分けて生成した まとめて生成するより見通しがいい spec → バリデーション用のAPI定義

Slide 9

Slide 9 text

生成ファイル server → echoのInterfaceの定義 // ServerInterface represents all server handlers. // Your GET endpoint // (GET /items) // (POST /items) // (DELETE /items/{id}) // Your GET endpoint // (GET /items/{id}) // (PUT /items/{id}) type interface int int int ServerInterface { GetItems(ctx echo.Context, params GetItemsParams) error PostItems(ctx echo.Context) error DeleteItem(ctx echo.Context, id ) error GetItem(ctx echo.Context, id ) error PutItem(ctx echo.Context, id ) error } Interfaceに従って実装していく パスがコメントに記載される operationIdが命名に使われる queryParam,pathParamはこの段階で取り出されている

Slide 10

Slide 10 text

生成ファイル types → API定義内で使われる型 // アイテム // アイテム説明 // アイテム名 // 価格 // GetItemRes defines model for GetItemRes. // アイテム type struct string string string int type struct Item { Description * Id Name Price } GetItemRes { Item Item } `json:"description,omitempty"` `json:"id"` `json:"name"` `json:"price"` `json:"item"` RequestやResponse、汎用的な型定義をGoに変換

Slide 11

Slide 11 text

使う

Slide 12

Slide 12 text

使う OpenAPI周りのコードは https://github.com/getkin/kin-openapi に依存している ValidationやError Handling // 生成コード spec からの読み込み // /api 等のprefixがOpenAPIに定義しているURLに追加で必要な場合 swagger, err := openapi.GetSwagger() err != {} swagger.Servers = newPath := openapi3.Paths{} k := swagger.Paths { newPath[apiPathPrefix+k] = swagger.Paths[k] } swagger.Paths = newPath if nil nil for range main.go

Slide 13

Slide 13 text

使う Optionsを指定することでRequestValidatorの挙動をある程度制御できる 以下は認証ヘッダーを追加でチェックする例 validatorOpts := &oapiMw.Options{} validatorOpts.Options.AuthenticationFunc = { h := input.RequestValidationInput.Request.Header[input.SecuritySchemeName] (h) == { errors.New( ) } input.SecuritySchemeName { HeaderAuthorization: err := checkToken(h[ ]); err != { errors.New( ) } HeaderAuthorizationAdmin: err := checkTokenAdmin(h[ ]); err != { errors.New( ) } : errors.New( ) } } // Authorization ヘッダーが必要なルーターに対する存在チェック // ユーザーと管理者のトークンチェック func if return switch case if nil return case if nil return default return return nil (ctx context.Context, input *openapi3filter.AuthenticationInput) len error 0 0 0 "HeaderAuthorizationNotFound" "HeaderAuthorization" "HeaderAuthorizationAdmin" "HeaderAuthorizationNotFound"

Slide 14

Slide 14 text

使う echoの初期化 生成したroutingを登録 // init echo // バリデーション // 定義した struct を登録 // 自動生成されているRoutingを登録 // handlerはInterfaceを満たす必要がある // Start server e := echo.New() g := e.Group(apiPathPrefix) g.Use(echoMw.Recover()) g.Use(echoMw.Logger()) g.Use(oapiMw.OapiRequestValidatorWithOptions(swagger, validatorOpts)) handler := Handler{ ItemRouterInterface: &router.ItemRouter{}, } openapi.RegisterHandlers(g, handler)

Slide 15

Slide 15 text

叩く curlで叩いた結果 get-items curl -v "http://localhost:9000/v0/items?\$top=10" → 200 OK post-items curl -v -X POST "http://localhost:9000/v0/items" → 403 Forbidden curl -v -X POST "http://localhost:9000/v0/items" -H "Authorization:a" → 400 Bad Request curl -v -X POST "http://localhost:9000/v0/items" -H "Authorization:a" \ -H "Content-Type: application/json" -d '{ "id":"0", "name":"a", "price":1 }' → 201 Created get-item curl -v "http://localhost:9000/v0/items/a" → 400 Bad Request curl -v "http://localhost:9000/v0/items/1" → 200 OK put-item curl -v -X PUT "http://localhost:9000/v0/items/1" -H "Authorization-Admin:a" \ -H "Content-Type: application/json" -d '{ "id":"0", "name":"a", "price":1 }' → 204 No Content delete-item curl -v -X DELETE "http://localhost:9000/v0/items/1" -H "Authorization-Admin:a" → 204 No Content

Slide 16

Slide 16 text

微妙な点

Slide 17

Slide 17 text

微妙な点 ¨ middlewareが全体にしか適用できな‡ openapi.RegisterHandlersが全てのルーティングを同じGroupに割り当てるた€ 特定のルートのみログインしているユーザーをctxに入れたい等– ¨ Validation ErrorのMessageがイケてな‡ 英語かつフィールド名を含む文字列で返ってく† 処理しやすい or そのまま表示できるものにした™ ¨ RequestBodyのBindとResponseは手動で用意する必要があ† 手作業なので間違えることがあ† 薄いラッパーの宿命なので、諦めて命名規則でカバŠ ¨ やはりopenapi.ymlを書くのが辛™ Stoplight Studioで改善されたとはいえ、GUIでは書けない部分があ† yaml力はある程度求められる

Slide 18

Slide 18 text

改善

Slide 19

Slide 19 text

特定のルートのみにmiddlewareを適用する echoのRoutingとmiddlewareの話 // 自動生成されているRoutingを登録 // handlerはInterfaceを満たす必要がある openapi.RegisterHandlers(g, handler) // RegisterHandlers adds each server route to the EchoRouter. // Registers handlers, and prepends BaseURL to the paths, so that the paths // can be served under a prefix. func func { RegisterHandlersWithBaseURL(router, si, ) } wrapper := ServerInterfaceWrapper{ Handler: si, } router.GET(baseURL+ , wrapper.GetItems) router.POST(baseURL+ , wrapper.PostItems) router.DELETE(baseURL+ , wrapper.DeleteItem) router.GET(baseURL+ , wrapper.GetItem) router.PUT(baseURL+ , wrapper.PutItem) } RegisterHandlers RegisterHandlersWithBaseURL (router EchoRouter, si ServerInterface) (router EchoRouter, si ServerInterface, bas "" "/items" "/items" "/items/:id" "/items/:id" "/items/:id" この関数を使いつつ各ルートに 別々のmiddlewareを適用するには? →全体で使うmiddlewareを作り、 その中で適用するかを判断する

Slide 20

Slide 20 text

特定のルートのみにmiddlewareを適用する // カスタムMiddlewareの定義 // 各ルートに適用 // カスタムMiddlewareの適用 mwRoot := middleware.NewMiddlewareRoot() userAuthRoute := mwRoot.Group(apiPathPrefix) userAuthRoute.Use(authUser) { userAuthRoute.POST( ) } adminAuthRoute := mwRoot.Group(apiPathPrefix) adminAuthRoute.Use(authAdmin) { adminAuthRoute.PUT( ) adminAuthRoute.DELETE( ) } g.Use(mwRoot.Exec) "/items" "/items/:id" "/items/:id" Groupごとにmiddlewareを定義できるmiddleware echoのinterfaceに近いが、handlerを第二引数に渡さない →handlerはRegisterHandlersが登録するため mwRoot.ExecがPath/Methodに応じて適用する type struct map string map string type struct string middlewareRoot { middlewares [ ] [ ][]echo.MiddlewareFunc router *echo.Router echo *echo.Echo } group { prefix middleware []echo.MiddlewareFunc middlewareRoot *middlewareRoot } // [method][path]

Slide 21

Slide 21 text

mwRoot.Exec 特定のルートのみにmiddlewareを適用する func return func if return . { { method := c.Request().Method path := c.Request().URL.Path _, ok := mwr.middlewares[method]; !ok { next(c) } mwc := mwr.echo.NewContext(c.Request(), c.Response()) mwr.router.Find(method, path, mwc) routePath := mwc.Path() (mwr *middlewareRoot) (next echo.HandlerFunc) (c echo.Context) Exec echo HandlerFunc error // パスの検索 _, ok := mwr.middlewares[method][routePath]; !ok { next(c) } middleware := mwr.middlewares[method][routePath] i := (middleware) - ; i >= ; i-- { next = middleware[i](next) } next(c) } } if return for return len 1 0

Slide 22

Slide 22 text

Validation Error Messageを改善する validatorOpts := &oapiMw.Options{} validatorOpts.ErrorHandler = { rerr, ok := err.Internal.(*openapi3filter.RequestError); ok { rerr.Parameter != { fmt.Print(rerr.Parameter.Name) err.Message = rerr.Parameter.Name + } } err } // custom error message func if if nil return (c echo.Context, err *echo.HTTPError) error "が不正な値です。" echo.HTTPErrorとopenapi3filter.RequestErrorの話 ErrorHandlerを定義することでカスタム可能

Slide 23

Slide 23 text

Validation Error Messageを改善する // HTTPError represents an error that occurred while handling a request. // Stores the error returned by an external dependency HTTPError { Code Message {} Internal error } struct int interface `json:"-"` `json:"message"` `json:"-"` // OapiRequestValidatorWithOptions creates a validator from a swagger object, with validation options // --snip-- // --snip-- func return func return func if nil if nil nil return return return . { . { { err := ValidateRequestFromContext(c, router, options) err != { options != && options.ErrorHandler != { options.ErrorHandler(c, err) } err } next(c) } } } OapiRequestValidatorWithOptions echo MiddlewareFunc echo HandlerFunc error (swagger *openapi3.T, options *Options) (next echo.HandlerFunc) (c echo.Context) echo.HTTPError

Slide 24

Slide 24 text

Validation Error Messageを改善する func if nil switch type case return return nil * . { err = openapi3filter.ValidateRequest(requestContext, validationInput) err != { e := err.( ) { *openapi3filter.RequestError: errorLines := strings.Split(e.Error(), ) &echo.HTTPError{ Code: http.StatusBadRequest, Message: errorLines[ ], Internal: err, } } } } ValidateRequestFromContext echo HTTPError 0 (ctx echo.Context, router routers.Router, options *Options) // --snip-- // We've got a bad request // Split up the verbose error by lines and return the first one // openapi errors seem to be multi-line with a decent message on the first // --snip-- "\n" ValidateRequestFromContext

Slide 25

Slide 25 text

Validation Error Messageを改善する openapi3filter.RequestError type struct string type struct RequestError { Input *RequestValidationInput Parameter *openapi3.Parameter RequestBody *openapi3.RequestBody Reason Err error } SecurityRequirementsError { SecurityRequirements openapi3.SecurityRequirements Errors []error } エラー情報を抽出してメッセージを組み立てることができる echo.Contextがあるので自由にレスポンスを作れる

Slide 26

Slide 26 text

まとめ

Slide 27

Slide 27 text

まとめ c 生成されるコードが読みやす c 最低限使えるまでの学習コストが低 c バリデーションやstruct、interfaceの生成が良 c ある程度のカスタマイズは可È c templateも使えQ c 追随するのが面倒なので程々に c OpenAPIのyamlをどう書いていくˆ c 薄めの生成コードなので実装者がスキーマ定義を守る意識は必要 OpenAPIでのAPI定義から実装に落とし込む際に、まず試してみたい https://github.com/deepmap/oapi-codegen

Slide 28

Slide 28 text

Thank you! 参考URL https://github.com/deepmap/oapi-codegen https://github.com/Tim0401/oapi-codegen-demo https://github.com/labstack/echo https://github.com/getkin/kin-openapi https://stoplight.io/studio https://github.com/OpenAPITools/openapi-generator