Skip to content

Commit

Permalink
Add follow-schema layout for exec (#1309)
Browse files Browse the repository at this point in the history
* Define ExecConfig separate from PackageConfig

When support for writing generated code to a directory instead of
a single file is added, ExecConfig will need additional fields
that will not be relevant to other users of PackageConfig.

* Add single-file, follow-schema layouts

When `ExecLayout` is set to `follow-schema`, output generated code to a
directory instead of a single file. Each file in the output directory
will correspond to a single *.graphql schema file (plus a
root!.generated.go file containing top-level definitions that are not
specific to a single schema file).

`ExecLayout` defaults to `single-file`, which is the current behavior, so
this new functionality is opt-in.

These layouts expose similar functionality to the `ResolverLayout`s with
the same name, just applied to `exec` instead of `resolver`.

Resolves issue #1265.

* Rebase, regenerate

Signed-off-by: Steve Coffman <steve@khanacademy.org>

Co-authored-by: Steve Coffman <steve@khanacademy.org>
  • Loading branch information
kevinmbeaulieu and StevenACoffman authored Oct 15, 2021
1 parent 1297835 commit 1f50001
Show file tree
Hide file tree
Showing 189 changed files with 24,954 additions and 574 deletions.
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

0 comments on commit 1f50001

Please sign in to comment.