Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add follow-schema layout for exec #1309

Merged
merged 3 commits into from
Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codegen/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func (a *Data) Args() map[string][]*FieldArgument {
}
}

for _, d := range a.Directives {
for _, d := range a.Directives() {
if len(d.Args) > 0 {
ret[d.ArgsFunc()] = d.Args
}
Expand Down
4 changes: 2 additions & 2 deletions codegen/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (

type Config struct {
SchemaFilename StringList `yaml:"schema,omitempty"`
Exec PackageConfig `yaml:"exec"`
Exec ExecConfig `yaml:"exec"`
Model PackageConfig `yaml:"model,omitempty"`
Federation PackageConfig `yaml:"federation,omitempty"`
Resolver ResolverConfig `yaml:"resolver,omitempty"`
Expand All @@ -43,7 +43,7 @@ func DefaultConfig() *Config {
return &Config{
SchemaFilename: StringList{"schema.graphql"},
Model: PackageConfig{Filename: "models_gen.go"},
Exec: PackageConfig{Filename: "generated.go"},
Exec: ExecConfig{Filename: "generated.go"},
Directives: map[string]DirectiveConfig{},
Models: TypeMap{},
}
Expand Down
74 changes: 39 additions & 35 deletions codegen/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,41 +132,45 @@ func TestReferencedPackages(t *testing.T) {
}

func TestConfigCheck(t *testing.T) {
t.Run("invalid config format due to conflicting package names", func(t *testing.T) {
config := Config{
Exec: PackageConfig{Filename: "generated/exec.go", Package: "graphql"},
Model: PackageConfig{Filename: "generated/models.go"},
}

require.EqualError(t, config.check(), "exec and model define the same import path (github.com/99designs/gqlgen/codegen/config/generated) with different package names (graphql vs generated)")
})

t.Run("federation must be in exec package", func(t *testing.T) {
config := Config{
Exec: PackageConfig{Filename: "generated/exec.go"},
Federation: PackageConfig{Filename: "anotherpkg/federation.go"},
}

require.EqualError(t, config.check(), "federation and exec must be in the same package")
})

t.Run("federation must have same package name as exec", func(t *testing.T) {
config := Config{
Exec: PackageConfig{Filename: "generated/exec.go"},
Federation: PackageConfig{Filename: "generated/federation.go", Package: "federation"},
}

require.EqualError(t, config.check(), "exec and federation define the same import path (github.com/99designs/gqlgen/codegen/config/generated) with different package names (generated vs federation)")
})

t.Run("deprecated federated flag raises an error", func(t *testing.T) {
config := Config{
Exec: PackageConfig{Filename: "generated/exec.go"},
Federated: true,
}

require.EqualError(t, config.check(), "federated has been removed, instead use\nfederation:\n filename: path/to/federated.go")
})
for _, execLayout := range []ExecLayout{ExecLayoutSingleFile, ExecLayoutFollowSchema} {
t.Run(string(execLayout), func(t *testing.T) {
t.Run("invalid config format due to conflicting package names", func(t *testing.T) {
config := Config{
Exec: ExecConfig{Layout: execLayout, Filename: "generated/exec.go", DirName: "generated", Package: "graphql"},
Model: PackageConfig{Filename: "generated/models.go"},
}

require.EqualError(t, config.check(), "exec and model define the same import path (github.com/99designs/gqlgen/codegen/config/generated) with different package names (graphql vs generated)")
})

t.Run("federation must be in exec package", func(t *testing.T) {
config := Config{
Exec: ExecConfig{Layout: execLayout, Filename: "generated/exec.go", DirName: "generated"},
Federation: PackageConfig{Filename: "anotherpkg/federation.go"},
}

require.EqualError(t, config.check(), "federation and exec must be in the same package")
})

t.Run("federation must have same package name as exec", func(t *testing.T) {
config := Config{
Exec: ExecConfig{Layout: execLayout, Filename: "generated/exec.go", DirName: "generated"},
Federation: PackageConfig{Filename: "generated/federation.go", Package: "federation"},
}

require.EqualError(t, config.check(), "exec and federation define the same import path (github.com/99designs/gqlgen/codegen/config/generated) with different package names (generated vs federation)")
})

t.Run("deprecated federated flag raises an error", func(t *testing.T) {
config := Config{
Exec: ExecConfig{Layout: execLayout, Filename: "generated/exec.go", DirName: "generated"},
Federated: true,
}

require.EqualError(t, config.check(), "federated has been removed, instead use\nfederation:\n filename: path/to/federated.go")
})
})
}
}

func TestAutobinding(t *testing.T) {
Expand Down
97 changes: 97 additions & 0 deletions codegen/config/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package config

import (
"fmt"
"go/types"
"path/filepath"
"strings"

"github.com/99designs/gqlgen/internal/code"
)

type ExecConfig struct {
Package string `yaml:"package,omitempty"`
Layout ExecLayout `yaml:"layout,omitempty"` // Default: single-file

// Only for single-file layout:
Filename string `yaml:"filename,omitempty"`

// Only for follow-schema layout:
FilenameTemplate string `yaml:"filename_template,omitempty"` // String template with {name} as placeholder for base name.
DirName string `yaml:"dir"`
}

type ExecLayout string

var (
// Write all generated code to a single file.
ExecLayoutSingleFile ExecLayout = "single-file"
// Write generated code to a directory, generating one Go source file for each GraphQL schema file.
ExecLayoutFollowSchema ExecLayout = "follow-schema"
)

func (r *ExecConfig) Check() error {
if r.Layout == "" {
r.Layout = ExecLayoutSingleFile
}

switch r.Layout {
case ExecLayoutSingleFile:
if r.Filename == "" {
return fmt.Errorf("filename must be specified when using single-file layout")
}
if !strings.HasSuffix(r.Filename, ".go") {
return fmt.Errorf("filename should be path to a go source file when using single-file layout")
}
r.Filename = abs(r.Filename)
case ExecLayoutFollowSchema:
if r.DirName == "" {
return fmt.Errorf("dir must be specified when using follow-schema layout")
}
r.DirName = abs(r.DirName)
default:
return fmt.Errorf("invalid layout %s", r.Layout)
}

if strings.ContainsAny(r.Package, "./\\") {
return fmt.Errorf("package should be the output package name only, do not include the output filename")
}

if r.Package == "" && r.Dir() != "" {
r.Package = code.NameForDir(r.Dir())
}

return nil
}

func (r *ExecConfig) ImportPath() string {
if r.Dir() == "" {
return ""
}
return code.ImportPathForDir(r.Dir())
}

func (r *ExecConfig) Dir() string {
switch r.Layout {
case ExecLayoutSingleFile:
if r.Filename == "" {
return ""
}
return filepath.Dir(r.Filename)
case ExecLayoutFollowSchema:
return abs(r.DirName)
default:
panic("invalid layout " + r.Layout)
}
}

func (r *ExecConfig) Pkg() *types.Package {
if r.Dir() == "" {
return nil
}
return types.NewPackage(r.ImportPath(), r.Package)
}

func (r *ExecConfig) IsDefined() bool {
return r.Filename != "" || r.DirName != ""
}
34 changes: 27 additions & 7 deletions codegen/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ import (
// Data is a unified model of the code to be generated. Plugins may modify this structure to do things like implement
// resolvers or directives automatically (eg grpc, validation)
type Data struct {
Config *config.Config
Schema *ast.Schema
Directives DirectiveList
Config *config.Config
Schema *ast.Schema
// If a schema is broken up into multiple Data instance, each representing part of the schema,
// AllDirectives should contain the directives for the entire schema. Directives() can
// then be used to get the directives that were defined in this Data instance's sources.
// If a single Data instance is used for the entire schema, AllDirectives and Directives()
// will be identical.
// AllDirectives should rarely be used directly.
AllDirectives DirectiveList
Objects Objects
Inputs Objects
Interfaces map[string]*Interface
Expand All @@ -33,6 +39,20 @@ type builder struct {
Directives map[string]*Directive
}

// Get only the directives which are defined in the config's sources.
func (d *Data) Directives() DirectiveList {
res := DirectiveList{}
for k, directive := range d.AllDirectives {
for _, s := range d.Config.Sources {
if directive.Position.Src.Name == s.Name {
res[k] = directive
break
}
}
}
return res
}

func BuildData(cfg *config.Config) (*Data, error) {
b := builder{
Config: cfg,
Expand All @@ -55,10 +75,10 @@ func BuildData(cfg *config.Config) (*Data, error) {
}

s := Data{
Config: cfg,
Directives: dataDirectives,
Schema: b.Schema,
Interfaces: map[string]*Interface{},
Config: cfg,
AllDirectives: dataDirectives,
Schema: b.Schema,
Interfaces: map[string]*Interface{},
}

for _, schemaType := range b.Schema.Types {
Expand Down
68 changes: 68 additions & 0 deletions codegen/data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package codegen

import (
"testing"

"github.com/99designs/gqlgen/codegen/config"
"github.com/vektah/gqlparser/v2/ast"

"github.com/stretchr/testify/assert"
)

func TestData_Directives(t *testing.T) {
d := Data{
Config: &config.Config{
Sources: []*ast.Source{
{
Name: "schema.graphql",
},
},
},
AllDirectives: DirectiveList{
"includeDirective": {
DirectiveDefinition: &ast.DirectiveDefinition{
Name: "includeDirective",
Position: &ast.Position{
Src: &ast.Source{
Name: "schema.graphql",
},
},
},
Name: "includeDirective",
Args: nil,
Builtin: false,
},
"excludeDirective": {
DirectiveDefinition: &ast.DirectiveDefinition{
Name: "excludeDirective",
Position: &ast.Position{
Src: &ast.Source{
Name: "anothersource.graphql",
},
},
},
Name: "excludeDirective",
Args: nil,
Builtin: false,
},
},
}

expected := DirectiveList{
"includeDirective": {
DirectiveDefinition: &ast.DirectiveDefinition{
Name: "includeDirective",
Position: &ast.Position{
Src: &ast.Source{
Name: "schema.graphql",
},
},
},
Name: "includeDirective",
Args: nil,
Builtin: false,
},
}

assert.Equal(t, expected, d.Directives())
}
2 changes: 1 addition & 1 deletion codegen/field.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Contex
}
fc.Args = args
{{- end }}
{{- if $.Directives.LocationDirectives "FIELD" }}
{{- if $.AllDirectives.LocationDirectives "FIELD" }}
resTmp := ec._fieldMiddleware(ctx, {{if $object.Root}}nil{{else}}obj{{end}}, func(rctx context.Context) (interface{}, error) {
{{ template "field" $field }}
})
Expand Down
Loading