はじめに
Goaを使用したAPIを開発しており、今回Goaのバージョンをv1からv3にアップグレードしたのでログ的に情報を記録しておきます。
公式のドキュメントにはv1からv3への変更点がまとめられているので、読んで手を動かせばアップグレードできるはずですが、所々困った点や時間がかかったりしました。
誰かが記事にしていたりするかと思って調べてみるもののほとんど記事が見つからなかったので、ここに情報を残しておけば誰かの役に立つのではという気持ちで書いています。
想定する読者
普段Goをあまり触っておらず、API開発に取りあえずGoaを採用してみてv3出たからアップグレードしようかなという方を想定しています。
Goに詳しくて、Goaにも習熟しているという方はあまり迷うことなく対処できるような気がするのでこの記事はあまり参考にならないかもしれません。
変更内容
v1からv3への変更内容は公式ドキュメントの通りです。
コマンド
v1 | v3 |
---|---|
goagen |
goa gen |
アップグレード手順
基本的には上記のドキュメント通りなのですが、ファイルのアップロードやJWT認証の実装部分がv1のときと変わっていたりしたのでその辺も合わせて書いています。
また、公式でv1とv3それぞれにコード例が用意されているので参考になるはずです。
以下にアップグレードの情報を記録しています。
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を見ると良さそうです。
クエリパラメータ、パスパラメータ、ボディパラメータを切り替える
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は記事が少ないので増えてくるといいなという感じです。