diff --git a/docs/content/recipes/modelgen-hook.md b/docs/content/recipes/modelgen-hook.md index 88d9fd967de..eb65d625c26 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 +``` diff --git a/plugin/modelgen/models.go b/plugin/modelgen/models.go index eca6a3064d4..1a26a7463fd 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{} @@ -167,12 +174,22 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error { tag = tag + " " + extraTag } - it.Fields = append(it.Fields, &Field{ + f := &Field{ Name: name, Type: typ, Description: field.Description, Tag: tag, - }) + } + + 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 20becda9140..a5f02624de8 100644 --- a/plugin/modelgen/models_test.go +++ b/plugin/modelgen/models_test.go @@ -7,6 +7,8 @@ import ( "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" @@ -18,6 +20,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 +68,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 +98,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 bddf95bbfff..d2ea061b685 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 7f0c0c93e95..cc36774f84e 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