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に指定しても効果がありません。そういう場合はこんな感じで ResponseCollectionOf に指定します。

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では NotFoundBadRequest を定義していませんが、使えるようになります。

ただし、実際には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 はヘッダが必要で、 /unsecureNoSecurity() を指定しているのでヘッダが必要ありません。

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