2017年2月24日金曜日

go-swagger を使って生成したコードに独自のロジックを実装してみた

概要

前回 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 ファイルが生成されます

restapi/configure_todo_list.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}]
  • curl -XDELETE -H "Content-Type: application/io.goswagger.examples.todo-list.v1+json" "http://127.0.0.1:18080/v1/1"

  • curl -XGET "http://127.0.0.1:18080/v1"

[{"completed":true,"description":"test2","id":2}]

こんな感じになれば OK です

最後に

go-swagger で実際にロジック部分を実装してみました
生成されるコードのほとんどは基本触れないでメインとなる部分だけいじればいいので簡単です
逆に言うと生成されたコードの部分は何しているさっぱりになるので、swagger の内容の理解を深めるためにコードを追ってみてもいいかもしれません

今回の実装したロジックは単純なオンメモリの情報なのでサーバを停止すると情報は消えてしまいます
なので、本来あれば DB を使ったりして実装します

その場合でも基本的な実装の流れは変わらないかなと思います

今回コードの紹介は全部だと長いので一部分とさせていただきました
基本は以下の参考サイトにあるコードを元にして作成しているので、以下を参考にするとコードの全容をイメージしやすくなるかなと思います

参考サイト

0 件のコメント:

コメントを投稿