📑

GitHub GraphQL v4 から Go のコードを自分で生成する

2022/12/23に公開

この記事は Go Advent Calendar 2022 24日目の記事です。

対象

  • Goを既に理解していてGraphQLを少し触ったことある人、コード生成やAST好きな人、Linterなど作る人

注意事項

  • 本記事はコード生成の完璧さや正しさは目指していません。コード生成のエントリーレベル相当と理解していただければ幸いです。

GitHub GraphQL に関する資料

https://docs.github.com/ja/graphql

パブリックスキーマ

https://docs.github.com/ja/graphql/overview/public-schema (直リンク: https://docs.github.com/public/schema.docs.graphql)

Parser

graphql ファイルの中身をチラ見

image.png

gqlparser のパッケージを import

import (
	"github.com/vektah/gqlparser/v2"
	"github.com/vektah/gqlparser/v2/ast"
)

実際パースするコード

    // ...
	fileName := "schema.docs.graphql"
	// ファイルを読み取る
	content, err := os.ReadFile(fileName)
	if err != nil {
		panic(err)
	}

	source := &ast.Source{
		Name:  fileName,
		Input: string(content),
	}
	// パース
	schema, err := gqlparser.LoadSchema(source)
	if err != nil {
		panic(err)
	}
    // ...

AST をチラ見

image.png

image.png

GraphQL の 基本的な Type (Kind)

  • OBJECT (type Name { ... })
  • INPUT_OBJECT (input Name { ... })
  • ENUM (enum Name { ... })
  • INTERFACE (interface Name { ... })
  • UNION (union Name = Type1 | Type2 ...)

Type の定義は全て Types の中であるので、schema.Types をループするだけで読み取ることができます。

	for _, t := range schema.Types {
		fmt.Println(t.Name, t.Kind)
	}
/* Milestone で grep した場合
Milestone OBJECT
MilestoneItem UNION
MilestoneEdge OBJECT
MilestoneOrder INPUT_OBJECT
MilestoneOrderField ENUM
ProjectV2ItemFieldMilestoneValue OBJECT
MilestoneConnection OBJECT
MilestoneState ENUM
MilestonedEvent OBJECT
*/

Union の場合

Go に Union 型がないため interface として定義します。MilestoneItem を例として使います。

type MilestoneItem interface {
	MilestoneItem()
}

実装はこちら

	for _, t := range schema.Types {
		switch t.Kind {
		case "UNION":
			fmt.Println("type " + t.Name + " interface {")
			fmt.Println(t.Name + "()\n}")
		}
	}

Union の型であるMilestoneItem はGraphqlではこんな感じで定義されてます

union MilestoneItem = Issue | PullRequest

Go の場合以下になります。

func (i Issue) MilestoneItem()        {}
func (pr PullRequest) MilestoneItem() {}

以下のコードを加えれば生成可能になります。

	for _, tt := range t.Types {
		fmt.Println("func (i " + tt + ") " + t.Name + "() {}")
	}

これで GraphQL の Union 型のコード生成は完了です。

OBJECT

次は Milestone という OBJECT を生成したいと思います。
AST は以下のようになっています

image.png

作成したい Go の構造体は以下のイメージです。

image.png

色んな型を利用していて複雑のように見えますが、全ての情報が Fieldsの中にあるので、ループして生成するだけです。

まずは case 追加して struct を書き出します。

	case "OBJECT":
			fmt.Println("type " + t.Name + " struct {")
			//
			fmt.Println("}")

Name とType.NameType だけ書き出せばそれらしいもの作成できます。

	for _, f := range t.Fields {
		fmt.Println(f.Name + " " + f.Type.NamedType)
	}
type Milestone struct {
        closed Boolean
        closedAt DateTime
        createdAt DateTime
        creator Actor
        description String
        dueOn DateTime
        id ID
// ....
}

このままだと3つほど問題があります。

  • Nullable じゃないものは NonNull という属性がありますので、
    チェックして * つければよいのです。
		notNull := ""
		if !f.Type.NonNull {
			notNull = "*"
		}
		fmt.Println(f.Name + " " + notNull + f.Type.NamedType)

Boolean などは Go の型じゃないので変換する必要あります。
GraphQL の標準の型は String, Int, Float, Boolean, ID だけですので
以下のような型のmapping 作って変換できます。(DateTime は標準じゃないですが、よく使われるのでここで変換しています)

var baseTypes = map[string]string{
	"ID":      "string",
	"String":  "string",
	"Boolean": "bool",
	"Float":   "float64",
	"Int":     "int",
	"DateTime": "time.Time",
}
		namedType := f.Type.NamedType
		if v, ok := baseTypes[namedType]; ok {
			namedType = v
		}
		fmt.Println(f.Name + " " + notNull + namedType)

これでだいぶ Go らしくなりました。

type Milestone struct {
        closed bool
        closedAt *time.Time
        createdAt time.Time
        creator *Actor
        description *string
        dueOn *time.Time
        id string
        issues IssueConnection
        number int
        progressPercentage float64
// ...
}

あとは小文字を大文字に変更すればよいです。
いろんなライブラリありますが、以下のライブラリどれかを選択できます

	"github.com/iancoleman/strcase"
	"github.com/jinzhu/inflection"
	"github.com/kenshaw/snaker"

今回は snaker.ForceCamelIdentifier を使います。

fmt.Println("\t" + snaker.ForceCamelIdentifier(f.Name) + " " + notNull + namedType)
type Milestone struct {
        Closed bool
        ClosedAt *time.Time
        CreatedAt time.Time
        Creator *Actor
        Description *string
        DueOn *time.Time
        ID string
        Issues IssueConnection
        Number int
        ProgressPercentage float64
/// ...
}

上記の構造体にスライスの型含まれていないですが、スライスの場合 NamedType が空になり、Elem の中の NamedType が実際の型になります。

image.png

空欄をチェックして [] つければよいと思います。

	namedType := f.Type.NamedType
	if namedType == "" {
		arrayPrefix = "[]"
		namedType = f.Type.Elem.NamedType
	}
    // ...
	fmt.Println("\t" + snaker.ForceCamelIdentifier(f.Name) + " " + arrayPrefix + notNull + namedType)

もうこれで完成ですね。 json タグなどは好みによって Print すればよいと思います。

INPUT_OBJECT

INPUT_OBJECT は Object と基本同じ構造なため、case "OBJECT", "INPUT_OBJECT": 変更すれば、コード生成可能かと思います。

ENUM

次は ENUM に進めたいと思います。

MilestoneOrderField という ENUM を使います。

image.png

作成したい Go のコードは以下になります。

type MilestoneOrderField = string

const (
	MilestoneOrderFieldCreatedAt MilestoneOrderField = "CREATED_AT"
	MilestoneOrderFieldDueDate   MilestoneOrderField = "DUE_DATE"
	MilestoneOrderFieldNumber    MilestoneOrderField = "NUMBER"
	MilestoneOrderFieldUpdatedAt MilestoneOrderField = "UPDATED_AT"
)

AST はこちら
image.png

EnumValues をループすればよいのです。

    case "ENUM":
			fmt.Println("type " + t.Name + " = string")
			fmt.Println("const (")
			for _, e := range t.EnumValues {
				fmt.Println(t.Name + e.Name + " " + t.Name + " = \"" + e.Name + "\"")
			}
			fmt.Println(")")
type MilestoneOrderField = string
const (
        MilestoneOrderFieldCREATED_AT MilestoneOrderField = "CREATED_AT"
        MilestoneOrderFieldDUE_DATE MilestoneOrderField = "DUE_DATE"
        MilestoneOrderFieldNUMBER MilestoneOrderField = "NUMBER"
        MilestoneOrderFieldUPDATED_AT MilestoneOrderField = "UPDATED_AT"
)

Go 的に少し見苦しいところがあるのが分かります。
今回は snaker と strcase を合わせて CREATED_AT を CreatedAt に変更します。

snaker.ForceCamelIdentifier(strcase.ToSnake(e.Name))

※おそらく、一つのライブラリだけでも出来るかもしれませんが、どなたか知っている方がいれば教えていただけますと幸いです。

type MilestoneOrderField = string
const (
        MilestoneOrderFieldCreatedAt MilestoneOrderField = "CREATED_AT"
        MilestoneOrderFieldDueDate MilestoneOrderField = "DUE_DATE"
        MilestoneOrderFieldNumber MilestoneOrderField = "NUMBER"
        MilestoneOrderFieldUpdatedAt MilestoneOrderField = "UPDATED_AT"
)

これで思い通りのコードが出力されました。 Enum完成です。

INTERFACE

あとは INTERFACE ですが、GraphQLの INTERFACEは Goと違ってフィールを持つことが出来るので、Go の構造体に変換します。
image.png

こちら Go の対象のコードになりますが、

type Actor struct {
	AvatarURL    URI   `json:"avatarUrl"`
	Login        string `json:"login"`
	ResourcePath URI   `json:"resourcePath"`
	URL          URI   `json:"url"`
}

以下の変更するだけで、コード生成可能になります。

case "OBJECT", "INPUT_OBJECT", "INTERFACE":

SCALAR

他に SCALAR という GraphQL の Custom 型がありますが、
こちらは type Name = string でもよいし、生成せず別ファイルで手動で定義してもよいでしょう。

他の注意点

GraphQL の Object 型をポインターにするか、構造体にするかは使い方によりますが、個人的にポインターにしています。
notNull の有無に関わらず*を付ける形になります。time.Time や Go の標準の型は無差別に*を付ける必要ないと思います。例

image.png

他に生成したファイルに

package entity 

import "time" 

など追加すれば実際使える形になるじゃないでしょうか。

これで GitHub GraphQL v4 の GraphQL を Go コードに生成完了になりますが、いかがでしょうか?

本当は fmt.Println ではなく、中間層や別の形でデータを持ってテンプレートエンジンに渡した方が拡張しやすくなりますが、今回は一番シンプルに直接生成する形で書かせていただきました。それでは、また!

Discussion