The Finatext Tech Blog

THE Finatext Tech Blog

Follow publication

自動生成されたhttpエンドポイントごとにカスタムミドルウェアを挿入したい話 ~実装編~

はじめに

この記事は、Go Conference 2024 で発表した「自動生成されたhttpエンドポイントごとにカスタムミドルウェアを挿入したい話」にて、スライドには収まらなかったサンプル実装について書きます。

概要をざっくり掴みたい方は、スライドも併せてご覧ください 👋

参加レポも書いたので、こちらもチェックよろしくです 👍

発表で話したことのおさらい

OpenAPI定義からGoコードを生成するツールにoapi-codegenがあります。oapi-codegenでは現状、個別のエンドポイントに対してミドルウェア処理を行う方法がサポートされていません。

そのため、発表ではOpenAPI側からのアプローチと独自のContextを用意することで解決した話をしました 👋

方針

発表で説明したとおり、個別のエンドポイントに対してミドルウェアを設定するように実装します。

最終的なコードはこちらです。

OpenAPI

通常のエンドポイントに加えて、Admin用のエンドポイントを用意しています。

ここで注目するのは secrutirysecuritySchemes の部分です。

paths:
/things:
get:
security:
- Role:
- "normal"
responses:
200:
description: a list of things
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ThingWithID'
/admin/things:
get:
security:
- Role:
- "admin"
responses:
200:
description: a list of things
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ThingWithID'

components:
securitySchemes:
Role:
type: http
scheme: bearer

どちらもOpenAPI 3.0 で定義されているキーワードです。

securitySchemes キーワードを使って、任意の名前でスコープを作成します。

作成したスコープは、 security キーワードを使って各APIに対して設定します。

上記の内容で、 oapi-codegen にてGoコードを生成します。

生成したコードには指定したルーティングエンジンの処理が含まれます。

サンプルでは、ルーティングエンジンは Echo を指定しています。

RegisterHandlers を呼び出すことで、エンドポイントとハンドラ関数の紐づけを行います。

// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
RegisterHandlersWithBaseURL(router, si, "")
}

// Registers handlers, and prepends BaseURL to the paths, so that the paths
// can be served under a prefix.
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {

wrapper := ServerInterfaceWrapper{
Handler: si,
}

router.GET(baseURL+"/admin/things", wrapper.GetAdminThings)
router.GET(baseURL+"/things", wrapper.GetThings)
}

ルーティングされるハンドラ関数も以下のように生成されます。

// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
}

// GetAdminThings converts echo context to params.
func (w *ServerInterfaceWrapper) GetAdminThings(ctx echo.Context) error {
var err error

ctx.Set(RoleScopes, []string{"admin"})

// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetAdminThings(ctx)
return err
}

// GetThings converts echo context to params.
func (w *ServerInterfaceWrapper) GetThings(ctx echo.Context) error {
var err error

ctx.Set(RoleScopes, []string{"normal"})

// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetThings(ctx)
return err
}

OpenAPIで設定したスコープが設定されていることが確認できます。

スコープは、 RoleScopes という名前で ctx に格納されます。

アプリケーションではスコープを取り出して、スコープに紐づくミドルウェアを実行するような実装を行います。

また、上記のハンドラ関数で呼び出している w.Handler の型はServereInterface です。

これはルーティング関数、ハンドラ関数と同様に生成されたインタフェースです。

// ServerInterface represents all server handlers.
type ServerInterface interface {

// (GET /admin/things)
GetAdminThings(ctx echo.Context) error

// (GET /things)
GetThings(ctx echo.Context) error
}

アプリケーションは、このインタフェースを満たす実装を用意していきます。

アプリケーション

生成されたインタフェースを満たすように実装したコードは以下のとおりです。

var _ gen.ServerInterface = (*Server)(nil)

type Server struct {}

func NewServer() Server {
return Server{}
}

func (s Server) GetAdminThings(ctx echo.Context) error {
log.Printf("GetAdminThings")
return nil
}

func (s Server) GetThings(ctx echo.Context) error {
log.Printf("GetThings")
return nil
}

上記の実装と生成されたコードを、main関数にて紐づけを行います。

func main() {
s := server.NewServer()

e := echo.New()

gen.RegisterHandlers(e, s)

// And we serve HTTP until the world ends.
log.Fatal(e.Start("0.0.0.0:8080"))
}

独自のContext

各エンドポイントごとのミドルウェア実行を実現するために、独自のContextとそれに紐づくレシーバメソッドを用意します。

実装は、以下の記事を参考にしています。

type (
Middleware func()
Middlewares map[string][]Middleware
)

type OriginalContext struct {
echo.Context
}

func NewOriginalContext(ctx echo.Context) *OriginalContext {
return &OriginalContext{ctx}
}

func (c OriginalContext) BindValidate(m Middlewares, i interface{}) error {
scopes, ok := c.Get(gen.RoleScopes).([]string)
if !ok {
scopes = []string{}
}
for _, scope := range scopes {
if middleware, ok := m[scope]; ok {
for _, mw := range middleware {
mw()
}
}
}

if i == nil {
return nil
}

if err := c.Bind(i); err != nil {
return err
}

if err := c.Validate(i); err != nil {
return err
}
return nil
}

独自のContextであるOriginalContext は、 echo.Context をラップしています。

生成されたコードで確認した通り、echo.Context からOpenAPIで定義したスコープを取得できます。

取得したスコープをもとに、割り当てたミドルウェアのリストを取り出して順次実行することができます。

ミドルウェアは以下のように、サーバを初期化するときにDIすることができます。

type Server struct {
m Middlewares
}

func NewServer() Server {
m := Middlewares{
"admin": []Middleware{
func() {
log.Printf("admin middleware")
},
func() {
log.Printf("admin middleware 2")
},
},
"normal": []Middleware{
func() {
log.Printf("normal middleware")
},
},
}

return Server{m: m}
}

func (s Server) GetAdminThings(ctx echo.Context) error {
log.Printf("GetAdminThings")

c := ctx.(*OriginalContext)
if err := c.BindValidate(s.m, nil); err != nil {
return err
}
return nil
}

func (s Server) GetThings(ctx echo.Context) error {
log.Printf("GetThings")

c := ctx.(*OriginalContext)
if err := c.BindValidate(s.m, nil); err != nil {
return err
}
return nil
}

まとめ

発表では説明できなかった具体的な実装について紹介しました。

あくまで oapi-codegen で現状サポートされていないという前提のアプローチなので、参考程度に見ていただけると幸いです。

さいごに

引き続き、issueの動向を見守りつつ、アップデートがあれば共有しますので、ぜひフォローもよろしくお願いします 👋

感想・ご意見ありましたら、メンション・DMでコメントよろしくお願いします(引用ポストでも泣いて喜びます)。

採用情報

Finatext グループでは一緒に働く仲間を募集中です。

会社のことも含めてカジュアルにお話したい方はこちらのフォームからもご応募お待ちしております 👍

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response