Skip to content

Commit

Permalink
Merge pull request 99designs#93 from vektah/input-type-error-handling
Browse files Browse the repository at this point in the history
Input type error handling
  • Loading branch information
vektah authored Apr 26, 2018
2 parents dabd335 + e314b15 commit 0c9c16a
Show file tree
Hide file tree
Showing 12 changed files with 378 additions and 228 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/docs/public
/example/chat/node_modules
/example/chat/package-lock.json
/codegen/tests/gen
65 changes: 37 additions & 28 deletions codegen/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import (
"go/build"
"go/types"
"os"
"path/filepath"

"github.com/vektah/gqlgen/neelance/schema"
"github.com/pkg/errors"
"golang.org/x/tools/go/loader"
)

Expand All @@ -31,58 +30,68 @@ type ModelBuild struct {
}

// Create a list of models that need to be generated
func Models(schema *schema.Schema, userTypes map[string]string, destDir string) *ModelBuild {
namedTypes := buildNamedTypes(schema, userTypes)
func (cfg *Config) models() (*ModelBuild, error) {
namedTypes := cfg.buildNamedTypes()

imports := buildImports(namedTypes, destDir)
prog, err := loadProgram(imports, true)
imports := buildImports(namedTypes, cfg.modelDir)
prog, err := cfg.loadProgram(imports, true)
if err != nil {
panic(err)
return nil, errors.Wrap(err, "loading failed")
}

bindTypes(imports, namedTypes, destDir, prog)
cfg.bindTypes(imports, namedTypes, cfg.modelDir, prog)

models := buildModels(namedTypes, schema, prog)
models, err := cfg.buildModels(namedTypes, prog)
if err != nil {
return nil, err
}
return &ModelBuild{
PackageName: filepath.Base(destDir),
PackageName: cfg.ModelPackageName,
Models: models,
Enums: buildEnums(namedTypes, schema),
Imports: buildImports(namedTypes, destDir),
}
Enums: cfg.buildEnums(namedTypes),
Imports: buildImports(namedTypes, cfg.modelDir),
}, nil
}

// Bind a schema together with some code to generate a Build
func Bind(schema *schema.Schema, userTypes map[string]string, destDir string) (*Build, error) {
namedTypes := buildNamedTypes(schema, userTypes)
// bind a schema together with some code to generate a Build
func (cfg *Config) bind() (*Build, error) {
namedTypes := cfg.buildNamedTypes()

imports := buildImports(namedTypes, destDir)
prog, err := loadProgram(imports, false)
imports := buildImports(namedTypes, cfg.execDir)
prog, err := cfg.loadProgram(imports, false)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "loading failed")
}

imports = bindTypes(imports, namedTypes, destDir, prog)
imports = cfg.bindTypes(imports, namedTypes, cfg.execDir, prog)

objects, err := cfg.buildObjects(namedTypes, prog, imports)
if err != nil {
return nil, err
}

objects := buildObjects(namedTypes, schema, prog, imports)
inputs := buildInputs(namedTypes, schema, prog, imports)
inputs, err := cfg.buildInputs(namedTypes, prog, imports)
if err != nil {
return nil, err
}

b := &Build{
PackageName: filepath.Base(destDir),
PackageName: cfg.ExecPackageName,
Objects: objects,
Interfaces: buildInterfaces(namedTypes, schema, prog),
Interfaces: cfg.buildInterfaces(namedTypes, prog),
Inputs: inputs,
Imports: imports,
}

if qr, ok := schema.EntryPoints["query"]; ok {
if qr, ok := cfg.schema.EntryPoints["query"]; ok {
b.QueryRoot = b.Objects.ByName(qr.TypeName())
}

if mr, ok := schema.EntryPoints["mutation"]; ok {
if mr, ok := cfg.schema.EntryPoints["mutation"]; ok {
b.MutationRoot = b.Objects.ByName(mr.TypeName())
}

if sr, ok := schema.EntryPoints["subscription"]; ok {
if sr, ok := cfg.schema.EntryPoints["subscription"]; ok {
b.SubscriptionRoot = b.Objects.ByName(sr.TypeName())
}

Expand Down Expand Up @@ -113,7 +122,7 @@ func Bind(schema *schema.Schema, userTypes map[string]string, destDir string) (*
return b, nil
}

func loadProgram(imports Imports, allowErrors bool) (*loader.Program, error) {
func (cfg *Config) loadProgram(imports Imports, allowErrors bool) (*loader.Program, error) {
conf := loader.Config{}
if allowErrors {
conf = loader.Config{
Expand Down
181 changes: 181 additions & 0 deletions codegen/codegen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package codegen

import (
"bytes"
"fmt"
"go/build"
"io/ioutil"
"os"
"path/filepath"
"strings"
"syscall"

"github.com/pkg/errors"
"github.com/vektah/gqlgen/codegen/templates"
"github.com/vektah/gqlgen/neelance/schema"
"golang.org/x/tools/imports"
)

type Config struct {
SchemaStr string
Typemap map[string]string

schema *schema.Schema

ExecFilename string
ExecPackageName string
execPackagePath string
execDir string

ModelFilename string
ModelPackageName string
modelPackagePath string
modelDir string
}

func Generate(cfg Config) error {
if err := cfg.normalize(); err != nil {
return err
}

_ = syscall.Unlink(cfg.ExecFilename)
_ = syscall.Unlink(cfg.ModelFilename)

modelsBuild, err := cfg.models()
if err != nil {
return errors.Wrap(err, "model plan failed")
}
if len(modelsBuild.Models) > 0 {
modelsBuild.PackageName = cfg.ModelPackageName
var buf *bytes.Buffer
buf, err = templates.Run("models.gotpl", modelsBuild)
if err != nil {
return errors.Wrap(err, "model generation failed")
}

if err = write(cfg.ModelFilename, buf.Bytes()); err != nil {
return err
}
for _, model := range modelsBuild.Models {
cfg.Typemap[model.GQLType] = cfg.modelPackagePath + "." + model.GoType
}

for _, enum := range modelsBuild.Enums {
cfg.Typemap[enum.GQLType] = cfg.modelPackagePath + "." + enum.GoType
}
}

build, err := cfg.bind()
if err != nil {
return errors.Wrap(err, "exec plan failed")
}
build.SchemaRaw = cfg.SchemaStr
build.PackageName = cfg.ExecPackageName

var buf *bytes.Buffer
buf, err = templates.Run("generated.gotpl", build)
if err != nil {
return errors.Wrap(err, "exec codegen failed")
}

if err = write(cfg.ExecFilename, buf.Bytes()); err != nil {
return err
}
return nil
}

func (cfg *Config) normalize() error {
if cfg.ModelFilename == "" {
return errors.New("ModelFilename is required")
}
cfg.ModelFilename = abs(cfg.ModelFilename)
cfg.modelDir = filepath.Dir(cfg.ModelFilename)
if cfg.ModelPackageName == "" {
cfg.ModelPackageName = filepath.Base(cfg.modelDir)
}
cfg.modelPackagePath = fullPackageName(cfg.modelDir, cfg.ModelPackageName)

if cfg.ExecFilename == "" {
return errors.New("ModelFilename is required")
}
cfg.ExecFilename = abs(cfg.ExecFilename)
cfg.execDir = filepath.Dir(cfg.ExecFilename)
if cfg.ExecPackageName == "" {
cfg.ExecPackageName = filepath.Base(cfg.execDir)
}
cfg.execPackagePath = fullPackageName(cfg.execDir, cfg.ExecPackageName)

builtins := map[string]string{
"__Directive": "github.com/vektah/gqlgen/neelance/introspection.Directive",
"__Type": "github.com/vektah/gqlgen/neelance/introspection.Type",
"__Field": "github.com/vektah/gqlgen/neelance/introspection.Field",
"__EnumValue": "github.com/vektah/gqlgen/neelance/introspection.EnumValue",
"__InputValue": "github.com/vektah/gqlgen/neelance/introspection.InputValue",
"__Schema": "github.com/vektah/gqlgen/neelance/introspection.Schema",
"Int": "github.com/vektah/gqlgen/graphql.Int",
"Float": "github.com/vektah/gqlgen/graphql.Float",
"String": "github.com/vektah/gqlgen/graphql.String",
"Boolean": "github.com/vektah/gqlgen/graphql.Boolean",
"ID": "github.com/vektah/gqlgen/graphql.ID",
"Time": "github.com/vektah/gqlgen/graphql.Time",
"Map": "github.com/vektah/gqlgen/graphql.Map",
}

if cfg.Typemap == nil {
cfg.Typemap = map[string]string{}
}
for k, v := range builtins {
if _, ok := cfg.Typemap[k]; !ok {
cfg.Typemap[k] = v
}
}

cfg.schema = schema.New()
return cfg.schema.Parse(cfg.SchemaStr)
}

func abs(path string) string {
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
}
return absPath
}

func fullPackageName(dir string, pkgName string) string {
fullPkgName := filepath.Join(filepath.Dir(dir), pkgName)

for _, gopath := range filepath.SplitList(build.Default.GOPATH) {
gopath = filepath.Join(gopath, "src") + string(os.PathSeparator)
fullPkgName = strings.TrimPrefix(fullPkgName, gopath)
}
return filepath.ToSlash(fullPkgName)
}

func gofmt(filename string, b []byte) ([]byte, error) {
out, err := imports.Process(filename, b, nil)
if err != nil {
return b, errors.Wrap(err, "unable to gofmt")
}
return out, nil
}

func write(filename string, b []byte) error {
err := os.MkdirAll(filepath.Dir(filename), 0755)
if err != nil {
return errors.Wrap(err, "failed to create directory")
}

formatted, err := gofmt(filename, b)
if err != nil {
fmt.Fprintf(os.Stderr, "gofmt failed: %s", err.Error())
formatted = b
}

err = ioutil.WriteFile(filename, formatted, 0644)
if err != nil {
return errors.Wrapf(err, "failed to write %s", filename)
}

return nil
}
4 changes: 2 additions & 2 deletions codegen/enum_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"github.com/vektah/gqlgen/neelance/schema"
)

func buildEnums(types NamedTypes, s *schema.Schema) []Enum {
func (cfg *Config) buildEnums(types NamedTypes) []Enum {
var enums []Enum

for _, typ := range s.Types {
for _, typ := range cfg.schema.Types {
namedType := types[typ.TypeName()]
e, isEnum := typ.(*schema.Enum)
if !isEnum || strings.HasPrefix(typ.TypeName(), "__") || namedType.IsUserDefined {
Expand Down
36 changes: 24 additions & 12 deletions codegen/input_build.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
package codegen

import (
"fmt"
"go/types"
"os"
"sort"
"strings"

"github.com/pkg/errors"
"github.com/vektah/gqlgen/neelance/schema"
"golang.org/x/tools/go/loader"
)

func buildInputs(namedTypes NamedTypes, s *schema.Schema, prog *loader.Program, imports Imports) Objects {
func (cfg *Config) buildInputs(namedTypes NamedTypes, prog *loader.Program, imports Imports) (Objects, error) {
var inputs Objects

for _, typ := range s.Types {
for _, typ := range cfg.schema.Types {
switch typ := typ.(type) {
case *schema.InputObject:
input := buildInput(namedTypes, typ)
input, err := buildInput(namedTypes, typ)
if err != nil {
return nil, err
}

def, err := findGoType(prog, input.Package, input.GoType)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
return nil, errors.Wrap(err, "cannot find type")
}
if def != nil {
input.Marshaler = buildInputMarshaler(typ, def)
bindObject(def.Type(), input, imports)
err = bindObject(def.Type(), input, imports)
if err != nil {
return nil, err
}
}

inputs = append(inputs, input)
Expand All @@ -36,20 +41,27 @@ func buildInputs(namedTypes NamedTypes, s *schema.Schema, prog *loader.Program,
return strings.Compare(inputs[i].GQLType, inputs[j].GQLType) == -1
})

return inputs
return inputs, nil
}

func buildInput(types NamedTypes, typ *schema.InputObject) *Object {
func buildInput(types NamedTypes, typ *schema.InputObject) (*Object, error) {
obj := &Object{NamedType: types[typ.TypeName()]}

for _, field := range typ.Values {
obj.Fields = append(obj.Fields, Field{
newField := Field{
GQLName: field.Name.Name,
Type: types.getType(field.Type),
Object: obj,
})
}

if !newField.Type.IsInput && !newField.Type.IsScalar {
return nil, errors.Errorf("%s cannot be used as a field of %s. only input and scalar types are allowed", newField.GQLType, obj.GQLType)
}

obj.Fields = append(obj.Fields, newField)

}
return obj
return obj, nil
}

// if user has implemented an UnmarshalGQL method on the input type manually, use it
Expand Down
Loading

0 comments on commit 0c9c16a

Please sign in to comment.