package codegen
import (
"fmt"
"go/types"
"reflect"
"regexp"
"strings"
"github.com/pkg/errors"
"golang.org/x/tools/go/loader"
)
func findGoType(prog *loader.Program, pkgName string, typeName string) (types.Object, error) {
if pkgName == "" {
return nil, nil
}
fullName := typeName
if pkgName != "" {
fullName = pkgName + "." + typeName
}
pkgName, err := resolvePkg(pkgName)
if err != nil {
return nil, errors.Errorf("unable to resolve package for %s: %s\n", fullName, err.Error())
}
pkg := prog.Imported[pkgName]
if pkg == nil {
return nil, errors.Errorf("required package was not loaded: %s", fullName)
}
for astNode, def := range pkg.Defs {
if astNode.Name != typeName || def.Parent() == nil || def.Parent() != pkg.Pkg.Scope() {
continue
}
return def, nil
}
return nil, errors.Errorf("unable to find type %s\n", fullName)
}
func findGoNamedType(prog *loader.Program, pkgName string, typeName string) (*types.Named, error) {
def, err := findGoType(prog, pkgName, typeName)
if err != nil {
return nil, err
}
if def == nil {
return nil, nil
}
namedType, ok := def.Type().(*types.Named)
if !ok {
return nil, errors.Errorf("expected %s to be a named type, instead found %T\n", typeName, def.Type())
}
return namedType, nil
}
func findGoInterface(prog *loader.Program, pkgName string, typeName string) (*types.Interface, error) {
namedType, err := findGoNamedType(prog, pkgName, typeName)
if err != nil {
return nil, err
}
if namedType == nil {
return nil, nil
}
underlying, ok := namedType.Underlying().(*types.Interface)
if !ok {
return nil, errors.Errorf("expected %s to be a named interface, instead found %s", typeName, namedType.String())
}
return underlying, nil
}
func findMethod(typ *types.Named, name string) *types.Func {
for i := 0; i < typ.NumMethods(); i++ {
method := typ.Method(i)
if !method.Exported() {
continue
}
if strings.EqualFold(method.Name(), name) {
return method
}
}
if s, ok := typ.Underlying().(*types.Struct); ok {
for i := 0; i < s.NumFields(); i++ {
field := s.Field(i)
if !field.Anonymous() {
continue
}
if named, ok := field.Type().(*types.Named); ok {
if f := findMethod(named, name); f != nil {
return f
}
}
}
}
return nil
}
// findField attempts to match the name to a struct field with the following
// priorites:
// 1. If struct tag is passed then struct tag has highest priority
// 2. Field in an embedded struct
// 3. Actual Field name
func findField(typ *types.Struct, name, structTag string) (*types.Var, error) {
var foundField *types.Var
foundFieldWasTag := false
for i := 0; i < typ.NumFields(); i++ {
field := typ.Field(i)
if structTag != "" {
tags := reflect.StructTag(typ.Tag(i))
if val, ok := tags.Lookup(structTag); ok {
if strings.EqualFold(val, name) {
if foundField != nil && foundFieldWasTag {
return nil, errors.Errorf("tag %s is ambigious; multiple fields have the same tag value of %s", structTag, val)
}
foundField = field
foundFieldWasTag = true
}
}
}
if field.Anonymous() {
if named, ok := field.Type().(*types.Struct); ok {
f, err := findField(named, name, structTag)
if err != nil && !strings.HasPrefix(err.Error(), "no field named") {
return nil, err
}
if f != nil && foundField == nil {
foundField = f
}
}
if named, ok := field.Type().Underlying().(*types.Struct); ok {
f, err := findField(named, name, structTag)
if err != nil && !strings.HasPrefix(err.Error(), "no field named") {
return nil, err
}
if f != nil && foundField == nil {
foundField = f
}
}
}
if !field.Exported() {
continue
}
if strings.EqualFold(field.Name(), name) && foundField == nil {
foundField = field
}
}
if foundField == nil {
return nil, fmt.Errorf("no field named %s", name)
}
return foundField, nil
}
type BindError struct {
object *Object
field *Field
typ types.Type
methodErr error
varErr error
}
func (b BindError) Error() string {
return fmt.Sprintf(
"Unable to bind %s.%s to %s\n %s\n %s",
b.object.GQLType,
b.field.GQLName,
b.typ.String(),
b.methodErr.Error(),
b.varErr.Error(),
)
}
type BindErrors []BindError
func (b BindErrors) Error() string {
var errs []string
for _, err := range b {
errs = append(errs, err.Error())
}
return strings.Join(errs, "\n\n")
}
func bindObject(t types.Type, object *Object, imports *Imports, structTag string) BindErrors {
var errs BindErrors
for i := range object.Fields {
field := &object.Fields[i]
if field.ForceResolver {
continue
}
// first try binding to a method
methodErr := bindMethod(imports, t, field)
if methodErr == nil {
continue
}
// otherwise try binding to a var
varErr := bindVar(imports, t, field, structTag)
if varErr != nil {
errs = append(errs, BindError{
object: object,
typ: t,
field: field,
varErr: varErr,
methodErr: methodErr,
})
}
}
return errs
}
func bindMethod(imports *Imports, t types.Type, field *Field) error {
namedType, ok := t.(*types.Named)
if !ok {
return fmt.Errorf("not a named type")
}
goName := field.GQLName
if field.GoFieldName != "" {
goName = field.GoFieldName
}
method := findMethod(namedType, goName)
if method == nil {
return fmt.Errorf("no method named %s", field.GQLName)
}
sig := method.Type().(*types.Signature)
if sig.Results().Len() == 1 {
field.NoErr = true
} else if sig.Results().Len() != 2 {
return fmt.Errorf("method has wrong number of args")
}
newArgs, err := matchArgs(field, sig.Params())
if err != nil {
return err
}
result := sig.Results().At(0)
if err := validateTypeBinding(imports, field, result.Type()); err != nil {
return errors.Wrap(err, "method has wrong return type")
}
// success, args and return type match. Bind to method
field.GoFieldType = GoFieldMethod
field.GoReceiverName = "obj"
field.GoFieldName = method.Name()
field.Args = newArgs
return nil
}
func bindVar(imports *Imports, t types.Type, field *Field, structTag string) error {
underlying, ok := t.Underlying().(*types.Struct)
if !ok {
return fmt.Errorf("not a struct")
}
goName := field.GQLName
if field.GoFieldName != "" {
goName = field.GoFieldName
}
structField, err := findField(underlying, goName, structTag)
if err != nil {
return err
}
if err := validateTypeBinding(imports, field, structField.Type()); err != nil {
return errors.Wrap(err, "field has wrong type")
}
// success, bind to var
field.GoFieldType = GoFieldVariable
field.GoReceiverName = "obj"
field.GoFieldName = structField.Name()
return nil
}
func matchArgs(field *Field, params *types.Tuple) ([]FieldArgument, error) {
var newArgs []FieldArgument
nextArg:
for j := 0; j < params.Len(); j++ {
param := params.At(j)
for _, oldArg := range field.Args {
if strings.EqualFold(oldArg.GQLName, param.Name()) {
if !field.ForceResolver {
oldArg.Type.Modifiers = modifiersFromGoType(param.Type())
}
newArgs = append(newArgs, oldArg)
continue nextArg
}
}
// no matching arg found, abort
return nil, fmt.Errorf("arg %s not found on method", param.Name())
}
return newArgs, nil
}
func validateTypeBinding(imports *Imports, field *Field, goType types.Type) error {
gqlType := normalizeVendor(field.Type.FullSignature())
goTypeStr := normalizeVendor(goType.String())
if goTypeStr == gqlType || "*"+goTypeStr == gqlType || goTypeStr == "*"+gqlType {
field.Type.Modifiers = modifiersFromGoType(goType)
return nil
}
// deal with type aliases
underlyingStr := normalizeVendor(goType.Underlying().String())
if underlyingStr == gqlType || "*"+underlyingStr == gqlType || underlyingStr == "*"+gqlType {
field.Type.Modifiers = modifiersFromGoType(goType)
pkg, typ := pkgAndType(goType.String())
imp := imports.findByPath(pkg)
field.AliasedType = &Ref{GoType: typ, Import: imp}
return nil
}
return fmt.Errorf("%s is not compatible with %s", gqlType, goTypeStr)
}
func modifiersFromGoType(t types.Type) []string {
var modifiers []string
for {
switch val := t.(type) {
case *types.Pointer:
modifiers = append(modifiers, modPtr)
t = val.Elem()
case *types.Array:
modifiers = append(modifiers, modList)
t = val.Elem()
case *types.Slice:
modifiers = append(modifiers, modList)
t = val.Elem()
default:
return modifiers
}
}
}
var modsRegex = regexp.MustCompile(`^(\*|\[\])*`)
func normalizeVendor(pkg string) string {
modifiers := modsRegex.FindAllString(pkg, 1)[0]
pkg = strings.TrimPrefix(pkg, modifiers)
parts := strings.Split(pkg, "/vendor/")
return modifiers + parts[len(parts)-1]
}