goaを触ってみる

## goa

マイクロサービス開発を支援するフレームワーク

github
https://github.com/goadesign/goa

日本語doc
https://goa.design/ja/


## install

依存管理は`dep`で行いました

```bash
$ brew install dep
```

```bash
$ dep
dep is a tool for managing dependencies for Go projects

Usage: dep <command>

Commands:

  init    Initialize a new project with manifest and lock files
  status  Report the status of the project's dependencies
  ensure  Ensure a dependency is safely vendored in the project
  prune   Prune the vendor tree of unused packages

Examples:
  dep init                          set up a new project
  dep ensure                        install the project's dependencies
  dep ensure -update                update the locked versions of all dependencies
  dep ensure github.com/pkg/errors  add a dependency to the project

Use "dep help [command]" for more information about a command.
```

```bash
# 設定ファイル作成
$ dep init
```

__goaの追加__

goファイルがないと下記のように怒られるので気をつけてください
私はひとまずmain.goを作成することで解決しました

```bash
$ dep ensure -add github.com/goadesign/goa/goagen
all dirs lacked any go code
```

## 使ってみる

チュートリアルの作成物としては
ワインボトルを扱うセラーサービスをAPIとして提供する

### goaデザイン言語を書く

design/design.goの作成

```go:design/design.go
package design                               

import (
	. "github.com/goadesign/goa/design"        // Use . imports to enable the DSL
	. "github.com/goadesign/goa/design/apidsl"
)

// API defines the microservice endpoint and
// other global properties. There should be one
// and exactly one API definition appearing in the design.
var _ = API("cellar", func() {
	Title("The virtual wine cellar")
	Description("A simple goa service")
	Scheme("http")
	Host("localhost:8080")
})

// Resources group related API endpoints
// together. They map to REST resources for REST services.
var _ = Resource("bottle", func() {
	BasePath("/bottles")
	DefaultMedia(BottleMedia)

	// Actions define a single API endpoint together
	// with its path, parameters (both path
	// parameters and querystring values) and payload
	// (shape of the request body).
	Action("show", func() {
		Description("Get bottle by id")
		Routing(GET("/:bottleID"))
		Params(func() {
			Param("bottleID", Integer, "Bottle ID")
		})
		// Responses define the shape and status code of HTTP responses.
		Response(OK)
		Response(NotFound)
	})
})

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
	Description("A bottle of wine")
	// Attributes define the media type shape.
	Attributes(func() {
		Attribute("id", Integer, "Unique bottle ID")
		Attribute("href", String, "API href for making requests on the bottle")
		Attribute("name", String, "Name of wine")
		Required("id", "href", "name")
	})
	// View defines a rendering of the media type.
	// Media types may have multiple views and must
	// have a "default" view.
	View("default", func() {
		Attribute("id")
		Attribute("href")
		Attribute("name")
	})
})
```

- MediaTypeの第一引数には任意の値を入れる
- デフォではjsonになる

- default viewの設定は必須

- Attributesの中にはMediaType内で使われる可能性のあるものを定義する

- グローバルに置いているMediaTypeは__OK__レスポンスのときに参照される

### codeを生成する

depで入れた場合は、goagenをbuildする必要がある

```bash
$ cd vendor/github.com/goadesign/goa/goagen
$ go build # goagenが作られる
$ cd -
```

__code生成__

```bash
$ ./vendor/github.com/goadesign/goa/goagen/goagen bootstrap -d path/design
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/bottle_testing.go
main.go
bottle.go
tool/cellar-cli
tool/cellar-cli/main.go
tool/cli
tool/cli/commands.go
client
client/client.go
client/bottle.go
client/user_types.go
client/media_types.go
swagger
swagger/swagger.json
swagger/swagger.yaml
```

__path/__は、$GOPATH/src/__path__だと考えてください

### 作成されたものについて

`main.go`はapiのエンドポイント
`bottle.go`はbottleリソースに対応するコントローラ

上記は、既に存在する場合には再生成されない

コントローラであるbottle.goにロジックを実装すれば完成する

```go:bottle.go
package main

import (
	"github.com/goadesign/goa"
	"project/goa-tutorial/app"
)

// BottleController implements the bottle resource.
type BottleController struct {
	*goa.Controller
}

// NewBottleController creates a bottle controller.
func NewBottleController(service *goa.Service) *BottleController {
	return &BottleController{Controller: service.NewController("BottleController")}
}

// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {

        //ここにロジックを実装する

	res := &app.GoaExampleBottle{}
	return ctx.OK(res)
}
```

BottleControllerはmain.goのapp.MountBottleController()でmountされている

```go:main.go
package main

import (
	"github.com/goadesign/goa"
	"github.com/goadesign/goa/middleware"
	"project/goa-tutorial/app"
)

func main() {
	// Create service
	service := goa.New("cellar")

	// Mount middleware
	service.Use(middleware.RequestID())
	service.Use(middleware.LogRequest(true))
	service.Use(middleware.ErrorHandler(service, true))
	service.Use(middleware.Recover())

	// Mount "bottle" controller
	c := NewBottleController(service)
	app.MountBottleController(service, c)

	// Start service
	if err := service.ListenAndServe(":8080"); err != nil {
		service.LogError("startup", "err", err)
	}
}
```

app.MountBottleController()にルーティングが記載されている

```go:app/controllers.go
package app

import (
	"context"
	"github.com/goadesign/goa"
	"net/http"
)

// initService sets up the service encoders, decoders and mux.
func initService(service *goa.Service) {
	// Setup encoders and decoders
	service.Encoder.Register(goa.NewJSONEncoder, "application/json")
	service.Encoder.Register(goa.NewGobEncoder, "application/gob", "application/x-gob")
	service.Encoder.Register(goa.NewXMLEncoder, "application/xml")
	service.Decoder.Register(goa.NewJSONDecoder, "application/json")
	service.Decoder.Register(goa.NewGobDecoder, "application/gob", "application/x-gob")
	service.Decoder.Register(goa.NewXMLDecoder, "application/xml")

	// Setup default encoder and decoder
	service.Encoder.Register(goa.NewJSONEncoder, "*/*")
	service.Decoder.Register(goa.NewJSONDecoder, "*/*")
}

// BottleController is the controller interface for the Bottle actions.
type BottleController interface {
	goa.Muxer
	Show(*ShowBottleContext) error
}

// MountBottleController "mounts" a Bottle resource controller on the given service.
func MountBottleController(service *goa.Service, ctrl BottleController) {
	initService(service)
	var h goa.Handler

	h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
		// Check if there was an error loading the request
		if err := goa.ContextError(ctx); err != nil {
			return err
		}
		// Build the context
		rctx, err := NewShowBottleContext(ctx, req, service)
		if err != nil {
			return err
		}
		return ctrl.Show(rctx)
	}
	service.Mux.Handle("GET", "/bottles/:bottleID", ctrl.MuxHandler("Show", h, nil))
	service.LogInfo("mount", "ctrl", "Bottle", "action", "Show", "route", "GET /bottles/:bottleID")
}
```

BottleControllerのShowメソッドが受け取るcontextの実装は下記

```go:bottle.go
// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {

        //ここにロジックを実装する

	res := &app.GoaExampleBottle{}
	return ctx.OK(res)
}
```

```go:app/context.go
package app

import (
	"context"
	"github.com/goadesign/goa"
	"net/http"
	"strconv"
)

// ShowBottleContext provides the bottle show action context.
type ShowBottleContext struct {
	context.Context
	*goa.ResponseData
	*goa.RequestData
	BottleID int
}

// NewShowBottleContext parses the incoming request URL and body, performs validations and creates the
// context used by the bottle controller show action.
func NewShowBottleContext(ctx context.Context, r *http.Request, service *goa.Service) (*ShowBottleContext, error) {
	var err error
	resp := goa.ContextResponse(ctx)
	resp.Service = service
	req := goa.ContextRequest(ctx)
	req.Request = r
	rctx := ShowBottleContext{Context: ctx, ResponseData: resp, RequestData: req}
	paramBottleID := req.Params["bottleID"]
	if len(paramBottleID) > 0 {
		rawBottleID := paramBottleID[0]
		if bottleID, err2 := strconv.Atoi(rawBottleID); err2 == nil {
			rctx.BottleID = bottleID
		} else {
			err = goa.MergeErrors(err, goa.InvalidParamTypeError("bottleID", rawBottleID, "integer"))
		}
	}
	return &rctx, err
}

// OK sends a HTTP response with status code 200.
func (ctx *ShowBottleContext) OK(r *GoaExampleBottle) error {
	ctx.ResponseData.Header().Set("Content-Type", "application/vnd.goa.example.bottle+json")
	return ctx.ResponseData.Service.Send(ctx.Context, 200, r)
}

// NotFound sends a HTTP response with status code 404.
func (ctx *ShowBottleContext) NotFound() error {
	ctx.ResponseData.WriteHeader(404)
	return nil
}

```

OKとNotFoundのようにcontextがレスポンス結果の振る舞いを持つようになっている
コントローラを仮に実装して動かしてみる

```go:bottle.go
// Show implements the "show" action of the "bottles" controller.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
        if ctx.BottleID == 0 {
                // Emulate a missing record with ID 0
                return ctx.NotFound()
        }
        // Build the resource using the generated data structure
        bottle := app.GoaExampleBottle{
                ID:   ctx.BottleID,
                Name: fmt.Sprintf("Bottle #%d", ctx.BottleID),
                Href: app.BottleHref(ctx.BottleID),
        }

        // Let the generated code produce the HTTP response using the
        // media type described in the design (BottleMedia).
        return ctx.OK(&bottle)
}
```


__ビルドして実行__

```bash
$ go build -o cellar
$ ./celler
2017/08/20 00:10:26 [INFO] mount ctrl=Bottle action=Show route=GET /bottles/:bottleID
2017/08/20 00:10:26 [INFO] listen transport=http addr=:8080
・
・
・
```

__リクエストのテスト__

```bash
$ curl -i localhost:8080/bottles/1
HTTP/1.1 200 OK
Content-Type: application/vnd.goa.example.bottle+json
Date: Sat, 19 Aug 2017 16:55:47 GMT
Content-Length: 48

{"href":"/bottles/1","id":1,"name":"Bottle #1"}
```

__不正なID(非整数)のバリデーションテスト(goagenによって生成されている)__

```bash
$ curl -i localhost:8080/bottles/n
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.goa.error
Date: Sat, 19 Aug 2017 16:58:41 GMT
Content-Length: 194

{"id":"n2OmVVJH","code":"invalid_request","status":400,"detail":"invalid value \"n\" for parameter \"bottleID\", must be a integer","meta":{"expected":"integer","param":"bottleID","value":"n"}}
```

該当コード

```go:app/contexts.go
func NewShowBottleContext(ctx context.Context, r *http.Request, service *goa.Service) (*ShowBottleContext, error) {
	var err error
	resp := goa.ContextResponse(ctx)
	resp.Service = service
	req := goa.ContextRequest(ctx)
	req.Request = r
	rctx := ShowBottleContext{Context: ctx, ResponseData: resp, RequestData: req}
	paramBottleID := req.Params["bottleID"]
	if len(paramBottleID) > 0 {
		rawBottleID := paramBottleID[0]
		if bottleID, err2 := strconv.Atoi(rawBottleID); err2 == nil {
			rctx.BottleID = bottleID
		} else {
			err = goa.MergeErrors(err, goa.InvalidParamTypeError("bottleID", rawBottleID, "integer"))
		}
	}
	return &rctx, err
}
```

### curlの代わりに生成されているCLIツールを使う

サーバーは起動しておいてください

```bash
$ cd tool/cellar-cli
$ go build -o cellar-cli
$ ./cellar-cli
$ ./cellar-cli show bottle /bottles/1
2017/08/20 02:05:55 [INFO] started id=6Q+3ari3 GET=http://localhost:8080/bottles/1
2017/08/20 02:05:55 [INFO] completed id=6Q+3ari3 status=200 time=5.34273ms
{"href":"/bottles/1","id":1,"name":"Bottle #1"}
```

cliツールは該当APIのhelpにもアクセスできます

```bash
$ ./cellar-cli show bottle --help
Usage:
  cellar-cli show bottle ["/bottles/BOTTLEID"] [flags]

Flags:
      --bottleID int   Bottle ID
  -h, --help           help for bottle
      --pp             Pretty print response body

Global Flags:
      --dump               Dump HTTP request and response.
  -H, --host string        API hostname (default "localhost:8080")
  -s, --scheme string      Set the requests scheme
  -t, --timeout duration   Set the request timeout (default 20s)
```

### 感想

goa wayとでもいうのでしょうか

コードが自動生成されて、それを使うっていうのは現状抵抗ありますが
開発が早くなってドキュメントも残るのであれば慣れていきたいなーと思いました

もう少しAPIを増やして使いづらい点や良い点を見つけていければと

frameworkはいつ切り捨てられるかわからないので
なるべく薄くてコミッターが多いものを使っていきたいです