A8-S

GraphQLライブラリ gqlgencを作成した

all
Short Session (20 min)
Ask the speakers

GraphQLのQueryからクライアントを自動生成するgqlgencを作成しました。本発表では、GraphQLクライアントに必要な構成要素、実装方法、ライブラリを解説します。これらを知ることでGraphQLのクライアントツールをGoで実装可能になります。既存のライブラリはJSONを取得する機能だけを持ち、開発者が構造体へのMarshalingやQueryに対応する型を定義する必要があります。この手法ではQueryの数に応じて実装コストが増加するため開発者へ負担が大きいです。この問題を解決するためにGraphQL Queryから構造体とクライアントを自動生成生成するライブラリを作成しました。


概要

GraphQLのQueryからクライアントを生成するライブラリgqlgencを作成しました。既存にもいくつかGraphQLのライブラリは存在していますが、開発者が構造体へのMarshalingや、Queryごとに対応する型を定義する必要があります。GraphQLは独自のQuery文法によって表現力が高いので、それでは開発者への負担が大きくなります。それを解決するためにQueryからレスポンスの構造体とクライアントを生成するライブラリを作成しました

問題提起

現在GraphQLのAPIクライアントで必要となる要素は以下のものが最低限あります

  • GraphQL Schema Parser : GraphQLスキーマを静的解析してASTツリーを作る
  • Introspection to Client Schema:サーバーからSchema情報を取得してASTツリーを作る
  • GraphQL Query Parser:GraphQLクエリーを静的解析してASTツリーを作る
  • GraphQL Query Response Model Generator:スキーマとクエリーのASTからGraphQLの型を生成する
  • GraphQL Client Generator:生成された型を利用してGraphQLのリクエストとレスポンスを受けるコードを生成する
  • GraphQL JSON Parser:GraphQL特有のJSONをパースして、生成された型にマッピングする

GoにもGraphQLのクライアントはいくつか存在しています。

しかし、これらはシンプルなクライアントとなっており、上記の条件を満たしているものではありません。そうなると、開発者への負担が大きくなってしまいます。実際クライアントを使ってみて、Queryを変更する度に構造体を手動で直さないといけなくなってしまい、二重管理が大変でしたし、Queryの数が多くなると、ミスやバグが目立つようになりました。

考察

そこで上記のリストに当てはまるものがGoで存在しないかをまず探しました。

gqlgenというGoのGraphQLサーバーを作成するライブラリ周りで、SchemaやModel周りはサーバーと共通なので、同じように扱うことができそうでしたし、gqlgenのプラグイン機構を利用すれば、SchemaとQueryのASTのツリーを取得することが可能であることがわかりました。しかし、クライアント向けに特に必要な機能がいくつか見当たりませんでしたので、これらは自前で用意する必要がありそうだと考えました。よって以下を実装する必要があります

  • Introspection to Client Schema

    IntrospectionはGraphQLのAPIにIntrospection Queryというものを投げるとClient用のSchema情報のjsonを返してくれるものです。GraphQLのClientではこれを元にエンドポイントのみがわかっているAPIからクライアントに必要な型を生成したり、Queryの型チェックに使用します。

  • GraphQL Query Response Generator

    Queryで定義したレスポンス型を生成するもので、Introspection to Client SchemaかShcemaファイルによって手にいれったAST ツリーと定義したQueryから生成する必要があります。

  • GraphQL Client Generator

    GraphQL Query ResponseとModelを使用して、GraphQLへのリクエストを投げるクライアントを実現します。

提案

以上の条件をみたした形 でgqlgenのpluginを拡張してcliとしてgqlgencを作成しました

先程の例のようなGraphQL APIに対してエンドポイントをhttps://hoge/graphqlとして、UserGetのQueryが定義されているファイル名をquery.graphqlとします。そこから生成するためのgqlgencの設定ファイル.gqlgenc.yamlというものを用意します。今回の場合は以下のような設定ファイルにします


model:
    filename: ./gen/models_gen.go
client:
    filename: ./gen/client.go
query:
    - ./query/*.graphql
endpoint:
  url: https://hoge/graphql

そしてこの時のファイル構成を以下のようにします。

├── .gqlgenc.yml
├── query
    └── query.graphql.

.gqlgenc.yamlのある層で以下のようにコマンドを叩きます


$ gqlgenc

すると./gen/models_gen.goと./gen/client.goの二種類が生成されます

gen/models_gen.go

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

package gen

type User struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

こちらのファイルはSchemaに定義されている型を全て生成してくれます。

gen/client.go

// Code generated by github.com/Yamashou/gqlgenc, DO NOT EDIT.

package gen

import (
	"context"
	"net/http"

	"github.com/Yamashou/gqlgenc/client"
)

type Client struct {
	Client *client.Client
}

func NewClient(cli *http.Client, baseURL string, options ...client.HTTPRequestOption) *Client {
	return &Client{Client: client.NewClient(cli, baseURL, options...)}
}

type Query struct {
	User User "json:\"user\" graphql:\"user\""
}
type GetUser struct {
	User struct {
		ID string "json:\"id\" graphql:\"id\""
	} "json:\"user\" graphql:\"user\""
}

const GetUserQuery = `query GetUser ($id: ID!) {
	user(id: $id) {
		id
	}
}
`

func (c *Client) GetUser(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*GetUser, error) {
	vars := map[string]interface{}{
		"id": id,
	}

	var res GetUser
	if err := c.Client.Post(ctx, "GetUser", GetUserQuery, &res, vars, httpRequestOptions...); err != nil {
		return nil, err
	}

	return &res, nil
}

こちらのファイルでは、query.graphqlで定義されていたものをクライアントとして生成したものです。

このように、開発者が簡単にGraphQLのAPIと繋ぎこむことができるクライアントとして実現しています。