332 lines
7.9 KiB
Go
332 lines
7.9 KiB
Go
|
package resolvable
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/graph-gophers/graphql-go/internal/common"
|
||
|
"github.com/graph-gophers/graphql-go/internal/exec/packer"
|
||
|
"github.com/graph-gophers/graphql-go/internal/schema"
|
||
|
)
|
||
|
|
||
|
type Schema struct {
|
||
|
schema.Schema
|
||
|
Query Resolvable
|
||
|
Mutation Resolvable
|
||
|
Resolver reflect.Value
|
||
|
}
|
||
|
|
||
|
type Resolvable interface {
|
||
|
isResolvable()
|
||
|
}
|
||
|
|
||
|
type Object struct {
|
||
|
Name string
|
||
|
Fields map[string]*Field
|
||
|
TypeAssertions map[string]*TypeAssertion
|
||
|
}
|
||
|
|
||
|
type Field struct {
|
||
|
schema.Field
|
||
|
TypeName string
|
||
|
MethodIndex int
|
||
|
HasContext bool
|
||
|
HasError bool
|
||
|
ArgsPacker *packer.StructPacker
|
||
|
ValueExec Resolvable
|
||
|
TraceLabel string
|
||
|
}
|
||
|
|
||
|
type TypeAssertion struct {
|
||
|
MethodIndex int
|
||
|
TypeExec Resolvable
|
||
|
}
|
||
|
|
||
|
type List struct {
|
||
|
Elem Resolvable
|
||
|
}
|
||
|
|
||
|
type Scalar struct{}
|
||
|
|
||
|
func (*Object) isResolvable() {}
|
||
|
func (*List) isResolvable() {}
|
||
|
func (*Scalar) isResolvable() {}
|
||
|
|
||
|
func ApplyResolver(s *schema.Schema, resolver interface{}) (*Schema, error) {
|
||
|
b := newBuilder(s)
|
||
|
|
||
|
var query, mutation Resolvable
|
||
|
|
||
|
if t, ok := s.EntryPoints["query"]; ok {
|
||
|
if err := b.assignExec(&query, t, reflect.TypeOf(resolver)); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if t, ok := s.EntryPoints["mutation"]; ok {
|
||
|
if err := b.assignExec(&mutation, t, reflect.TypeOf(resolver)); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if err := b.finish(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &Schema{
|
||
|
Schema: *s,
|
||
|
Resolver: reflect.ValueOf(resolver),
|
||
|
Query: query,
|
||
|
Mutation: mutation,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
type execBuilder struct {
|
||
|
schema *schema.Schema
|
||
|
resMap map[typePair]*resMapEntry
|
||
|
packerBuilder *packer.Builder
|
||
|
}
|
||
|
|
||
|
type typePair struct {
|
||
|
graphQLType common.Type
|
||
|
resolverType reflect.Type
|
||
|
}
|
||
|
|
||
|
type resMapEntry struct {
|
||
|
exec Resolvable
|
||
|
targets []*Resolvable
|
||
|
}
|
||
|
|
||
|
func newBuilder(s *schema.Schema) *execBuilder {
|
||
|
return &execBuilder{
|
||
|
schema: s,
|
||
|
resMap: make(map[typePair]*resMapEntry),
|
||
|
packerBuilder: packer.NewBuilder(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (b *execBuilder) finish() error {
|
||
|
for _, entry := range b.resMap {
|
||
|
for _, target := range entry.targets {
|
||
|
*target = entry.exec
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return b.packerBuilder.Finish()
|
||
|
}
|
||
|
|
||
|
func (b *execBuilder) assignExec(target *Resolvable, t common.Type, resolverType reflect.Type) error {
|
||
|
k := typePair{t, resolverType}
|
||
|
ref, ok := b.resMap[k]
|
||
|
if !ok {
|
||
|
ref = &resMapEntry{}
|
||
|
b.resMap[k] = ref
|
||
|
var err error
|
||
|
ref.exec, err = b.makeExec(t, resolverType)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
ref.targets = append(ref.targets, target)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (b *execBuilder) makeExec(t common.Type, resolverType reflect.Type) (Resolvable, error) {
|
||
|
var nonNull bool
|
||
|
t, nonNull = unwrapNonNull(t)
|
||
|
|
||
|
switch t := t.(type) {
|
||
|
case *schema.Object:
|
||
|
return b.makeObjectExec(t.Name, t.Fields, nil, nonNull, resolverType)
|
||
|
|
||
|
case *schema.Interface:
|
||
|
return b.makeObjectExec(t.Name, t.Fields, t.PossibleTypes, nonNull, resolverType)
|
||
|
|
||
|
case *schema.Union:
|
||
|
return b.makeObjectExec(t.Name, nil, t.PossibleTypes, nonNull, resolverType)
|
||
|
}
|
||
|
|
||
|
if !nonNull {
|
||
|
if resolverType.Kind() != reflect.Ptr {
|
||
|
return nil, fmt.Errorf("%s is not a pointer", resolverType)
|
||
|
}
|
||
|
resolverType = resolverType.Elem()
|
||
|
}
|
||
|
|
||
|
switch t := t.(type) {
|
||
|
case *schema.Scalar:
|
||
|
return makeScalarExec(t, resolverType)
|
||
|
|
||
|
case *schema.Enum:
|
||
|
return &Scalar{}, nil
|
||
|
|
||
|
case *common.List:
|
||
|
if resolverType.Kind() != reflect.Slice {
|
||
|
return nil, fmt.Errorf("%s is not a slice", resolverType)
|
||
|
}
|
||
|
e := &List{}
|
||
|
if err := b.assignExec(&e.Elem, t.OfType, resolverType.Elem()); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return e, nil
|
||
|
|
||
|
default:
|
||
|
panic("invalid type: " + t.String())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func makeScalarExec(t *schema.Scalar, resolverType reflect.Type) (Resolvable, error) {
|
||
|
implementsType := false
|
||
|
switch r := reflect.New(resolverType).Interface().(type) {
|
||
|
case *int32:
|
||
|
implementsType = (t.Name == "Int")
|
||
|
case *float64:
|
||
|
implementsType = (t.Name == "Float")
|
||
|
case *string:
|
||
|
implementsType = (t.Name == "String")
|
||
|
case *bool:
|
||
|
implementsType = (t.Name == "Boolean")
|
||
|
case packer.Unmarshaler:
|
||
|
implementsType = r.ImplementsGraphQLType(t.Name)
|
||
|
}
|
||
|
if !implementsType {
|
||
|
return nil, fmt.Errorf("can not use %s as %s", resolverType, t.Name)
|
||
|
}
|
||
|
return &Scalar{}, nil
|
||
|
}
|
||
|
|
||
|
func (b *execBuilder) makeObjectExec(typeName string, fields schema.FieldList, possibleTypes []*schema.Object, nonNull bool, resolverType reflect.Type) (*Object, error) {
|
||
|
if !nonNull {
|
||
|
if resolverType.Kind() != reflect.Ptr && resolverType.Kind() != reflect.Interface {
|
||
|
return nil, fmt.Errorf("%s is not a pointer or interface", resolverType)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
methodHasReceiver := resolverType.Kind() != reflect.Interface
|
||
|
|
||
|
Fields := make(map[string]*Field)
|
||
|
for _, f := range fields {
|
||
|
methodIndex := findMethod(resolverType, f.Name)
|
||
|
if methodIndex == -1 {
|
||
|
hint := ""
|
||
|
if findMethod(reflect.PtrTo(resolverType), f.Name) != -1 {
|
||
|
hint = " (hint: the method exists on the pointer type)"
|
||
|
}
|
||
|
return nil, fmt.Errorf("%s does not resolve %q: missing method for field %q%s", resolverType, typeName, f.Name, hint)
|
||
|
}
|
||
|
|
||
|
m := resolverType.Method(methodIndex)
|
||
|
fe, err := b.makeFieldExec(typeName, f, m, methodIndex, methodHasReceiver)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("%s\n\treturned by (%s).%s", err, resolverType, m.Name)
|
||
|
}
|
||
|
Fields[f.Name] = fe
|
||
|
}
|
||
|
|
||
|
typeAssertions := make(map[string]*TypeAssertion)
|
||
|
for _, impl := range possibleTypes {
|
||
|
methodIndex := findMethod(resolverType, "To"+impl.Name)
|
||
|
if methodIndex == -1 {
|
||
|
return nil, fmt.Errorf("%s does not resolve %q: missing method %q to convert to %q", resolverType, typeName, "To"+impl.Name, impl.Name)
|
||
|
}
|
||
|
if resolverType.Method(methodIndex).Type.NumOut() != 2 {
|
||
|
return nil, fmt.Errorf("%s does not resolve %q: method %q should return a value and a bool indicating success", resolverType, typeName, "To"+impl.Name)
|
||
|
}
|
||
|
a := &TypeAssertion{
|
||
|
MethodIndex: methodIndex,
|
||
|
}
|
||
|
if err := b.assignExec(&a.TypeExec, impl, resolverType.Method(methodIndex).Type.Out(0)); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
typeAssertions[impl.Name] = a
|
||
|
}
|
||
|
|
||
|
return &Object{
|
||
|
Name: typeName,
|
||
|
Fields: Fields,
|
||
|
TypeAssertions: typeAssertions,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||
|
var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||
|
|
||
|
func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.Method, methodIndex int, methodHasReceiver bool) (*Field, error) {
|
||
|
in := make([]reflect.Type, m.Type.NumIn())
|
||
|
for i := range in {
|
||
|
in[i] = m.Type.In(i)
|
||
|
}
|
||
|
if methodHasReceiver {
|
||
|
in = in[1:] // first parameter is receiver
|
||
|
}
|
||
|
|
||
|
hasContext := len(in) > 0 && in[0] == contextType
|
||
|
if hasContext {
|
||
|
in = in[1:]
|
||
|
}
|
||
|
|
||
|
var argsPacker *packer.StructPacker
|
||
|
if len(f.Args) > 0 {
|
||
|
if len(in) == 0 {
|
||
|
return nil, fmt.Errorf("must have parameter for field arguments")
|
||
|
}
|
||
|
var err error
|
||
|
argsPacker, err = b.packerBuilder.MakeStructPacker(f.Args, in[0])
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
in = in[1:]
|
||
|
}
|
||
|
|
||
|
if len(in) > 0 {
|
||
|
return nil, fmt.Errorf("too many parameters")
|
||
|
}
|
||
|
|
||
|
if m.Type.NumOut() > 2 {
|
||
|
return nil, fmt.Errorf("too many return values")
|
||
|
}
|
||
|
|
||
|
hasError := m.Type.NumOut() == 2
|
||
|
if hasError {
|
||
|
if m.Type.Out(1) != errorType {
|
||
|
return nil, fmt.Errorf(`must have "error" as its second return value`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fe := &Field{
|
||
|
Field: *f,
|
||
|
TypeName: typeName,
|
||
|
MethodIndex: methodIndex,
|
||
|
HasContext: hasContext,
|
||
|
ArgsPacker: argsPacker,
|
||
|
HasError: hasError,
|
||
|
TraceLabel: fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name),
|
||
|
}
|
||
|
if err := b.assignExec(&fe.ValueExec, f.Type, m.Type.Out(0)); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return fe, nil
|
||
|
}
|
||
|
|
||
|
func findMethod(t reflect.Type, name string) int {
|
||
|
for i := 0; i < t.NumMethod(); i++ {
|
||
|
if strings.EqualFold(stripUnderscore(name), stripUnderscore(t.Method(i).Name)) {
|
||
|
return i
|
||
|
}
|
||
|
}
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
func unwrapNonNull(t common.Type) (common.Type, bool) {
|
||
|
if nn, ok := t.(*common.NonNull); ok {
|
||
|
return nn.OfType, true
|
||
|
}
|
||
|
return t, false
|
||
|
}
|
||
|
|
||
|
func stripUnderscore(s string) string {
|
||
|
return strings.Replace(s, "_", "", -1)
|
||
|
}
|