LoginSignup
4
2

More than 3 years have passed since last update.

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

Posted at

はじめに

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

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

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

想定する読者

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

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

変更内容

v1からv3への変更内容は公式ドキュメントの通りです。

コマンド

v1 v3
goagen goa gen

アップグレード手順

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

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

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

DSLを変更する

DSLに関しては公式ドキュメントに書かれている通りですね。

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

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
- 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のリンクを張っています)

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

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

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

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

公式のコード例では

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

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
}

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

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)
}

この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
}

各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
}

最後に

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2