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の環境がない場合は

などが用意されています。

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