diff --git a/graphql.go b/graphql.go index 5a4c801d928..7dfa9a21754 100644 --- a/graphql.go +++ b/graphql.go @@ -3,21 +3,14 @@ package graphql import ( "encoding/json" "fmt" - "reflect" - "strings" + "github.com/neelance/graphql-go/internal/exec" "github.com/neelance/graphql-go/internal/query" "github.com/neelance/graphql-go/internal/schema" ) type Schema struct { - *schema.Schema - resolver reflect.Value -} - -type request struct { - *query.Document - Variables map[string]interface{} + exec *exec.Exec } func NewSchema(schemaString string, filename string, resolver interface{}) (*Schema, error) { @@ -26,10 +19,8 @@ func NewSchema(schemaString string, filename string, resolver interface{}) (*Sch return nil, err } - // TODO type check resolver return &Schema{ - Schema: s, - resolver: reflect.ValueOf(resolver), + exec: exec.Make(s, resolver), }, nil } @@ -50,107 +41,6 @@ func (s *Schema) Exec(queryString string, operationName string, variables map[st return nil, fmt.Errorf("no operation with name %q", operationName) } - r := &request{Document: d, Variables: variables} - rawRes := exec(s, r, s.Types[s.EntryPoints["query"]], op.SelSet, s.resolver) + rawRes := s.exec.Exec(d, variables, op.SelSet) return json.Marshal(rawRes) } - -func exec(s *Schema, r *request, t schema.Type, selSet *query.SelectionSet, resolver reflect.Value) interface{} { - switch t := t.(type) { - case *schema.Scalar: - return resolver.Interface() - - case *schema.Object: - result := make(map[string]interface{}) - execSelectionSet(s, r, t, selSet, resolver, result) - return result - - case *schema.Enum: - return resolver.Interface() - - case *schema.List: - a := make([]interface{}, resolver.Len()) - for i := range a { - a[i] = exec(s, r, t.Elem, selSet, resolver.Index(i)) - } - return a - - case *schema.TypeReference: - return exec(s, r, s.Types[t.Name], selSet, resolver) - - default: - panic("invalid type") - } -} - -func execSelectionSet(s *Schema, r *request, t *schema.Object, selSet *query.SelectionSet, resolver reflect.Value, result map[string]interface{}) { - for _, sel := range selSet.Selections { - switch sel := sel.(type) { - case *query.Field: - if skipByDirective(r, sel.Directives) { - continue - } - execField(s, r, t, sel, resolver, result) - case *query.FragmentSpread: - if skipByDirective(r, sel.Directives) { - continue - } - execSelectionSet(s, r, t, r.Fragments[sel.Name].SelSet, resolver, result) - default: - panic("invalid type") - } - } -} - -func execField(s *Schema, r *request, t *schema.Object, f *query.Field, resolver reflect.Value, result map[string]interface{}) { - sf := t.Fields[f.Name] - m := resolver.Method(findMethod(resolver.Type(), f.Name)) - var in []reflect.Value - if len(sf.Parameters) != 0 { - args := reflect.New(m.Type().In(0)) - for name, param := range sf.Parameters { - value, ok := f.Arguments[name] - if !ok { - value = &query.Literal{Value: param.Default} - } - rf := args.Elem().FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, name) }) - rf.Set(reflect.ValueOf(execValue(r, value))) - } - in = []reflect.Value{args.Elem()} - } - result[f.Alias] = exec(s, r, sf.Type, f.SelSet, m.Call(in)[0]) -} - -func skipByDirective(r *request, d map[string]*query.Directive) bool { - if skip, ok := d["skip"]; ok { - if execValue(r, skip.Arguments["if"]).(bool) { - return true - } - } - if include, ok := d["include"]; ok { - if !execValue(r, include.Arguments["if"]).(bool) { - return true - } - } - return false -} - -func execValue(r *request, v query.Value) interface{} { - switch v := v.(type) { - case *query.Variable: - return r.Variables[v.Name] - case *query.Literal: - return v.Value - default: - panic("invalid value") - } -} - -func findMethod(t reflect.Type, name string) int { - for i := 0; i < t.NumMethod(); i++ { - if strings.EqualFold(name, t.Method(i).Name) { - return i - } - } - return -1 -} diff --git a/internal/exec/exec.go b/internal/exec/exec.go new file mode 100644 index 00000000000..e63be89b271 --- /dev/null +++ b/internal/exec/exec.go @@ -0,0 +1,195 @@ +package exec + +import ( + "reflect" + "strings" + + "github.com/neelance/graphql-go/internal/query" + "github.com/neelance/graphql-go/internal/schema" +) + +type Exec struct { + iExec + resolver reflect.Value +} + +func Make(s *schema.Schema, resolver interface{}) *Exec { + t := s.Types[s.EntryPoints["query"]] + return &Exec{ + iExec: makeExec(s, t, reflect.TypeOf(resolver), make(map[typeRefMapKey]*typeRefExec)), + resolver: reflect.ValueOf(resolver), + } +} + +func (e *Exec) Exec(document *query.Document, variables map[string]interface{}, selSet *query.SelectionSet) interface{} { + return e.exec(&request{document, variables}, selSet, e.resolver) +} + +func makeExec(s *schema.Schema, t schema.Type, resolverType reflect.Type, typeRefMap map[typeRefMapKey]*typeRefExec) iExec { + switch t := t.(type) { + case *schema.Scalar: + return &scalarExec{} + + case *schema.Object: + fields := make(map[string]*fieldExec) + + for name, f := range t.Fields { + methodIndex := -1 + for i := 0; i < resolverType.NumMethod(); i++ { + if strings.EqualFold(name, resolverType.Method(i).Name) { + methodIndex = i + break + } + } + if methodIndex == -1 { + continue // TODO error + } + + fields[name] = &fieldExec{ + field: f, + methodIndex: methodIndex, + valueExec: makeExec(s, f.Type, resolverType.Method(methodIndex).Type.Out(0), typeRefMap), + } + } + + return &objectExec{ + fields: fields, + } + + case *schema.Enum: + return &scalarExec{} + + case *schema.List: + return &listExec{ + elem: makeExec(s, t.Elem, resolverType.Elem(), typeRefMap), + } + + case *schema.TypeReference: + refT := s.Types[t.Name] + k := typeRefMapKey{refT, resolverType} + e, ok := typeRefMap[k] + if !ok { + e = &typeRefExec{} + typeRefMap[k] = e + e.iExec = makeExec(s, refT, resolverType, typeRefMap) + } + return e + + default: + panic("invalid type") + } +} + +type request struct { + *query.Document + Variables map[string]interface{} +} + +type iExec interface { + exec(r *request, selSet *query.SelectionSet, resolver reflect.Value) interface{} +} + +type scalarExec struct{} + +func (e *scalarExec) exec(r *request, selSet *query.SelectionSet, resolver reflect.Value) interface{} { + return resolver.Interface() +} + +type listExec struct { + elem iExec +} + +func (e *listExec) exec(r *request, selSet *query.SelectionSet, resolver reflect.Value) interface{} { + l := make([]interface{}, resolver.Len()) + for i := range l { + l[i] = e.elem.exec(r, selSet, resolver.Index(i)) + } + return l +} + +type typeRefExec struct { + iExec +} + +type typeRefMapKey struct { + s schema.Type + r reflect.Type +} + +type objectExec struct { + fields map[string]*fieldExec +} + +func (e *objectExec) exec(r *request, selSet *query.SelectionSet, resolver reflect.Value) interface{} { + result := make(map[string]interface{}) + e.execFragment(r, selSet, resolver, result) + return result +} + +func (e *objectExec) execFragment(r *request, selSet *query.SelectionSet, resolver reflect.Value, result map[string]interface{}) { + for _, sel := range selSet.Selections { + switch sel := sel.(type) { + case *query.Field: + if skipByDirective(r, sel.Directives) { + continue + } + e.fields[sel.Name].exec(r, sel, resolver, result) + case *query.FragmentSpread: + if skipByDirective(r, sel.Directives) { + continue + } + e.execFragment(r, r.Fragments[sel.Name].SelSet, resolver, result) + default: + panic("invalid type") + } + } +} + +type fieldExec struct { + field *schema.Field + methodIndex int + valueExec iExec +} + +func (e *fieldExec) exec(r *request, f *query.Field, resolver reflect.Value, result map[string]interface{}) { + m := resolver.Method(e.methodIndex) + var in []reflect.Value + if len(e.field.Parameters) != 0 { + args := reflect.New(m.Type().In(0)) + for name, param := range e.field.Parameters { + value, ok := f.Arguments[name] + if !ok { + value = &query.Literal{Value: param.Default} + } + rf := args.Elem().FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, name) }) + rf.Set(reflect.ValueOf(execValue(r, value))) + } + in = []reflect.Value{args.Elem()} + } + result[f.Alias] = e.valueExec.exec(r, f.SelSet, m.Call(in)[0]) +} + +func skipByDirective(r *request, d map[string]*query.Directive) bool { + if skip, ok := d["skip"]; ok { + if execValue(r, skip.Arguments["if"]).(bool) { + return true + } + } + if include, ok := d["include"]; ok { + if !execValue(r, include.Arguments["if"]).(bool) { + return true + } + } + return false +} + +func execValue(r *request, v query.Value) interface{} { + switch v := v.(type) { + case *query.Variable: + return r.Variables[v.Name] + case *query.Literal: + return v.Value + default: + panic("invalid value") + } +}