Goaをv1からv3にアップグレードしてみた

## はじめに
Goaを使用したAPIを開発しており、今回Goaのバージョンをv1からv3にアップグレードしたのでログ的に情報を記録しておきます。

公式のドキュメントにはv1からv3への変更点がまとめられているので、読んで手を動かせばアップグレードできるはずですが、所々困った点や時間がかかったりしました。

誰かが記事にしていたりするかと思って調べてみるもののほとんど記事が見つからなかったので、ここに情報を残しておけば誰かの役に立つのではという気持ちで書いています。

## 想定する読者

普段Goをあまり触っておらず、API開発に取りあえずGoaを採用してみてv3出たからアップグレードしようかなという方を想定しています。

Goに詳しくて、Goaにも習熟しているという方はあまり迷うことなく対処できるような気がするのでこの記事はあまり参考にならないかもしれません。


## 変更内容

v1からv3への変更内容は[公式ドキュメント](https://goa.design/ja/learn/upgrading/)の通りです。

### コマンド

|  v1  |  v3  |
| ---- | ---- |
|  `goagen`  |  `goa gen`  |

## アップグレード手順

基本的には上記のドキュメント通りなのですが、ファイルのアップロードやJWT認証の実装部分がv1のときと変わっていたりしたのでその辺も合わせて書いています。

また、公式でv1とv3それぞれにコード例が用意されているので参考になるはずです。
- https://github.com/goadesign/examples
- https://github.com/goadesign/examples/tree/v1

以下にアップグレードの情報を記録しています。

### DSLを変更する

DSLに関しては[公式ドキュメント](https://goa.design/ja/learn/upgrading/)に書かれている通りですね。

いくつか抜粋してみます。

|  v1  |  v3  |
| ---- | ---- |
|  MediaType  |  ResultType  |
|  Resource  |  Service  |
|  Action  |  Method  |
|  Routing  |  HTTP  |
|  BasePath  |  Path  |
|  DefaultMedia  |  Result  |
|  MultipartForm  | MultipartRequest  |
|  DateTime  |  String  |
|  Integer  |  Int  |
|  Number  |  Float64  |
|  File  |  Bytes  |
|  OK  |  StatusOK  |
|  NotFound  |  StatusNotFound  |

単純に置換するだけだったり、周辺の記述を変えたりするわけですがGoDocを見ると記述例が書かれているので参考になるはずです。

詳しくはv1とv3のGoDocを見ると良さそうです。
- [gopkg.in/goadesign/goa.v1/design/apidsl](https://godoc.org/gopkg.in/goadesign/goa.v1/design/apidsl)
- [gopkg.in/goadesign/goa.v3/dsl](https://godoc.org/gopkg.in/goadesign/goa.v3/dsl)


### クエリパラメータ、パスパラメータ、ボディパラメータを切り替える

v1では

>An action payload describes the HTTP request body data structure

とあるように、`Payload`はボディパラメータとして扱われていましたが、v3では

>Payload defines the data type of an method input

というように`Payload`はインプットとして扱われるようになったようです。

それに伴い、以下のような使い分けになった感じです。

#### クエリパラメータ

v3では以下のように記述することで`accountID`がクエリパラメータとして扱われるようになります。

```
		Payload(func() {
			Attribute("accountID", Int, "Account ID")
		})
		HTTP(func() {
			GET("")
			Params(func() {
				 Param("accountID:accountID")
			})
		})
```

#### パスパラメータ

v3では以下のように記述することで`accountID`がパスパラメータとして扱われるようになります。

```
		Payload(func() {
			Attribute("accountID", Int, "Account ID")
			Attribute("name", String, "Account name")
			Required("name")
		})
		HTTP(func() {
			PUT("/{accountID}")
		})
```

#### ボディパラメータ

v3では以下のように記述することで`name`がボディパラメータとして扱われるようになります。
`accountID`はパスパラメータとして処理されるので、Payloadで定義された`name`がボディパラメータとして扱われるようです。


```
		Payload(func() {
			Attribute("accountID", Int, "Account ID")
			Attribute("name", String, "Account name")
			Required("name")
		})
		HTTP(func() {
			PUT("/{accountID}")
		})
```

それぞれのパラメータについてはこちらが参考になります。
（この記事を書いている時点でバージョン2を生成するようなので、バージョン2のリンクを張っています）

https://swagger.io/docs/specification/2-0/describing-parameters/

### ファイルをアップロードする

v1では`File`型が用意されていましたが、v3では`File`型がなくなり代わりに`Bytes`型が用意されています。

ファイルアップロードの際はこの`Bytes`型を扱い、デコード処理することになるようです。

`goa example`でサンプルコード生成すると`multipart.go`が用意されるので、そこで実装すれば良さそうです。

公式のコード例では

https://github.com/goadesign/examples/blob/master/multipart/multipart.go#L16-L35

この辺りだと思いますが、ファイルを扱うのであれば例えば以下のような感じにしておけば良さそうです。
（以下のコードはあくまで参考例なので、動作するかどうかは微妙です）

```
func ResumeAddDecoderFunc(mr *multipart.Reader, **resume.Resume) error {
  var r server.ResumeRequestBody
  for {
    part, err := mr.NextPart()
    if err == io.EOF {
      break
    }
    if err != nil {
      return fmt.Errorf("failed to load part: %s", err)
    }
    if part.FileName() != "" {
      file, err := ioutil.ReadAll(part)
      if err != nil {
        return fmt.Errorf("failed to load file: %s", err)
      }
      r.File = file
      continue
    }
  }
  *p = &r
  return nil
}
```

詳しくはこの辺が参考になりそうです。

https://goa.design/ja/implement/encoding/

### JWT認証を実装する

JWT認証に関しては、interfaceが用意されています。

```
type Auther interface {
  // BasicAuth implements the authorization logic for the Basic security scheme.
  BasicAuth(ctx context.Context, user, pass string, schema *security.BasicScheme) (context.Context, error)
  // JWTAuth implements the authorization logic for the JWT security scheme.
  JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, error)
  // APIKeyAuth implements the authorization logic for the APIKey security scheme.
  APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error)
  // OAuth2Auth implements the authorization logic for the OAuth2 security scheme.
  OAuth2Auth(ctx context.Context, token string, schema *security.OAuth2Scheme) (context.Context, error)
}
```

https://github.com/goadesign/examples/blob/master/security/gen/secured_service/service.go#L33-L42

このinterfaceを満たすように処理を実装してやれば良さそうです。

公式のコード例での実装は以下のような形です。

```
// JWTAuth implements the authorization logic for service "secured_service" for
// the "jwt" security scheme.
func (s *securedServiceSvc) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) {
  claims := make(jwt.MapClaims)

  // authorize request
  // 1. parse JWT token, token key is hardcoded to "secret" in this example
  _, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { return Key, nil })
  if err != nil {
    return ctx, ErrInvalidToken
  }

  // 2. validate provided "scopes" claim
  if claims["scopes"] == nil {
    return ctx, ErrInvalidTokenScopes
  }
  scopes, ok := claims["scopes"].([]interface{})
  if !ok {
    return ctx, ErrInvalidTokenScopes
  }
  scopesInToken := make([]string, len(scopes))
  for _, scp := range scopes {
    scopesInToken = append(scopesInToken, scp.(string))
  }
  if err := scheme.Validate(scopesInToken); err != nil {
    return ctx, securedservice.InvalidScopes(err.Error())
  }
  return ctx, nil
}
```

https://github.com/goadesign/examples/blob/master/security/auth.go#L39-L67

各serviceのhandlerでは特に認証を気にすることがなくなったのでアプリケーションの処理に集中できるようになったという感じでしょうか。

```
// This action is secured with the jwt scheme
func (s *securedServiceSvc) Secure(ctx context.Context, p *securedservice.SecurePayload) (res string, err error) {
  res = fmt.Sprintf("User authorized using JWT token %q", p.Token)
  s.logger.Printf(res)
  if p.Fail != nil && *p.Fail {
    s.logger.Printf("Uh oh! `fail` passed in parameter. Auth failed!")
    return "", securedservice.Unauthorized("forced authentication failure")
  }
  return
}
```

https://github.com/goadesign/examples/blob/master/security/secured_service.go#L48-L57

## 最後に
この記事ではGoaをv1からv3にアップグレードする手順を書きました。
だいぶ雰囲気で書いている部分があるので、ご指摘あればみなさまでご議論いただければという感じです。
API開発にしばらくGoaを使ってみようという感じなので、またGoa関連の記事を書くかもしれません。
Goaは記事が少ないので増えてくるといいなという感じです。