From 10a9dead0813452dc0be467dd7f2577652a43c70 Mon Sep 17 00:00:00 2001 From: Tom Prebble Date: Fri, 8 Oct 2021 12:09:35 +0100 Subject: [PATCH 1/3] Add ast aware field level hook to modelgen Currently, the only mechanism for extending the model generation is to use a BuildMutateHook at the end of the model generation process. This can be quite limiting as the hook only has scope of the model build and not the graphql schema which has been parsed. This change adds a hook at the end of the field creation process which provides access to the parsed graphql type definition and field definition. This allows for more flexibility for example adding additional tags to the model based off custom directives --- plugin/modelgen/models.go | 21 +++++++++++++-- plugin/modelgen/models_test.go | 34 +++++++++++++++++++++++++ plugin/modelgen/out/generated.go | 5 ++++ plugin/modelgen/testdata/schema.graphql | 8 ++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/plugin/modelgen/models.go b/plugin/modelgen/models.go index e0ca186632..5504aba308 100644 --- a/plugin/modelgen/models.go +++ b/plugin/modelgen/models.go @@ -13,6 +13,11 @@ import ( type BuildMutateHook = func(b *ModelBuild) *ModelBuild +type FieldMutateHook = func(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error) + +func defaultFieldMutateHook(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error) { + return f, nil +} func defaultBuildMutateHook(b *ModelBuild) *ModelBuild { return b } @@ -58,11 +63,13 @@ type EnumValue struct { func New() plugin.Plugin { return &Plugin{ MutateHook: defaultBuildMutateHook, + FieldHook: defaultFieldMutateHook, } } type Plugin struct { MutateHook BuildMutateHook + FieldHook FieldMutateHook } var _ plugin.ConfigMutator = &Plugin{} @@ -162,12 +169,22 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error { typ = types.NewPointer(typ) } - it.Fields = append(it.Fields, &Field{ + f := &Field{ Name: name, Type: typ, Description: field.Description, Tag: `json:"` + field.Name + `"`, - }) + } + + if m.FieldHook != nil { + mf, err := m.FieldHook(schemaType, field, f) + if err != nil { + return fmt.Errorf("generror: field %v.%v: %w", it.Name, field.Name, err) + } + f = mf + } + + it.Fields = append(it.Fields, f) } b.Models = append(b.Models, it) diff --git a/plugin/modelgen/models_test.go b/plugin/modelgen/models_test.go index 20becda914..6b527c26c8 100644 --- a/plugin/modelgen/models_test.go +++ b/plugin/modelgen/models_test.go @@ -1,6 +1,7 @@ package modelgen import ( + "github.com/vektah/gqlparser/v2/ast" "go/parser" "go/token" "io/ioutil" @@ -18,6 +19,7 @@ func TestModelGeneration(t *testing.T) { require.NoError(t, cfg.Init()) p := Plugin{ MutateHook: mutateHook, + FieldHook: mutateFieldHook, } require.NoError(t, p.MutateConfig(cfg)) @@ -65,6 +67,22 @@ func TestModelGeneration(t *testing.T) { } }) + t.Run("field hooks are applied", func(t *testing.T) { + file, err := ioutil.ReadFile("./out/generated.go") + require.NoError(t, err) + + fileText := string(file) + + expectedTags := []string{ + `json:"name" anotherTag:"tag"`, + `json:"enum" yetAnotherTag:"12"`, + } + + for _, tag := range expectedTags { + require.True(t, strings.Contains(fileText, tag)) + } + }) + t.Run("concrete types implement interface", func(t *testing.T) { var _ out.FooBarer = out.FooBarr{} }) @@ -79,3 +97,19 @@ func mutateHook(b *ModelBuild) *ModelBuild { return b } + +func mutateFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error) { + + if fd.Directives == nil || td.Name != "FieldMutationHook" { + return f, nil + } + + directive := fd.Directives.ForName("addTag") + if directive != nil { + args := directive.ArgumentMap(map[string]interface{}{}) + if tag, ok := args["tag"]; ok { + f.Tag += " " + tag.(string) + } + } + return f, nil +} diff --git a/plugin/modelgen/out/generated.go b/plugin/modelgen/out/generated.go index bddf95bbff..d2ea061b68 100644 --- a/plugin/modelgen/out/generated.go +++ b/plugin/modelgen/out/generated.go @@ -30,6 +30,11 @@ type UnionWithDescription interface { IsUnionWithDescription() } +type FieldMutationHook struct { + Name *string `json:"name" anotherTag:"tag" database:"FieldMutationHookname"` + Enum *ExistingEnum `json:"enum" yetAnotherTag:"12" database:"FieldMutationHookenum"` +} + type MissingInput struct { Name *string `json:"name" database:"MissingInputname"` Enum *MissingEnum `json:"enum" database:"MissingInputenum"` diff --git a/plugin/modelgen/testdata/schema.graphql b/plugin/modelgen/testdata/schema.graphql index 7f0c0c93e9..cc36774f84 100644 --- a/plugin/modelgen/testdata/schema.graphql +++ b/plugin/modelgen/testdata/schema.graphql @@ -1,3 +1,6 @@ +directive @addTag(tag: String!) on INPUT_FIELD_DEFINITION + | FIELD_DEFINITION + type Query { thisShoudlntGetGenerated: Boolean } @@ -54,6 +57,11 @@ input ExistingInput { enum: ExistingEnum } +type FieldMutationHook { + name: String @addTag(tag :"anotherTag:\"tag\"") + enum: ExistingEnum @addTag(tag: "yetAnotherTag:\"12\"") +} + enum ExistingEnum { Hello Goodbye From b6febf02ff0b415e5eca0664364ff0e6c3846b6e Mon Sep 17 00:00:00 2001 From: Thomas Prebble <6523587+tprebs@users.noreply.github.com> Date: Fri, 8 Oct 2021 14:35:01 +0100 Subject: [PATCH 2/3] Add recipe for using the modelgen FieldMutateHook --- docs/content/recipes/modelgen-hook.md | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/docs/content/recipes/modelgen-hook.md b/docs/content/recipes/modelgen-hook.md index 88d9fd967d..eb65d625c2 100644 --- a/docs/content/recipes/modelgen-hook.md +++ b/docs/content/recipes/modelgen-hook.md @@ -5,6 +5,8 @@ linkTitle: "Modelgen hook" menu: { main: { parent: 'recipes' } } --- +## BuildMutateHook + The following recipe shows how to use a `modelgen` plugin hook to mutate generated models before they are rendered into a resulting file. This feature has many uses but the example focuses only on inserting ORM-specific tags into generated struct fields. This @@ -77,3 +79,88 @@ type Object struct { field2 *int `json:"field2" orm_binding:"Object.field2"` } ``` + +## FieldMutateHook + +For more fine grained control over model generation, a graphql schema aware a FieldHook can be provided. This hook has access to type and field graphql definitions enabling the hook to modify the `modelgen.Field` using directives defined within the schema. + +The below recipe uses this feature to add validate tags to the generated model for use with `go-playground/validator` where the validate tags are defined in a constraint directive in the schema. + +``` go +import ( + "fmt" + "github.com/vektah/gqlparser/v2/ast" + "os" + + "github.com/99designs/gqlgen/api" + "github.com/99designs/gqlgen/codegen/config" + "github.com/99designs/gqlgen/plugin/modelgen" +) + +// Defining mutation function +func constraintFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *modelgen.Field) (*modelgen.Field, error) { + + c := fd.Directives.ForName("constraint") + if c != nil { + formatConstraint := c.Arguments.ForName("format") + + if formatConstraint != nil{ + f.Tag += " validate:"+formatConstraint.Value.String() + } + + } + + return f, nil +} + +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{ + FieldHook: constraintFieldHook, + } + + err = api.Generate(cfg, + api.NoPlugins(), + api.AddPlugin(&p), + ) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(3) + } +} +``` + +This schema: + +```graphql +directive @constraint( + format: String +) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION + +input ObjectInput { + contactEmail: String @constraint(format: "email") + website: String @constraint(format: "uri") +} +``` + +Will generate the model: + +```go +type ObjectInput struct { + contactEmail *string `json:"contactEmail" validate:"email"` + website *string `json:"website" validate:"uri"` +} +``` + +If a constraint being used during generation shoud not be published during introspection, the directive should be listed with `skip_runtime:true` in gqlgen.yml +```yaml +directives: + constraint: + skip_runtime: true +``` From 45fa3ce6f64cc3de70ac0e0c9c8c028603ca571d Mon Sep 17 00:00:00 2001 From: Tom Prebble Date: Mon, 11 Oct 2021 09:14:15 +0100 Subject: [PATCH 3/3] fix goimport linting issue in models_test --- plugin/modelgen/models_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/modelgen/models_test.go b/plugin/modelgen/models_test.go index 6b527c26c8..a5f02624de 100644 --- a/plugin/modelgen/models_test.go +++ b/plugin/modelgen/models_test.go @@ -1,13 +1,14 @@ package modelgen import ( - "github.com/vektah/gqlparser/v2/ast" "go/parser" "go/token" "io/ioutil" "strings" "testing" + "github.com/vektah/gqlparser/v2/ast" + "github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/plugin/modelgen/out" "github.com/stretchr/testify/require"