GraphQL master FF for review (#18445)

* Initial work on a graphql API

* Added receipts, and more transaction fields.

* Finish receipts, add logs

* Add transactionCount to block

* Add types  and .

* Update Block type to be compatible with ethql

* Rename nonce to transactionCount in Account, to be compatible with ethql

* Update transaction, receipt and log to match ethql

* Add  query operator, for a range of blocks

* Added ommerCount to Block

* Add transactionAt and ommerAt to Block

* Added sendRawTransaction mutation

* Add Call and EstimateGas to graphQL API

* Refactored to use hexutil.Bytes instead of HexBytes

* Replace BigNum with hexutil.Big

* Refactor call and estimateGas to use ethapi struct type

* Replace ethgraphql.Address with common.Address

* Replace ethgraphql.Hash with common.Hash

* Converted most quantities to Long instead of Int

* Add support for logs

* Fix bug in runFilter

* Restructured Transaction to work primarily with headers, so uncle data is reported properly

* Add gasPrice API

* Add protocolVersion API

* Add syncing API

* Moved schema into its own source file

* Move some single use args types into anonymous structs

* Add doc-comments

* Fixed backend fetching to use context

* Added (very) basic tests

* Add documentation to the graphql schema

* Fix reversion for formatting of big numbers

* Correct spelling error

* s/BigInt/Long/

* Update common/types.go

* Fixes in response to review

* Fix lint error

* Updated calls on private functions

* Fix typo in graphql.go

* Rollback ethapi breaking changes for graphql support
Co-Authored-By: Arachnid <arachnid@notdot.net>
This commit is contained in:
Kris Shinn
2019-01-21 06:38:13 -08:00
committed by Guillaume Ballet
parent 105008b6a1
commit f91312dbdb
42 changed files with 6704 additions and 39 deletions

View File

@@ -0,0 +1,32 @@
package common
type Directive struct {
Name Ident
Args ArgumentList
}
func ParseDirectives(l *Lexer) DirectiveList {
var directives DirectiveList
for l.Peek() == '@' {
l.ConsumeToken('@')
d := &Directive{}
d.Name = l.ConsumeIdentWithLoc()
d.Name.Loc.Column--
if l.Peek() == '(' {
d.Args = ParseArguments(l)
}
directives = append(directives, d)
}
return directives
}
type DirectiveList []*Directive
func (l DirectiveList) Get(name string) *Directive {
for _, d := range l {
if d.Name.Name == name {
return d
}
}
return nil
}

View File

@@ -0,0 +1,161 @@
package common
import (
"fmt"
"strings"
"text/scanner"
"github.com/graph-gophers/graphql-go/errors"
)
type syntaxError string
type Lexer struct {
sc *scanner.Scanner
next rune
descComment string
}
type Ident struct {
Name string
Loc errors.Location
}
func NewLexer(s string) *Lexer {
sc := &scanner.Scanner{
Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings,
}
sc.Init(strings.NewReader(s))
return &Lexer{sc: sc}
}
func (l *Lexer) CatchSyntaxError(f func()) (errRes *errors.QueryError) {
defer func() {
if err := recover(); err != nil {
if err, ok := err.(syntaxError); ok {
errRes = errors.Errorf("syntax error: %s", err)
errRes.Locations = []errors.Location{l.Location()}
return
}
panic(err)
}
}()
f()
return
}
func (l *Lexer) Peek() rune {
return l.next
}
// Consume whitespace and tokens equivalent to whitespace (e.g. commas and comments).
//
// Consumed comment characters will build the description for the next type or field encountered.
// The description is available from `DescComment()`, and will be reset every time `Consume()` is
// executed.
func (l *Lexer) Consume() {
l.descComment = ""
for {
l.next = l.sc.Scan()
if l.next == ',' {
// Similar to white space and line terminators, commas (',') are used to improve the
// legibility of source text and separate lexical tokens but are otherwise syntactically and
// semantically insignificant within GraphQL documents.
//
// http://facebook.github.io/graphql/draft/#sec-Insignificant-Commas
continue
}
if l.next == '#' {
// GraphQL source documents may contain single-line comments, starting with the '#' marker.
//
// A comment can contain any Unicode code point except `LineTerminator` so a comment always
// consists of all code points starting with the '#' character up to but not including the
// line terminator.
l.consumeComment()
continue
}
break
}
}
func (l *Lexer) ConsumeIdent() string {
name := l.sc.TokenText()
l.ConsumeToken(scanner.Ident)
return name
}
func (l *Lexer) ConsumeIdentWithLoc() Ident {
loc := l.Location()
name := l.sc.TokenText()
l.ConsumeToken(scanner.Ident)
return Ident{name, loc}
}
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) ConsumeLiteral() *BasicLit {
lit := &BasicLit{Type: l.next, Text: l.sc.TokenText()}
l.Consume()
return lit
}
func (l *Lexer) ConsumeToken(expected rune) {
if l.next != expected {
l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %s", l.sc.TokenText(), scanner.TokenString(expected)))
}
l.Consume()
}
func (l *Lexer) DescComment() string {
return l.descComment
}
func (l *Lexer) SyntaxError(message string) {
panic(syntaxError(message))
}
func (l *Lexer) Location() errors.Location {
return errors.Location{
Line: l.sc.Line,
Column: l.sc.Column,
}
}
// consumeComment consumes all characters from `#` to the first encountered line terminator.
// The characters are appended to `l.descComment`.
func (l *Lexer) consumeComment() {
if l.next != '#' {
return
}
// TODO: count and trim whitespace so we can dedent any following lines.
if l.sc.Peek() == ' ' {
l.sc.Next()
}
if l.descComment != "" {
// TODO: use a bytes.Buffer or strings.Builder instead of this.
l.descComment += "\n"
}
for {
next := l.sc.Next()
if next == '\r' || next == '\n' || next == scanner.EOF {
break
}
// TODO: use a bytes.Buffer or strings.Build instead of this.
l.descComment += string(next)
}
}

View File

@@ -0,0 +1,206 @@
package common
import (
"strconv"
"strings"
"text/scanner"
"github.com/graph-gophers/graphql-go/errors"
)
type Literal interface {
Value(vars map[string]interface{}) interface{}
String() string
Location() errors.Location
}
type BasicLit struct {
Type rune
Text string
Loc errors.Location
}
func (lit *BasicLit) Value(vars map[string]interface{}) interface{} {
switch lit.Type {
case scanner.Int:
value, err := strconv.ParseInt(lit.Text, 10, 32)
if err != nil {
panic(err)
}
return int32(value)
case scanner.Float:
value, err := strconv.ParseFloat(lit.Text, 64)
if err != nil {
panic(err)
}
return value
case scanner.String:
value, err := strconv.Unquote(lit.Text)
if err != nil {
panic(err)
}
return value
case scanner.Ident:
switch lit.Text {
case "true":
return true
case "false":
return false
default:
return lit.Text
}
default:
panic("invalid literal")
}
}
func (lit *BasicLit) String() string {
return lit.Text
}
func (lit *BasicLit) Location() errors.Location {
return lit.Loc
}
type ListLit struct {
Entries []Literal
Loc errors.Location
}
func (lit *ListLit) Value(vars map[string]interface{}) interface{} {
entries := make([]interface{}, len(lit.Entries))
for i, entry := range lit.Entries {
entries[i] = entry.Value(vars)
}
return entries
}
func (lit *ListLit) String() string {
entries := make([]string, len(lit.Entries))
for i, entry := range lit.Entries {
entries[i] = entry.String()
}
return "[" + strings.Join(entries, ", ") + "]"
}
func (lit *ListLit) Location() errors.Location {
return lit.Loc
}
type ObjectLit struct {
Fields []*ObjectLitField
Loc errors.Location
}
type ObjectLitField struct {
Name Ident
Value Literal
}
func (lit *ObjectLit) Value(vars map[string]interface{}) interface{} {
fields := make(map[string]interface{}, len(lit.Fields))
for _, f := range lit.Fields {
fields[f.Name.Name] = f.Value.Value(vars)
}
return fields
}
func (lit *ObjectLit) String() string {
entries := make([]string, 0, len(lit.Fields))
for _, f := range lit.Fields {
entries = append(entries, f.Name.Name+": "+f.Value.String())
}
return "{" + strings.Join(entries, ", ") + "}"
}
func (lit *ObjectLit) Location() errors.Location {
return lit.Loc
}
type NullLit struct {
Loc errors.Location
}
func (lit *NullLit) Value(vars map[string]interface{}) interface{} {
return nil
}
func (lit *NullLit) String() string {
return "null"
}
func (lit *NullLit) Location() errors.Location {
return lit.Loc
}
type Variable struct {
Name string
Loc errors.Location
}
func (v Variable) Value(vars map[string]interface{}) interface{} {
return vars[v.Name]
}
func (v Variable) String() string {
return "$" + v.Name
}
func (v *Variable) Location() errors.Location {
return v.Loc
}
func ParseLiteral(l *Lexer, constOnly bool) Literal {
loc := l.Location()
switch l.Peek() {
case '$':
if constOnly {
l.SyntaxError("variable not allowed")
panic("unreachable")
}
l.ConsumeToken('$')
return &Variable{l.ConsumeIdent(), loc}
case scanner.Int, scanner.Float, scanner.String, scanner.Ident:
lit := l.ConsumeLiteral()
if lit.Type == scanner.Ident && lit.Text == "null" {
return &NullLit{loc}
}
lit.Loc = loc
return lit
case '-':
l.ConsumeToken('-')
lit := l.ConsumeLiteral()
lit.Text = "-" + lit.Text
lit.Loc = loc
return lit
case '[':
l.ConsumeToken('[')
var list []Literal
for l.Peek() != ']' {
list = append(list, ParseLiteral(l, constOnly))
}
l.ConsumeToken(']')
return &ListLit{list, loc}
case '{':
l.ConsumeToken('{')
var fields []*ObjectLitField
for l.Peek() != '}' {
name := l.ConsumeIdentWithLoc()
l.ConsumeToken(':')
value := ParseLiteral(l, constOnly)
fields = append(fields, &ObjectLitField{name, value})
}
l.ConsumeToken('}')
return &ObjectLit{fields, loc}
default:
l.SyntaxError("invalid value")
panic("unreachable")
}
}

View File

@@ -0,0 +1,80 @@
package common
import (
"github.com/graph-gophers/graphql-go/errors"
)
type Type interface {
Kind() string
String() string
}
type List struct {
OfType Type
}
type NonNull struct {
OfType Type
}
type TypeName struct {
Ident
}
func (*List) Kind() string { return "LIST" }
func (*NonNull) Kind() string { return "NON_NULL" }
func (*TypeName) Kind() string { panic("TypeName needs to be resolved to actual type") }
func (t *List) String() string { return "[" + t.OfType.String() + "]" }
func (t *NonNull) String() string { return t.OfType.String() + "!" }
func (*TypeName) String() string { panic("TypeName needs to be resolved to actual type") }
func ParseType(l *Lexer) Type {
t := parseNullType(l)
if l.Peek() == '!' {
l.ConsumeToken('!')
return &NonNull{OfType: t}
}
return t
}
func parseNullType(l *Lexer) Type {
if l.Peek() == '[' {
l.ConsumeToken('[')
ofType := ParseType(l)
l.ConsumeToken(']')
return &List{OfType: ofType}
}
return &TypeName{Ident: l.ConsumeIdentWithLoc()}
}
type Resolver func(name string) Type
func ResolveType(t Type, resolver Resolver) (Type, *errors.QueryError) {
switch t := t.(type) {
case *List:
ofType, err := ResolveType(t.OfType, resolver)
if err != nil {
return nil, err
}
return &List{OfType: ofType}, nil
case *NonNull:
ofType, err := ResolveType(t.OfType, resolver)
if err != nil {
return nil, err
}
return &NonNull{OfType: ofType}, nil
case *TypeName:
refT := resolver(t.Name)
if refT == nil {
err := errors.Errorf("Unknown type %q.", t.Name)
err.Rule = "KnownTypeNames"
err.Locations = []errors.Location{t.Loc}
return nil, err
}
return refT, nil
default:
return t, nil
}
}

View File

@@ -0,0 +1,78 @@
package common
import (
"github.com/graph-gophers/graphql-go/errors"
)
// http://facebook.github.io/graphql/draft/#InputValueDefinition
type InputValue struct {
Name Ident
Type Type
Default Literal
Desc string
Loc errors.Location
TypeLoc errors.Location
}
type InputValueList []*InputValue
func (l InputValueList) Get(name string) *InputValue {
for _, v := range l {
if v.Name.Name == name {
return v
}
}
return nil
}
func ParseInputValue(l *Lexer) *InputValue {
p := &InputValue{}
p.Loc = l.Location()
p.Desc = l.DescComment()
p.Name = l.ConsumeIdentWithLoc()
l.ConsumeToken(':')
p.TypeLoc = l.Location()
p.Type = ParseType(l)
if l.Peek() == '=' {
l.ConsumeToken('=')
p.Default = ParseLiteral(l, true)
}
return p
}
type Argument struct {
Name Ident
Value Literal
}
type ArgumentList []Argument
func (l ArgumentList) Get(name string) (Literal, bool) {
for _, arg := range l {
if arg.Name.Name == name {
return arg.Value, true
}
}
return nil, false
}
func (l ArgumentList) MustGet(name string) Literal {
value, ok := l.Get(name)
if !ok {
panic("argument not found")
}
return value
}
func ParseArguments(l *Lexer) ArgumentList {
var args ArgumentList
l.ConsumeToken('(')
for l.Peek() != ')' {
name := l.ConsumeIdentWithLoc()
l.ConsumeToken(':')
value := ParseLiteral(l, false)
args = append(args, Argument{Name: name, Value: value})
}
l.ConsumeToken(')')
return args
}

View File

@@ -0,0 +1,305 @@
package exec
import (
"bytes"
"context"
"encoding/json"
"reflect"
"sync"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/internal/exec/selected"
"github.com/graph-gophers/graphql-go/internal/query"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/log"
"github.com/graph-gophers/graphql-go/trace"
)
type Request struct {
selected.Request
Limiter chan struct{}
Tracer trace.Tracer
Logger log.Logger
}
func (r *Request) handlePanic(ctx context.Context) {
if value := recover(); value != nil {
r.Logger.LogPanic(ctx, value)
r.AddError(makePanicError(value))
}
}
func makePanicError(value interface{}) *errors.QueryError {
return errors.Errorf("graphql: panic occurred: %v", value)
}
func (r *Request) Execute(ctx context.Context, s *resolvable.Schema, op *query.Operation) ([]byte, []*errors.QueryError) {
var out bytes.Buffer
func() {
defer r.handlePanic(ctx)
sels := selected.ApplyOperation(&r.Request, s, op)
r.execSelections(ctx, sels, nil, s.Resolver, &out, op.Type == query.Mutation)
}()
if err := ctx.Err(); err != nil {
return nil, []*errors.QueryError{errors.Errorf("%s", err)}
}
return out.Bytes(), r.Errs
}
type fieldToExec struct {
field *selected.SchemaField
sels []selected.Selection
resolver reflect.Value
out *bytes.Buffer
}
func (r *Request) execSelections(ctx context.Context, sels []selected.Selection, path *pathSegment, resolver reflect.Value, out *bytes.Buffer, serially bool) {
async := !serially && selected.HasAsyncSel(sels)
var fields []*fieldToExec
collectFieldsToResolve(sels, resolver, &fields, make(map[string]*fieldToExec))
if async {
var wg sync.WaitGroup
wg.Add(len(fields))
for _, f := range fields {
go func(f *fieldToExec) {
defer wg.Done()
defer r.handlePanic(ctx)
f.out = new(bytes.Buffer)
execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, true)
}(f)
}
wg.Wait()
}
out.WriteByte('{')
for i, f := range fields {
if i > 0 {
out.WriteByte(',')
}
out.WriteByte('"')
out.WriteString(f.field.Alias)
out.WriteByte('"')
out.WriteByte(':')
if async {
out.Write(f.out.Bytes())
continue
}
f.out = out
execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, false)
}
out.WriteByte('}')
}
func collectFieldsToResolve(sels []selected.Selection, resolver reflect.Value, fields *[]*fieldToExec, fieldByAlias map[string]*fieldToExec) {
for _, sel := range sels {
switch sel := sel.(type) {
case *selected.SchemaField:
field, ok := fieldByAlias[sel.Alias]
if !ok { // validation already checked for conflict (TODO)
field = &fieldToExec{field: sel, resolver: resolver}
fieldByAlias[sel.Alias] = field
*fields = append(*fields, field)
}
field.sels = append(field.sels, sel.Sels...)
case *selected.TypenameField:
sf := &selected.SchemaField{
Field: resolvable.MetaFieldTypename,
Alias: sel.Alias,
FixedResult: reflect.ValueOf(typeOf(sel, resolver)),
}
*fields = append(*fields, &fieldToExec{field: sf, resolver: resolver})
case *selected.TypeAssertion:
out := resolver.Method(sel.MethodIndex).Call(nil)
if !out[1].Bool() {
continue
}
collectFieldsToResolve(sel.Sels, out[0], fields, fieldByAlias)
default:
panic("unreachable")
}
}
}
func typeOf(tf *selected.TypenameField, resolver reflect.Value) string {
if len(tf.TypeAssertions) == 0 {
return tf.Name
}
for name, a := range tf.TypeAssertions {
out := resolver.Method(a.MethodIndex).Call(nil)
if out[1].Bool() {
return name
}
}
return ""
}
func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *pathSegment, applyLimiter bool) {
if applyLimiter {
r.Limiter <- struct{}{}
}
var result reflect.Value
var err *errors.QueryError
traceCtx, finish := r.Tracer.TraceField(ctx, f.field.TraceLabel, f.field.TypeName, f.field.Name, !f.field.Async, f.field.Args)
defer func() {
finish(err)
}()
err = func() (err *errors.QueryError) {
defer func() {
if panicValue := recover(); panicValue != nil {
r.Logger.LogPanic(ctx, panicValue)
err = makePanicError(panicValue)
err.Path = path.toSlice()
}
}()
if f.field.FixedResult.IsValid() {
result = f.field.FixedResult
return nil
}
if err := traceCtx.Err(); err != nil {
return errors.Errorf("%s", err) // don't execute any more resolvers if context got cancelled
}
var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(traceCtx))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}
callOut := f.resolver.Method(f.field.MethodIndex).Call(in)
result = callOut[0]
if f.field.HasError && !callOut[1].IsNil() {
resolverErr := callOut[1].Interface().(error)
err := errors.Errorf("%s", resolverErr)
err.Path = path.toSlice()
err.ResolverError = resolverErr
return err
}
return nil
}()
if applyLimiter {
<-r.Limiter
}
if err != nil {
r.AddError(err)
f.out.WriteString("null") // TODO handle non-nil
return
}
r.execSelectionSet(traceCtx, f.sels, f.field.Type, path, result, f.out)
}
func (r *Request) execSelectionSet(ctx context.Context, sels []selected.Selection, typ common.Type, path *pathSegment, resolver reflect.Value, out *bytes.Buffer) {
t, nonNull := unwrapNonNull(typ)
switch t := t.(type) {
case *schema.Object, *schema.Interface, *schema.Union:
// a reflect.Value of a nil interface will show up as an Invalid value
if resolver.Kind() == reflect.Invalid || ((resolver.Kind() == reflect.Ptr || resolver.Kind() == reflect.Interface) && resolver.IsNil()) {
if nonNull {
panic(errors.Errorf("got nil for non-null %q", t))
}
out.WriteString("null")
return
}
r.execSelections(ctx, sels, path, resolver, out, false)
return
}
if !nonNull {
if resolver.IsNil() {
out.WriteString("null")
return
}
resolver = resolver.Elem()
}
switch t := t.(type) {
case *common.List:
l := resolver.Len()
if selected.HasAsyncSel(sels) {
var wg sync.WaitGroup
wg.Add(l)
entryouts := make([]bytes.Buffer, l)
for i := 0; i < l; i++ {
go func(i int) {
defer wg.Done()
defer r.handlePanic(ctx)
r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), &entryouts[i])
}(i)
}
wg.Wait()
out.WriteByte('[')
for i, entryout := range entryouts {
if i > 0 {
out.WriteByte(',')
}
out.Write(entryout.Bytes())
}
out.WriteByte(']')
return
}
out.WriteByte('[')
for i := 0; i < l; i++ {
if i > 0 {
out.WriteByte(',')
}
r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), out)
}
out.WriteByte(']')
case *schema.Scalar:
v := resolver.Interface()
data, err := json.Marshal(v)
if err != nil {
panic(errors.Errorf("could not marshal %v: %s", v, err))
}
out.Write(data)
case *schema.Enum:
out.WriteByte('"')
out.WriteString(resolver.String())
out.WriteByte('"')
default:
panic("unreachable")
}
}
func unwrapNonNull(t common.Type) (common.Type, bool) {
if nn, ok := t.(*common.NonNull); ok {
return nn.OfType, true
}
return t, false
}
type pathSegment struct {
parent *pathSegment
value interface{}
}
func (p *pathSegment) toSlice() []interface{} {
if p == nil {
return nil
}
return append(p.parent.toSlice(), p.value)
}

View File

@@ -0,0 +1,371 @@
package packer
import (
"fmt"
"math"
"reflect"
"strings"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/schema"
)
type packer interface {
Pack(value interface{}) (reflect.Value, error)
}
type Builder struct {
packerMap map[typePair]*packerMapEntry
structPackers []*StructPacker
}
type typePair struct {
graphQLType common.Type
resolverType reflect.Type
}
type packerMapEntry struct {
packer packer
targets []*packer
}
func NewBuilder() *Builder {
return &Builder{
packerMap: make(map[typePair]*packerMapEntry),
}
}
func (b *Builder) Finish() error {
for _, entry := range b.packerMap {
for _, target := range entry.targets {
*target = entry.packer
}
}
for _, p := range b.structPackers {
p.defaultStruct = reflect.New(p.structType).Elem()
for _, f := range p.fields {
if defaultVal := f.field.Default; defaultVal != nil {
v, err := f.fieldPacker.Pack(defaultVal.Value(nil))
if err != nil {
return err
}
p.defaultStruct.FieldByIndex(f.fieldIndex).Set(v)
}
}
}
return nil
}
func (b *Builder) assignPacker(target *packer, schemaType common.Type, reflectType reflect.Type) error {
k := typePair{schemaType, reflectType}
ref, ok := b.packerMap[k]
if !ok {
ref = &packerMapEntry{}
b.packerMap[k] = ref
var err error
ref.packer, err = b.makePacker(schemaType, reflectType)
if err != nil {
return err
}
}
ref.targets = append(ref.targets, target)
return nil
}
func (b *Builder) makePacker(schemaType common.Type, reflectType reflect.Type) (packer, error) {
t, nonNull := unwrapNonNull(schemaType)
if !nonNull {
if reflectType.Kind() != reflect.Ptr {
return nil, fmt.Errorf("%s is not a pointer", reflectType)
}
elemType := reflectType.Elem()
addPtr := true
if _, ok := t.(*schema.InputObject); ok {
elemType = reflectType // keep pointer for input objects
addPtr = false
}
elem, err := b.makeNonNullPacker(t, elemType)
if err != nil {
return nil, err
}
return &nullPacker{
elemPacker: elem,
valueType: reflectType,
addPtr: addPtr,
}, nil
}
return b.makeNonNullPacker(t, reflectType)
}
func (b *Builder) makeNonNullPacker(schemaType common.Type, reflectType reflect.Type) (packer, error) {
if u, ok := reflect.New(reflectType).Interface().(Unmarshaler); ok {
if !u.ImplementsGraphQLType(schemaType.String()) {
return nil, fmt.Errorf("can not unmarshal %s into %s", schemaType, reflectType)
}
return &unmarshalerPacker{
ValueType: reflectType,
}, nil
}
switch t := schemaType.(type) {
case *schema.Scalar:
return &ValuePacker{
ValueType: reflectType,
}, nil
case *schema.Enum:
if reflectType.Kind() != reflect.String {
return nil, fmt.Errorf("wrong type, expected %s", reflect.String)
}
return &ValuePacker{
ValueType: reflectType,
}, nil
case *schema.InputObject:
e, err := b.MakeStructPacker(t.Values, reflectType)
if err != nil {
return nil, err
}
return e, nil
case *common.List:
if reflectType.Kind() != reflect.Slice {
return nil, fmt.Errorf("expected slice, got %s", reflectType)
}
p := &listPacker{
sliceType: reflectType,
}
if err := b.assignPacker(&p.elem, t.OfType, reflectType.Elem()); err != nil {
return nil, err
}
return p, nil
case *schema.Object, *schema.Interface, *schema.Union:
return nil, fmt.Errorf("type of kind %s can not be used as input", t.Kind())
default:
panic("unreachable")
}
}
func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Type) (*StructPacker, error) {
structType := typ
usePtr := false
if typ.Kind() == reflect.Ptr {
structType = typ.Elem()
usePtr = true
}
if structType.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected struct or pointer to struct, got %s", typ)
}
var fields []*structPackerField
for _, v := range values {
fe := &structPackerField{field: v}
fx := func(n string) bool {
return strings.EqualFold(stripUnderscore(n), stripUnderscore(v.Name.Name))
}
sf, ok := structType.FieldByNameFunc(fx)
if !ok {
return nil, fmt.Errorf("missing argument %q", v.Name)
}
if sf.PkgPath != "" {
return nil, fmt.Errorf("field %q must be exported", sf.Name)
}
fe.fieldIndex = sf.Index
ft := v.Type
if v.Default != nil {
ft, _ = unwrapNonNull(ft)
ft = &common.NonNull{OfType: ft}
}
if err := b.assignPacker(&fe.fieldPacker, ft, sf.Type); err != nil {
return nil, fmt.Errorf("field %q: %s", sf.Name, err)
}
fields = append(fields, fe)
}
p := &StructPacker{
structType: structType,
usePtr: usePtr,
fields: fields,
}
b.structPackers = append(b.structPackers, p)
return p, nil
}
type StructPacker struct {
structType reflect.Type
usePtr bool
defaultStruct reflect.Value
fields []*structPackerField
}
type structPackerField struct {
field *common.InputValue
fieldIndex []int
fieldPacker packer
}
func (p *StructPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Value{}, errors.Errorf("got null for non-null")
}
values := value.(map[string]interface{})
v := reflect.New(p.structType)
v.Elem().Set(p.defaultStruct)
for _, f := range p.fields {
if value, ok := values[f.field.Name.Name]; ok {
packed, err := f.fieldPacker.Pack(value)
if err != nil {
return reflect.Value{}, err
}
v.Elem().FieldByIndex(f.fieldIndex).Set(packed)
}
}
if !p.usePtr {
return v.Elem(), nil
}
return v, nil
}
type listPacker struct {
sliceType reflect.Type
elem packer
}
func (e *listPacker) Pack(value interface{}) (reflect.Value, error) {
list, ok := value.([]interface{})
if !ok {
list = []interface{}{value}
}
v := reflect.MakeSlice(e.sliceType, len(list), len(list))
for i := range list {
packed, err := e.elem.Pack(list[i])
if err != nil {
return reflect.Value{}, err
}
v.Index(i).Set(packed)
}
return v, nil
}
type nullPacker struct {
elemPacker packer
valueType reflect.Type
addPtr bool
}
func (p *nullPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Zero(p.valueType), nil
}
v, err := p.elemPacker.Pack(value)
if err != nil {
return reflect.Value{}, err
}
if p.addPtr {
ptr := reflect.New(p.valueType.Elem())
ptr.Elem().Set(v)
return ptr, nil
}
return v, nil
}
type ValuePacker struct {
ValueType reflect.Type
}
func (p *ValuePacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Value{}, errors.Errorf("got null for non-null")
}
coerced, err := unmarshalInput(p.ValueType, value)
if err != nil {
return reflect.Value{}, fmt.Errorf("could not unmarshal %#v (%T) into %s: %s", value, value, p.ValueType, err)
}
return reflect.ValueOf(coerced), nil
}
type unmarshalerPacker struct {
ValueType reflect.Type
}
func (p *unmarshalerPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
return reflect.Value{}, errors.Errorf("got null for non-null")
}
v := reflect.New(p.ValueType)
if err := v.Interface().(Unmarshaler).UnmarshalGraphQL(value); err != nil {
return reflect.Value{}, err
}
return v.Elem(), nil
}
type Unmarshaler interface {
ImplementsGraphQLType(name string) bool
UnmarshalGraphQL(input interface{}) error
}
func unmarshalInput(typ reflect.Type, input interface{}) (interface{}, error) {
if reflect.TypeOf(input) == typ {
return input, nil
}
switch typ.Kind() {
case reflect.Int32:
switch input := input.(type) {
case int:
if input < math.MinInt32 || input > math.MaxInt32 {
return nil, fmt.Errorf("not a 32-bit integer")
}
return int32(input), nil
case float64:
coerced := int32(input)
if input < math.MinInt32 || input > math.MaxInt32 || float64(coerced) != input {
return nil, fmt.Errorf("not a 32-bit integer")
}
return coerced, nil
}
case reflect.Float64:
switch input := input.(type) {
case int32:
return float64(input), nil
case int:
return float64(input), nil
}
case reflect.String:
if reflect.TypeOf(input).ConvertibleTo(typ) {
return reflect.ValueOf(input).Convert(typ).Interface(), nil
}
}
return nil, fmt.Errorf("incompatible type")
}
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)
}

View File

@@ -0,0 +1,58 @@
package resolvable
import (
"fmt"
"reflect"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/introspection"
)
var MetaSchema *Object
var MetaType *Object
func init() {
var err error
b := newBuilder(schema.Meta)
metaSchema := schema.Meta.Types["__Schema"].(*schema.Object)
MetaSchema, err = b.makeObjectExec(metaSchema.Name, metaSchema.Fields, nil, false, reflect.TypeOf(&introspection.Schema{}))
if err != nil {
panic(err)
}
metaType := schema.Meta.Types["__Type"].(*schema.Object)
MetaType, err = b.makeObjectExec(metaType.Name, metaType.Fields, nil, false, reflect.TypeOf(&introspection.Type{}))
if err != nil {
panic(err)
}
if err := b.finish(); err != nil {
panic(err)
}
}
var MetaFieldTypename = Field{
Field: schema.Field{
Name: "__typename",
Type: &common.NonNull{OfType: schema.Meta.Types["String"]},
},
TraceLabel: fmt.Sprintf("GraphQL field: __typename"),
}
var MetaFieldSchema = Field{
Field: schema.Field{
Name: "__schema",
Type: schema.Meta.Types["__Schema"],
},
TraceLabel: fmt.Sprintf("GraphQL field: __schema"),
}
var MetaFieldType = Field{
Field: schema.Field{
Name: "__type",
Type: schema.Meta.Types["__Type"],
},
TraceLabel: fmt.Sprintf("GraphQL field: __type"),
}

View File

@@ -0,0 +1,331 @@
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)
}

View File

@@ -0,0 +1,238 @@
package selected
import (
"fmt"
"reflect"
"sync"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/exec/packer"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/internal/query"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/introspection"
)
type Request struct {
Schema *schema.Schema
Doc *query.Document
Vars map[string]interface{}
Mu sync.Mutex
Errs []*errors.QueryError
}
func (r *Request) AddError(err *errors.QueryError) {
r.Mu.Lock()
r.Errs = append(r.Errs, err)
r.Mu.Unlock()
}
func ApplyOperation(r *Request, s *resolvable.Schema, op *query.Operation) []Selection {
var obj *resolvable.Object
switch op.Type {
case query.Query:
obj = s.Query.(*resolvable.Object)
case query.Mutation:
obj = s.Mutation.(*resolvable.Object)
}
return applySelectionSet(r, obj, op.Selections)
}
type Selection interface {
isSelection()
}
type SchemaField struct {
resolvable.Field
Alias string
Args map[string]interface{}
PackedArgs reflect.Value
Sels []Selection
Async bool
FixedResult reflect.Value
}
type TypeAssertion struct {
resolvable.TypeAssertion
Sels []Selection
}
type TypenameField struct {
resolvable.Object
Alias string
}
func (*SchemaField) isSelection() {}
func (*TypeAssertion) isSelection() {}
func (*TypenameField) isSelection() {}
func applySelectionSet(r *Request, e *resolvable.Object, sels []query.Selection) (flattenedSels []Selection) {
for _, sel := range sels {
switch sel := sel.(type) {
case *query.Field:
field := sel
if skipByDirective(r, field.Directives) {
continue
}
switch field.Name.Name {
case "__typename":
flattenedSels = append(flattenedSels, &TypenameField{
Object: *e,
Alias: field.Alias.Name,
})
case "__schema":
flattenedSels = append(flattenedSels, &SchemaField{
Field: resolvable.MetaFieldSchema,
Alias: field.Alias.Name,
Sels: applySelectionSet(r, resolvable.MetaSchema, field.Selections),
Async: true,
FixedResult: reflect.ValueOf(introspection.WrapSchema(r.Schema)),
})
case "__type":
p := packer.ValuePacker{ValueType: reflect.TypeOf("")}
v, err := p.Pack(field.Arguments.MustGet("name").Value(r.Vars))
if err != nil {
r.AddError(errors.Errorf("%s", err))
return nil
}
t, ok := r.Schema.Types[v.String()]
if !ok {
return nil
}
flattenedSels = append(flattenedSels, &SchemaField{
Field: resolvable.MetaFieldType,
Alias: field.Alias.Name,
Sels: applySelectionSet(r, resolvable.MetaType, field.Selections),
Async: true,
FixedResult: reflect.ValueOf(introspection.WrapType(t)),
})
default:
fe := e.Fields[field.Name.Name]
var args map[string]interface{}
var packedArgs reflect.Value
if fe.ArgsPacker != nil {
args = make(map[string]interface{})
for _, arg := range field.Arguments {
args[arg.Name.Name] = arg.Value.Value(r.Vars)
}
var err error
packedArgs, err = fe.ArgsPacker.Pack(args)
if err != nil {
r.AddError(errors.Errorf("%s", err))
return
}
}
fieldSels := applyField(r, fe.ValueExec, field.Selections)
flattenedSels = append(flattenedSels, &SchemaField{
Field: *fe,
Alias: field.Alias.Name,
Args: args,
PackedArgs: packedArgs,
Sels: fieldSels,
Async: fe.HasContext || fe.ArgsPacker != nil || fe.HasError || HasAsyncSel(fieldSels),
})
}
case *query.InlineFragment:
frag := sel
if skipByDirective(r, frag.Directives) {
continue
}
flattenedSels = append(flattenedSels, applyFragment(r, e, &frag.Fragment)...)
case *query.FragmentSpread:
spread := sel
if skipByDirective(r, spread.Directives) {
continue
}
flattenedSels = append(flattenedSels, applyFragment(r, e, &r.Doc.Fragments.Get(spread.Name.Name).Fragment)...)
default:
panic("invalid type")
}
}
return
}
func applyFragment(r *Request, e *resolvable.Object, frag *query.Fragment) []Selection {
if frag.On.Name != "" && frag.On.Name != e.Name {
a, ok := e.TypeAssertions[frag.On.Name]
if !ok {
panic(fmt.Errorf("%q does not implement %q", frag.On, e.Name)) // TODO proper error handling
}
return []Selection{&TypeAssertion{
TypeAssertion: *a,
Sels: applySelectionSet(r, a.TypeExec.(*resolvable.Object), frag.Selections),
}}
}
return applySelectionSet(r, e, frag.Selections)
}
func applyField(r *Request, e resolvable.Resolvable, sels []query.Selection) []Selection {
switch e := e.(type) {
case *resolvable.Object:
return applySelectionSet(r, e, sels)
case *resolvable.List:
return applyField(r, e.Elem, sels)
case *resolvable.Scalar:
return nil
default:
panic("unreachable")
}
}
func skipByDirective(r *Request, directives common.DirectiveList) bool {
if d := directives.Get("skip"); d != nil {
p := packer.ValuePacker{ValueType: reflect.TypeOf(false)}
v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars))
if err != nil {
r.AddError(errors.Errorf("%s", err))
}
if err == nil && v.Bool() {
return true
}
}
if d := directives.Get("include"); d != nil {
p := packer.ValuePacker{ValueType: reflect.TypeOf(false)}
v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars))
if err != nil {
r.AddError(errors.Errorf("%s", err))
}
if err == nil && !v.Bool() {
return true
}
}
return false
}
func HasAsyncSel(sels []Selection) bool {
for _, sel := range sels {
switch sel := sel.(type) {
case *SchemaField:
if sel.Async {
return true
}
case *TypeAssertion:
if HasAsyncSel(sel.Sels) {
return true
}
case *TypenameField:
// sync
default:
panic("unreachable")
}
}
return false
}

View File

@@ -0,0 +1,234 @@
package query
import (
"fmt"
"text/scanner"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
)
type Document struct {
Operations OperationList
Fragments FragmentList
}
type OperationList []*Operation
func (l OperationList) Get(name string) *Operation {
for _, f := range l {
if f.Name.Name == name {
return f
}
}
return nil
}
type FragmentList []*FragmentDecl
func (l FragmentList) Get(name string) *FragmentDecl {
for _, f := range l {
if f.Name.Name == name {
return f
}
}
return nil
}
type Operation struct {
Type OperationType
Name common.Ident
Vars common.InputValueList
Selections []Selection
Directives common.DirectiveList
Loc errors.Location
}
type OperationType string
const (
Query OperationType = "QUERY"
Mutation = "MUTATION"
Subscription = "SUBSCRIPTION"
)
type Fragment struct {
On common.TypeName
Selections []Selection
}
type FragmentDecl struct {
Fragment
Name common.Ident
Directives common.DirectiveList
Loc errors.Location
}
type Selection interface {
isSelection()
}
type Field struct {
Alias common.Ident
Name common.Ident
Arguments common.ArgumentList
Directives common.DirectiveList
Selections []Selection
SelectionSetLoc errors.Location
}
type InlineFragment struct {
Fragment
Directives common.DirectiveList
Loc errors.Location
}
type FragmentSpread struct {
Name common.Ident
Directives common.DirectiveList
Loc errors.Location
}
func (Field) isSelection() {}
func (InlineFragment) isSelection() {}
func (FragmentSpread) isSelection() {}
func Parse(queryString string) (*Document, *errors.QueryError) {
l := common.NewLexer(queryString)
var doc *Document
err := l.CatchSyntaxError(func() { doc = parseDocument(l) })
if err != nil {
return nil, err
}
return doc, nil
}
func parseDocument(l *common.Lexer) *Document {
d := &Document{}
l.Consume()
for l.Peek() != scanner.EOF {
if l.Peek() == '{' {
op := &Operation{Type: Query, Loc: l.Location()}
op.Selections = parseSelectionSet(l)
d.Operations = append(d.Operations, op)
continue
}
loc := l.Location()
switch x := l.ConsumeIdent(); x {
case "query":
op := parseOperation(l, Query)
op.Loc = loc
d.Operations = append(d.Operations, op)
case "mutation":
d.Operations = append(d.Operations, parseOperation(l, Mutation))
case "subscription":
d.Operations = append(d.Operations, parseOperation(l, Subscription))
case "fragment":
frag := parseFragment(l)
frag.Loc = loc
d.Fragments = append(d.Fragments, frag)
default:
l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "fragment"`, x))
}
}
return d
}
func parseOperation(l *common.Lexer, opType OperationType) *Operation {
op := &Operation{Type: opType}
op.Name.Loc = l.Location()
if l.Peek() == scanner.Ident {
op.Name = l.ConsumeIdentWithLoc()
}
op.Directives = common.ParseDirectives(l)
if l.Peek() == '(' {
l.ConsumeToken('(')
for l.Peek() != ')' {
loc := l.Location()
l.ConsumeToken('$')
iv := common.ParseInputValue(l)
iv.Loc = loc
op.Vars = append(op.Vars, iv)
}
l.ConsumeToken(')')
}
op.Selections = parseSelectionSet(l)
return op
}
func parseFragment(l *common.Lexer) *FragmentDecl {
f := &FragmentDecl{}
f.Name = l.ConsumeIdentWithLoc()
l.ConsumeKeyword("on")
f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()}
f.Directives = common.ParseDirectives(l)
f.Selections = parseSelectionSet(l)
return f
}
func parseSelectionSet(l *common.Lexer) []Selection {
var sels []Selection
l.ConsumeToken('{')
for l.Peek() != '}' {
sels = append(sels, parseSelection(l))
}
l.ConsumeToken('}')
return sels
}
func parseSelection(l *common.Lexer) Selection {
if l.Peek() == '.' {
return parseSpread(l)
}
return parseField(l)
}
func parseField(l *common.Lexer) *Field {
f := &Field{}
f.Alias = l.ConsumeIdentWithLoc()
f.Name = f.Alias
if l.Peek() == ':' {
l.ConsumeToken(':')
f.Name = l.ConsumeIdentWithLoc()
}
if l.Peek() == '(' {
f.Arguments = common.ParseArguments(l)
}
f.Directives = common.ParseDirectives(l)
if l.Peek() == '{' {
f.SelectionSetLoc = l.Location()
f.Selections = parseSelectionSet(l)
}
return f
}
func parseSpread(l *common.Lexer) Selection {
loc := l.Location()
l.ConsumeToken('.')
l.ConsumeToken('.')
l.ConsumeToken('.')
f := &InlineFragment{Loc: loc}
if l.Peek() == scanner.Ident {
ident := l.ConsumeIdentWithLoc()
if ident.Name != "on" {
fs := &FragmentSpread{
Name: ident,
Loc: loc,
}
fs.Directives = common.ParseDirectives(l)
return fs
}
f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()}
}
f.Directives = common.ParseDirectives(l)
f.Selections = parseSelectionSet(l)
return f
}

View File

@@ -0,0 +1,190 @@
package schema
var Meta *Schema
func init() {
Meta = &Schema{} // bootstrap
Meta = New()
if err := Meta.Parse(metaSrc); err != nil {
panic(err)
}
}
var metaSrc = `
# The ` + "`" + `Int` + "`" + ` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
scalar Int
# The ` + "`" + `Float` + "`" + ` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).
scalar Float
# The ` + "`" + `String` + "`" + ` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
scalar String
# The ` + "`" + `Boolean` + "`" + ` scalar type represents ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `.
scalar Boolean
# The ` + "`" + `ID` + "`" + ` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as ` + "`" + `"4"` + "`" + `) or integer (such as ` + "`" + `4` + "`" + `) input value will be accepted as an ID.
scalar ID
# Directs the executor to include this field or fragment only when the ` + "`" + `if` + "`" + ` argument is true.
directive @include(
# Included when true.
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
# Directs the executor to skip this field or fragment when the ` + "`" + `if` + "`" + ` argument is true.
directive @skip(
# Skipped when true.
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
# Marks an element of a GraphQL schema as no longer supported.
directive @deprecated(
# Explains why this element was deprecated, usually also including a suggestion
# for how to access supported similar data. Formatted in
# [Markdown](https://daringfireball.net/projects/markdown/).
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
# A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
#
# In some cases, you need to provide options to alter GraphQL's execution behavior
# in ways field arguments will not suffice, such as conditionally including or
# skipping a field. Directives provide this by describing additional information
# to the executor.
type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
}
# A Directive can be adjacent to many parts of the GraphQL language, a
# __DirectiveLocation describes one such possible adjacencies.
enum __DirectiveLocation {
# Location adjacent to a query operation.
QUERY
# Location adjacent to a mutation operation.
MUTATION
# Location adjacent to a subscription operation.
SUBSCRIPTION
# Location adjacent to a field.
FIELD
# Location adjacent to a fragment definition.
FRAGMENT_DEFINITION
# Location adjacent to a fragment spread.
FRAGMENT_SPREAD
# Location adjacent to an inline fragment.
INLINE_FRAGMENT
# Location adjacent to a schema definition.
SCHEMA
# Location adjacent to a scalar definition.
SCALAR
# Location adjacent to an object type definition.
OBJECT
# Location adjacent to a field definition.
FIELD_DEFINITION
# Location adjacent to an argument definition.
ARGUMENT_DEFINITION
# Location adjacent to an interface definition.
INTERFACE
# Location adjacent to a union definition.
UNION
# Location adjacent to an enum definition.
ENUM
# Location adjacent to an enum value definition.
ENUM_VALUE
# Location adjacent to an input object type definition.
INPUT_OBJECT
# Location adjacent to an input object field definition.
INPUT_FIELD_DEFINITION
}
# One possible value for a given Enum. Enum values are unique values, not a
# placeholder for a string or numeric value. However an Enum value is returned in
# a JSON response as a string.
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
# Object and Interface types are described by a list of Fields, each of which has
# a name, potentially a list of arguments, and a return type.
type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
# Arguments provided to Fields or Directives and the input fields of an
# InputObject are represented as Input Values which describe their type and
# optionally a default value.
type __InputValue {
name: String!
description: String
type: __Type!
# A GraphQL-formatted string representing the default value for this input value.
defaultValue: String
}
# A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all
# available types and directives on the server, as well as the entry points for
# query, mutation, and subscription operations.
type __Schema {
# A list of all types supported by this server.
types: [__Type!]!
# The type that query operations will be rooted at.
queryType: __Type!
# If this server supports mutation, the type that mutation operations will be rooted at.
mutationType: __Type
# If this server support subscription, the type that subscription operations will be rooted at.
subscriptionType: __Type
# A list of all directives supported by this server.
directives: [__Directive!]!
}
# The fundamental unit of any GraphQL Schema is the type. There are many kinds of
# types in GraphQL as represented by the ` + "`" + `__TypeKind` + "`" + ` enum.
#
# Depending on the kind of a type, certain fields describe information about that
# type. Scalar types provide no information beyond a name and description, while
# Enum types provide their values. Object and Interface types provide the fields
# they describe. Abstract types, Union and Interface, provide the Object types
# possible at runtime. List and NonNull types compose other types.
type __Type {
kind: __TypeKind!
name: String
description: String
fields(includeDeprecated: Boolean = false): [__Field!]
interfaces: [__Type!]
possibleTypes: [__Type!]
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
inputFields: [__InputValue!]
ofType: __Type
}
# An enum describing what kind of type a given ` + "`" + `__Type` + "`" + ` is.
enum __TypeKind {
# Indicates this type is a scalar.
SCALAR
# Indicates this type is an object. ` + "`" + `fields` + "`" + ` and ` + "`" + `interfaces` + "`" + ` are valid fields.
OBJECT
# Indicates this type is an interface. ` + "`" + `fields` + "`" + ` and ` + "`" + `possibleTypes` + "`" + ` are valid fields.
INTERFACE
# Indicates this type is a union. ` + "`" + `possibleTypes` + "`" + ` is a valid field.
UNION
# Indicates this type is an enum. ` + "`" + `enumValues` + "`" + ` is a valid field.
ENUM
# Indicates this type is an input object. ` + "`" + `inputFields` + "`" + ` is a valid field.
INPUT_OBJECT
# Indicates this type is a list. ` + "`" + `ofType` + "`" + ` is a valid field.
LIST
# Indicates this type is a non-null. ` + "`" + `ofType` + "`" + ` is a valid field.
NON_NULL
}
`

View File

@@ -0,0 +1,570 @@
package schema
import (
"fmt"
"text/scanner"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
)
// Schema represents a GraphQL service's collective type system capabilities.
// A schema is defined in terms of the types and directives it supports as well as the root
// operation types for each kind of operation: `query`, `mutation`, and `subscription`.
//
// For a more formal definition, read the relevant section in the specification:
//
// http://facebook.github.io/graphql/draft/#sec-Schema
type Schema struct {
// EntryPoints determines the place in the type system where `query`, `mutation`, and
// `subscription` operations begin.
//
// http://facebook.github.io/graphql/draft/#sec-Root-Operation-Types
//
// NOTE: The specification refers to this concept as "Root Operation Types".
// TODO: Rename the `EntryPoints` field to `RootOperationTypes` to align with spec terminology.
EntryPoints map[string]NamedType
// Types are the fundamental unit of any GraphQL schema.
// There are six kinds of named types, and two wrapping types.
//
// http://facebook.github.io/graphql/draft/#sec-Types
Types map[string]NamedType
// TODO: Type extensions?
// http://facebook.github.io/graphql/draft/#sec-Type-Extensions
// Directives are used to annotate various parts of a GraphQL document as an indicator that they
// should be evaluated differently by a validator, executor, or client tool such as a code
// generator.
//
// http://facebook.github.io/graphql/draft/#sec-Type-System.Directives
Directives map[string]*DirectiveDecl
entryPointNames map[string]string
objects []*Object
unions []*Union
enums []*Enum
}
// Resolve a named type in the schema by its name.
func (s *Schema) Resolve(name string) common.Type {
return s.Types[name]
}
// NamedType represents a type with a name.
//
// http://facebook.github.io/graphql/draft/#NamedType
type NamedType interface {
common.Type
TypeName() string
Description() string
}
// Scalar types represent primitive leaf values (e.g. a string or an integer) in a GraphQL type
// system.
//
// GraphQL responses take the form of a hierarchical tree; the leaves on these trees are GraphQL
// scalars.
//
// http://facebook.github.io/graphql/draft/#sec-Scalars
type Scalar struct {
Name string
Desc string
// TODO: Add a list of directives?
}
// Object types represent a list of named fields, each of which yield a value of a specific type.
//
// GraphQL queries are hierarchical and composed, describing a tree of information.
// While Scalar types describe the leaf values of these hierarchical types, Objects describe the
// intermediate levels.
//
// http://facebook.github.io/graphql/draft/#sec-Objects
type Object struct {
Name string
Interfaces []*Interface
Fields FieldList
Desc string
// TODO: Add a list of directives?
interfaceNames []string
}
// Interface types represent a list of named fields and their arguments.
//
// GraphQL objects can then implement these interfaces which requires that the object type will
// define all fields defined by those interfaces.
//
// http://facebook.github.io/graphql/draft/#sec-Interfaces
type Interface struct {
Name string
PossibleTypes []*Object
Fields FieldList // NOTE: the spec refers to this as `FieldsDefinition`.
Desc string
// TODO: Add a list of directives?
}
// Union types represent objects that could be one of a list of GraphQL object types, but provides no
// guaranteed fields between those types.
//
// They also differ from interfaces in that object types declare what interfaces they implement, but
// are not aware of what unions contain them.
//
// http://facebook.github.io/graphql/draft/#sec-Unions
type Union struct {
Name string
PossibleTypes []*Object // NOTE: the spec refers to this as `UnionMemberTypes`.
Desc string
// TODO: Add a list of directives?
typeNames []string
}
// Enum types describe a set of possible values.
//
// Like scalar types, Enum types also represent leaf values in a GraphQL type system.
//
// http://facebook.github.io/graphql/draft/#sec-Enums
type Enum struct {
Name string
Values []*EnumValue // NOTE: the spec refers to this as `EnumValuesDefinition`.
Desc string
// TODO: Add a list of directives?
}
// EnumValue types are unique values that may be serialized as a string: the name of the
// represented value.
//
// http://facebook.github.io/graphql/draft/#EnumValueDefinition
type EnumValue struct {
Name string
Directives common.DirectiveList
Desc string
// TODO: Add a list of directives?
}
// InputObject types define a set of input fields; the input fields are either scalars, enums, or
// other input objects.
//
// This allows arguments to accept arbitrarily complex structs.
//
// http://facebook.github.io/graphql/draft/#sec-Input-Objects
type InputObject struct {
Name string
Desc string
Values common.InputValueList
// TODO: Add a list of directives?
}
// FieldsList is a list of an Object's Fields.
//
// http://facebook.github.io/graphql/draft/#FieldsDefinition
type FieldList []*Field
// Get iterates over the field list, returning a pointer-to-Field when the field name matches the
// provided `name` argument.
// Returns nil when no field was found by that name.
func (l FieldList) Get(name string) *Field {
for _, f := range l {
if f.Name == name {
return f
}
}
return nil
}
// Names returns a string slice of the field names in the FieldList.
func (l FieldList) Names() []string {
names := make([]string, len(l))
for i, f := range l {
names[i] = f.Name
}
return names
}
// http://facebook.github.io/graphql/draft/#sec-Type-System.Directives
type DirectiveDecl struct {
Name string
Desc string
Locs []string
Args common.InputValueList
}
func (*Scalar) Kind() string { return "SCALAR" }
func (*Object) Kind() string { return "OBJECT" }
func (*Interface) Kind() string { return "INTERFACE" }
func (*Union) Kind() string { return "UNION" }
func (*Enum) Kind() string { return "ENUM" }
func (*InputObject) Kind() string { return "INPUT_OBJECT" }
func (t *Scalar) String() string { return t.Name }
func (t *Object) String() string { return t.Name }
func (t *Interface) String() string { return t.Name }
func (t *Union) String() string { return t.Name }
func (t *Enum) String() string { return t.Name }
func (t *InputObject) String() string { return t.Name }
func (t *Scalar) TypeName() string { return t.Name }
func (t *Object) TypeName() string { return t.Name }
func (t *Interface) TypeName() string { return t.Name }
func (t *Union) TypeName() string { return t.Name }
func (t *Enum) TypeName() string { return t.Name }
func (t *InputObject) TypeName() string { return t.Name }
func (t *Scalar) Description() string { return t.Desc }
func (t *Object) Description() string { return t.Desc }
func (t *Interface) Description() string { return t.Desc }
func (t *Union) Description() string { return t.Desc }
func (t *Enum) Description() string { return t.Desc }
func (t *InputObject) Description() string { return t.Desc }
// Field is a conceptual function which yields values.
// http://facebook.github.io/graphql/draft/#FieldDefinition
type Field struct {
Name string
Args common.InputValueList // NOTE: the spec refers to this as `ArgumentsDefinition`.
Type common.Type
Directives common.DirectiveList
Desc string
}
// New initializes an instance of Schema.
func New() *Schema {
s := &Schema{
entryPointNames: make(map[string]string),
Types: make(map[string]NamedType),
Directives: make(map[string]*DirectiveDecl),
}
for n, t := range Meta.Types {
s.Types[n] = t
}
for n, d := range Meta.Directives {
s.Directives[n] = d
}
return s
}
// Parse the schema string.
func (s *Schema) Parse(schemaString string) error {
l := common.NewLexer(schemaString)
err := l.CatchSyntaxError(func() { parseSchema(s, l) })
if err != nil {
return err
}
for _, t := range s.Types {
if err := resolveNamedType(s, t); err != nil {
return err
}
}
for _, d := range s.Directives {
for _, arg := range d.Args {
t, err := common.ResolveType(arg.Type, s.Resolve)
if err != nil {
return err
}
arg.Type = t
}
}
s.EntryPoints = make(map[string]NamedType)
for key, name := range s.entryPointNames {
t, ok := s.Types[name]
if !ok {
if !ok {
return errors.Errorf("type %q not found", name)
}
}
s.EntryPoints[key] = t
}
for _, obj := range s.objects {
obj.Interfaces = make([]*Interface, len(obj.interfaceNames))
for i, intfName := range obj.interfaceNames {
t, ok := s.Types[intfName]
if !ok {
return errors.Errorf("interface %q not found", intfName)
}
intf, ok := t.(*Interface)
if !ok {
return errors.Errorf("type %q is not an interface", intfName)
}
obj.Interfaces[i] = intf
intf.PossibleTypes = append(intf.PossibleTypes, obj)
}
}
for _, union := range s.unions {
union.PossibleTypes = make([]*Object, len(union.typeNames))
for i, name := range union.typeNames {
t, ok := s.Types[name]
if !ok {
return errors.Errorf("object type %q not found", name)
}
obj, ok := t.(*Object)
if !ok {
return errors.Errorf("type %q is not an object", name)
}
union.PossibleTypes[i] = obj
}
}
for _, enum := range s.enums {
for _, value := range enum.Values {
if err := resolveDirectives(s, value.Directives); err != nil {
return err
}
}
}
return nil
}
func resolveNamedType(s *Schema, t NamedType) error {
switch t := t.(type) {
case *Object:
for _, f := range t.Fields {
if err := resolveField(s, f); err != nil {
return err
}
}
case *Interface:
for _, f := range t.Fields {
if err := resolveField(s, f); err != nil {
return err
}
}
case *InputObject:
if err := resolveInputObject(s, t.Values); err != nil {
return err
}
}
return nil
}
func resolveField(s *Schema, f *Field) error {
t, err := common.ResolveType(f.Type, s.Resolve)
if err != nil {
return err
}
f.Type = t
if err := resolveDirectives(s, f.Directives); err != nil {
return err
}
return resolveInputObject(s, f.Args)
}
func resolveDirectives(s *Schema, directives common.DirectiveList) error {
for _, d := range directives {
dirName := d.Name.Name
dd, ok := s.Directives[dirName]
if !ok {
return errors.Errorf("directive %q not found", dirName)
}
for _, arg := range d.Args {
if dd.Args.Get(arg.Name.Name) == nil {
return errors.Errorf("invalid argument %q for directive %q", arg.Name.Name, dirName)
}
}
for _, arg := range dd.Args {
if _, ok := d.Args.Get(arg.Name.Name); !ok {
d.Args = append(d.Args, common.Argument{Name: arg.Name, Value: arg.Default})
}
}
}
return nil
}
func resolveInputObject(s *Schema, values common.InputValueList) error {
for _, v := range values {
t, err := common.ResolveType(v.Type, s.Resolve)
if err != nil {
return err
}
v.Type = t
}
return nil
}
func parseSchema(s *Schema, l *common.Lexer) {
l.Consume()
for l.Peek() != scanner.EOF {
desc := l.DescComment()
switch x := l.ConsumeIdent(); x {
case "schema":
l.ConsumeToken('{')
for l.Peek() != '}' {
name := l.ConsumeIdent()
l.ConsumeToken(':')
typ := l.ConsumeIdent()
s.entryPointNames[name] = typ
}
l.ConsumeToken('}')
case "type":
obj := parseObjectDef(l)
obj.Desc = desc
s.Types[obj.Name] = obj
s.objects = append(s.objects, obj)
case "interface":
iface := parseInterfaceDef(l)
iface.Desc = desc
s.Types[iface.Name] = iface
case "union":
union := parseUnionDef(l)
union.Desc = desc
s.Types[union.Name] = union
s.unions = append(s.unions, union)
case "enum":
enum := parseEnumDef(l)
enum.Desc = desc
s.Types[enum.Name] = enum
s.enums = append(s.enums, enum)
case "input":
input := parseInputDef(l)
input.Desc = desc
s.Types[input.Name] = input
case "scalar":
name := l.ConsumeIdent()
s.Types[name] = &Scalar{Name: name, Desc: desc}
case "directive":
directive := parseDirectiveDef(l)
directive.Desc = desc
s.Directives[directive.Name] = directive
default:
// TODO: Add support for type extensions.
l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "schema", "type", "enum", "interface", "union", "input", "scalar" or "directive"`, x))
}
}
}
func parseObjectDef(l *common.Lexer) *Object {
object := &Object{Name: l.ConsumeIdent()}
if l.Peek() == scanner.Ident {
l.ConsumeKeyword("implements")
for l.Peek() != '{' {
if l.Peek() == '&' {
l.ConsumeToken('&')
}
object.interfaceNames = append(object.interfaceNames, l.ConsumeIdent())
}
}
l.ConsumeToken('{')
object.Fields = parseFieldsDef(l)
l.ConsumeToken('}')
return object
}
func parseInterfaceDef(l *common.Lexer) *Interface {
i := &Interface{Name: l.ConsumeIdent()}
l.ConsumeToken('{')
i.Fields = parseFieldsDef(l)
l.ConsumeToken('}')
return i
}
func parseUnionDef(l *common.Lexer) *Union {
union := &Union{Name: l.ConsumeIdent()}
l.ConsumeToken('=')
union.typeNames = []string{l.ConsumeIdent()}
for l.Peek() == '|' {
l.ConsumeToken('|')
union.typeNames = append(union.typeNames, l.ConsumeIdent())
}
return union
}
func parseInputDef(l *common.Lexer) *InputObject {
i := &InputObject{}
i.Name = l.ConsumeIdent()
l.ConsumeToken('{')
for l.Peek() != '}' {
i.Values = append(i.Values, common.ParseInputValue(l))
}
l.ConsumeToken('}')
return i
}
func parseEnumDef(l *common.Lexer) *Enum {
enum := &Enum{Name: l.ConsumeIdent()}
l.ConsumeToken('{')
for l.Peek() != '}' {
v := &EnumValue{
Desc: l.DescComment(),
Name: l.ConsumeIdent(),
Directives: common.ParseDirectives(l),
}
enum.Values = append(enum.Values, v)
}
l.ConsumeToken('}')
return enum
}
func parseDirectiveDef(l *common.Lexer) *DirectiveDecl {
l.ConsumeToken('@')
d := &DirectiveDecl{Name: l.ConsumeIdent()}
if l.Peek() == '(' {
l.ConsumeToken('(')
for l.Peek() != ')' {
v := common.ParseInputValue(l)
d.Args = append(d.Args, v)
}
l.ConsumeToken(')')
}
l.ConsumeKeyword("on")
for {
loc := l.ConsumeIdent()
d.Locs = append(d.Locs, loc)
if l.Peek() != '|' {
break
}
l.ConsumeToken('|')
}
return d
}
func parseFieldsDef(l *common.Lexer) FieldList {
var fields FieldList
for l.Peek() != '}' {
f := &Field{}
f.Desc = l.DescComment()
f.Name = l.ConsumeIdent()
if l.Peek() == '(' {
l.ConsumeToken('(')
for l.Peek() != ')' {
f.Args = append(f.Args, common.ParseInputValue(l))
}
l.ConsumeToken(')')
}
l.ConsumeToken(':')
f.Type = common.ParseType(l)
f.Directives = common.ParseDirectives(l)
fields = append(fields, f)
}
return fields
}

View File

@@ -0,0 +1,71 @@
package validation
import (
"fmt"
"sort"
"strconv"
"strings"
)
func makeSuggestion(prefix string, options []string, input string) string {
var selected []string
distances := make(map[string]int)
for _, opt := range options {
distance := levenshteinDistance(input, opt)
threshold := max(len(input)/2, max(len(opt)/2, 1))
if distance < threshold {
selected = append(selected, opt)
distances[opt] = distance
}
}
if len(selected) == 0 {
return ""
}
sort.Slice(selected, func(i, j int) bool {
return distances[selected[i]] < distances[selected[j]]
})
parts := make([]string, len(selected))
for i, opt := range selected {
parts[i] = strconv.Quote(opt)
}
if len(parts) > 1 {
parts[len(parts)-1] = "or " + parts[len(parts)-1]
}
return fmt.Sprintf(" %s %s?", prefix, strings.Join(parts, ", "))
}
func levenshteinDistance(s1, s2 string) int {
column := make([]int, len(s1)+1)
for y := range s1 {
column[y+1] = y + 1
}
for x, rx := range s2 {
column[0] = x + 1
lastdiag := x
for y, ry := range s1 {
olddiag := column[y+1]
if rx != ry {
lastdiag++
}
column[y+1] = min(column[y+1]+1, min(column[y]+1, lastdiag))
lastdiag = olddiag
}
}
return column[len(s1)]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View File

@@ -0,0 +1,909 @@
package validation
import (
"fmt"
"math"
"reflect"
"strconv"
"strings"
"text/scanner"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/query"
"github.com/graph-gophers/graphql-go/internal/schema"
)
type varSet map[*common.InputValue]struct{}
type selectionPair struct{ a, b query.Selection }
type fieldInfo struct {
sf *schema.Field
parent schema.NamedType
}
type context struct {
schema *schema.Schema
doc *query.Document
errs []*errors.QueryError
opErrs map[*query.Operation][]*errors.QueryError
usedVars map[*query.Operation]varSet
fieldMap map[*query.Field]fieldInfo
overlapValidated map[selectionPair]struct{}
maxDepth int
}
func (c *context) addErr(loc errors.Location, rule string, format string, a ...interface{}) {
c.addErrMultiLoc([]errors.Location{loc}, rule, format, a...)
}
func (c *context) addErrMultiLoc(locs []errors.Location, rule string, format string, a ...interface{}) {
c.errs = append(c.errs, &errors.QueryError{
Message: fmt.Sprintf(format, a...),
Locations: locs,
Rule: rule,
})
}
type opContext struct {
*context
ops []*query.Operation
}
func newContext(s *schema.Schema, doc *query.Document, maxDepth int) *context {
return &context{
schema: s,
doc: doc,
opErrs: make(map[*query.Operation][]*errors.QueryError),
usedVars: make(map[*query.Operation]varSet),
fieldMap: make(map[*query.Field]fieldInfo),
overlapValidated: make(map[selectionPair]struct{}),
maxDepth: maxDepth,
}
}
func Validate(s *schema.Schema, doc *query.Document, maxDepth int) []*errors.QueryError {
c := newContext(s, doc, maxDepth)
opNames := make(nameSet)
fragUsedBy := make(map[*query.FragmentDecl][]*query.Operation)
for _, op := range doc.Operations {
c.usedVars[op] = make(varSet)
opc := &opContext{c, []*query.Operation{op}}
// Check if max depth is exceeded, if it's set. If max depth is exceeded,
// don't continue to validate the document and exit early.
if validateMaxDepth(opc, op.Selections, 1) {
return c.errs
}
if op.Name.Name == "" && len(doc.Operations) != 1 {
c.addErr(op.Loc, "LoneAnonymousOperation", "This anonymous operation must be the only defined operation.")
}
if op.Name.Name != "" {
validateName(c, opNames, op.Name, "UniqueOperationNames", "operation")
}
validateDirectives(opc, string(op.Type), op.Directives)
varNames := make(nameSet)
for _, v := range op.Vars {
validateName(c, varNames, v.Name, "UniqueVariableNames", "variable")
t := resolveType(c, v.Type)
if !canBeInput(t) {
c.addErr(v.TypeLoc, "VariablesAreInputTypes", "Variable %q cannot be non-input type %q.", "$"+v.Name.Name, t)
}
if v.Default != nil {
validateLiteral(opc, v.Default)
if t != nil {
if nn, ok := t.(*common.NonNull); ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q is required and will not use the default value. Perhaps you meant to use type %q.", "$"+v.Name.Name, t, nn.OfType)
}
if ok, reason := validateValueType(opc, v.Default, t); !ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q has invalid default value %s.\n%s", "$"+v.Name.Name, t, v.Default, reason)
}
}
}
}
var entryPoint schema.NamedType
switch op.Type {
case query.Query:
entryPoint = s.EntryPoints["query"]
case query.Mutation:
entryPoint = s.EntryPoints["mutation"]
case query.Subscription:
entryPoint = s.EntryPoints["subscription"]
default:
panic("unreachable")
}
validateSelectionSet(opc, op.Selections, entryPoint)
fragUsed := make(map[*query.FragmentDecl]struct{})
markUsedFragments(c, op.Selections, fragUsed)
for frag := range fragUsed {
fragUsedBy[frag] = append(fragUsedBy[frag], op)
}
}
fragNames := make(nameSet)
fragVisited := make(map[*query.FragmentDecl]struct{})
for _, frag := range doc.Fragments {
opc := &opContext{c, fragUsedBy[frag]}
validateName(c, fragNames, frag.Name, "UniqueFragmentNames", "fragment")
validateDirectives(opc, "FRAGMENT_DEFINITION", frag.Directives)
t := unwrapType(resolveType(c, &frag.On))
// continue even if t is nil
if t != nil && !canBeFragment(t) {
c.addErr(frag.On.Loc, "FragmentsOnCompositeTypes", "Fragment %q cannot condition on non composite type %q.", frag.Name.Name, t)
continue
}
validateSelectionSet(opc, frag.Selections, t)
if _, ok := fragVisited[frag]; !ok {
detectFragmentCycle(c, frag.Selections, fragVisited, nil, map[string]int{frag.Name.Name: 0})
}
}
for _, frag := range doc.Fragments {
if len(fragUsedBy[frag]) == 0 {
c.addErr(frag.Loc, "NoUnusedFragments", "Fragment %q is never used.", frag.Name.Name)
}
}
for _, op := range doc.Operations {
c.errs = append(c.errs, c.opErrs[op]...)
opUsedVars := c.usedVars[op]
for _, v := range op.Vars {
if _, ok := opUsedVars[v]; !ok {
opSuffix := ""
if op.Name.Name != "" {
opSuffix = fmt.Sprintf(" in operation %q", op.Name.Name)
}
c.addErr(v.Loc, "NoUnusedVariables", "Variable %q is never used%s.", "$"+v.Name.Name, opSuffix)
}
}
}
return c.errs
}
// validates the query doesn't go deeper than maxDepth (if set). Returns whether
// or not query validated max depth to avoid excessive recursion.
func validateMaxDepth(c *opContext, sels []query.Selection, depth int) bool {
// maxDepth checking is turned off when maxDepth is 0
if c.maxDepth == 0 {
return false
}
exceededMaxDepth := false
for _, sel := range sels {
switch sel := sel.(type) {
case *query.Field:
if depth > c.maxDepth {
exceededMaxDepth = true
c.addErr(sel.Alias.Loc, "MaxDepthExceeded", "Field %q has depth %d that exceeds max depth %d", sel.Name.Name, depth, c.maxDepth)
continue
}
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.Selections, depth+1)
case *query.InlineFragment:
// Depth is not checked because inline fragments resolve to other fields which are checked.
// Depth is not incremented because inline fragments have the same depth as neighboring fields
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.Selections, depth)
case *query.FragmentSpread:
// Depth is not checked because fragments resolve to other fields which are checked.
frag := c.doc.Fragments.Get(sel.Name.Name)
if frag == nil {
// In case of unknown fragment (invalid request), ignore max depth evaluation
c.addErr(sel.Loc, "MaxDepthEvaluationError", "Unknown fragment %q. Unable to evaluate depth.", sel.Name.Name)
continue
}
// Depth is not incremented because fragments have the same depth as surrounding fields
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, frag.Selections, depth)
}
}
return exceededMaxDepth
}
func validateSelectionSet(c *opContext, sels []query.Selection, t schema.NamedType) {
for _, sel := range sels {
validateSelection(c, sel, t)
}
for i, a := range sels {
for _, b := range sels[i+1:] {
c.validateOverlap(a, b, nil, nil)
}
}
}
func validateSelection(c *opContext, sel query.Selection, t schema.NamedType) {
switch sel := sel.(type) {
case *query.Field:
validateDirectives(c, "FIELD", sel.Directives)
fieldName := sel.Name.Name
var f *schema.Field
switch fieldName {
case "__typename":
f = &schema.Field{
Name: "__typename",
Type: c.schema.Types["String"],
}
case "__schema":
f = &schema.Field{
Name: "__schema",
Type: c.schema.Types["__Schema"],
}
case "__type":
f = &schema.Field{
Name: "__type",
Args: common.InputValueList{
&common.InputValue{
Name: common.Ident{Name: "name"},
Type: &common.NonNull{OfType: c.schema.Types["String"]},
},
},
Type: c.schema.Types["__Type"],
}
default:
f = fields(t).Get(fieldName)
if f == nil && t != nil {
suggestion := makeSuggestion("Did you mean", fields(t).Names(), fieldName)
c.addErr(sel.Alias.Loc, "FieldsOnCorrectType", "Cannot query field %q on type %q.%s", fieldName, t, suggestion)
}
}
c.fieldMap[sel] = fieldInfo{sf: f, parent: t}
validateArgumentLiterals(c, sel.Arguments)
if f != nil {
validateArgumentTypes(c, sel.Arguments, f.Args, sel.Alias.Loc,
func() string { return fmt.Sprintf("field %q of type %q", fieldName, t) },
func() string { return fmt.Sprintf("Field %q", fieldName) },
)
}
var ft common.Type
if f != nil {
ft = f.Type
sf := hasSubfields(ft)
if sf && sel.Selections == nil {
c.addErr(sel.Alias.Loc, "ScalarLeafs", "Field %q of type %q must have a selection of subfields. Did you mean \"%s { ... }\"?", fieldName, ft, fieldName)
}
if !sf && sel.Selections != nil {
c.addErr(sel.SelectionSetLoc, "ScalarLeafs", "Field %q must not have a selection since type %q has no subfields.", fieldName, ft)
}
}
if sel.Selections != nil {
validateSelectionSet(c, sel.Selections, unwrapType(ft))
}
case *query.InlineFragment:
validateDirectives(c, "INLINE_FRAGMENT", sel.Directives)
if sel.On.Name != "" {
fragTyp := unwrapType(resolveType(c.context, &sel.On))
if fragTyp != nil && !compatible(t, fragTyp) {
c.addErr(sel.Loc, "PossibleFragmentSpreads", "Fragment cannot be spread here as objects of type %q can never be of type %q.", t, fragTyp)
}
t = fragTyp
// continue even if t is nil
}
if t != nil && !canBeFragment(t) {
c.addErr(sel.On.Loc, "FragmentsOnCompositeTypes", "Fragment cannot condition on non composite type %q.", t)
return
}
validateSelectionSet(c, sel.Selections, unwrapType(t))
case *query.FragmentSpread:
validateDirectives(c, "FRAGMENT_SPREAD", sel.Directives)
frag := c.doc.Fragments.Get(sel.Name.Name)
if frag == nil {
c.addErr(sel.Name.Loc, "KnownFragmentNames", "Unknown fragment %q.", sel.Name.Name)
return
}
fragTyp := c.schema.Types[frag.On.Name]
if !compatible(t, fragTyp) {
c.addErr(sel.Loc, "PossibleFragmentSpreads", "Fragment %q cannot be spread here as objects of type %q can never be of type %q.", frag.Name.Name, t, fragTyp)
}
default:
panic("unreachable")
}
}
func compatible(a, b common.Type) bool {
for _, pta := range possibleTypes(a) {
for _, ptb := range possibleTypes(b) {
if pta == ptb {
return true
}
}
}
return false
}
func possibleTypes(t common.Type) []*schema.Object {
switch t := t.(type) {
case *schema.Object:
return []*schema.Object{t}
case *schema.Interface:
return t.PossibleTypes
case *schema.Union:
return t.PossibleTypes
default:
return nil
}
}
func markUsedFragments(c *context, sels []query.Selection, fragUsed map[*query.FragmentDecl]struct{}) {
for _, sel := range sels {
switch sel := sel.(type) {
case *query.Field:
if sel.Selections != nil {
markUsedFragments(c, sel.Selections, fragUsed)
}
case *query.InlineFragment:
markUsedFragments(c, sel.Selections, fragUsed)
case *query.FragmentSpread:
frag := c.doc.Fragments.Get(sel.Name.Name)
if frag == nil {
return
}
if _, ok := fragUsed[frag]; ok {
return
}
fragUsed[frag] = struct{}{}
markUsedFragments(c, frag.Selections, fragUsed)
default:
panic("unreachable")
}
}
}
func detectFragmentCycle(c *context, sels []query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) {
for _, sel := range sels {
detectFragmentCycleSel(c, sel, fragVisited, spreadPath, spreadPathIndex)
}
}
func detectFragmentCycleSel(c *context, sel query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) {
switch sel := sel.(type) {
case *query.Field:
if sel.Selections != nil {
detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex)
}
case *query.InlineFragment:
detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex)
case *query.FragmentSpread:
frag := c.doc.Fragments.Get(sel.Name.Name)
if frag == nil {
return
}
spreadPath = append(spreadPath, sel)
if i, ok := spreadPathIndex[frag.Name.Name]; ok {
cyclePath := spreadPath[i:]
via := ""
if len(cyclePath) > 1 {
names := make([]string, len(cyclePath)-1)
for i, frag := range cyclePath[:len(cyclePath)-1] {
names[i] = frag.Name.Name
}
via = " via " + strings.Join(names, ", ")
}
locs := make([]errors.Location, len(cyclePath))
for i, frag := range cyclePath {
locs[i] = frag.Loc
}
c.addErrMultiLoc(locs, "NoFragmentCycles", "Cannot spread fragment %q within itself%s.", frag.Name.Name, via)
return
}
if _, ok := fragVisited[frag]; ok {
return
}
fragVisited[frag] = struct{}{}
spreadPathIndex[frag.Name.Name] = len(spreadPath)
detectFragmentCycle(c, frag.Selections, fragVisited, spreadPath, spreadPathIndex)
delete(spreadPathIndex, frag.Name.Name)
default:
panic("unreachable")
}
}
func (c *context) validateOverlap(a, b query.Selection, reasons *[]string, locs *[]errors.Location) {
if a == b {
return
}
if _, ok := c.overlapValidated[selectionPair{a, b}]; ok {
return
}
c.overlapValidated[selectionPair{a, b}] = struct{}{}
c.overlapValidated[selectionPair{b, a}] = struct{}{}
switch a := a.(type) {
case *query.Field:
switch b := b.(type) {
case *query.Field:
if b.Alias.Loc.Before(a.Alias.Loc) {
a, b = b, a
}
if reasons2, locs2 := c.validateFieldOverlap(a, b); len(reasons2) != 0 {
locs2 = append(locs2, a.Alias.Loc, b.Alias.Loc)
if reasons == nil {
c.addErrMultiLoc(locs2, "OverlappingFieldsCanBeMerged", "Fields %q conflict because %s. Use different aliases on the fields to fetch both if this was intentional.", a.Alias.Name, strings.Join(reasons2, " and "))
return
}
for _, r := range reasons2 {
*reasons = append(*reasons, fmt.Sprintf("subfields %q conflict because %s", a.Alias.Name, r))
}
*locs = append(*locs, locs2...)
}
case *query.InlineFragment:
for _, sel := range b.Selections {
c.validateOverlap(a, sel, reasons, locs)
}
case *query.FragmentSpread:
if frag := c.doc.Fragments.Get(b.Name.Name); frag != nil {
for _, sel := range frag.Selections {
c.validateOverlap(a, sel, reasons, locs)
}
}
default:
panic("unreachable")
}
case *query.InlineFragment:
for _, sel := range a.Selections {
c.validateOverlap(sel, b, reasons, locs)
}
case *query.FragmentSpread:
if frag := c.doc.Fragments.Get(a.Name.Name); frag != nil {
for _, sel := range frag.Selections {
c.validateOverlap(sel, b, reasons, locs)
}
}
default:
panic("unreachable")
}
}
func (c *context) validateFieldOverlap(a, b *query.Field) ([]string, []errors.Location) {
if a.Alias.Name != b.Alias.Name {
return nil, nil
}
if asf := c.fieldMap[a].sf; asf != nil {
if bsf := c.fieldMap[b].sf; bsf != nil {
if !typesCompatible(asf.Type, bsf.Type) {
return []string{fmt.Sprintf("they return conflicting types %s and %s", asf.Type, bsf.Type)}, nil
}
}
}
at := c.fieldMap[a].parent
bt := c.fieldMap[b].parent
if at == nil || bt == nil || at == bt {
if a.Name.Name != b.Name.Name {
return []string{fmt.Sprintf("%s and %s are different fields", a.Name.Name, b.Name.Name)}, nil
}
if argumentsConflict(a.Arguments, b.Arguments) {
return []string{"they have differing arguments"}, nil
}
}
var reasons []string
var locs []errors.Location
for _, a2 := range a.Selections {
for _, b2 := range b.Selections {
c.validateOverlap(a2, b2, &reasons, &locs)
}
}
return reasons, locs
}
func argumentsConflict(a, b common.ArgumentList) bool {
if len(a) != len(b) {
return true
}
for _, argA := range a {
valB, ok := b.Get(argA.Name.Name)
if !ok || !reflect.DeepEqual(argA.Value.Value(nil), valB.Value(nil)) {
return true
}
}
return false
}
func fields(t common.Type) schema.FieldList {
switch t := t.(type) {
case *schema.Object:
return t.Fields
case *schema.Interface:
return t.Fields
default:
return nil
}
}
func unwrapType(t common.Type) schema.NamedType {
if t == nil {
return nil
}
for {
switch t2 := t.(type) {
case schema.NamedType:
return t2
case *common.List:
t = t2.OfType
case *common.NonNull:
t = t2.OfType
default:
panic("unreachable")
}
}
}
func resolveType(c *context, t common.Type) common.Type {
t2, err := common.ResolveType(t, c.schema.Resolve)
if err != nil {
c.errs = append(c.errs, err)
}
return t2
}
func validateDirectives(c *opContext, loc string, directives common.DirectiveList) {
directiveNames := make(nameSet)
for _, d := range directives {
dirName := d.Name.Name
validateNameCustomMsg(c.context, directiveNames, d.Name, "UniqueDirectivesPerLocation", func() string {
return fmt.Sprintf("The directive %q can only be used once at this location.", dirName)
})
validateArgumentLiterals(c, d.Args)
dd, ok := c.schema.Directives[dirName]
if !ok {
c.addErr(d.Name.Loc, "KnownDirectives", "Unknown directive %q.", dirName)
continue
}
locOK := false
for _, allowedLoc := range dd.Locs {
if loc == allowedLoc {
locOK = true
break
}
}
if !locOK {
c.addErr(d.Name.Loc, "KnownDirectives", "Directive %q may not be used on %s.", dirName, loc)
}
validateArgumentTypes(c, d.Args, dd.Args, d.Name.Loc,
func() string { return fmt.Sprintf("directive %q", "@"+dirName) },
func() string { return fmt.Sprintf("Directive %q", "@"+dirName) },
)
}
}
type nameSet map[string]errors.Location
func validateName(c *context, set nameSet, name common.Ident, rule string, kind string) {
validateNameCustomMsg(c, set, name, rule, func() string {
return fmt.Sprintf("There can be only one %s named %q.", kind, name.Name)
})
}
func validateNameCustomMsg(c *context, set nameSet, name common.Ident, rule string, msg func() string) {
if loc, ok := set[name.Name]; ok {
c.addErrMultiLoc([]errors.Location{loc, name.Loc}, rule, msg())
return
}
set[name.Name] = name.Loc
}
func validateArgumentTypes(c *opContext, args common.ArgumentList, argDecls common.InputValueList, loc errors.Location, owner1, owner2 func() string) {
for _, selArg := range args {
arg := argDecls.Get(selArg.Name.Name)
if arg == nil {
c.addErr(selArg.Name.Loc, "KnownArgumentNames", "Unknown argument %q on %s.", selArg.Name.Name, owner1())
continue
}
value := selArg.Value
if ok, reason := validateValueType(c, value, arg.Type); !ok {
c.addErr(value.Location(), "ArgumentsOfCorrectType", "Argument %q has invalid value %s.\n%s", arg.Name.Name, value, reason)
}
}
for _, decl := range argDecls {
if _, ok := decl.Type.(*common.NonNull); ok {
if _, ok := args.Get(decl.Name.Name); !ok {
c.addErr(loc, "ProvidedNonNullArguments", "%s argument %q of type %q is required but not provided.", owner2(), decl.Name.Name, decl.Type)
}
}
}
}
func validateArgumentLiterals(c *opContext, args common.ArgumentList) {
argNames := make(nameSet)
for _, arg := range args {
validateName(c.context, argNames, arg.Name, "UniqueArgumentNames", "argument")
validateLiteral(c, arg.Value)
}
}
func validateLiteral(c *opContext, l common.Literal) {
switch l := l.(type) {
case *common.ObjectLit:
fieldNames := make(nameSet)
for _, f := range l.Fields {
validateName(c.context, fieldNames, f.Name, "UniqueInputFieldNames", "input field")
validateLiteral(c, f.Value)
}
case *common.ListLit:
for _, entry := range l.Entries {
validateLiteral(c, entry)
}
case *common.Variable:
for _, op := range c.ops {
v := op.Vars.Get(l.Name)
if v == nil {
byOp := ""
if op.Name.Name != "" {
byOp = fmt.Sprintf(" by operation %q", op.Name.Name)
}
c.opErrs[op] = append(c.opErrs[op], &errors.QueryError{
Message: fmt.Sprintf("Variable %q is not defined%s.", "$"+l.Name, byOp),
Locations: []errors.Location{l.Loc, op.Loc},
Rule: "NoUndefinedVariables",
})
continue
}
c.usedVars[op][v] = struct{}{}
}
}
}
func validateValueType(c *opContext, v common.Literal, t common.Type) (bool, string) {
if v, ok := v.(*common.Variable); ok {
for _, op := range c.ops {
if v2 := op.Vars.Get(v.Name); v2 != nil {
t2, err := common.ResolveType(v2.Type, c.schema.Resolve)
if _, ok := t2.(*common.NonNull); !ok && v2.Default != nil {
t2 = &common.NonNull{OfType: t2}
}
if err == nil && !typeCanBeUsedAs(t2, t) {
c.addErrMultiLoc([]errors.Location{v2.Loc, v.Loc}, "VariablesInAllowedPosition", "Variable %q of type %q used in position expecting type %q.", "$"+v.Name, t2, t)
}
}
}
return true, ""
}
if nn, ok := t.(*common.NonNull); ok {
if isNull(v) {
return false, fmt.Sprintf("Expected %q, found null.", t)
}
t = nn.OfType
}
if isNull(v) {
return true, ""
}
switch t := t.(type) {
case *schema.Scalar, *schema.Enum:
if lit, ok := v.(*common.BasicLit); ok {
if validateBasicLit(lit, t) {
return true, ""
}
}
case *common.List:
list, ok := v.(*common.ListLit)
if !ok {
return validateValueType(c, v, t.OfType) // single value instead of list
}
for i, entry := range list.Entries {
if ok, reason := validateValueType(c, entry, t.OfType); !ok {
return false, fmt.Sprintf("In element #%d: %s", i, reason)
}
}
return true, ""
case *schema.InputObject:
v, ok := v.(*common.ObjectLit)
if !ok {
return false, fmt.Sprintf("Expected %q, found not an object.", t)
}
for _, f := range v.Fields {
name := f.Name.Name
iv := t.Values.Get(name)
if iv == nil {
return false, fmt.Sprintf("In field %q: Unknown field.", name)
}
if ok, reason := validateValueType(c, f.Value, iv.Type); !ok {
return false, fmt.Sprintf("In field %q: %s", name, reason)
}
}
for _, iv := range t.Values {
found := false
for _, f := range v.Fields {
if f.Name.Name == iv.Name.Name {
found = true
break
}
}
if !found {
if _, ok := iv.Type.(*common.NonNull); ok && iv.Default == nil {
return false, fmt.Sprintf("In field %q: Expected %q, found null.", iv.Name.Name, iv.Type)
}
}
}
return true, ""
}
return false, fmt.Sprintf("Expected type %q, found %s.", t, v)
}
func validateBasicLit(v *common.BasicLit, t common.Type) bool {
switch t := t.(type) {
case *schema.Scalar:
switch t.Name {
case "Int":
if v.Type != scanner.Int {
return false
}
f, err := strconv.ParseFloat(v.Text, 64)
if err != nil {
panic(err)
}
return f >= math.MinInt32 && f <= math.MaxInt32
case "Float":
return v.Type == scanner.Int || v.Type == scanner.Float
case "String":
return v.Type == scanner.String
case "Boolean":
return v.Type == scanner.Ident && (v.Text == "true" || v.Text == "false")
case "ID":
return v.Type == scanner.Int || v.Type == scanner.String
default:
//TODO: Type-check against expected type by Unmarshalling
return true
}
case *schema.Enum:
if v.Type != scanner.Ident {
return false
}
for _, option := range t.Values {
if option.Name == v.Text {
return true
}
}
return false
}
return false
}
func canBeFragment(t common.Type) bool {
switch t.(type) {
case *schema.Object, *schema.Interface, *schema.Union:
return true
default:
return false
}
}
func canBeInput(t common.Type) bool {
switch t := t.(type) {
case *schema.InputObject, *schema.Scalar, *schema.Enum:
return true
case *common.List:
return canBeInput(t.OfType)
case *common.NonNull:
return canBeInput(t.OfType)
default:
return false
}
}
func hasSubfields(t common.Type) bool {
switch t := t.(type) {
case *schema.Object, *schema.Interface, *schema.Union:
return true
case *common.List:
return hasSubfields(t.OfType)
case *common.NonNull:
return hasSubfields(t.OfType)
default:
return false
}
}
func isLeaf(t common.Type) bool {
switch t.(type) {
case *schema.Scalar, *schema.Enum:
return true
default:
return false
}
}
func isNull(lit interface{}) bool {
_, ok := lit.(*common.NullLit)
return ok
}
func typesCompatible(a, b common.Type) bool {
al, aIsList := a.(*common.List)
bl, bIsList := b.(*common.List)
if aIsList || bIsList {
return aIsList && bIsList && typesCompatible(al.OfType, bl.OfType)
}
ann, aIsNN := a.(*common.NonNull)
bnn, bIsNN := b.(*common.NonNull)
if aIsNN || bIsNN {
return aIsNN && bIsNN && typesCompatible(ann.OfType, bnn.OfType)
}
if isLeaf(a) || isLeaf(b) {
return a == b
}
return true
}
func typeCanBeUsedAs(t, as common.Type) bool {
nnT, okT := t.(*common.NonNull)
if okT {
t = nnT.OfType
}
nnAs, okAs := as.(*common.NonNull)
if okAs {
as = nnAs.OfType
if !okT {
return false // nullable can not be used as non-null
}
}
if t == as {
return true
}
if lT, ok := t.(*common.List); ok {
if lAs, ok := as.(*common.List); ok {
return typeCanBeUsedAs(lT.OfType, lAs.OfType)
}
}
return false
}