Upgrade to Pro — share decks privately, control downloads, hide ads and more …

oapi-codegenを使ってみた

 oapi-codegenを使ってみた

あけの

June 25, 2022
Tweet

More Decks by あけの

Other Decks in Programming

Transcript

  1. 構成 oapi-codegen Material UI + Theme openapi-generator sql-migrate sqlboiler OpenAPI

    3.0スキーマから Goのコードを生成する • Stoplight Studi† • echo/v4 (CIでコード生成してテスト・ビルド)
  2. スキーマ駆動開発の動機 F API定義の認識を合わせるのが面2 F API仕様書を作る必要があっ" F 同じ定義を参照してコードを手で書いていくのが辛かっ" F 楽をして品質を上げたい gRPCやGraphQLという選択肢

    F 納期があるので大きなリスクはとれなk F インフラが自由に触れなk F 既存のやり方に戻れる程度の改‡ F コード生成部分さえ整えてしまえば広げるのが楽
  3. 生成ファイル 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はこの段階で取り出されている
  4. 生成ファイル 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に変換
  5. 使う 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
  6. 使う 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"
  7. 使う 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)
  8. 叩く 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
  9. 微妙な点 ¨ middlewareが全体にしか適用できな‡ openapi.RegisterHandlersが全てのルーティングを同じGroupに割り当てるた€ 特定のルートのみログインしているユーザーをctxに入れたい等– ¨ Validation ErrorのMessageがイケてな‡ 英語かつフィールド名を含む文字列で返ってく† 処理しやすい

    or そのまま表示できるものにした™ ¨ RequestBodyのBindとResponseは手動で用意する必要があ† 手作業なので間違えることがあ† 薄いラッパーの宿命なので、諦めて命名規則でカバŠ ¨ やはりopenapi.ymlを書くのが辛™ Stoplight Studioで改善されたとはいえ、GUIでは書けない部分があ† yaml力はある程度求められる
  10. 特定のルートのみに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を作り、 その中で適用するかを判断する
  11. 特定のルートのみに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]
  12. 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
  13. 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を定義することでカスタム可能
  14. 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
  15. 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
  16. 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があるので自由にレスポンスを作れる
  17. まとめ c 生成されるコードが読みやす c 最低限使えるまでの学習コストが低 c バリデーションやstruct、interfaceの生成が良 c ある程度のカスタマイズは可È c

    templateも使えQ c 追随するのが面倒なので程々に c OpenAPIのyamlをどう書いていくˆ c 薄めの生成コードなので実装者がスキーマ定義を守る意識は必要 OpenAPIでのAPI定義から実装に落とし込む際に、まず試してみたい https://github.com/deepmap/oapi-codegen