このエントリーをはてなブックマークに追加

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でした。