reflect-openapiにswagger-uiを組み込んだ

github.com

この記事の続き。いろいろ変更を加えてswagger-uiを組み込んだ。

これまでのreflect-openapi

以下のような "Hello " と返すだけの関数を公開する。

type HelloInput struct{ Name string }

func Hello(input HelloInput) string {
    return fmt.Sprintf("Hello %s", input.Name)
}

使い方はこういう感じ。/helloというAPIが登録されたhandlerを作る。

echo '{"name": "World"}' | http --json POST :33333/hello
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Date: Sat, 12 Dec 2020 13:25:47 GMT

"Hello World"

というのがこれまでの話。

swagger-uiなど諸々を公開

どのようなAPIが公開されているかわからない。現在どのようなAPIが存在しているのかが知りたい。このための機能を追加した。指定したパス以下にswagger-uiを表示するUIを含めたhandlerを組み込めるようにした。これを毎回書くのは怠いので github.com/podhmo/reflect-openapi/handler パッケージとしてサブパッケージを切った。

今回は/openapi 以下に組み込んでみた。/openapi/ にアクセスすると以下のようなendpointの一覧が返ってくる。 POST /hello 以外は勝手に生えたもの。

$ http :33333/openapi/
HTTP/1.1 200 OK
Content-Length: 350
Content-Type: application/json
Date: Sat, 12 Dec 2020 13:24:20 GMT

[
    {
        "method": "POST",
        "operationId": "main.Hello",
        "path": "/hello",
        "summary": ""
    },
    {
        "method": "GET",
        "operationId": "OpenAPIDocHandler",
        "path": "/openapi/doc",
        "summary": "(added by github.com/podhmo/reflect-openapi/handler)"
    },
    {
        "method": "GET",
        "operationId": "SwaggerUIHandler",
        "path": "/openapi/ui",
        "summary": "(added by github.com/podhmo/reflect-openapi/handler)"
    }
]

そうそう /openapi/doc/openapi/ui がある。 /doc の方はopenapi docが返ってくる。/uiの方はswagger-uiが使われる1。実際にブラウザから動かしてみる。

swagger-ui 1 swagger-ui 2

動く。

コード

コードはこんな感じ。少し冗長ではあるけれど。 net/http だけのhandlerにopenAPI docをつけれるのは便利なんじゃないか?

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/getkin/kin-openapi/openapi3"
    reflectopenapi "github.com/podhmo/reflect-openapi"
    "github.com/podhmo/reflect-openapi/handler"
)

func main() {
    if err := run(); err != nil {
        log.Fatalf("!! %+v", err)
    }
}

func run() error {
    addr := ":44444"
    if v := os.Getenv("ADDR"); v != "" {
        addr = v
    }
    h := setupHandler(addr)
    log.Println("Listen ...", addr)
    return http.ListenAndServe(addr, h)
}

type HelloInput struct{ Name string }

func Hello(input HelloInput) string {
    return fmt.Sprintf("Hello %s", input.Name)
}

func setupHandler(addr string) http.Handler {
    mux := &http.ServeMux{}

    c := &reflectopenapi.Config{}
    c.BuildDoc(context.Background(), func(m *reflectopenapi.Manager) {
        {
            path := "/hello"
            mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
                var input HelloInput
                if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
                    fmt.Fprintf(w, `{"error": %q}`, err.Error())
                    return
                }
                defer r.Body.Close()
                fmt.Fprintf(w, `%q`, Hello(input))
            })

            op := m.Visitor.VisitFunc(Hello)
            m.Doc.AddOperation(path, "POST", op)
        }

        // swagger-ui
        doc := m.Doc
        doc.Servers = append([]*openapi3.Server{{
            URL:         fmt.Sprintf("http://localhost%s", addr),
            Description: "local development server",
        }}, doc.Servers...)
        mux.Handle("/openapi/", handler.NewHandler(doc, "/openapi/"))
    })
    return mux
}

(ちなみに、echoの例をgithubには挙げてみていた https://github.com/podhmo/reflect-openapi/blob/main/_examples/03echo-mixed/main.go)

HelloInput を定義するのはだるくない?

ところで、APIの元となる関数は以下のようなものだった。このHelloInputの定義もRPC的なことを考えるとめんどくさくない?

type HelloInput struct{ Name string }

func Hello(input HelloInput) string {
    return fmt.Sprintf("Hello %s", input.Name)
}

以下のようにも書けるようにした。

func Hello(name string) string {
    return fmt.Sprintf("Hello %s", name)
}

関数を受け取って、そのシグネチャからoepnAPI docのOperationItemを生成しているのだけれど。通常は第一引数のstructを見る。これをすべての引数をマージしたstructを使うということにできる。これはConfigにSelectorというフィールドがあるのでそこで MergeParamsInputSelector を使うように変更する。

// これを
    c := &reflectopenapi.Config{}

// こう
    c := &reflectopenapi.Config{
        Selector: &struct {
            reflectopenapi.MergeParamsInputSelector
            reflectopenapi.FirstParamOutputSelector
        }{},
    }

diff全体はこういう感じ。

--- 03reflect-openapi/main.go    2020-12-12 20:33:31.000000000 +0900
+++ 04reflect-openapi/main.go 2020-12-12 22:43:58.000000000 +0900
@@ -29,27 +29,32 @@
    return http.ListenAndServe(addr, h)
 }
 
-type HelloInput struct{ Name string }
-
-func Hello(input HelloInput) string {
-  return fmt.Sprintf("Hello %s", input.Name)
+func Hello(name string) string {
+   return fmt.Sprintf("Hello %s", name)
 }
 
 func setupHandler(addr string) http.Handler {
    mux := &http.ServeMux{}
 
-  c := &reflectopenapi.Config{}
+   c := &reflectopenapi.Config{
+       Selector: &struct {
+           reflectopenapi.MergeParamsInputSelector
+           reflectopenapi.FirstParamOutputSelector
+       }{},
+   }
    c.BuildDoc(context.Background(), func(m *reflectopenapi.Manager) {
        {
            path := "/hello"
            mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
-              var input HelloInput
+               var input struct {
+                   Name string `json:"name"`
+               }
                if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
                    fmt.Fprintf(w, `{"error": %q}`, err.Error())
                    return
                }
                defer r.Body.Close()
-              fmt.Fprintf(w, `%q`, Hello(input))
+               fmt.Fprintf(w, `%q`, Hello(input.Name))
            })
 
            op := m.Visitor.VisitFunc(Hello)

はい。

custom response

あと、SelectorのOutputの方は、戻り値の解釈を変えられる。例えば、配列を直接返さずオブジェクトとしてwrapして返したいような場合がある。

// こうではなく
[1, 2, 3]

// こう
{
  "count": 3,
  "items": [1, 2, 3],
  "hasNext": false
}

この様なレスポンスを返すAPIfunc() []int のような関数から作るときに使う。

gist


  1. 組み込み方は https://github.com/abersheeran/rpc.py をかなり参考にした。https://www.npmjs.com/package/swagger-ui-dist を使っている。