はじめてのgoa

#`goa`について
`Go`を使ってREST APIを用意したいときに利用できるマイクロサービス用のフレームワークの一つです。
ビジネスロジック中心で開発ができる、`swagger`のドキュメントが自動生成されるなどの利点があります。

`goa`のGitHubは[こちら](https://github.com/goadesign/goa)にあります。

#サンプルについて
今回用意したサンプルは[こちら](https://github.com/hiroykam/goa-sample)です。

#環境設定等
##`mercurial`のインストール
依存関係の解決にあたり、`dep`を使ったが、`Mac`で`dep ensure`をすると、なぜかフリーズしたので、`pstree`で確認すると下記の箇所で固まっていました。

```shell
$ pstree 69752
-+= 69752 xxx dep ensure -v
 \-+= 71086 xxx /Applications/Xcode.app/Contents/Developer/usr/bin/git ls-remote ssh://git@bitbucket.org/pkg/inflect
   \--- 71087 xxx /usr/bin/ssh git@bitbucket.org git-upload-pack '/pkg/inflect'
```

調べたところ、`mercurial`が必要とのことだったので、`brew`を実行してインストールしました。

`brew install mercurial`

##`goa`と`dep`のインストール
`goa`と`dep`を`go get`する。


```shell
$ go get -u github.com/goadesign/goa/...
$ go get -u github.com/golang/dep/cmd/dep
```

#`goa`を使ったREST API設計
##API、Resource、MediaTypeの定義
###`design`の定義
1つのgoファイルで集約しても問題ないのですが、便宜上複数のファイルの分割しました。

APIのベースとなる基本的な定義です。

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

import (
	. "github.com/goadesign/goa/design/apidsl"
)

var _ = API("goa-sample", func() {
	Title("The Sample API")
	Description("A simple goa service")
	Version("v1")
	Scheme("http", "https")
	BasePath("/api/v1")
	Consumes("application/json")
	Produces("application/json")
	Host("localhost:8080")

	Origin("http://localhost:8080/swagger", func() {
		Expose("X-Time")
		Methods("GET", "POST", "PUT", "PATCH", "DELETE")
		MaxAge(600)
		Credentials()
	})
})
```

`swagger`の定義です。
この例では、`http://localhost:8080/swagger-ui`にアクセスすると、`swaggerのドキュメント`を閲覧できます。
なお、[swagger-ui](https://github.com/swagger-api/swagger-ui/tree/master/dist)をダウンロードし、`public/swagger-ui/dist`に配置する必要があります。

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

import (
	. "github.com/goadesign/goa/design/apidsl"
)

// Swagger routing
var _ = Resource("swagger", func() {
	Origin("*", func() {
		Methods("GET")
	})
	Files("/swagger.json", "swagger/swagger.json")
	Files("/swagger.yaml", "swagger/swagger.yaml")
	Files("/swagger-ui/*filepath", "public/swagger-ui/dist")
})
```

続いて、レスポンスデータの型を定義します。

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

import (
	"time"

	. "github.com/goadesign/goa/design"
	. "github.com/goadesign/goa/design/apidsl"

)

var MediaSamples = MediaType("application/vnd.samples+json", func() {
	Description("sample list")
	Attribute("id", Integer, "id", func() {
		Example(1)
	})
	Attribute("name", String, "名前", func() {
		Example("サンプル1")
	})
	Attribute("created_at", DateTime, "作成日", func() {
		loc, _ := time.LoadLocation("Asia/Tokyo")
		Example(time.Date(2019, 01, 31, 0, 0, 0, 0, loc).Format(time.RFC3339))
	})
	Attribute("updated_at", DateTime, "更新日", func() {
		loc, _ := time.LoadLocation("Asia/Tokyo")
		Example(time.Date(2019, 01, 31, 12, 30, 50, 0, loc).Format(time.RFC3339))
	})
	Required("id", "name", "created_at", "updated_at")
	View("default", func() {
		Attribute("id")
		Attribute("name")
		Attribute("created_at")
		Attribute("updated_at")
	})
})

var MediaSample = MediaType("application/vnd.sample+json", func() {
	Description("sample detail")
	Attribute("id", Integer, "sample id", func() {
		Example(1)
	})
	Attribute("user_id", Integer, "user id", func() {
		Example(1)
	})
	Attribute("name", String, "名前", func() {
		Example("サンプル1")
	})
	Attribute("detail", String, "詳細", func() {
		Example("サンプル1の詳細")
	})
	Attribute("created_at", DateTime, "作成日", func() {
		loc, _ := time.LoadLocation("Asia/Tokyo")
		Example(time.Date(2019, 01, 31, 0, 0, 0, 0, loc).Format(time.RFC3339))
	})
	Attribute("updated_at", DateTime, "更新日", func() {
		loc, _ := time.LoadLocation("Asia/Tokyo")
		Example(time.Date(2019, 01, 31, 12, 30, 50, 0, loc).Format(time.RFC3339))
	})
	Required("id", "user_id", "name", "detail", "created_at", "updated_at")
	View("default", func() {
		Attribute("id")
		Attribute("user_id")
		Attribute("name")
		Attribute("detail")
		Attribute("created_at")
		Attribute("updated_at")
	})
})
```

最後にAPIの引数とレスポンスの内容を記載します。

```go
package design

import (
	. "github.com/goadesign/goa/design"
	. "github.com/goadesign/goa/design/apidsl"
)

var _ = Resource("samples", func() {
	BasePath("/samples")
	Action("list", func() {
		Description("複数")
		Routing(
			GET("/"),
		)
		Params(func() {
			Param("user_id", Integer, "user id", func() {
				Example(1)
			})
		})
		Response(OK, CollectionOf(MediaSamples))
		Response(NotFound)
		Response(BadRequest, ErrorMedia)
	})
	Action("show", func() {
		Description("単数")
		Routing(
			GET("/:id"),
		)
		Params(func() {
			Param("user_id", Integer, "user id", func() {
				Example(1)
			})
			Param("id", Integer, "sample data id", func() {
				Example(123)
			})
		})
		Response(OK, CollectionOf(MediaSample))
		Response(NotFound)
		Response(Unauthorized)
		Response(BadRequest, ErrorMedia)
	})
	Action("add", func() {
		Description("追加")
		Routing(
			POST("/"),
		)
		Payload(func() {
			Attribute("user_id", String, "user id", func() {
				Example("12345")
			})
			Attribute("name", String, "name of sample", func() {
				Example("sample1")
			})
			Attribute("detail", String, "detail of sample", func() {
				Example("sample1の詳細")
			})
			Required("user_id", "name", "detail")
		})
		Response(OK, CollectionOf(MediaSample))
		Response(NotFound)
		Response(Unauthorized)
		Response(BadRequest, ErrorMedia)
	})
	Action("delete", func() {
		Description("削除")
		Routing(
			DELETE("/:id"),
		)
		Params(func() {
			Param("id", Integer, "sample id", func() {
				Example(1)
			})
		})
		Response(NoContent)
		Response(NotFound)
		Response(Unauthorized)
		Response(BadRequest, ErrorMedia)
	})
	Action("update", func() {
		Description("更新")
		Routing(
			PUT("/:id"),
		)
		Params(func() {
			Param("id", Integer, "sample id")
		})
		Payload(func() {
			Param("name", String, "name of sample", func() {
				Example("sample1")
			})
			Param("detail", String, "detail of sample", func() {
				Example("sample1")
			})
			Required("name", "detail")
		})
		Response(NoContent)
		Response(NotFound)
		Response(BadRequest, ErrorMedia)
	})
})
```

###`goagen`の実行
下記コマンドを実行すると諸々ファイルが自動生成されます。

```shell
$ goagen bootstrap -d github.com/hiroykam/goa-sample/design
```

`sample.go`と`swagger.go`を`./controller`に移動させます。

##`Model`の定義
###自動生成する場合
まずは、`./design/models.go`を用意します。
`id`を`string`にしたい場合、`gorma.UUID`とします。
`deleted_at`を指定すると、`gorm`で`delete`を実行した際、論理削除になります。


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

import (
	"github.com/goadesign/gorma"
	. "github.com/goadesign/gorma/dsl"
)

var _ = StorageGroup("goa-sample", func() {
	Description("Sample Model")
	Store("MySQL", gorma.MySQL, func() {
		Description("MySQL models")
		Model("Sample", func() {
			RendersTo(MediaSample)
			Description("sample table")
			Field("id", gorma.Integer)
			Field("user_id", gorma.Integer)
			Field("name", gorma.String)
			Field("detail", gorma.String)
			Field("created_at", gorma.Timestamp)
			Field("updated_at", gorma.Timestamp)
			Field("deleted_at", gorma.NullableTimestamp)
		})
	})
})
```

```shell
$ goagen --design=github.com/hiroykam/goa-sample/design gen --pkg-path=github.com/goadesign/gorma

models/sample.go
models/sample_helper.go
```

実行すると下記ファイルが生成されますが、上位層にエラーをそのまま伝搬している、この[サンプル](https://github.com/hiroykam/goa-sample)の例ですと`user_id`で絞り込む`Get`と`List`を別に用意する必要があるなどの手間は発生します。
またmodelを追加し、上記コマンドを実行しますと`sample.go`が新規に作成されてしまいますので、`./model/sample_extention.go`といったように別ファイルで定義する必要があります。

```go:models/sample.go
// Code generated by goagen v1.3.1, DO NOT EDIT.
//
// API "goa-sample": Models
//
// Command:
// $ goagen
// --design=github.com/hiroykam/goa-sample/design
// --out=$(GOPATH)/src/github.com/hiroykam/goa-sample
// --version=v1.3.1

package models

import (
	"context"
	"github.com/goadesign/goa"
	"github.com/hiroykam/goa-sample/app"
	"github.com/jinzhu/gorm"
	"time"
)

// sample table
type Sample struct {
	ID        int `gorm:"primary_key"`
	Detail    string
	Name      string
	UserID    int
	CreatedAt time.Time  // timestamp
	DeletedAt *time.Time // nullable timestamp (soft delete)
	UpdatedAt time.Time  // timestamp
}

// TableName overrides the table name settings in Gorm to force a specific table name
// in the database.
func (m Sample) TableName() string {
	return "samples"

}

// SampleDB is the implementation of the storage interface for
// Sample.
type SampleDB struct {
	Db *gorm.DB
}

// NewSampleDB creates a new storage type.
func NewSampleDB(db *gorm.DB) *SampleDB {
	return &SampleDB{Db: db}
}

// DB returns the underlying database.
func (m *SampleDB) DB() interface{} {
	return m.Db
}

// SampleStorage represents the storage interface.
type SampleStorage interface {
	DB() interface{}
	List(ctx context.Context) ([]*Sample, error)
	Get(ctx context.Context) (*Sample, error)
	Add(ctx context.Context, sample *Sample) error
	Update(ctx context.Context, sample *Sample) error
	Delete(ctx context.Context) error

	ListSample(ctx context.Context) []*app.Sample
	OneSample(ctx context.Context) (*app.Sample, error)
}

// TableName overrides the table name settings in Gorm to force a specific table name
// in the database.
func (m *SampleDB) TableName() string {
	return "samples"

}

// CRUD Functions

// Get returns a single Sample as a Database Model
// This is more for use internally, and probably not what you want in  your controllers
func (m *SampleDB) Get(ctx context.Context) (*Sample, error) {
	defer goa.MeasureSince([]string{"goa", "db", "sample", "get"}, time.Now())

	var native Sample
	err := m.Db.Table(m.TableName()).Where("").Find(&native).Error
	if err == gorm.ErrRecordNotFound {
		return nil, err
	}

	return &native, err
}

// List returns an array of Sample
func (m *SampleDB) List(ctx context.Context) ([]*Sample, error) {
	defer goa.MeasureSince([]string{"goa", "db", "sample", "list"}, time.Now())

	var objs []*Sample
	err := m.Db.Table(m.TableName()).Find(&objs).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		return nil, err
	}

	return objs, nil
}

// Add creates a new record.
func (m *SampleDB) Add(ctx context.Context, model *Sample) error {
	defer goa.MeasureSince([]string{"goa", "db", "sample", "add"}, time.Now())

	err := m.Db.Create(model).Error
	if err != nil {
		goa.LogError(ctx, "error adding Sample", "error", err.Error())
		return err
	}

	return nil
}

// Update modifies a single record.
func (m *SampleDB) Update(ctx context.Context, model *Sample) error {
	defer goa.MeasureSince([]string{"goa", "db", "sample", "update"}, time.Now())

	obj, err := m.Get(ctx)
	if err != nil {
		goa.LogError(ctx, "error updating Sample", "error", err.Error())
		return err
	}
	err = m.Db.Model(obj).Updates(model).Error

	return err
}

// Delete removes a single record.
func (m *SampleDB) Delete(ctx context.Context) error {
	defer goa.MeasureSince([]string{"goa", "db", "sample", "delete"}, time.Now())

	var obj Sample
	err := m.Db.Delete(&obj).Where("").Error

	if err != nil {
		goa.LogError(ctx, "error deleting Sample", "error", err.Error())
		return err
	}

	return nil
}
```

```go:models/sample_helper.go
// Code generated by goagen v1.3.1, DO NOT EDIT.
//
// API "goa-sample": Model Helpers
//
// Command:
// $ goagen
// --design=github.com/hiroykam/goa-sample/design
// --out=$(GOPATH)/src/github.com/hiroykam/goa-sample
// --version=v1.3.1

package models

import (
	"context"
	"github.com/goadesign/goa"
	"github.com/hiroykam/goa-sample/app"
	"github.com/jinzhu/gorm"
	"time"
)

// MediaType Retrieval Functions

// ListSample returns an array of view: default.
func (m *SampleDB) ListSample(ctx context.Context) []*app.Sample {
	defer goa.MeasureSince([]string{"goa", "db", "sample", "listsample"}, time.Now())

	var native []*Sample
	var objs []*app.Sample
	err := m.Db.Scopes().Table(m.TableName()).Find(&native).Error

	if err != nil {
		goa.LogError(ctx, "error listing Sample", "error", err.Error())
		return objs
	}

	for _, t := range native {
		objs = append(objs, t.SampleToSample())
	}

	return objs
}

// SampleToSample loads a Sample and builds the default view of media type Sample.
func (m *Sample) SampleToSample() *app.Sample {
	sample := &app.Sample{}
	sample.CreatedAt = m.CreatedAt
	sample.Detail = m.Detail
	sample.ID = m.ID
	sample.Name = m.Name
	sample.UpdatedAt = m.UpdatedAt
	sample.UserID = m.UserID

	return sample
}

// OneSample loads a Sample and builds the default view of media type Sample.
func (m *SampleDB) OneSample(ctx context.Context) (*app.Sample, error) {
	defer goa.MeasureSince([]string{"goa", "db", "sample", "onesample"}, time.Now())

	var native Sample
	err := m.Db.Scopes().Table(m.TableName()).Where("").Find(&native).Error

	if err != nil && err != gorm.ErrRecordNotFound {
		goa.LogError(ctx, "error getting Sample", "error", err.Error())
		return nil, err
	}

	view := *native.SampleToSample()
	return &view, err
}
```

###手動で頑張る場合
[サンプル](https://github.com/hiroykam/goa-sample)では`models`と`service`を用意しました。
`di`も用意すべきかもしれません。

##Controllerの実装
下記コマンドで雛形が作成されます。
コマンド再実行により、ここに追記したコードは削除されません。

```shell
$ goagen controller -d github.com/hiroykam/goa-sample/design --out=./controller
```

##Resource、MediaTypeの変更
Resource、MediaTypeを変更した際、下記を実行します。

```shell
$ goagen app -d github.com/hiroykam/goa-sample/design
$ goagen swagger -d github.com/hiroykam/goa-sample/design
$ goagen controller -d github.com/hiroykam/goa-sample/design --out=./controller
```

##`goa`の起動
###`make`
[サンプル](https://github.com/hiroykam/goa-sample)で下記のコマンドで、`docker-compose build`します。

```
$ make docker-build
```

ビルド完了後、下記のコマンドで起動できます。

```
$ make docker-up
```

###`swagger-ui`
起動後、`http://localhost:8080/swagger-ui/`にアクセスすると下記が表示されます。

<img width="869" alt="スクリーンショット 2019-02-03 15.06.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/8ab67197-e5e9-1269-0e02-5747940bf0da.png">

###`REST API`
`Postman`を使いました。

<img width="584" alt="スクリーンショット 2019-02-03 18.04.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/fe0fb8b1-33b1-77dc-c4e2-d24cf110ab25.png">

#その他
##`goagen`に失敗する
`goagen`を実行すると、下記のようなエラーが発生する時があります。

```shell
exit status 1
missing API definition, make sure design is properly initialized
make: *** [_controller] Error 1
```

`github`の`issue`で散見されていましたが、簡単な解決方法は`vendor`ディレクトリを一旦削除すると解決しました。

##`dep`を使っている場合、`goagen`に失敗する
issueを拝見する限りですと、`dep`の問題といった記載があり、`Gopkg.toml`に下記を追記すると解決できる。
`dep`を使わないほうが良いのでしょうか・・・

```shell
[[override]]
  name = "github.com/satori/go.uuid"
  revision = "master"
```

##エラーの内容が分かりづらい
例えば、`./design/media_types.go` で下記のような属性を定義します。

```go
	Attribute("created_at", DateTime, "作成日", func() {
		Example("2019-01-01")
	})

```

そこで、`goagen`しても、下記のようなエラーが表示されますが、内容から`RFC3339`フォーマットに準拠していないのが原因であることが分かりませんでした。

```shell
$ goagen app -d github.com/hiroykam/goa-sample/design
exit status 1
[design/media_types.go:24] example value "2019-01-01" is incompatible with attribute of type string
```

##log出力が扱いづらい
`main.go` でこのように`stodout`に`JSON`形式のログ出力をしたかったが、`reqest ID`などの情報まで出力することができませんでした。
また、`goa`内部で出力するログは別の`logger`が使われているため、思うようなログ出力が期待できませんでした。

```go
	logger := log15.New(log15.Ctx{"module": "goa-sample"})
	logger.SetHandler(log15.StreamHandler(os.Stdout, log15.JsonFormat()))
	service.WithLogger(goalog15.New(logger))
```

そのため、[サンプル](https://github.com/hiroykam/goa-sample)では`json`出力する`logger`を別途用意しました。

##trailing slashの扱いについて
URLの末尾に`/`ありとなしでAPIを実行すると、意図していないAPIが呼び出されるケースがありました（調査中）。

#参考
[こちら](https://tikasan.hatenablog.com/entry/2017/05/07/213258)の記事をベースに勉強をさせていただきました。


#今後
- テストコードを書く
- CircleCIなどの環境を整える
- jwtで認証
- Makefile、READMEの修正・更新
- React/ReduxからAPIを呼び出す
- `gorm.ErrRecordNotFound`が検出できない原因調査