この記事の続き。いろいろ変更を加えて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。実際にブラウザから動かしてみる。

動く。
コード
コードはこんな感じ。少し冗長ではあるけれど。 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
}
この様なレスポンスを返すAPIを func() []int のような関数から作るときに使う。
gist
- https://gist.github.com/podhmo/cfe75b1965025271cfb656ab3e094506
- https://gist.github.com/podhmo/42e61a3f210a28177ccecf2abbcc7b75
-
組み込み方は https://github.com/abersheeran/rpc.py をかなり参考にした。https://www.npmjs.com/package/swagger-ui-dist を使っている。↩