🥺

gqlgenで生成される構造体にカスタムディレクティブで指定した任意のタグを設定する

2021/10/16に公開

こんなスキーマファイルから

directive @tag(
    validate: String
) on INPUT_FIELD_DEFINITION

input NewUser {
  email: String! @tag(validate: "required,email")
  password: String! @tag(validate: "required,max=50")
  name: String! @tag(validate: "required,max=50")
}

こんなモデルを作りたい。

type NewUser struct {
	Email    string `json:"email" validate:"required,email"`
	Password string `json:"password" validate:"required,max=50"`
	Name     string `json:"name" validate:"required,max=50"`
}

modelgen.PluginのMutateHookを使用してタグを設定するレシピが載っているのでこれをベースにカスタマイズしていく。
https://gqlgen.com/recipes/modelgen-hook/

元のコードではモデルのフィールド情報しか参照出来ないので、スキーマファイルで定義した情報を取得してくる必要がある

fd *ast.FieldDefinition = cfg.Schema.Types[model.Name].Fields[i]

これが取得できれば、フィールド定義からディレクティブを取得してレシピと同じように field.Tag に追記すればいい。

//go:build ignore

package main

import (
	"fmt"
	"os"

	"github.com/99designs/gqlgen/api"
	"github.com/99designs/gqlgen/codegen/config"
	"github.com/99designs/gqlgen/plugin"
	"github.com/99designs/gqlgen/plugin/modelgen"
	"github.com/vektah/gqlparser/v2/ast"
)

func fieldHook(f *modelgen.Field, fd *ast.FieldDefinition) {
	// @tagディレクティブ
	directive := fd.Directives.ForName("tag")
	if directive != nil {
		// validateタグを追加
		validateTag := directive.Arguments.ForName("validate")
		if validateTag != nil {
			f.Tag += fmt.Sprintf(` validate:"%s"`, validateTag.Value.Raw)
		}
	}
}

func main() {
	cfg, err := config.LoadConfigFromDefaultLocations()
	if err != nil {
		fmt.Fprintln(os.Stderr, "failed to load config", err.Error())
		os.Exit(2)
	}

	// Attaching the mutation function onto modelgen plugin
	p := modelgen.Plugin{
		MutateHook: func(b *modelgen.ModelBuild) *modelgen.ModelBuild {
			for _, model := range b.Models {
				for i, field := range model.Fields {
					fieldHook(field, cfg.Schema.Types[model.Name].Fields[i])
				}
			}

			return b
		},
	}

	// ディレクティブとしては使用しないのでコードに出力されないように設定
	// https://github.com/99designs/gqlgen/blob/v0.14.0/codegen/config/config.go#L231
	cfg.Directives["tag"] = config.DirectiveConfig{
		SkipRuntime: true,
	}

	err = api.Generate(cfg,
		func(cfg *config.Config, plugins *[]plugin.Plugin) {
			for i, plugin := range *plugins {
				if _, ok := plugin.(*modelgen.Plugin); ok {
					// modelgen.Pluginを置き換える
					(*plugins)[i] = &p
				}
			}
		},
	)
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(3)
	}
}

これを適当な場所において実行すればタグが設定されたモデルが生成される。

go run cmd/gqlgen/gen.go

ポイントとしては、そのままだとディレクティブとしてのコードが生成されてしまうので、 @goField と同じようにコードが生成されないようにする必要がある。

cfg.Directives["tag"] = config.DirectiveConfig{
	SkipRuntime: true,
}

また、元のコードのまま

err = api.Generate(cfg,
	api.NoPlugins(),
	api.AddPlugin(&p),
)

とすると、モデルしか生成されなくなってしまうので、うまく差し替える方法がないか探してみたが見つけられなかったので型変換できたものを置き換えるようにしてみた。

この例では validate にしか対応していないが、以下の部分と同じようにやれば任意のフォーマットでやりたいように出来る。

// validateタグを追加
validateTag := directive.Arguments.ForName("validate")
if validateTag != nil {
	f.Tag += fmt.Sprintf(` validate:"%s"`, validateTag.Value.Raw)
}

Discussion