概要
前回 go-swagger のインストールと簡単なサーバの生成と起動まで実施しました
今回は生成されたコードを修正し実際のロジックまで作成してみました
環境
- CentOS 6.7 64bit
- go-swagger dev
- golang 1.6
swagger.yml 編集
TODO リストに必要な REST API を追加します
前回の swagger.yml から追記する必要がある差分は以下の通りです
基本的には paths の「/」に post 命令を追加するのと新規の paths「/{id}」に対して put と delete の定義を追加しています
TODO アプリに必要な CRUD 機能を追加してる感じです
65,118d65
< post:
< tags:
< - todos
< operationId: addOne
< parameters:
< - name: body
< in: body
< schema:
< $ref: "#/definitions/item"
< responses:
< 201:
< description: Created
< schema:
< $ref: "#/definitions/item"
< default:
< description: error
< schema:
< $ref: "#/definitions/error"
< /{id}:
< parameters:
< - type: integer
< format: int64
< name: id
< in: path
< required: true
< put:
< tags:
< - todos
< operationId: updateOne
< parameters:
< - name: body
< in: body
< schema:
< $ref: "#/definitions/item"
< responses:
< 200:
< description: OK
< schema:
< $ref: "#/definitions/item"
< default:
< description: error
< schema:
< $ref: "#/definitions/error"
< delete:
< tags:
< - todos
< operationId: destroyOne
< responses:
< 204:
< description: Deleted
< default:
< description: error
< schema:
< $ref: "#/definitions/error"
追記できたら validation して再生成します
swagger validate swagger.yml
swagger generate server -A TodoList -f swagger.yml
で再度 .go ファイルが生成されます
では、実際に TODO アプリに必要な機能を実装してみます
編集する箇所がやや多いのでポイントごとに紹介します
import
import (
"crypto/tls"
"fmt"
"net/http"
"sync"
"sync/atomic"
errors "github.com/go-openapi/errors"
runtime "github.com/go-openapi/runtime"
middleware "github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
graceful "github.com/tylerb/graceful"
"github.com/hawksnowlog/todo-list/models"
"github.com/hawksnowlog/todo-list/restapi/operations"
"github.com/hawksnowlog/todo-list/restapi/operations/todos"
)
既存 import にいくつかライブラリを追加しています
足りない部分を追加すれば基本は OK です
sync や swag, モデルを管理するための models が追加になっていると思います
ロジック
ちょっと長いです
が、これが TODO アプリのコアの機能の部分になっています
var items = make(map[int64]*models.Item)
var lastID int64
var itemsLock = &sync.Mutex{}
func newItemID() int64 {
return atomic.AddInt64(&lastID, 1)
}
まずは TODO を保存するを定義します
TODO にはインクリメントな ID が振られるため、それを生成するための関数を定義します
次に各 CRUD 処理のメインとなる関数をそれぞれ準備します
func addItem(item *models.Item) error {
if item == nil {
return errors.New(500, "item must be present")
}
itemsLock.Lock()
defer itemsLock.Unlock()
newID := newItemID()
item.ID = newID
items[newID] = item
return nil
}
func updateItem(id int64, item *models.Item) error {
if item == nil {
return errors.New(500, "item must be present")
}
itemsLock.Lock()
defer itemsLock.Unlock()
_, exists := items[id]
if !exists {
return errors.NotFound("not found: item %d", id)
}
item.ID = id
items[id] = item
return nil
}
func deleteItem(id int64) error {
itemsLock.Lock()
defer itemsLock.Unlock()
_, exists := items[id]
if !exists {
return errors.NotFound("not found: item %d", id)
}
delete(items, id)
return nil
}
func allItems(since int64, limit int32) (result []*models.Item) {
result = make([]*models.Item, 0)
for id, item := range items {
if len(result) >= int(limit) {
return
}
if since == 0 || id > since {
result = append(result, item)
}
}
return
}
関数の名前の通りなのでそれほど読み解くのは難しくないと思います
先程定義した items という変数に対して値を追加したり削除したり更新したりする処理をそれぞれの関数で行っているだけです
またこのロジックは // This file is safe to edit. Once it exists it will not be overwritten
というコメントがあるので、その直下に記載してください
ハンドラで各ロジックをコールする
実装したロジックをハンドラ側でコールします
configureAPI というメソッドがあるのでその中のハンドラを修正します
api.TodosAddOneHandler = todos.AddOneHandlerFunc(func(params todos.AddOneParams) middleware.Responder {
fmt.Println("TodosAddOneHandler")
if err := addItem(params.Body); err != nil {
return todos.NewAddOneDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return todos.NewAddOneCreated().WithPayload(params.Body)
})
api.TodosDestroyOneHandler = todos.DestroyOneHandlerFunc(func(params todos.DestroyOneParams) middleware.Responder {
fmt.Println("TodosDestroyOneHandler")
if err := deleteItem(params.ID); err != nil {
return todos.NewDestroyOneDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return todos.NewDestroyOneNoContent()
})
api.TodosFindTodosHandler = todos.FindTodosHandlerFunc(func(params todos.FindTodosParams) middleware.Responder {
fmt.Println("TodosFindTodosHandler")
mergedParams := todos.NewFindTodosParams()
mergedParams.Since = swag.Int64(0)
if params.Since != nil {
mergedParams.Since = params.Since
}
if params.Limit != nil {
mergedParams.Limit = params.Limit
}
return todos.NewFindTodosOK().WithPayload(allItems(*mergedParams.Since, *mergedParams.Limit))
})
api.TodosUpdateOneHandler = todos.UpdateOneHandlerFunc(func(params todos.UpdateOneParams) middleware.Responder {
fmt.Println("TodosUpdateOneHandler")
if err := updateItem(params.ID, params.Body); err != nil {
return todos.NewUpdateOneDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return todos.NewUpdateOneOK().WithPayload(params.Body)
})
デバッグ用に fmt していますが必須ではないので不要であれば削除してください
基本は先程定義したロジックをコールしてその結果を見て成功 or 失敗のレスポンス情報を返却してます
レスポンスを返却するようの関数はすでに swagger が生成してくれているのでそれを素直に使います
記載できたらフォーマットしてインストールしましょう
go install でビルドもされるのでバイナリが新規に作成されます
go fmt restapi/configure_todo_list.go && go install ./cmd/todo-list-server/
バイナリが生成できたら起動します
todo-list-server --host 0.0.0.0 --port=18080
P.S 20190206 解説追記
api.TodosFindTodosHandler
で引数の params todos.FindTodosParams
をそのまま参照せず、なんでわざわざ todos.NewFindTodosParams()
し直しているかというと Since パラメータに default の定義がないからです
もしそのまま params.Since
という感じでポインタ参照すると invalid memory address or nil pointer dereference
になります
なので swagger.yml で
paths:
/:
get:
tags:
- todos
operationId: findTodos
parameters:
- name: since
in: query
type: integer
format: int64
default: 0
- name: limit
in: query
type: integer
format: int32
default: 20
という感じで since に default:0
を追加して swagger generate server -A TodoList -f swagger.yml
し直してあげると以下のように直接 params を参照してもエラーになりません
api.TodosFindTodosHandler = todos.FindTodosHandlerFunc(func(params todos.FindTodosParams) middleware.Responder {
return todos.NewFindTodosOK().WithPayload(allItems(*params.Since, *params.Limit))
})
go-swagger はこんな感じで引数やロジック側からのレスポンスをわざわざ正しい構造体に変換してから扱わなければいけない箇所が多いような気がします、、、
動作確認
それぞれ curl を叩けば OK です
なぞの Content-Type ヘッダがありますが、今回の swagger ファイルだとこの Content-Type が必須になります
curl -XPOST -H "Content-Type: application/io.goswagger.examples.todo-list.v1+json" "http://127.0.0.1:18080/v1/" -d '{"description":"test", "completed":false}'
{"description":"test","id":1}
curl -XGET "http://127.0.0.1:18080/v1"
[{"description":"test","id":1}]
curl -XPOST -H "Content-Type: application/io.goswagger.examples.todo-list.v1+json" "http://127.0.0.1:18080/v1/" -d '{"description":"test2", "completed":true}'
{"completed":true,"description":"test2","id":2}
curl -XGET "http://127.0.0.1:18080/v1"
[{"description":"test","id":1},{"completed":true,"description":"test2","id":2}]
curl -XPUT -H "Content-Type: application/io.goswagger.examples.todo-list.v1+json" "http://127.0.0.1:18080/v1/1" -d '{"description":"put test", "completed":true}'
{"completed":true,"description":"put test","id":1}
curl -XGET "http://127.0.0.1:18080/v1"
[{"completed":true,"description":"put test","id":1},{"completed":true,"description":"test2","id":2}]
[{"completed":true,"description":"test2","id":2}]
こんな感じになれば OK です
最後に
go-swagger で実際にロジック部分を実装してみました
生成されるコードのほとんどは基本触れないでメインとなる部分だけいじればいいので簡単です
逆に言うと生成されたコードの部分は何しているさっぱりになるので、swagger の内容の理解を深めるためにコードを追ってみてもいいかもしれません
今回の実装したロジックは単純なオンメモリの情報なのでサーバを停止すると情報は消えてしまいます
なので、本来あれば DB を使ったりして実装します
その場合でも基本的な実装の流れは変わらないかなと思います
今回コードの紹介は全部だと長いので一部分とさせていただきました
基本は以下の参考サイトにあるコードを元にして作成しているので、以下を参考にするとコードの全容をイメージしやすくなるかなと思います
参考サイト