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

Input type error handling #93

Merged
merged 3 commits into from
Apr 26, 2018
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
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