go-swaggerを使う¶
この記事は Go (その2) Advent Calendar 2016 の7日目の記事です。
APIからのコード自動生成ツールとして、swaggerが最近流行ってます(異論は認めます)。
この分野では、 goa が有名ですが、goaはあくまでGoのDSLから生成するという方式です。 swagger定義がすでにある場合には使えません。
swagger定義ありきの場合、今回説明する go-swagger を使うと便利です。
なお、この内容は https://github.com/shirou/swagger_sample にて公開しています。
go-swaggerとは¶
go-swaggerは、swagger定義からgoのサーバーおよびクライアントを生成してくれるツールです。
インストールはgo getでインストールしてもよいのですが、goの環境がない場合は
docker
quay.io/goswagger/swagger
homebrew
brew tap go-swagger/go-swagger
brew install go-swagger
バイナリ
などが用意されています。
go-swaggerの使い方 -- サーバー編¶
例として、以下のようなswagger.ymlで定義したとしましょう。
produces:
- application/json
paths:
'/search':
get:
summary: get user information
consumes:
- application/x-www-form-urlencoded
tags:
- user
parameters:
- name: user_id
type: integer
in: formData
description: user id
responses:
200:
schema:
$ref: '#/definitions/User'
default:
schema:
$ref: '#/definitions/Error'
definitions:
User:
type: object
required:
- user_id
properties:
user_id:
type: integer
description: user id
name:
type: string
description: name
Error:
type: object
properties:
message:
type: string
サーバーサイドのコードを生成するには以下のコマンドを実行します。
$ swagger generate server -f swagger.yml -A swaggertest
これで、
cmd
models
restapi
というディレクトリが作成され、その中に必要なソースコードが生成されます。具体的にはこんな感じです。
restapi/
|-- configure_swaggertest.go
|-- doc.go
|-- embedded_spec.go
|-- operations
| |-- swaggertest_api.go
| `-- user
| |-- get_search.go
| |-- get_search_parameters.go
| |-- get_search_responses.go
| `-- get_search_urlbuilder.go
`-- server.go
-A swaggertest はアプリ名です。指定すると、いろいろなところで使われるようになります。
operationsの下の user はtagから取ってきています。複数のtagを設定していると複数のディレクトリに同じ内容が生成されてしまうので気をつけてください。
以降は、後ほど説明するconfigure_swaggertest.go以外のファイルは触りません。
Handlerの実装¶
生成されたファイルには触らず、パッケージ配下に別のファイルを作成しましょう。 restapi/operations/user/search.go というファイルを作成し、以下のようなコードを実装します。
package user
import (
middleware "github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
"github.com/shirou/swagger_sample/models"
)
func Search(params GetSearchParams) middleware.Responder {
payload := &models.User{
UserID: params.UserID,
Name: fmt.Sprintf("name_%d", swag.Int64Value(params.UserID)),
}
return NewGetSearchOK().WithPayload(payload)
}
paramsが渡ってくるパラメータです。swagger定義に従って事前にvalidateされていますので、安心して使えます。
NewGetSearchOK はswagger定義での 200 の場合に返す構造体を作成します。payloadとして、Userを定義して渡しておきます。
swag パッケージはポインタと実体とを変換したり、pathを検索したりといった便利関数を提供している便利ライブラリです。goの言語上の制限として、値なしとデフォルト値を区別するためにポインタにする必要があり、そのためにswagでの変換を使う必要があります。
Handlerの登録¶
restapi/configure_swaggertest.go に今実装したHandlerを付け加えます。 最初は middleware.NotImplemented が書かれているので、先程実装したHandlerに置き換えます。
api.UserGetSearchHandler = user.GetSearchHandlerFunc(user.Search)
configure.goは一度生成するとその後自動では変更されないので、swagger.ymlを更新して、Handlerを追加したりするときには注意が必要です。ださいですが、一旦renameして、再度生成されたファイルからコピーしたりしています。
なお、configure.goにはsetupのコードも含まれているので、必要に応じて事前設定を追加したり、ミドルウェアを追加したりするとよいでしょう。
実行¶
これで準備は整いました。 cmd/swaggertest-server 以下でbuildしましょう。
$ cd cmd/swaggertest-server && go build -o ../../server
$ ./server -port=8000
デフォルトではランダムなportで起動するので、 port 引数で指定しています。
あとは普通にアクセスできるようになります。
$ curl "http://localhost:8080/search?user_id=10"
{"name":"name_10","user_id":10}
簡単ですね。
生Request¶
paramsの中には http.Request がありますので、Requestそのものを得たい場合はここから得られます。 contextもここから得ればよいかと思います。
go-swaggerの使い方: クライアント編¶
go-swaggerはサーバーだけではなく、以下のコマンドでクライアントを生成出来ます。
$ swagger generate client -f swagger.yml -A swaggertest
あとは以下のようなコードを書けば、clientとして実行できます。
package main
import (
"fmt"
"log"
"time"
"github.com/go-openapi/swag"
apiclient "github.com/shirou/swagger_sample/client"
"github.com/shirou/swagger_sample/client/user"
)
func main() {
// make the request to get all items
p := &user.GetSearchParams{
UserID: swag.Int64(10),
}
resp, err := apiclient.Default.User.GetSearch(p.WithTimeout(10 * time.Second))
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Payload.Name)
}
この例ではパラメータ固定で叩くだけですが、引数とかをちゃんと設定していけば、CLIツールが簡単に実装できます。
model¶
swaggerではmodelを定義して、それを返答に含めたり出来ます。definitions以下ですね。
今回使っている例の User モデルは models/user.go で以下のように生成されます。
// User user
// swagger:model User
type User struct {
// name
Name string `json:"name,omitempty"`
// user id
// Required: true
UserID *int64 `json:"user_id"`
}
models/user.go そのものは再生成すると書きつぶされてしまうので、 models 以下に user_hoge.go などを作ってそちらでいろいろと関数を追加していくと良いと思います。
テスト¶
サーバー実装のテストはHandlerに対して指定されたパラメータを渡してあげれば良いです。ただし、http.Requestを入れる必要があるので、そのときには httptest.NewRecorder() を使って返り値を記録するようにします。あとは、各Handlerは単なる関数なので、その関数を実行するだけでよくて、 httptest.Server を立ち上げる必要はありません。
func TestSearch(t *testing.T) {
req, err := http.NewRequest("GET", "", nil)
if err != nil {
t.Fatal(err)
}
params := GetSearchParams{
HTTPRequest: req,
UserID: swag.Int64(10),
}
r := Search(params)
w := httptest.NewRecorder()
r.WriteResponse(w, runtime.JSONProducer())
if w.Code != 200 {
t.Error("status code")
}
var a models.User
err = json.Unmarshal(w.Body.Bytes(), &a)
if err != nil {
t.Error("unmarshal")
}
if swag.Int64Value(a.UserID) != 10 {
t.Error("wrong user id")
}
}
まとめ¶
go-swaggerを使ってswagger定義ファイルからサーバーとクライアントのコードを生成する例を示しました。
swaggerは swagger ui がcurlのコマンドが出てくるなどかなり使いやすく、定義ファイルも慣れれば難しくありません(ファイル分割が出来ないという問題はありますが)。
クライアントとサーバーとで別れて開発しているときなど、swaggerとswagger uiを使ってプロトコル定義を確認しつつ開発を進めていくと、理解の不整合が起きにくくなります。
Goaから生成するのも良いですが、swagger定義ファイルから生成する場合はgo-swaggerも検討してみると良いのではないでしょうか。
Comments
comments powered by Disqus