interface / union を gqlgen で実装してみる
はじめに
gqlgen を使って union や interface を実装した事例に関する情報があまりなかったので紹介します。
union / interface は GraphQL における型の一種です。
gqlgen によって 生成される型
GraphQL スキーマ上で union や interface を使った定義をしたときにどういった型が生成されるか紹介します。
下記のような、id で商品 (product) を取得する Query を定義した場合を例に挙げます。
extend type Query {
product(id: ID!): Result!
}
union Result = Product | ErrorNotUnauthorized | ErrorUnauthenticated
interface Node {
id: ID!
}
"成功レスポンス"
type Product implements Node {
id: ID!
name: String!
price: Int!
}
interface Error {
code: String!
message: String!
}
"異常レスポンス - 認証エラー"
type ErrorNotUnauthorized implements Error {
code: String!
message: String!
}
"異常レスポンス - 認可エラー"
type ErrorUnauthenticated implements Error {
code: String!
message: String!
}
products
の返り値 Result
は Node
ErrorNotUnauthorized
ErrorUnauthenticated
の union で定義されています。これらの型はすべて interface を実装しています。成功レスポンスの場合は Node
interface を、異常レスポンスの場合は Error
interface を実装しています。
union
上記の例では Result
型が union で定義されていましたが、これを gqlgen で Go の型とすると下記のような interface として定義されます。
type Result interface {
IsResult()
}
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
}
func (Product) IsResult() {}
type ErrorNotUnauthorized struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (ErrorNotUnauthorized) IsResult() {}
type ErrorUnauthenticated struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (ErrorUnauthenticated) IsResult() {}
Result 型自体は IsResult()
というメソッドが実装されている interface として定義されています。union に含まれる Product
ErrorNotUnauthorized
ErrorUnauthenticated
の 3 つの型は何も処理がない IsResult()
メソッドが生えているので、すべて Result
interface を満たしています。
Go でこのような union の表現方法はあまり見たことがありませんでしたが、個人的には割とシンプルでわかりやすいと感じました。
resolver は下記のようになります。
func (r *queryResolver) Product(ctx context.Context, id string) (model.Result, error) {
if auth.IsAuthenticated() { // 認証済みかどうかを返す架空の関数
return model.ErrorUnauthenticated{
Code: http.StatusText(http.StatusUnauthorized),
Message: "認証してください",
}, nil
}
if auth.IsAuthorized() { // 取得権限があるかどうかを返す架空の関数
return model.ErrorNotUnauthorized{
Code: http.StatusText(http.StatusUnauthorized),
Message: "権限がありません",
}, nil
}
return model.Product{
ID: "hoge",
Name: "商品1",
Price: 1000,
}, nil
}
Product
メソッドの返り値が Result
なので Product
ErrorNotUnauthorized
ErrorUnauthenticated
いずれかの構造体を返却することができます。
interface
続いて interface です。
ここでは Error
interface とそれを実装する型を紹介します。
type Error interface {
IsError()
GetCode() string
GetMessage() string
}
type ErrorNotUnauthorized struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (ErrorNotUnauthorized) IsError() {}
func (this ErrorNotUnauthorized) GetCode() string { return this.Code }
func (this ErrorNotUnauthorized) GetMessage() string { return this.Message }
type ErrorUnauthenticated struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (ErrorUnauthenticated) IsError() {}
func (this ErrorUnauthenticated) GetCode() string { return this.Code }
func (this ErrorUnauthenticated) GetMessage() string { return this.Message }
union と同じように、interface を満たすか判別するための IsError()
メソッドが ErrorUnauthenticated
・ErrorNotUnauthorized
型それぞれに実装されています。また、Error
interface を満たす型が必ず持つフィールドである code
と message
を取得する GetCode()
GetMessage()
も実装されています。
ミドルウェア層などで、特定の interface に対して resolver をまたいで共通で処理を挟みたい場合などに何か使えそうな予感がします。
fyi.
gqlgen が用意してくれているミドルウェアについては下記スクラップで軽く触ってみています。
ユースケース
実務では Query / Mutation の返り値は共通の interface を実装した type の union で定義するやり方をとっています。このような方法を採用した背景としては、エラーの型を GraphQL スキーマとしてクライアントに提供したかったという背景があります。
経緯やコンテキスト等の詳細はこちらの記事に書いています。よろしければご参照ください。
Discussion