GitHub GraphQL v4 から Go のコードを自分で生成する
この記事は Go Advent Calendar 2022 24日目の記事です。
対象
- Goを既に理解していてGraphQLを少し触ったことある人、コード生成やAST好きな人、Linterなど作る人
注意事項
- 本記事はコード生成の完璧さや正しさは目指していません。コード生成のエントリーレベル相当と理解していただければ幸いです。
GitHub GraphQL に関する資料
パブリックスキーマ
https://docs.github.com/ja/graphql/overview/public-schema (直リンク: https://docs.github.com/public/schema.docs.graphql)
Parser
- go-graphql と gqlparser 2つありますが、今回は gqlparser/v2 を使います。
graphql ファイルの中身をチラ見
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 をチラ見
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 は以下のようになっています
作成したい Go の構造体は以下のイメージです。
色んな型を利用していて複雑のように見えますが、全ての情報が 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 が実際の型になります。
空欄をチェックして [] つければよいと思います。
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 を使います。
作成したい Go のコードは以下になります。
type MilestoneOrderField = string
const (
MilestoneOrderFieldCreatedAt MilestoneOrderField = "CREATED_AT"
MilestoneOrderFieldDueDate MilestoneOrderField = "DUE_DATE"
MilestoneOrderFieldNumber MilestoneOrderField = "NUMBER"
MilestoneOrderFieldUpdatedAt MilestoneOrderField = "UPDATED_AT"
)
AST はこちら
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 の構造体に変換します。
こちら 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 の標準の型は無差別に*
を付ける必要ないと思います。例
他に生成したファイルに
package entity
や
import "time"
など追加すれば実際使える形になるじゃないでしょうか。
これで GitHub GraphQL v4 の GraphQL を Go コードに生成完了になりますが、いかがでしょうか?
本当は fmt.Println ではなく、中間層や別の形でデータを持ってテンプレートエンジンに渡した方が拡張しやすくなりますが、今回は一番シンプルに直接生成する形で書かせていただきました。それでは、また!
Discussion