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

25
vendor/github.com/graph-gophers/graphql-go/Gopkg.lock generated vendored Normal file
View File

@ -0,0 +1,25 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"ext",
"log"
]
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context"]
revision = "f5dfe339be1d06f81b22525fe34671ee7d2c8904"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "f417062128566756a9360b1c13ada79bdeeb6bab1f53ee9147a3328d95c1653f"
solver-name = "gps-cdcl"
solver-version = 1

10
vendor/github.com/graph-gophers/graphql-go/Gopkg.toml generated vendored Normal file
View File

@ -0,0 +1,10 @@
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
[[constraint]]
name = "github.com/opentracing/opentracing-go"
version = "1.0.2"
[prune]
go-tests = true
unused-packages = true

24
vendor/github.com/graph-gophers/graphql-go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,24 @@
Copyright (c) 2016 Richard Musiol. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

100
vendor/github.com/graph-gophers/graphql-go/README.md generated vendored Normal file
View File

@ -0,0 +1,100 @@
# graphql-go [![Sourcegraph](https://sourcegraph.com/github.com/graph-gophers/graphql-go/-/badge.svg)](https://sourcegraph.com/github.com/graph-gophers/graphql-go?badge) [![Build Status](https://semaphoreci.com/api/v1/graph-gophers/graphql-go/branches/master/badge.svg)](https://semaphoreci.com/graph-gophers/graphql-go) [![GoDoc](https://godoc.org/github.com/graph-gophers/graphql-go?status.svg)](https://godoc.org/github.com/graph-gophers/graphql-go)
<p align="center"><img src="docs/img/logo.png" width="300"></p>
The goal of this project is to provide full support of the [GraphQL draft specification](https://facebook.github.io/graphql/draft) with a set of idiomatic, easy to use Go packages.
While still under heavy development (`internal` APIs are almost certainly subject to change), this library is
safe for production use.
## Features
- minimal API
- support for `context.Context`
- support for the `OpenTracing` standard
- schema type-checking against resolvers
- resolvers are matched to the schema based on method sets (can resolve a GraphQL schema with a Go interface or Go struct).
- handles panics in resolvers
- parallel execution of resolvers
## Roadmap
We're trying out the GitHub Project feature to manage `graphql-go`'s [development roadmap](https://github.com/graph-gophers/graphql-go/projects/1).
Feedback is welcome and appreciated.
## (Some) Documentation
### Basic Sample
```go
package main
import (
"log"
"net/http"
graphql "github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
)
type query struct{}
func (_ *query) Hello() string { return "Hello, world!" }
func main() {
s := `
schema {
query: Query
}
type Query {
hello: String!
}
`
schema := graphql.MustParseSchema(s, &query{})
http.Handle("/query", &relay.Handler{Schema: schema})
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
To test:
```sh
$ curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query
```
### Resolvers
A resolver must have one method for each field of the GraphQL type it resolves. The method name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the field's name in a non-case-sensitive way.
The method has up to two arguments:
- Optional `context.Context` argument.
- Mandatory `*struct { ... }` argument if the corresponding GraphQL field has arguments. The names of the struct fields have to be [exported](https://golang.org/ref/spec#Exported_identifiers) and have to match the names of the GraphQL arguments in a non-case-sensitive way.
The method has up to two results:
- The GraphQL field's value as determined by the resolver.
- Optional `error` result.
Example for a simple resolver method:
```go
func (r *helloWorldResolver) Hello() string {
return "Hello world!"
}
```
The following signature is also allowed:
```go
func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) {
return "Hello world!", nil
}
```
### Community Examples
[tonyghita/graphql-go-example](https://github.com/tonyghita/graphql-go-example) - A more "productionized" version of the Star Wars API example given in this repository.
[deltaskelta/graphql-go-pets-example](https://github.com/deltaskelta/graphql-go-pets-example) - graphql-go resolving against a sqlite database
[OscarYuen/go-graphql-starter](https://github.com/OscarYuen/go-graphql-starter) - a starter application integrated with dataloader, psql and basic authentication

View File

@ -0,0 +1,41 @@
package errors
import (
"fmt"
)
type QueryError struct {
Message string `json:"message"`
Locations []Location `json:"locations,omitempty"`
Path []interface{} `json:"path,omitempty"`
Rule string `json:"-"`
ResolverError error `json:"-"`
}
type Location struct {
Line int `json:"line"`
Column int `json:"column"`
}
func (a Location) Before(b Location) bool {
return a.Line < b.Line || (a.Line == b.Line && a.Column < b.Column)
}
func Errorf(format string, a ...interface{}) *QueryError {
return &QueryError{
Message: fmt.Sprintf(format, a...),
}
}
func (err *QueryError) Error() string {
if err == nil {
return "<nil>"
}
str := fmt.Sprintf("graphql: %s", err.Message)
for _, loc := range err.Locations {
str += fmt.Sprintf(" (line %d, column %d)", loc.Line, loc.Column)
}
return str
}
var _ error = &QueryError{}

205
vendor/github.com/graph-gophers/graphql-go/graphql.go generated vendored Normal file
View File

@ -0,0 +1,205 @@
package graphql
import (
"context"
"fmt"
"encoding/json"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/exec"
"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/internal/validation"
"github.com/graph-gophers/graphql-go/introspection"
"github.com/graph-gophers/graphql-go/log"
"github.com/graph-gophers/graphql-go/trace"
)
// ParseSchema parses a GraphQL schema and attaches the given root resolver. It returns an error if
// the Go type signature of the resolvers does not match the schema. If nil is passed as the
// resolver, then the schema can not be executed, but it may be inspected (e.g. with ToJSON).
func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (*Schema, error) {
s := &Schema{
schema: schema.New(),
maxParallelism: 10,
tracer: trace.OpenTracingTracer{},
validationTracer: trace.NoopValidationTracer{},
logger: &log.DefaultLogger{},
}
for _, opt := range opts {
opt(s)
}
if err := s.schema.Parse(schemaString); err != nil {
return nil, err
}
if resolver != nil {
r, err := resolvable.ApplyResolver(s.schema, resolver)
if err != nil {
return nil, err
}
s.res = r
}
return s, nil
}
// MustParseSchema calls ParseSchema and panics on error.
func MustParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) *Schema {
s, err := ParseSchema(schemaString, resolver, opts...)
if err != nil {
panic(err)
}
return s
}
// Schema represents a GraphQL schema with an optional resolver.
type Schema struct {
schema *schema.Schema
res *resolvable.Schema
maxDepth int
maxParallelism int
tracer trace.Tracer
validationTracer trace.ValidationTracer
logger log.Logger
}
// SchemaOpt is an option to pass to ParseSchema or MustParseSchema.
type SchemaOpt func(*Schema)
// MaxDepth specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking.
func MaxDepth(n int) SchemaOpt {
return func(s *Schema) {
s.maxDepth = n
}
}
// MaxParallelism specifies the maximum number of resolvers per request allowed to run in parallel. The default is 10.
func MaxParallelism(n int) SchemaOpt {
return func(s *Schema) {
s.maxParallelism = n
}
}
// Tracer is used to trace queries and fields. It defaults to trace.OpenTracingTracer.
func Tracer(tracer trace.Tracer) SchemaOpt {
return func(s *Schema) {
s.tracer = tracer
}
}
// ValidationTracer is used to trace validation errors. It defaults to trace.NoopValidationTracer.
func ValidationTracer(tracer trace.ValidationTracer) SchemaOpt {
return func(s *Schema) {
s.validationTracer = tracer
}
}
// Logger is used to log panics during query execution. It defaults to exec.DefaultLogger.
func Logger(logger log.Logger) SchemaOpt {
return func(s *Schema) {
s.logger = logger
}
}
// Response represents a typical response of a GraphQL server. It may be encoded to JSON directly or
// it may be further processed to a custom response type, for example to include custom error data.
// Errors are intentionally serialized first based on the advice in https://github.com/facebook/graphql/commit/7b40390d48680b15cb93e02d46ac5eb249689876#diff-757cea6edf0288677a9eea4cfc801d87R107
type Response struct {
Errors []*errors.QueryError `json:"errors,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
// Validate validates the given query with the schema.
func (s *Schema) Validate(queryString string) []*errors.QueryError {
doc, qErr := query.Parse(queryString)
if qErr != nil {
return []*errors.QueryError{qErr}
}
return validation.Validate(s.schema, doc, s.maxDepth)
}
// Exec executes the given query with the schema's resolver. It panics if the schema was created
// without a resolver. If the context get cancelled, no further resolvers will be called and a
// the context error will be returned as soon as possible (not immediately).
func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response {
if s.res == nil {
panic("schema created without resolver, can not exec")
}
return s.exec(ctx, queryString, operationName, variables, s.res)
}
func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response {
doc, qErr := query.Parse(queryString)
if qErr != nil {
return &Response{Errors: []*errors.QueryError{qErr}}
}
validationFinish := s.validationTracer.TraceValidation()
errs := validation.Validate(s.schema, doc, s.maxDepth)
validationFinish(errs)
if len(errs) != 0 {
return &Response{Errors: errs}
}
op, err := getOperation(doc, operationName)
if err != nil {
return &Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}}
}
r := &exec.Request{
Request: selected.Request{
Doc: doc,
Vars: variables,
Schema: s.schema,
},
Limiter: make(chan struct{}, s.maxParallelism),
Tracer: s.tracer,
Logger: s.logger,
}
varTypes := make(map[string]*introspection.Type)
for _, v := range op.Vars {
t, err := common.ResolveType(v.Type, s.schema.Resolve)
if err != nil {
return &Response{Errors: []*errors.QueryError{err}}
}
varTypes[v.Name.Name] = introspection.WrapType(t)
}
traceCtx, finish := s.tracer.TraceQuery(ctx, queryString, operationName, variables, varTypes)
data, errs := r.Execute(traceCtx, res, op)
finish(errs)
return &Response{
Data: data,
Errors: errs,
}
}
func getOperation(document *query.Document, operationName string) (*query.Operation, error) {
if len(document.Operations) == 0 {
return nil, fmt.Errorf("no operations in query document")
}
if operationName == "" {
if len(document.Operations) > 1 {
return nil, fmt.Errorf("more than one operation in query document and no operation name given")
}
for _, op := range document.Operations {
return op, nil // return the one and only operation
}
}
op := document.Operations.Get(operationName)
if op == nil {
return nil, fmt.Errorf("no operation with name %q", operationName)
}
return op, nil
}

30
vendor/github.com/graph-gophers/graphql-go/id.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package graphql
import (
"errors"
"strconv"
)
// ID represents GraphQL's "ID" scalar type. A custom type may be used instead.
type ID string
func (ID) ImplementsGraphQLType(name string) bool {
return name == "ID"
}
func (id *ID) UnmarshalGraphQL(input interface{}) error {
var err error
switch input := input.(type) {
case string:
*id = ID(input)
case int32:
*id = ID(strconv.Itoa(int(input)))
default:
err = errors.New("wrong type")
}
return err
}
func (id ID) MarshalJSON() ([]byte, error) {
return strconv.AppendQuote(nil, string(id)), nil
}

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
}

View File

@ -0,0 +1,117 @@
package graphql
import (
"context"
"encoding/json"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/introspection"
)
// Inspect allows inspection of the given schema.
func (s *Schema) Inspect() *introspection.Schema {
return introspection.WrapSchema(s.schema)
}
// ToJSON encodes the schema in a JSON format used by tools like Relay.
func (s *Schema) ToJSON() ([]byte, error) {
result := s.exec(context.Background(), introspectionQuery, "", nil, &resolvable.Schema{
Query: &resolvable.Object{},
Schema: *s.schema,
})
if len(result.Errors) != 0 {
panic(result.Errors[0])
}
return json.MarshalIndent(result.Data, "", "\t")
}
var introspectionQuery = `
query {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
`

View File

@ -0,0 +1,313 @@
package introspection
import (
"sort"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/schema"
)
type Schema struct {
schema *schema.Schema
}
// WrapSchema is only used internally.
func WrapSchema(schema *schema.Schema) *Schema {
return &Schema{schema}
}
func (r *Schema) Types() []*Type {
var names []string
for name := range r.schema.Types {
names = append(names, name)
}
sort.Strings(names)
l := make([]*Type, len(names))
for i, name := range names {
l[i] = &Type{r.schema.Types[name]}
}
return l
}
func (r *Schema) Directives() []*Directive {
var names []string
for name := range r.schema.Directives {
names = append(names, name)
}
sort.Strings(names)
l := make([]*Directive, len(names))
for i, name := range names {
l[i] = &Directive{r.schema.Directives[name]}
}
return l
}
func (r *Schema) QueryType() *Type {
t, ok := r.schema.EntryPoints["query"]
if !ok {
return nil
}
return &Type{t}
}
func (r *Schema) MutationType() *Type {
t, ok := r.schema.EntryPoints["mutation"]
if !ok {
return nil
}
return &Type{t}
}
func (r *Schema) SubscriptionType() *Type {
t, ok := r.schema.EntryPoints["subscription"]
if !ok {
return nil
}
return &Type{t}
}
type Type struct {
typ common.Type
}
// WrapType is only used internally.
func WrapType(typ common.Type) *Type {
return &Type{typ}
}
func (r *Type) Kind() string {
return r.typ.Kind()
}
func (r *Type) Name() *string {
if named, ok := r.typ.(schema.NamedType); ok {
name := named.TypeName()
return &name
}
return nil
}
func (r *Type) Description() *string {
if named, ok := r.typ.(schema.NamedType); ok {
desc := named.Description()
if desc == "" {
return nil
}
return &desc
}
return nil
}
func (r *Type) Fields(args *struct{ IncludeDeprecated bool }) *[]*Field {
var fields schema.FieldList
switch t := r.typ.(type) {
case *schema.Object:
fields = t.Fields
case *schema.Interface:
fields = t.Fields
default:
return nil
}
var l []*Field
for _, f := range fields {
if d := f.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated {
l = append(l, &Field{f})
}
}
return &l
}
func (r *Type) Interfaces() *[]*Type {
t, ok := r.typ.(*schema.Object)
if !ok {
return nil
}
l := make([]*Type, len(t.Interfaces))
for i, intf := range t.Interfaces {
l[i] = &Type{intf}
}
return &l
}
func (r *Type) PossibleTypes() *[]*Type {
var possibleTypes []*schema.Object
switch t := r.typ.(type) {
case *schema.Interface:
possibleTypes = t.PossibleTypes
case *schema.Union:
possibleTypes = t.PossibleTypes
default:
return nil
}
l := make([]*Type, len(possibleTypes))
for i, intf := range possibleTypes {
l[i] = &Type{intf}
}
return &l
}
func (r *Type) EnumValues(args *struct{ IncludeDeprecated bool }) *[]*EnumValue {
t, ok := r.typ.(*schema.Enum)
if !ok {
return nil
}
var l []*EnumValue
for _, v := range t.Values {
if d := v.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated {
l = append(l, &EnumValue{v})
}
}
return &l
}
func (r *Type) InputFields() *[]*InputValue {
t, ok := r.typ.(*schema.InputObject)
if !ok {
return nil
}
l := make([]*InputValue, len(t.Values))
for i, v := range t.Values {
l[i] = &InputValue{v}
}
return &l
}
func (r *Type) OfType() *Type {
switch t := r.typ.(type) {
case *common.List:
return &Type{t.OfType}
case *common.NonNull:
return &Type{t.OfType}
default:
return nil
}
}
type Field struct {
field *schema.Field
}
func (r *Field) Name() string {
return r.field.Name
}
func (r *Field) Description() *string {
if r.field.Desc == "" {
return nil
}
return &r.field.Desc
}
func (r *Field) Args() []*InputValue {
l := make([]*InputValue, len(r.field.Args))
for i, v := range r.field.Args {
l[i] = &InputValue{v}
}
return l
}
func (r *Field) Type() *Type {
return &Type{r.field.Type}
}
func (r *Field) IsDeprecated() bool {
return r.field.Directives.Get("deprecated") != nil
}
func (r *Field) DeprecationReason() *string {
d := r.field.Directives.Get("deprecated")
if d == nil {
return nil
}
reason := d.Args.MustGet("reason").Value(nil).(string)
return &reason
}
type InputValue struct {
value *common.InputValue
}
func (r *InputValue) Name() string {
return r.value.Name.Name
}
func (r *InputValue) Description() *string {
if r.value.Desc == "" {
return nil
}
return &r.value.Desc
}
func (r *InputValue) Type() *Type {
return &Type{r.value.Type}
}
func (r *InputValue) DefaultValue() *string {
if r.value.Default == nil {
return nil
}
s := r.value.Default.String()
return &s
}
type EnumValue struct {
value *schema.EnumValue
}
func (r *EnumValue) Name() string {
return r.value.Name
}
func (r *EnumValue) Description() *string {
if r.value.Desc == "" {
return nil
}
return &r.value.Desc
}
func (r *EnumValue) IsDeprecated() bool {
return r.value.Directives.Get("deprecated") != nil
}
func (r *EnumValue) DeprecationReason() *string {
d := r.value.Directives.Get("deprecated")
if d == nil {
return nil
}
reason := d.Args.MustGet("reason").Value(nil).(string)
return &reason
}
type Directive struct {
directive *schema.DirectiveDecl
}
func (r *Directive) Name() string {
return r.directive.Name
}
func (r *Directive) Description() *string {
if r.directive.Desc == "" {
return nil
}
return &r.directive.Desc
}
func (r *Directive) Locations() []string {
return r.directive.Locs
}
func (r *Directive) Args() []*InputValue {
l := make([]*InputValue, len(r.directive.Args))
for i, v := range r.directive.Args {
l[i] = &InputValue{v}
}
return l
}

23
vendor/github.com/graph-gophers/graphql-go/log/log.go generated vendored Normal file
View File

@ -0,0 +1,23 @@
package log
import (
"context"
"log"
"runtime"
)
// Logger is the interface used to log panics that occur during query execution. It is settable via graphql.ParseSchema
type Logger interface {
LogPanic(ctx context.Context, value interface{})
}
// DefaultLogger is the default logger used to log panics that occur during query execution
type DefaultLogger struct{}
// LogPanic is used to log recovered panic values that occur during query execution
func (l *DefaultLogger) LogPanic(_ context.Context, value interface{}) {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("graphql: panic occurred: %v\n%s", value, buf)
}

View File

@ -0,0 +1,70 @@
package relay
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
graphql "github.com/graph-gophers/graphql-go"
)
func MarshalID(kind string, spec interface{}) graphql.ID {
d, err := json.Marshal(spec)
if err != nil {
panic(fmt.Errorf("relay.MarshalID: %s", err))
}
return graphql.ID(base64.URLEncoding.EncodeToString(append([]byte(kind+":"), d...)))
}
func UnmarshalKind(id graphql.ID) string {
s, err := base64.URLEncoding.DecodeString(string(id))
if err != nil {
return ""
}
i := strings.IndexByte(string(s), ':')
if i == -1 {
return ""
}
return string(s[:i])
}
func UnmarshalSpec(id graphql.ID, v interface{}) error {
s, err := base64.URLEncoding.DecodeString(string(id))
if err != nil {
return err
}
i := strings.IndexByte(string(s), ':')
if i == -1 {
return errors.New("invalid graphql.ID")
}
return json.Unmarshal([]byte(s[i+1:]), v)
}
type Handler struct {
Schema *graphql.Schema
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables)
responseJSON, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseJSON)
}

51
vendor/github.com/graph-gophers/graphql-go/time.go generated vendored Normal file
View File

@ -0,0 +1,51 @@
package graphql
import (
"encoding/json"
"fmt"
"time"
)
// Time is a custom GraphQL type to represent an instant in time. It has to be added to a schema
// via "scalar Time" since it is not a predeclared GraphQL type like "ID".
type Time struct {
time.Time
}
// ImplementsGraphQLType maps this custom Go type
// to the graphql scalar type in the schema.
func (Time) ImplementsGraphQLType(name string) bool {
return name == "Time"
}
// UnmarshalGraphQL is a custom unmarshaler for Time
//
// This function will be called whenever you use the
// time scalar as an input
func (t *Time) UnmarshalGraphQL(input interface{}) error {
switch input := input.(type) {
case time.Time:
t.Time = input
return nil
case string:
var err error
t.Time, err = time.Parse(time.RFC3339, input)
return err
case int:
t.Time = time.Unix(int64(input), 0)
return nil
case float64:
t.Time = time.Unix(int64(input), 0)
return nil
default:
return fmt.Errorf("wrong type")
}
}
// MarshalJSON is a custom marshaler for Time
//
// This function will be called whenever you
// query for fields that use the Time type
func (t Time) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Time)
}

View File

@ -0,0 +1,80 @@
package trace
import (
"context"
"fmt"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/introspection"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
)
type TraceQueryFinishFunc func([]*errors.QueryError)
type TraceFieldFinishFunc func(*errors.QueryError)
type Tracer interface {
TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc)
TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc)
}
type OpenTracingTracer struct{}
func (OpenTracingTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) {
span, spanCtx := opentracing.StartSpanFromContext(ctx, "GraphQL request")
span.SetTag("graphql.query", queryString)
if operationName != "" {
span.SetTag("graphql.operationName", operationName)
}
if len(variables) != 0 {
span.LogFields(log.Object("graphql.variables", variables))
}
return spanCtx, func(errs []*errors.QueryError) {
if len(errs) > 0 {
msg := errs[0].Error()
if len(errs) > 1 {
msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1)
}
ext.Error.Set(span, true)
span.SetTag("graphql.error", msg)
}
span.Finish()
}
}
func (OpenTracingTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) {
if trivial {
return ctx, noop
}
span, spanCtx := opentracing.StartSpanFromContext(ctx, label)
span.SetTag("graphql.type", typeName)
span.SetTag("graphql.field", fieldName)
for name, value := range args {
span.SetTag("graphql.args."+name, value)
}
return spanCtx, func(err *errors.QueryError) {
if err != nil {
ext.Error.Set(span, true)
span.SetTag("graphql.error", err.Error())
}
span.Finish()
}
}
func noop(*errors.QueryError) {}
type NoopTracer struct{}
func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) {
return ctx, func(errs []*errors.QueryError) {}
}
func (NoopTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) {
return ctx, func(err *errors.QueryError) {}
}

View File

@ -0,0 +1,17 @@
package trace
import (
"github.com/graph-gophers/graphql-go/errors"
)
type TraceValidationFinishFunc = TraceQueryFinishFunc
type ValidationTracer interface {
TraceValidation() TraceValidationFinishFunc
}
type NoopValidationTracer struct{}
func (NoopValidationTracer) TraceValidation() TraceValidationFinishFunc {
return func(errs []*errors.QueryError) {}
}