goaを書く時に知っておくとちょっと便利なこと¶
goa を書く時に知っておくと便利なことを記しておきます。
ちなみに文中の snip とは省略のことです。
Defaultの値を指定したい¶
Default を指定します。ただし、goa 1.2でないと効かないので注意してください。
Attribute("lang", String, "言語", func() {
Default("ja")
})
ちなみに、Defaultを指定するなら、指定なしを示すためのpointerにする必要はないだろう、という気もするのですが、普通にPointerになります。
ContentTypeを指定したい¶
goaは標準では "application/vnd.foo+json" などのように、MediaTypeで設定した文字列がContentTypeに使われます。 これを application/json というようにしたい場合は ContentType を明示的に指定します。
var Foo = MediaType("application/vnd.foo+json", func() {
ContentType("application/json")
Attributes(func() {
})
...
しかし、 CollectionOf を使っている場合だと、MediaTypeに指定しても効果がありません。そういう場合はこんな感じで Response の CollectionOf に指定します。
Response(OK, func() {
Media(CollectionOf(Foo, func() {
ContentType("application/json")
}))
})
Attributeを簡単に指定したい¶
同じExampleやValidationを何度も何度も書くのは大変です。一箇所で指定してしまいましょう。
var FooDSL = func() {
Description("FooFooFoo")
Example(100)
Minimum(10)
Maximum(4200)
}
(snip)
Attributes(func() {
Attribute("bar", Integer, FooDSL)
Attribute("boo", Integer, FooDSL)
Attribute("bee", Integer, FooDSL)
}
共通のHTTP Statusを定義したい¶
例えば、InternalServerErrorはすべてのActionで返す場合、個々のActionで指定するのではなく、Resource指定してしまえば、その配下のすべてのActionで指定する必要がなくなります。
var _ = Resource("foo", func() {
BasePath("/:FooID")
Response(InternalServerError) // Here
Action("index", func() {
( snip..... )
})
Action("search", func() {
Routing(GET("/search"))
( snip..... )
})
共通処理をくくり出したい¶
もっと汎用的な共通処理のくくり出し方として、 Trait があります。
まず最初に Trait で任意の名前を付けて定義します。ここでは Attribute を指定するMedia用のTraitと、 Response を指定するAction用のTraitを二つ定義しています。
var _ = API("TraitTest", func() {
Trait("MediaTrait", func() {
Attributes(func() {
Attribute("foo")
})
})
Trait("ActionTrait", func() {
Response(NotFound)
Response(BadRequest)
})
})
定義したTraitは UserTrait で使用します。
var TraitUser = MediaType("application/vnd.user+json", func() {
UseTrait("MediaTrait")
Attributes(func() {
Attribute("id", Integer)
})
View("default", func() {
Attribute("id")
Attribute("foo")
})
})
var _ = Resource("example", func() {
Action("index", func() {
Routing(GET("/"))
UseTrait("ActionTrait")
Response(OK)
})
})
これにより、 TraitUserというMediaでは、 foo が定義されていませんが使えるようになります。また、example Actionでは NotFound と BadRequest を定義していませんが、使えるようになります。
ただし、実際にはMediaTypeの方では Viewを指定していないので指定する必要はあります。Trait側でViewを指定するとすでに定義済み、と言われてしまいます。
なお、UseTraitは UseTrait("Trait1", "Trait2") と複数指定可能です。
複数のViewを定義する¶
同じMediaだけれども、場合によって違う情報を返したい、というときには default 以外の View を指定します。
例えば、以下は通常ユーザー用と管理者ユーザー用で異なる情報を返すようにしています。
var User = MediaType("application/vnd.user+json", func() {
Attributes(func() {
Attribute("id", Integer)
Attribute("super_secret", Integer)
})
View("default", func() {
Attribute("id")
})
View("admin", func() {
Attribute("id")
Attribute("super_secret")
})
})
こう定義すると、以下のように User の他に UserAdmin という型が定義されますので、Actionの実装中でどちらかを返せばよくなります。
// User media type (admin view)
//
// Identifier: application/vnd.user+json; view=admin
type UserAdmin struct {
ID *int `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"`
SuperSecret *int `form:"super_secret,omitempty" json:"super_secret,omitempty" xml:"super_secret,omitempty"`
}
// User media type (default view)
//
// Identifier: application/vnd.user+json; view=default
type User struct {
ID *int `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"`
}
返す時は、 OKAdmin と、それぞれのViewごとに専用のResponseを返す関数を使う必要がありますので、注意してください。
return ctx.OKAdmin(&app.UserAdmin{ID: 10, SuperSecret: 10})
Viewを再利用¶
せっかく作ったViewですので、さらに使いましょう。
var Foo = MediaType("application/vnd.foo+json", func() {
Attributes(func() {
Attribute("id", Integer)
Attribute("user", User) // UserはAttributeとして指定する
Link("user", "admin") // UserのAdminViewを定義
})
View("default", func() {
Attribute("id")
Attribute("links") // "links"という名前で呼び出す
})
})
こうしておくと、Foo MediaTypeに Links というフィールドが作成され、その中にUserAdminへのリンクが保持されます。
// Foo media type (default view)
//
// Identifier: application/vnd.foo+json; view=default
type Foo struct {
ID *int `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"`
// Links to related resources
Links *FooLinks `form:"links,omitempty" json:"links,omitempty" xml:"links,omitempty"`
}
// FooLinks contains links to related resources of Foo.
type FooLinks struct {
User *UserAdmin `form:"user,omitempty" json:"user,omitempty" xml:"user,omitempty"`
}
この例では一個だけですが、 Links を使うことで
Links(func() {
Link("user") // 指定なしだと `link` Viewを使う
Link("bar")
})
と、複数のMedia TypeからViewを集めることもできます。
一旦 FooLinks という型を通さないといけないのは、このLinksがあるせいかな、という気がします。
APIKeyによる認証制御を実装したい¶
APIKeySecurity を定義しResourceで Security を定義することで、HTTPヘッダによる認証を簡単に実装できます。 この例では X-Shared-Secret ヘッダでの認証となります。 /secure はヘッダが必要で、 /unsecure は NoSecurity() を指定しているのでヘッダが必要ありません。
var APIKey = APIKeySecurity("api_key", func() {
Header("X-Shared-Secret")
})
var _ = Resource("secure_resource", func() {
Security(APIKey)
Action("secure", func() {
Routing(GET("/secure"))
Response(OK)
})
Action("unsecure", func() {
Routing(GET("/unsecure"))
NoSecurity()
Response(OK)
})
})
こうしておいて、以下のようなミドルウェアを実装します。 SomethingSecureChecker はDB引いたりなんなりと、ご自身で実装してください。
// Error definition
var ErrUnauthorized = goa.NewErrorClass("unauthorized", 401)
func NewAPIKeyMiddleware() goa.Middleware {
scheme := app.NewAPIKeySecurity() // 使用するHeaderなどを得る
return func(h goa.Handler) goa.Handler {
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
key := req.Header.Get(scheme.Name) // 最初に得たschemeからkeyを得る
if SomethingSecureChecker(key) { // なにか
return ErrUnauthorized("auth failed") // 認証失敗
}
// 合格
return h(ctx, rw, req)
}
}
}
最後はmainでこのMiddlewareを使うように指定します。
app.UseAPIKeyMiddleware(service, NewAPIKeyMiddleware())
こんな感じで、簡単にAPI ヘッダでの認証を実装できます。同じような感じで
OAuth2
JWT
BasicAuth
による認証も実装できます。
まとめ¶
goaはDSLなので、こういう細々としたtipsを知っておく必要があります。そのため、最初敷居が高く、また、使い回しが効かない、という点はあります。
ただ、DSLといってもGo言語の形式(内部DSL)なので、gofmtも効きますし、補完も効きます。goのライブラリと組み合わせられるのはとても便利です。また、どんなDSLにも言えることですが、慣れると特になにも考えずに書けるようになります。
ということで、goaを書く時のちょっとしたtipsでした。
Comments
comments powered by Disqus