Skip to content

Commit

Permalink
added support for query fragments
Browse files Browse the repository at this point in the history
  • Loading branch information
neelance committed Oct 14, 2016
1 parent 84f532b commit 18645e6
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 28 deletions.
42 changes: 30 additions & 12 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,46 @@ func (s *Schema) Exec(queryString string) (res []byte, errRes error) {
return nil, err
}

rawRes := exec(s, s.Types[s.EntryPoints["query"]], q, s.resolver)
rawRes := exec(s, q, s.Types[s.EntryPoints["query"]], q.Root, s.resolver)
return json.Marshal(rawRes)
}

func exec(s *Schema, t schema.Type, sel *query.SelectionSet, resolver reflect.Value) interface{} {
func exec(s *Schema, q *query.Query, t schema.Type, selSet *query.SelectionSet, resolver reflect.Value) interface{} {
switch t := t.(type) {
case *schema.Scalar:
return resolver.Interface()

case *schema.Array:
a := make([]interface{}, resolver.Len())
for i := range a {
a[i] = exec(s, t.Elem, sel, resolver.Index(i))
a[i] = exec(s, q, t.Elem, selSet, resolver.Index(i))
}
return a

case *schema.TypeName:
return exec(s, s.Types[t.Name], sel, resolver)
return exec(s, q, s.Types[t.Name], selSet, resolver)

case *schema.Object:
res := make(map[string]interface{})
for _, f := range sel.Selections {
sf := t.Fields[f.Name]
m := resolver.Method(findMethod(resolver.Type(), f.Name))
result := make(map[string]interface{})
execSelectionSet(s, q, t, selSet, resolver, result)
return result

default:
panic("invalid type")
}
}

func execSelectionSet(s *Schema, q *query.Query, 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:
sf := t.Fields[sel.Name]
m := resolver.Method(findMethod(resolver.Type(), sel.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]
value, ok := sel.Arguments[name]
if !ok {
value = &query.Value{Value: param.Default}
}
Expand All @@ -67,11 +81,15 @@ func exec(s *Schema, t schema.Type, sel *query.SelectionSet, resolver reflect.Va
}
in = []reflect.Value{args.Elem()}
}
res[f.Alias] = exec(s, sf.Type, f.Sel, m.Call(in)[0])
result[sel.Alias] = exec(s, q, sf.Type, sel.SelSet, m.Call(in)[0])

case *query.FragmentSpread:
execSelectionSet(s, q, t, q.Fragments[sel.Name].SelSet, resolver, result)

default:
panic("invalid type")
}
return res
}
return nil
}

func findMethod(t reflect.Type, name string) int {
Expand Down
87 changes: 81 additions & 6 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ type characterResolver interface {
ID() string
Name() string
Friends() []characterResolver
AppearsIn() []string
}

type humanResolver struct {
Expand All @@ -324,7 +325,11 @@ func (r *humanResolver) Height(args struct{ Unit string }) float64 {
}

func (r *humanResolver) Friends() []characterResolver {
return nil
return resolveCharacters(r.h.Friends)
}

func (r *humanResolver) AppearsIn() []string {
return r.h.AppearsIn
}

type droidResolver struct {
Expand All @@ -340,16 +345,24 @@ func (r *droidResolver) Name() string {
}

func (r *droidResolver) Friends() []characterResolver {
var friends []characterResolver
for _, id := range r.d.Friends {
return resolveCharacters(r.d.Friends)
}

func (r *droidResolver) AppearsIn() []string {
return r.d.AppearsIn
}

func resolveCharacters(ids []string) []characterResolver {
var characters []characterResolver
for _, id := range ids {
if h, ok := humanData[id]; ok {
friends = append(friends, &humanResolver{h})
characters = append(characters, &humanResolver{h})
}
if d, ok := droidData[id]; ok {
friends = append(friends, &droidResolver{d})
characters = append(characters, &droidResolver{d})
}
}
return friends
return characters
}

var tests = []struct {
Expand Down Expand Up @@ -382,6 +395,7 @@ var tests = []struct {
}
`,
},

{
name: "StarWarsBasic",
schema: starWarsSchema,
Expand Down Expand Up @@ -417,6 +431,7 @@ var tests = []struct {
}
`,
},

{
name: "StarWarsArguments1",
schema: starWarsSchema,
Expand All @@ -438,6 +453,7 @@ var tests = []struct {
}
`,
},

{
name: "StarWarsArguments2",
schema: starWarsSchema,
Expand All @@ -459,6 +475,7 @@ var tests = []struct {
}
`,
},

{
name: "StarWarsAliases",
schema: starWarsSchema,
Expand All @@ -484,6 +501,64 @@ var tests = []struct {
}
`,
},

{
name: "StarWarsFragments",
schema: starWarsSchema,
resolver: &starWarsResolver{},
query: `
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
friends {
name
}
}
`,
result: `
{
"leftComparison": {
"name": "Luke Skywalker",
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
`,
},
}

func TestAll(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions internal/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ func (l *Lexer) ConsumeIdent() string {
return text
}

func (l *Lexer) ConsumeKeyword(keyword string) {
if l.next != scanner.Ident || l.sc.TokenText() != keyword {
l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %q", l.sc.TokenText(), keyword))
}
l.Consume()
}

func (l *Lexer) ConsumeString() string {
text := l.sc.TokenText()
l.ConsumeToken(scanner.String)
Expand Down
80 changes: 74 additions & 6 deletions internal/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,51 @@ package query

import (
"errors"
"fmt"
"strings"
"text/scanner"

"github.com/neelance/graphql-go/internal/lexer"
)

type Query struct {
Root *SelectionSet
Fragments map[string]*Fragment
}

type Fragment struct {
Name string
Type string
SelSet *SelectionSet
}

type SelectionSet struct {
Selections []*Field
Selections []Selection
}

type Selection interface {
isSelection()
}

type Field struct {
Alias string
Name string
Arguments map[string]*Value
Sel *SelectionSet
SelSet *SelectionSet
}

type FragmentSpread struct {
Name string
}

func (Field) isSelection() {}
func (FragmentSpread) isSelection() {}

type Value struct {
Value interface{}
}

func Parse(queryString string) (res *SelectionSet, errRes error) {
func Parse(queryString string) (res *Query, errRes error) {
sc := &scanner.Scanner{
Mode: scanner.ScanIdents | scanner.ScanFloats | scanner.ScanStrings,
}
Expand All @@ -39,19 +62,57 @@ func Parse(queryString string) (res *SelectionSet, errRes error) {
}
}()

return parseSelectionSet(lexer.New(sc)), nil
return parseQuery(lexer.New(sc)), nil
}

func parseQuery(l *lexer.Lexer) *Query {
q := &Query{
Fragments: make(map[string]*Fragment),
}
for l.Peek() != scanner.EOF {
if l.Peek() == '{' {
q.Root = parseSelectionSet(l)
continue
}

switch x := l.ConsumeIdent(); x {
case "fragment":
f := parseFragment(l)
q.Fragments[f.Name] = f

default:
l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "fragment"`, x))
}
}
return q
}

func parseFragment(l *lexer.Lexer) *Fragment {
f := &Fragment{}
f.Name = l.ConsumeIdent()
l.ConsumeKeyword("on")
f.Type = l.ConsumeIdent()
f.SelSet = parseSelectionSet(l)
return f
}

func parseSelectionSet(l *lexer.Lexer) *SelectionSet {
sel := &SelectionSet{}
l.ConsumeToken('{')
for l.Peek() != '}' {
sel.Selections = append(sel.Selections, parseField(l))
sel.Selections = append(sel.Selections, parseSelection(l))
}
l.ConsumeToken('}')
return sel
}

func parseSelection(l *lexer.Lexer) Selection {
if l.Peek() == '.' {
return parseFragmentSpread(l)
}
return parseField(l)
}

func parseField(l *lexer.Lexer) *Field {
f := &Field{
Arguments: make(map[string]*Value),
Expand All @@ -76,11 +137,18 @@ func parseField(l *lexer.Lexer) *Field {
l.ConsumeToken(')')
}
if l.Peek() == '{' {
f.Sel = parseSelectionSet(l)
f.SelSet = parseSelectionSet(l)
}
return f
}

func parseFragmentSpread(l *lexer.Lexer) *FragmentSpread {
l.ConsumeToken('.')
l.ConsumeToken('.')
l.ConsumeToken('.')
return &FragmentSpread{Name: l.ConsumeIdent()}
}

func parseArgument(l *lexer.Lexer) (string, *Value) {
name := l.ConsumeIdent()
l.ConsumeToken(':')
Expand Down
Loading

0 comments on commit 18645e6

Please sign in to comment.