From 1f272d1bd14a9a5b0457238ba28f8070d7352307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Skrip?= Date: Fri, 11 Oct 2019 14:13:40 +0200 Subject: [PATCH 1/3] Add possibility to hook into modelgen plugin This change introduces option to implement custom hook for model generation plugin without the need to completly copy the whole `modelgen` plugin. One very possible case is as described in #876 and with this change the solution for that can be: ```golang func mutateHook(b *ModelBuild) *ModelBuild { for _, model := range b.Models { for _, field := range model.Fields { field.Tag += ` orm_binding:"` + model.Name + `.` + field.Name + `"` } } return b } ... func main() { p := modelgen.Plugin { MutateHook: mutateHook, } ... } ``` --- plugin/modelgen/models.go | 18 ++++++++++++++++-- plugin/modelgen/models_test.go | 31 ++++++++++++++++++++++++++++++- plugin/modelgen/out/generated.go | 26 +++++++++++++------------- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/plugin/modelgen/models.go b/plugin/modelgen/models.go index 765d83320ff..b7b224d7cbc 100644 --- a/plugin/modelgen/models.go +++ b/plugin/modelgen/models.go @@ -11,6 +11,12 @@ import ( "github.com/vektah/gqlparser/ast" ) +type BuildMutateHook = func(b *ModelBuild) *ModelBuild + +func defaultBuildMutateHook(b *ModelBuild) *ModelBuild { + return b +} + type ModelBuild struct { PackageName string Interfaces []*Interface @@ -50,10 +56,14 @@ type EnumValue struct { } func New() plugin.Plugin { - return &Plugin{} + return &Plugin{ + MutateHook: defaultBuildMutateHook, + } } -type Plugin struct{} +type Plugin struct { + MutateHook BuildMutateHook +} var _ plugin.ConfigMutator = &Plugin{} @@ -221,6 +231,10 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error { return nil } + if m.MutateHook != nil { + b = m.MutateHook(b) + } + return templates.Render(templates.Options{ PackageName: cfg.Model.Package, Filename: cfg.Model.Filename, diff --git a/plugin/modelgen/models_test.go b/plugin/modelgen/models_test.go index 619157625c0..bddca6b7cb9 100644 --- a/plugin/modelgen/models_test.go +++ b/plugin/modelgen/models_test.go @@ -1,6 +1,7 @@ package modelgen import ( + "go/ast" "go/parser" "go/token" "io/ioutil" @@ -14,7 +15,9 @@ import ( func TestModelGeneration(t *testing.T) { cfg, err := config.LoadConfig("testdata/gqlgen.yml") require.NoError(t, err) - p := Plugin{} + p := Plugin{ + MutateHook: mutateHook, + } require.NoError(t, p.MutateConfig(cfg)) require.True(t, cfg.Models.UserDefined("MissingTypeNotNull")) @@ -42,4 +45,30 @@ func TestModelGeneration(t *testing.T) { require.True(t, len(words) > 1, "expected description %q to have more than one word", text) } }) + + t.Run("tags are applied", func(t *testing.T) { + node, err := parser.ParseFile(token.NewFileSet(), "./out/generated.go", nil, 0) + require.NoError(t, err) + for _, obj := range node.Scope.Objects { + if spec, ok := (obj.Decl).(*ast.TypeSpec); ok { + if st, ok := (spec.Type).(*ast.StructType); ok { + for _, field := range st.Fields.List { + fieldName := strings.ToLower(field.Names[0].String()) + expectedTag := "`json:\"" + fieldName + "\" database:\"" + spec.Name.String() + fieldName + "\"`" + require.True(t, field.Tag.Value == expectedTag) + } + } + } + } + }) +} + +func mutateHook(b *ModelBuild) *ModelBuild { + for _, model := range b.Models { + for _, field := range model.Fields { + field.Tag += ` database:"` + model.Name + field.Name + `"` + } + } + + return b } diff --git a/plugin/modelgen/out/generated.go b/plugin/modelgen/out/generated.go index 4914aa7c528..d37542d5e79 100644 --- a/plugin/modelgen/out/generated.go +++ b/plugin/modelgen/out/generated.go @@ -27,16 +27,16 @@ type UnionWithDescription interface { } type MissingInput struct { - Name *string `json:"name"` - Enum *MissingEnum `json:"enum"` + Name *string `json:"name" database:"MissingInputname"` + Enum *MissingEnum `json:"enum" database:"MissingInputenum"` } type MissingTypeNotNull struct { - Name string `json:"name"` - Enum MissingEnum `json:"enum"` - Int MissingInterface `json:"int"` - Existing *ExistingType `json:"existing"` - Missing2 *MissingTypeNullable `json:"missing2"` + Name string `json:"name" database:"MissingTypeNotNullname"` + Enum MissingEnum `json:"enum" database:"MissingTypeNotNullenum"` + Int MissingInterface `json:"int" database:"MissingTypeNotNullint"` + Existing *ExistingType `json:"existing" database:"MissingTypeNotNullexisting"` + Missing2 *MissingTypeNullable `json:"missing2" database:"MissingTypeNotNullmissing2"` } func (MissingTypeNotNull) IsMissingInterface() {} @@ -45,11 +45,11 @@ func (MissingTypeNotNull) IsMissingUnion() {} func (MissingTypeNotNull) IsExistingUnion() {} type MissingTypeNullable struct { - Name *string `json:"name"` - Enum *MissingEnum `json:"enum"` - Int MissingInterface `json:"int"` - Existing *ExistingType `json:"existing"` - Missing2 *MissingTypeNotNull `json:"missing2"` + Name *string `json:"name" database:"MissingTypeNullablename"` + Enum *MissingEnum `json:"enum" database:"MissingTypeNullableenum"` + Int MissingInterface `json:"int" database:"MissingTypeNullableint"` + Existing *ExistingType `json:"existing" database:"MissingTypeNullableexisting"` + Missing2 *MissingTypeNotNull `json:"missing2" database:"MissingTypeNullablemissing2"` } func (MissingTypeNullable) IsMissingInterface() {} @@ -59,7 +59,7 @@ func (MissingTypeNullable) IsExistingUnion() {} // TypeWithDescription is a type with a description type TypeWithDescription struct { - Name *string `json:"name"` + Name *string `json:"name" database:"TypeWithDescriptionname"` } func (TypeWithDescription) IsUnionWithDescription() {} From 6ceb76b632ec117b8b22adaddae68ad7f56e36df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Skrip?= Date: Tue, 29 Oct 2019 12:55:13 +0100 Subject: [PATCH 2/3] Test tag generation only by looking up extected tag strings --- plugin/modelgen/models_test.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/plugin/modelgen/models_test.go b/plugin/modelgen/models_test.go index bddca6b7cb9..54785766598 100644 --- a/plugin/modelgen/models_test.go +++ b/plugin/modelgen/models_test.go @@ -1,7 +1,6 @@ package modelgen import ( - "go/ast" "go/parser" "go/token" "io/ioutil" @@ -47,18 +46,20 @@ func TestModelGeneration(t *testing.T) { }) t.Run("tags are applied", func(t *testing.T) { - node, err := parser.ParseFile(token.NewFileSet(), "./out/generated.go", nil, 0) + file, err := ioutil.ReadFile("./out/generated.go") require.NoError(t, err) - for _, obj := range node.Scope.Objects { - if spec, ok := (obj.Decl).(*ast.TypeSpec); ok { - if st, ok := (spec.Type).(*ast.StructType); ok { - for _, field := range st.Fields.List { - fieldName := strings.ToLower(field.Names[0].String()) - expectedTag := "`json:\"" + fieldName + "\" database:\"" + spec.Name.String() + fieldName + "\"`" - require.True(t, field.Tag.Value == expectedTag) - } - } - } + + fileText := string(file) + + expectedTags := []string{ + `json:"missing2" database:"MissingTypeNotNullmissing2"`, + `json:"name" database:"MissingInputname"`, + `json:"missing2" database:"MissingTypeNullablemissing2"`, + `json:"name" database:"TypeWithDescriptionname"`, + } + + for _, tag := range expectedTags { + require.True(t, strings.Contains(fileText, tag)) } }) } From 2a269dd3e4303fa748be4e2ec3d264f8a29bd6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Skrip?= Date: Mon, 11 Nov 2019 11:18:00 +0100 Subject: [PATCH 3/3] Add modelgen hook recipe --- docs/content/recipes/modelgen-hook.md | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/content/recipes/modelgen-hook.md diff --git a/docs/content/recipes/modelgen-hook.md b/docs/content/recipes/modelgen-hook.md new file mode 100644 index 00000000000..edc28861fbc --- /dev/null +++ b/docs/content/recipes/modelgen-hook.md @@ -0,0 +1,79 @@ +--- +title: "Allowing mutation of generated models before rendering" +description: How to use a model mutation function to insert a ORM-specific tags onto struct fields. +linkTitle: "Modelgen hook" +menu: { main: { parent: 'recipes' } } +--- + +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 +is a common use case since it allows for better field matching of DB queries and +the generated data structure. + +First of all, we need to create a function that will mutate the generated model. +Then we can attach the function to the plugin and use it like any other plugin. + +``` go +import ( + "fmt" + "os" + + "github.com/99designs/gqlgen/api" + "github.com/99designs/gqlgen/codegen/config" + "github.com/99designs/gqlgen/plugin/modelgen" +) + +// Defining mutation function +func mutateHook(b *ModelBuild) *ModelBuild { + for _, model := range b.Models { + for _, field := range model.Fields { + field.Tag += ` orm_binding:"` + model.Name + `.` + field.Name + `"` + } + } + + return b +} + +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: mutateHook, + } + + err = api.Generate(cfg, + api.NoPlugins(), + api.AddPlugin(p), + ) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(3) + } +} +``` + +Now fields from generated models will contain a additional tag `orm_binding`. + +This schema: + +```graphql +type Object { + field1: String + field2: Int +} +``` + +Will gen generated into: + +```go +type Object struct { + field1 *string `json:"field1" orm_binding:"Object.field1"` + field2 *int `json:"field2" orm_binding:"Object.field2"` +} +```