Skip to content

Commit

Permalink
Config option for generating embedded structs from interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
ianling committed May 27, 2022
1 parent e5baeaa commit d2294fb
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 50 deletions.
35 changes: 18 additions & 17 deletions codegen/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,24 @@ import (
)

type Config struct {
SchemaFilename StringList `yaml:"schema,omitempty"`
Exec ExecConfig `yaml:"exec"`
Model PackageConfig `yaml:"model,omitempty"`
Federation PackageConfig `yaml:"federation,omitempty"`
Resolver ResolverConfig `yaml:"resolver,omitempty"`
AutoBind []string `yaml:"autobind"`
Models TypeMap `yaml:"models,omitempty"`
StructTag string `yaml:"struct_tag,omitempty"`
Directives map[string]DirectiveConfig `yaml:"directives,omitempty"`
OmitSliceElementPointers bool `yaml:"omit_slice_element_pointers,omitempty"`
StructFieldsAlwaysPointers bool `yaml:"struct_fields_always_pointers,omitempty"`
ResolversAlwaysReturnPointers bool `yaml:"resolvers_always_return_pointers,omitempty"`
SkipValidation bool `yaml:"skip_validation,omitempty"`
SkipModTidy bool `yaml:"skip_mod_tidy,omitempty"`
Sources []*ast.Source `yaml:"-"`
Packages *code.Packages `yaml:"-"`
Schema *ast.Schema `yaml:"-"`
SchemaFilename StringList `yaml:"schema,omitempty"`
Exec ExecConfig `yaml:"exec"`
Model PackageConfig `yaml:"model,omitempty"`
Federation PackageConfig `yaml:"federation,omitempty"`
Resolver ResolverConfig `yaml:"resolver,omitempty"`
AutoBind []string `yaml:"autobind"`
Models TypeMap `yaml:"models,omitempty"`
StructTag string `yaml:"struct_tag,omitempty"`
Directives map[string]DirectiveConfig `yaml:"directives,omitempty"`
OmitSliceElementPointers bool `yaml:"omit_slice_element_pointers,omitempty"`
StructFieldsAlwaysPointers bool `yaml:"struct_fields_always_pointers,omitempty"`
ResolversAlwaysReturnPointers bool `yaml:"resolvers_always_return_pointers,omitempty"`
GenerateEmbeddedStructsForInterfaces bool `yaml:"generate_embedded_structs_for_interfaces,omitempty"`
SkipValidation bool `yaml:"skip_validation,omitempty"`
SkipModTidy bool `yaml:"skip_mod_tidy,omitempty"`
Sources []*ast.Source `yaml:"-"`
Packages *code.Packages `yaml:"-"`
Schema *ast.Schema `yaml:"-"`

// Deprecated: use Federation instead. Will be removed next release
Federated bool `yaml:"federated,omitempty"`
Expand Down
16 changes: 9 additions & 7 deletions codegen/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,15 @@ func BuildData(cfg *config.Config) (*Data, error) {

for _, schemaType := range b.Schema.Types {
switch schemaType.Kind {
case ast.Object:
case ast.Union, ast.Interface, ast.Object:
if (schemaType.Kind == ast.Union || schemaType.Kind == ast.Interface) && !cfg.GenerateEmbeddedStructsForInterfaces {
s.Interfaces[schemaType.Name], err = b.buildInterface(schemaType)
if err != nil {
return nil, fmt.Errorf("unable to bind to interface: %w", err)
}

continue
}
obj, err := b.buildObject(schemaType)
if err != nil {
return nil, fmt.Errorf("unable to build object definition: %w", err)
Expand All @@ -123,12 +131,6 @@ func BuildData(cfg *config.Config) (*Data, error) {
}

s.Inputs = append(s.Inputs, input)

case ast.Union, ast.Interface:
s.Interfaces[schemaType.Name], err = b.buildInterface(schemaType)
if err != nil {
return nil, fmt.Errorf("unable to bind to interface: %w", err)
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ resolver:
# Optional: turn off to make resolvers return values instead of pointers for structs
# resolvers_always_return_pointers: true

# Optional: turn on to generate getter/setter methods for accessing interface fields instead of exporting the fields
# generate_interface_getters_setters: false

# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true

Expand Down
3 changes: 3 additions & 0 deletions init-templates/gqlgen.yml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ resolver:
# Optional: turn off to make resolvers return values instead of pointers for structs
# resolvers_always_return_pointers: true

# Optional: turn on to generate embedded structs when processing GraphQL interfaces
# generate_embedded_structs_for_interfaces: false

# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true

Expand Down
34 changes: 21 additions & 13 deletions plugin/modelgen/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ func defaultBuildMutateHook(b *ModelBuild) *ModelBuild {
}

type ModelBuild struct {
PackageName string
Interfaces []*Interface
Models []*Object
Enums []*Enum
Scalars []string
Config *config.Config
Interfaces []*Interface
Models []*Object
Enums []*Enum
Scalars []string
}

type Interface struct {
Description string
Name string
Fields ast.FieldList
Implements []string
}

Expand Down Expand Up @@ -86,23 +87,30 @@ func (m *Plugin) MutateConfig(cfg *config.Config) error {
binder := cfg.NewBinder()

b := &ModelBuild{
PackageName: cfg.Model.Package,
Config: cfg,
}

for _, schemaType := range cfg.Schema.Types {
if cfg.Models.UserDefined(schemaType.Name) {
continue
}
switch schemaType.Kind {
case ast.Interface, ast.Union:
it := &Interface{
Description: schemaType.Description,
Name: schemaType.Name,
Implements: schemaType.Interfaces,
case ast.Interface, ast.Union, ast.Object, ast.InputObject:
if schemaType.Kind == ast.Interface || schemaType.Kind == ast.Union {
it := &Interface{
Description: schemaType.Description,
Name: schemaType.Name,
Implements: schemaType.Interfaces,
Fields: schemaType.Fields,
}

b.Interfaces = append(b.Interfaces, it)

if !cfg.GenerateEmbeddedStructsForInterfaces {
continue
}
}

b.Interfaces = append(b.Interfaces, it)
case ast.Object, ast.InputObject:
if schemaType == cfg.Schema.Query || schemaType == cfg.Schema.Mutation || schemaType == cfg.Schema.Subscription {
continue
}
Expand Down
61 changes: 48 additions & 13 deletions plugin/modelgen/models.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,64 @@
{{ reserveImport "github.com/99designs/gqlgen/graphql" }}
{{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }}

{{- range $model := .Interfaces }}
{{ with .Description }} {{.|prefixLines "// "}} {{ end }}
type {{.Name|go }} interface {
{{- range $impl := .Implements }}
{{ $impl|go }}
{{- end }}
Is{{.Name|go }}()
}
{{- if not $.Config.GenerateEmbeddedStructsForInterfaces }}
{{- range $interface := .Interfaces }}
{{ with .Description }} {{.|prefixLines "// "}} {{ end }}
type {{.Name|go }} interface {
{{- range $impl := .Implements }}
{{ $impl|go }}
{{- end }}
Is{{.Name|go }}()
}
{{- end }}
{{- end }}

{{ range $model := .Models }}
{{with .Description }} {{.|prefixLines "// "}} {{end}}
type {{ .Name|go }} struct {
{{- range $impl := $model.Implements }}
{{ $impl|go }}
{{- end}}
{{- range $field := .Fields }}
{{- with .Description }}
{{.|prefixLines "// "}}
{{- /* If we are generating embedded structs for GraphQL interfaces,
we need to determine which of the struct's fields are for the purpose of implementing an interface
and ignore those in favor of simply embedding the interface's struct */ -}}
{{- $found := false }}
{{- if and $.Config.GenerateEmbeddedStructsForInterfaces $model.Implements}}
{{- range $impl := $model.Implements }}
{{- range $interface := $.Interfaces }}
{{- if eq $impl $interface.Name }}
{{- range $interfaceField := $interface.Fields }}
{{- if eq $interfaceField.Name $field.Name }}
{{- $found = true }}
{{- break}}
{{- end }}
{{- end }}
{{- end }}

{{- if $found }}
{{- break}}
{{- end }}
{{- end }}

{{- if $found }}
{{- break}}
{{- end }}
{{- end }}
{{- end }}
{{- if not $found }}
{{- with .Description }}
{{.|prefixLines "// "}}
{{- end}}
{{ $field.Name|go }} {{$field.Type | ref}} `{{$field.Tag}}`
{{- end}}
{{ $field.Name|go }} {{$field.Type | ref}} `{{$field.Tag}}`
{{- end }}
}

{{- range $iface := .Implements }}
func ({{ $model.Name|go }}) Is{{ $iface|go }}() {}
{{- if not $.Config.GenerateEmbeddedStructsForInterfaces }}
{{- range $iface := .Implements }}
func ({{ $model.Name|go }}) Is{{ $iface|go }}() {}
{{- end }}
{{- end }}
{{- end}}

Expand Down
27 changes: 27 additions & 0 deletions plugin/modelgen/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"testing"

"github.com/99designs/gqlgen/plugin/modelgen/out_interface_field_methods"
"github.com/99designs/gqlgen/plugin/modelgen/out_struct_pointers"

"github.com/99designs/gqlgen/codegen/config"
Expand Down Expand Up @@ -279,6 +280,32 @@ func TestModelGenerationStructFieldPointers(t *testing.T) {
})
}

func TestModelGenerationInterfaceFieldMethods(t *testing.T) {
cfg, err := config.LoadConfig("testdata/gqlgen_interface_field_methods.yml")
require.NoError(t, err)
require.NoError(t, cfg.Init())
p := Plugin{
MutateHook: mutateHook,
FieldHook: defaultFieldMutateHook,
}
require.NoError(t, p.MutateConfig(cfg))

t.Run("no pointer pointers", func(t *testing.T) {
generated, err := ioutil.ReadFile("./out_interface_field_methods/generated.go")
require.NoError(t, err)
require.NotContains(t, string(generated), "**")
})

t.Run("interfaces become embedded structs", func(t *testing.T) {
human := out_interface_field_methods.Human{
Animal: out_interface_field_methods.Animal{Species: "human"},
Name: "ian",
}
require.Equal(t, "human", human.Species)
require.Equal(t, "ian", human.Name)
})
}

func mutateHook(b *ModelBuild) *ModelBuild {
for _, model := range b.Models {
for _, field := range model.Fields {
Expand Down
26 changes: 26 additions & 0 deletions plugin/modelgen/out/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions plugin/modelgen/out_interface_field_methods/existing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package out_interface_field_methods

type ExistingType struct {
Name *string `json:"name"`
Enum *ExistingEnum `json:"enum"`
Int ExistingInterface `json:"int"`
Existing *MissingTypeNullable `json:"existing"`
}

type ExistingModel struct {
Name string
Enum ExistingEnum
Int ExistingInterface
}

type ExistingInput struct {
Name string
Enum ExistingEnum
Int ExistingInterface
}

type ExistingEnum string

type ExistingInterface interface {
IsExistingInterface()
}

type ExistingUnion interface {
IsExistingUnion()
}

0 comments on commit d2294fb

Please sign in to comment.