同じシグネチャを持つ異なる関数たちのreflect.Typeのidentityについて
シグネチャが同じ関数のreflect.Typeが一致することに気づかずハマったりしていた1。
package main import ( "fmt" "reflect" ) type Foo string type Bar string type Alias = string func Hello(string) string { return "" } func Byebye(string) string { return "" } func main() { fmt.Println(reflect.TypeOf(func() string { return "" }())) // new typeはそれぞれ異なるreflect.Type fmt.Println("----------------------------------------") fmt.Println(reflect.TypeOf(func() Foo { return Foo("") }())) fmt.Println(reflect.TypeOf(func() Bar { return Bar("") }())) // aliasはもちろん元の型と同じ。 fmt.Println("----------------------------------------") fmt.Println(reflect.TypeOf(func() Alias { return Alias("") }())) // この関数たちのreflect.Typeが同じことに気づかなかった fmt.Println("========================================") fmt.Println(reflect.TypeOf(func() interface{} { return Hello }())) fmt.Println(reflect.TypeOf(func() interface{} { return Byebye }())) }
実行結果。
string ---------------------------------------- main.Foo main.Bar ---------------------------------------- string ======================================== func(string) string func(string) string
考えてみれば当たり前だけど、たしかにね、と言う感じ。
gist
reflect-openapiにswagger-uiを組み込んだ
この記事の続き。いろいろ変更を加えて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 を使っている。↩