🚶‍♂️

stepCIのOpenAPI定義インポートを試してみた

2023/03/17に公開

概要

OpenAPI定義書ドリブンで開発を進めるケースもあり、stepCIワークフローとgoのechoのコードを自動生成して試したメモ。

やってみた

OpenAPI定義の作成

簡単なGETとPOSTの定義を作成します。  
公式の下記記載を参考に、バージョンは3.0.3で作成します。

WARNING

Only OpenAPI 3.0 and higher is supported. Prepare your OpenAPI spec before > running the command. Add request and response examples, default values and > specify required params
https://docs.stepci.com/import/openapi.html

openapi.yml
version: "1.0"
name: step-ci-testing
config:
  http:
    baseURL: http://localhost:8888
tests:
  morphs:
    steps:
      - id: getMorphs
        http:
          url: /morphs
          method: GET
          check:
            status: 200
            schema:
              $ref: "#/components/schemas/Morphs"
      - id: postMorph
        http:
          url: /morphs
          method: POST
          headers:
            Content-Type: application/json
            accept: application/json
          json:
            name: sed magna in
          check:
            status: 200
            schema:
              $ref: "#/components/schemas/Morph"
      - id: getMorphById
        http:
          url: /morphs/1
          method: GET
          check:
            status: 200
            schema:
              $ref: "#/components/schemas/Morph"
components:
  schemas:
    Morph:
      allOf:
        - type: object
          required:
            - id
          properties:
            id:
              type: integer
              example: 1
        - $ref: "#/components/schemas/NewMorph"
    NewMorph:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          example: Normal
    Morphs:
      type: array
      items:
        $ref: "#/components/schemas/Morph"
    Id:
      type: integer
      example: 1

API実装

oapi-codegenを利用します。

$ go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest

oapi-codegenを実行するgo generate用のファイルgen/gen.goを作成します。

gen/gen.go
//go:generate oapi-codegen -package oapi -include-tags "morphs" -o ../oapi/morphs.gen.go ../openapi.yml

package main

func main() {}

実行してmorphs.gen.goを生成します。

$ go generate ./gen

生成コードを参照しmain.goを実装します。
JSONファイルをDBとして読み書きするAPIとして簡易に実装します。

main.go
package main

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"os"

	"github.com/labstack/echo/v4"

	"step-ci-testing/oapi"
)

// ServerInterfaceを満たすよう実装
type controller struct {}

func (c controller) GetMorphs(ctx echo.Context) error {
	morphs := readData("data.json")
	return ctx.JSON(http.StatusOK, &morphs)
}

func (c controller) PostMorph(ctx echo.Context) error {
	newMorph := new(oapi.NewMorph)
	ctx.Bind(newMorph)
	if len(newMorph.Name) > 64 {
		resp := &oapi.BadRequest{"Invalid value"}
		return ctx.JSON(http.StatusBadRequest, resp)
	}
	morph := writeData("data.json", newMorph)
	return ctx.JSON(http.StatusOK, &morph)
}

func (c controller) GetMorphById(ctx echo.Context, id oapi.Id) error {
	morphs := readData("data.json")
	for _, m := range morphs {
		if m.Id == id {
			return ctx.JSON(http.StatusOK, &m)
		}
	}
	resp := &oapi.NotFound{"Not found"}
	return ctx.JSON(http.StatusNotFound, &resp)
}

// dataaccess
func readData(filepath string) oapi.Morphs {
	data, _ := ioutil.ReadFile(filepath)
	var morphs oapi.Morphs
	json.Unmarshal(data, &morphs)
	return morphs
}

func writeData(filepath string, newMorph *oapi.NewMorph) oapi.Morph {
	morphs := readData(filepath)

	id := len(morphs) + 1
	morph := oapi.Morph{
		Id: id,
		Name: newMorph.Name,
	}
	morphs = append(morphs, morph)

	f, err := os.OpenFile(filepath, os.O_WRONLY, 0666)
	if err != nil {
		panic(err)
	}
	defer f.Close()
	if err = json.NewEncoder(f).Encode(morphs); err != nil {
		panic(err)
	}

	return morph
}

func main() {
	e := echo.New()
	c := controller{}

	oapi.RegisterHandlers(e, c)

	e.Logger.Fatal(e.Start(":8888"))
}
data.json
[]

stepciインストールとファイル生成

Homebrewでインストールします。

$ brew install stepci

ワークフローファイル生成コマンドのstepci generateのヘルプを確認します。
対象のopenapi定義ファイルと生成先ファイルを引数で順に渡せばいいようです。

$ stepci generate --help

ⓘ  Anonymous usage data collected. Learn more on https://step.ci/privacy

stepci generate [spec] [path]

generate workflow from OpenAPI spec

位置:
  spec                    openapi file url [文字列] [デフォルト: "openapi.json"]
  path                    output file path
...

生成コマンドをgen/gen.goに追記し、実行して生成します。(2行目が追記箇所)

gen/gen.go
//go:generate oapi-codegen -package oapi -include-tags "morphs" -o ../oapi/morphs.gen.go ../openapi.yml
//go:generate stepci generate ../openapi.yml ../workflow.yml

package main

func main() {}
$ go generate ./gen

ⓘ  Anonymous usage data collected. Learn more on https://step.ci/privacy

Success! The workflow file can be found at ./workflow.yml
Give us your feedback on https://step.ci/feedback

workflow.ymlが以下の通り生成されました。
各リクエストでopenapiで定義したステータスとレスポンスボディが得られるかテストしてくれそうです。
stepsのgetMorphByIdのパスパラメータについては、openapi定義の該当パラメータのexample値が利用されます。未指定の場合はランダム生成の値になるため注意が必要そうです。

workflow.yml
version: "1.0"
name: step-ci-testing
config:
  http:
    baseURL: http://localhost:8888
tests:
  morphs:
    steps:
      - id: getMorphs
        http:
          url: /morphs
          method: GET
          check:
            status: 200
            schema:
              $ref: "#/components/schemas/Morphs"
      - id: postMorph
        http:
          url: /morphs
          method: POST
          headers:
            Content-Type: application/json
            accept: application/json
          json:
            name: sed magna in
          check:
            status: 200
            schema:
              $ref: "#/components/schemas/Morph"
      - id: getMorphById
        http:
          url: /morphs/1
          method: GET
          check:
            status: 200
            schema:
              $ref: "#/components/schemas/Morph"
components:
  schemas:
    Morph:
      allOf:
        - type: object
          required:
            - id
          properties:
            id:
              type: integer
              example: 1
        - $ref: "#/components/schemas/NewMorph"
    NewMorph:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          example: Normal
    Morphs:
      type: array
      items:
        $ref: "#/components/schemas/Morph"
    Id:
      type: integer
      example: 1

テスト

サーバを起動します。

$ go run ./main.go 

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.9.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8888

stepciテストを実行します。
無事テストが通りました!

$ stepci run ./workflow.yml

ⓘ  Anonymous usage data collected. Learn more on https://step.ci/privacy

 PASS  morphs

Tests: 0 failed, 1 passed, 1 total
Steps: 0 failed, 0 skipped, 3 passed, 3 total
Time:  0.112s, estimated 0s
CO2:   0.00007g

Workflow passed after 0.112s
Give us your feedback on https://step.ci/feedback

所感

最低限のテストとして簡単に生成できるのはとても良いなと思う反面、異常系については結局自分でworkflowを書く必要があるので、規模が大きくなるにつれ自動生成は使わなくなるかもと思いました。
複数のworkflowをincludeして実行といったこともできるようなので、自動生成した正常系と手動作成の異常系の2ファイルで運用もありですかね。

-> workflow.ymlをincludeするworkflow_main.ymlを作成してみたが、エラーで実行できず。詳細はちょっと調べましたがわかっておらずです。

workflow_main.yml
version: "1.0"
name: step-ci-testing
include:
  - workflow.yml

エラー

$ stepci run ./workflow_main.yml

ⓘ  Anonymous usage data collected. Learn more on https://step.ci/privacy

 FAIL  morphs

Summary

  ✕ getMorphs failed after 0.065s
  ⚠︎ postMorph skipped after 0s
  ⚠︎ getMorphById skipped after 0s

⚠︎ morphs › undefined

can't resolve reference #/components/schemas/Morphs from id #


⚠︎ morphs › undefined

Step was skipped because previous one failed


⚠︎ morphs › undefined

Step was skipped because previous one failed


Tests: 1 failed, 0 passed, 1 total
Steps: 3 failed, 2 skipped, 0 passed, 3 total
Time:  0.095s, estimated 0s
CO2:   0.00007g

Workflow failed after 0.095s
Give us your feedback on https://step.ci/feedback

Discussion