├── internal ├── tests │ ├── empty.go │ ├── testdata │ │ ├── gen.go │ │ ├── LICENSE │ │ └── export.js │ └── all_test.go ├── common │ ├── directive.go │ ├── values.go │ ├── types.go │ ├── lexer.go │ └── literals.go ├── exec │ ├── resolvable │ │ ├── meta.go │ │ └── resolvable.go │ ├── selected │ │ └── selected.go │ ├── exec.go │ └── packer │ │ └── packer.go ├── validation │ ├── suggestion.go │ └── validation.go ├── query │ └── query.go └── schema │ ├── meta.go │ └── schema.go ├── .gitignore ├── id.go ├── log └── log.go ├── time.go ├── errors └── errors.go ├── relay ├── relay_test.go └── relay.go ├── LICENSE ├── gqltesting └── testing.go ├── example └── starwars │ ├── server │ └── server.go │ └── starwars.go ├── README.md ├── introspection.go ├── trace └── trace.go ├── graphql.go ├── introspection └── introspection.go └── graphql_test.go /internal/tests/empty.go: -------------------------------------------------------------------------------- 1 | package tests 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /internal/tests/testdata/graphql-js 2 | -------------------------------------------------------------------------------- /internal/tests/testdata/gen.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | //go:generate cp export.js graphql-js/export.js 4 | //go:generate babel-node graphql-js/export.js 5 | -------------------------------------------------------------------------------- /id.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | ) 7 | 8 | // ID represents GraphQL's "ID" scalar type. A custom type may be used instead. 9 | type ID string 10 | 11 | func (_ ID) ImplementsGraphQLType(name string) bool { 12 | return name == "ID" 13 | } 14 | 15 | func (id *ID) UnmarshalGraphQL(input interface{}) error { 16 | var err error 17 | switch input := input.(type) { 18 | case string: 19 | *id = ID(input) 20 | case int32: 21 | *id = ID(strconv.Itoa(int(input))) 22 | default: 23 | err = errors.New("wrong type") 24 | } 25 | return err 26 | } 27 | 28 | func (id ID) MarshalJSON() ([]byte, error) { 29 | return strconv.AppendQuote(nil, string(id)), nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/common/directive.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type Directive struct { 4 | Name Ident 5 | Args ArgumentList 6 | } 7 | 8 | func ParseDirectives(l *Lexer) DirectiveList { 9 | var directives DirectiveList 10 | for l.Peek() == '@' { 11 | l.ConsumeToken('@') 12 | d := &Directive{} 13 | d.Name = l.ConsumeIdentWithLoc() 14 | d.Name.Loc.Column-- 15 | if l.Peek() == '(' { 16 | d.Args = ParseArguments(l) 17 | } 18 | directives = append(directives, d) 19 | } 20 | return directives 21 | } 22 | 23 | type DirectiveList []*Directive 24 | 25 | func (l DirectiveList) Get(name string) *Directive { 26 | for _, d := range l { 27 | if d.Name.Name == name { 28 | return d 29 | } 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "runtime" 7 | ) 8 | 9 | // Logger is the interface used to log panics that occur durring query execution. It is setable via graphql.ParseSchema 10 | type Logger interface { 11 | LogPanic(ctx context.Context, value interface{}) 12 | } 13 | 14 | // DefaultLogger is the default logger used to log panics that occur durring query execution 15 | type DefaultLogger struct{} 16 | 17 | // LogPanic is used to log recovered panic values that occur durring query execution 18 | func (l *DefaultLogger) LogPanic(_ context.Context, value interface{}) { 19 | const size = 64 << 10 20 | buf := make([]byte, size) 21 | buf = buf[:runtime.Stack(buf, false)] 22 | log.Printf("graphql: panic occurred: %v\n%s", value, buf) 23 | } 24 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Time is a custom GraphQL type to represent an instant in time. It has to be added to a schema 9 | // via "scalar Time" since it is not a predeclared GraphQL type like "ID". 10 | type Time struct { 11 | time.Time 12 | } 13 | 14 | func (_ Time) ImplementsGraphQLType(name string) bool { 15 | return name == "Time" 16 | } 17 | 18 | func (t *Time) UnmarshalGraphQL(input interface{}) error { 19 | switch input := input.(type) { 20 | case time.Time: 21 | t.Time = input 22 | return nil 23 | case string: 24 | var err error 25 | t.Time, err = time.Parse(time.RFC3339, input) 26 | return err 27 | case int: 28 | t.Time = time.Unix(int64(input), 0) 29 | return nil 30 | case float64: 31 | t.Time = time.Unix(int64(input), 0) 32 | return nil 33 | default: 34 | return fmt.Errorf("wrong type") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type QueryError struct { 8 | Message string `json:"message"` 9 | Locations []Location `json:"locations,omitempty"` 10 | Path []interface{} `json:"path,omitempty"` 11 | Rule string `json:"-"` 12 | ResolverError error `json:"-"` 13 | } 14 | 15 | type Location struct { 16 | Line int `json:"line"` 17 | Column int `json:"column"` 18 | } 19 | 20 | func (a Location) Before(b Location) bool { 21 | return a.Line < b.Line || (a.Line == b.Line && a.Column < b.Column) 22 | } 23 | 24 | func Errorf(format string, a ...interface{}) *QueryError { 25 | return &QueryError{ 26 | Message: fmt.Sprintf(format, a...), 27 | } 28 | } 29 | 30 | func (err *QueryError) Error() string { 31 | if err == nil { 32 | return "" 33 | } 34 | str := fmt.Sprintf("graphql: %s", err.Message) 35 | for _, loc := range err.Locations { 36 | str += fmt.Sprintf(" (line %d, column %d)", loc.Line, loc.Column) 37 | } 38 | return str 39 | } 40 | 41 | var _ error = &QueryError{} 42 | -------------------------------------------------------------------------------- /relay/relay_test.go: -------------------------------------------------------------------------------- 1 | package relay_test 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/neelance/graphql-go" 9 | "github.com/neelance/graphql-go/example/starwars" 10 | "github.com/neelance/graphql-go/relay" 11 | ) 12 | 13 | var starwarsSchema = graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{}) 14 | 15 | func TestServeHTTP(t *testing.T) { 16 | w := httptest.NewRecorder() 17 | r := httptest.NewRequest("POST", "/some/path/here", strings.NewReader(`{"query":"{ hero { name } }", "operationName":"", "variables": null}`)) 18 | h := relay.Handler{Schema: starwarsSchema} 19 | 20 | h.ServeHTTP(w, r) 21 | 22 | if w.Code != 200 { 23 | t.Fatalf("Expected status code 200, got %d.", w.Code) 24 | } 25 | 26 | contentType := w.Header().Get("Content-Type") 27 | if contentType != "application/json" { 28 | t.Fatalf("Invalid content-type. Expected [application/json], but instead got [%s]", contentType) 29 | } 30 | 31 | expectedResponse := `{"data":{"hero":{"name":"R2-D2"}}}` 32 | actualResponse := w.Body.String() 33 | if expectedResponse != actualResponse { 34 | t.Fatalf("Invalid response. Expected [%s], but instead got [%s]", expectedResponse, actualResponse) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Richard Musiol. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /internal/exec/resolvable/meta.go: -------------------------------------------------------------------------------- 1 | package resolvable 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/neelance/graphql-go/internal/common" 8 | "github.com/neelance/graphql-go/internal/schema" 9 | "github.com/neelance/graphql-go/introspection" 10 | ) 11 | 12 | var MetaSchema *Object 13 | var MetaType *Object 14 | 15 | func init() { 16 | var err error 17 | b := newBuilder(schema.Meta) 18 | 19 | metaSchema := schema.Meta.Types["__Schema"].(*schema.Object) 20 | MetaSchema, err = b.makeObjectExec(metaSchema.Name, metaSchema.Fields, nil, false, reflect.TypeOf(&introspection.Schema{})) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | metaType := schema.Meta.Types["__Type"].(*schema.Object) 26 | MetaType, err = b.makeObjectExec(metaType.Name, metaType.Fields, nil, false, reflect.TypeOf(&introspection.Type{})) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | if err := b.finish(); err != nil { 32 | panic(err) 33 | } 34 | } 35 | 36 | var MetaFieldTypename = Field{ 37 | Field: schema.Field{ 38 | Name: "__typename", 39 | Type: &common.NonNull{OfType: schema.Meta.Types["String"]}, 40 | }, 41 | TraceLabel: fmt.Sprintf("GraphQL field: __typename"), 42 | } 43 | 44 | var MetaFieldSchema = Field{ 45 | Field: schema.Field{ 46 | Name: "__schema", 47 | Type: schema.Meta.Types["__Schema"], 48 | }, 49 | TraceLabel: fmt.Sprintf("GraphQL field: __schema"), 50 | } 51 | 52 | var MetaFieldType = Field{ 53 | Field: schema.Field{ 54 | Name: "__type", 55 | Type: schema.Meta.Types["__Type"], 56 | }, 57 | TraceLabel: fmt.Sprintf("GraphQL field: __type"), 58 | } 59 | -------------------------------------------------------------------------------- /internal/validation/suggestion.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func makeSuggestion(prefix string, options []string, input string) string { 11 | var selected []string 12 | distances := make(map[string]int) 13 | for _, opt := range options { 14 | distance := levenshteinDistance(input, opt) 15 | threshold := max(len(input)/2, max(len(opt)/2, 1)) 16 | if distance < threshold { 17 | selected = append(selected, opt) 18 | distances[opt] = distance 19 | } 20 | } 21 | 22 | if len(selected) == 0 { 23 | return "" 24 | } 25 | sort.Slice(selected, func(i, j int) bool { 26 | return distances[selected[i]] < distances[selected[j]] 27 | }) 28 | 29 | parts := make([]string, len(selected)) 30 | for i, opt := range selected { 31 | parts[i] = strconv.Quote(opt) 32 | } 33 | if len(parts) > 1 { 34 | parts[len(parts)-1] = "or " + parts[len(parts)-1] 35 | } 36 | return fmt.Sprintf(" %s %s?", prefix, strings.Join(parts, ", ")) 37 | } 38 | 39 | func levenshteinDistance(s1, s2 string) int { 40 | column := make([]int, len(s1)+1) 41 | for y := range s1 { 42 | column[y+1] = y + 1 43 | } 44 | for x, rx := range s2 { 45 | column[0] = x + 1 46 | lastdiag := x 47 | for y, ry := range s1 { 48 | olddiag := column[y+1] 49 | if rx != ry { 50 | lastdiag++ 51 | } 52 | column[y+1] = min(column[y+1]+1, min(column[y]+1, lastdiag)) 53 | lastdiag = olddiag 54 | } 55 | } 56 | return column[len(s1)] 57 | } 58 | 59 | func min(a, b int) int { 60 | if a < b { 61 | return a 62 | } 63 | return b 64 | } 65 | 66 | func max(a, b int) int { 67 | if a > b { 68 | return a 69 | } 70 | return b 71 | } 72 | -------------------------------------------------------------------------------- /gqltesting/testing.go: -------------------------------------------------------------------------------- 1 | package gqltesting 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "strconv" 8 | "testing" 9 | 10 | graphql "github.com/neelance/graphql-go" 11 | ) 12 | 13 | // Test is a GraphQL test case to be used with RunTest(s). 14 | type Test struct { 15 | Context context.Context 16 | Schema *graphql.Schema 17 | Query string 18 | OperationName string 19 | Variables map[string]interface{} 20 | ExpectedResult string 21 | } 22 | 23 | // RunTests runs the given GraphQL test cases as subtests. 24 | func RunTests(t *testing.T, tests []*Test) { 25 | if len(tests) == 1 { 26 | RunTest(t, tests[0]) 27 | return 28 | } 29 | 30 | for i, test := range tests { 31 | t.Run(strconv.Itoa(i+1), func(t *testing.T) { 32 | RunTest(t, test) 33 | }) 34 | } 35 | } 36 | 37 | // RunTest runs a single GraphQL test case. 38 | func RunTest(t *testing.T, test *Test) { 39 | if test.Context == nil { 40 | test.Context = context.Background() 41 | } 42 | result := test.Schema.Exec(test.Context, test.Query, test.OperationName, test.Variables) 43 | if len(result.Errors) != 0 { 44 | t.Fatal(result.Errors[0]) 45 | } 46 | got := formatJSON(t, result.Data) 47 | 48 | want := formatJSON(t, []byte(test.ExpectedResult)) 49 | 50 | if !bytes.Equal(got, want) { 51 | t.Logf("got: %s", got) 52 | t.Logf("want: %s", want) 53 | t.Fail() 54 | } 55 | } 56 | 57 | func formatJSON(t *testing.T, data []byte) []byte { 58 | var v interface{} 59 | if err := json.Unmarshal(data, &v); err != nil { 60 | t.Fatalf("invalid JSON: %s", err) 61 | } 62 | formatted, err := json.Marshal(v) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | return formatted 67 | } 68 | -------------------------------------------------------------------------------- /internal/common/values.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/neelance/graphql-go/errors" 5 | ) 6 | 7 | type InputValue struct { 8 | Name Ident 9 | Type Type 10 | Default Literal 11 | Desc string 12 | Loc errors.Location 13 | TypeLoc errors.Location 14 | } 15 | 16 | type InputValueList []*InputValue 17 | 18 | func (l InputValueList) Get(name string) *InputValue { 19 | for _, v := range l { 20 | if v.Name.Name == name { 21 | return v 22 | } 23 | } 24 | return nil 25 | } 26 | 27 | func ParseInputValue(l *Lexer) *InputValue { 28 | p := &InputValue{} 29 | p.Loc = l.Location() 30 | p.Desc = l.DescComment() 31 | p.Name = l.ConsumeIdentWithLoc() 32 | l.ConsumeToken(':') 33 | p.TypeLoc = l.Location() 34 | p.Type = ParseType(l) 35 | if l.Peek() == '=' { 36 | l.ConsumeToken('=') 37 | p.Default = ParseLiteral(l, true) 38 | } 39 | return p 40 | } 41 | 42 | type Argument struct { 43 | Name Ident 44 | Value Literal 45 | } 46 | 47 | type ArgumentList []Argument 48 | 49 | func (l ArgumentList) Get(name string) (Literal, bool) { 50 | for _, arg := range l { 51 | if arg.Name.Name == name { 52 | return arg.Value, true 53 | } 54 | } 55 | return nil, false 56 | } 57 | 58 | func (l ArgumentList) MustGet(name string) Literal { 59 | value, ok := l.Get(name) 60 | if !ok { 61 | panic("argument not found") 62 | } 63 | return value 64 | } 65 | 66 | func ParseArguments(l *Lexer) ArgumentList { 67 | var args ArgumentList 68 | l.ConsumeToken('(') 69 | for l.Peek() != ')' { 70 | name := l.ConsumeIdentWithLoc() 71 | l.ConsumeToken(':') 72 | value := ParseLiteral(l, false) 73 | args = append(args, Argument{Name: name, Value: value}) 74 | } 75 | l.ConsumeToken(')') 76 | return args 77 | } 78 | -------------------------------------------------------------------------------- /internal/tests/testdata/LICENSE: -------------------------------------------------------------------------------- 1 | The files in this testdata directory are derived from the graphql-js project: 2 | https://github.com/graphql/graphql-js 3 | 4 | BSD License 5 | 6 | For GraphQL software 7 | 8 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without modification, 11 | are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | * Neither the name Facebook nor the names of its contributors may be used to 21 | endorse or promote products derived from this software without specific 22 | prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 28 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 31 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /internal/tests/all_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "sort" 7 | "testing" 8 | 9 | "encoding/json" 10 | 11 | "github.com/neelance/graphql-go/errors" 12 | "github.com/neelance/graphql-go/internal/query" 13 | "github.com/neelance/graphql-go/internal/schema" 14 | "github.com/neelance/graphql-go/internal/validation" 15 | ) 16 | 17 | type Test struct { 18 | Name string 19 | Rule string 20 | Schema int 21 | Query string 22 | Errors []*errors.QueryError 23 | } 24 | 25 | func TestAll(t *testing.T) { 26 | f, err := os.Open("testdata/tests.json") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | var testData struct { 32 | Schemas []string 33 | Tests []*Test 34 | } 35 | if err := json.NewDecoder(f).Decode(&testData); err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | schemas := make([]*schema.Schema, len(testData.Schemas)) 40 | for i, schemaStr := range testData.Schemas { 41 | schemas[i] = schema.New() 42 | if err := schemas[i].Parse(schemaStr); err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | 47 | for _, test := range testData.Tests { 48 | t.Run(test.Name, func(t *testing.T) { 49 | d, err := query.Parse(test.Query) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | errs := validation.Validate(schemas[test.Schema], d) 54 | got := []*errors.QueryError{} 55 | for _, err := range errs { 56 | if err.Rule == test.Rule { 57 | err.Rule = "" 58 | got = append(got, err) 59 | } 60 | } 61 | sortLocations(test.Errors) 62 | sortLocations(got) 63 | if !reflect.DeepEqual(test.Errors, got) { 64 | t.Errorf("wrong errors\nexpected: %v\ngot: %v", test.Errors, got) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func sortLocations(errs []*errors.QueryError) { 71 | for _, err := range errs { 72 | locs := err.Locations 73 | sort.Slice(locs, func(i, j int) bool { return locs[i].Before(locs[j]) }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /relay/relay.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | graphql "github.com/neelance/graphql-go" 12 | ) 13 | 14 | func MarshalID(kind string, spec interface{}) graphql.ID { 15 | d, err := json.Marshal(spec) 16 | if err != nil { 17 | panic(fmt.Errorf("relay.MarshalID: %s", err)) 18 | } 19 | return graphql.ID(base64.URLEncoding.EncodeToString(append([]byte(kind+":"), d...))) 20 | } 21 | 22 | func UnmarshalKind(id graphql.ID) string { 23 | s, err := base64.URLEncoding.DecodeString(string(id)) 24 | if err != nil { 25 | return "" 26 | } 27 | i := strings.IndexByte(string(s), ':') 28 | if i == -1 { 29 | return "" 30 | } 31 | return string(s[:i]) 32 | } 33 | 34 | func UnmarshalSpec(id graphql.ID, v interface{}) error { 35 | s, err := base64.URLEncoding.DecodeString(string(id)) 36 | if err != nil { 37 | return err 38 | } 39 | i := strings.IndexByte(string(s), ':') 40 | if i == -1 { 41 | return errors.New("invalid graphql.ID") 42 | } 43 | return json.Unmarshal([]byte(s[i+1:]), v) 44 | } 45 | 46 | type Handler struct { 47 | Schema *graphql.Schema 48 | } 49 | 50 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 51 | var params struct { 52 | Query string `json:"query"` 53 | OperationName string `json:"operationName"` 54 | Variables map[string]interface{} `json:"variables"` 55 | } 56 | if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { 57 | http.Error(w, err.Error(), http.StatusBadRequest) 58 | return 59 | } 60 | 61 | response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables) 62 | responseJSON, err := json.Marshal(response) 63 | if err != nil { 64 | http.Error(w, err.Error(), http.StatusInternalServerError) 65 | return 66 | } 67 | 68 | w.Header().Set("Content-Type", "application/json") 69 | w.Write(responseJSON) 70 | } 71 | -------------------------------------------------------------------------------- /example/starwars/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/neelance/graphql-go" 8 | "github.com/neelance/graphql-go/example/starwars" 9 | "github.com/neelance/graphql-go/relay" 10 | ) 11 | 12 | var schema *graphql.Schema 13 | 14 | func init() { 15 | schema = graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{}) 16 | } 17 | 18 | func main() { 19 | http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | w.Write(page) 21 | })) 22 | 23 | http.Handle("/query", &relay.Handler{Schema: schema}) 24 | 25 | log.Fatal(http.ListenAndServe(":8080", nil)) 26 | } 27 | 28 | var page = []byte(` 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
Loading...
40 | 62 | 63 | 64 | `) 65 | -------------------------------------------------------------------------------- /internal/common/types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/neelance/graphql-go/errors" 5 | ) 6 | 7 | type Type interface { 8 | Kind() string 9 | String() string 10 | } 11 | 12 | type List struct { 13 | OfType Type 14 | } 15 | 16 | type NonNull struct { 17 | OfType Type 18 | } 19 | 20 | type TypeName struct { 21 | Ident 22 | } 23 | 24 | func (*List) Kind() string { return "LIST" } 25 | func (*NonNull) Kind() string { return "NON_NULL" } 26 | func (*TypeName) Kind() string { panic("TypeName needs to be resolved to actual type") } 27 | 28 | func (t *List) String() string { return "[" + t.OfType.String() + "]" } 29 | func (t *NonNull) String() string { return t.OfType.String() + "!" } 30 | func (*TypeName) String() string { panic("TypeName needs to be resolved to actual type") } 31 | 32 | func ParseType(l *Lexer) Type { 33 | t := parseNullType(l) 34 | if l.Peek() == '!' { 35 | l.ConsumeToken('!') 36 | return &NonNull{OfType: t} 37 | } 38 | return t 39 | } 40 | 41 | func parseNullType(l *Lexer) Type { 42 | if l.Peek() == '[' { 43 | l.ConsumeToken('[') 44 | ofType := ParseType(l) 45 | l.ConsumeToken(']') 46 | return &List{OfType: ofType} 47 | } 48 | 49 | return &TypeName{Ident: l.ConsumeIdentWithLoc()} 50 | } 51 | 52 | type Resolver func(name string) Type 53 | 54 | func ResolveType(t Type, resolver Resolver) (Type, *errors.QueryError) { 55 | switch t := t.(type) { 56 | case *List: 57 | ofType, err := ResolveType(t.OfType, resolver) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return &List{OfType: ofType}, nil 62 | case *NonNull: 63 | ofType, err := ResolveType(t.OfType, resolver) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return &NonNull{OfType: ofType}, nil 68 | case *TypeName: 69 | refT := resolver(t.Name) 70 | if refT == nil { 71 | err := errors.Errorf("Unknown type %q.", t.Name) 72 | err.Rule = "KnownTypeNames" 73 | err.Locations = []errors.Location{t.Loc} 74 | return nil, err 75 | } 76 | return refT, nil 77 | default: 78 | return t, nil 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-go 2 | 3 | [![Sourcegraph](https://sourcegraph.com/github.com/neelance/graphql-go/-/badge.svg)](https://sourcegraph.com/github.com/neelance/graphql-go?badge) 4 | [![Build Status](https://semaphoreci.com/api/v1/neelance/graphql-go/branches/master/badge.svg)](https://semaphoreci.com/neelance/graphql-go) 5 | [![GoDoc](https://godoc.org/github.com/neelance/graphql-go?status.svg)](https://godoc.org/github.com/neelance/graphql-go) 6 | 7 | ## Status 8 | 9 | The project is under heavy development. It is stable enough so we use it in production at [Sourcegraph](https://sourcegraph.com), but expect changes. 10 | 11 | ## Goals 12 | 13 | * [ ] full support of [GraphQL spec (October 2016)](https://facebook.github.io/graphql/) 14 | * [ ] propagation of `null` on resolver errors 15 | * [x] everything else 16 | * [x] minimal API 17 | * [x] support for context.Context and OpenTracing 18 | * [x] early error detection at application startup by type-checking if the given resolver matches the schema 19 | * [x] resolvers are purely based on method sets (e.g. it's up to you if you want to resolve a GraphQL interface with a Go interface or a Go struct) 20 | * [ ] nice error messages (no internal panics, even with an invalid schema or resolver; please file a bug if you see an internal panic) 21 | * [x] nice errors on resolver validation 22 | * [ ] nice errors on all invalid schemas 23 | * [ ] nice errors on all invalid queries 24 | * [x] panic handling (a panic in a resolver should not take down the whole app) 25 | * [x] parallel execution of resolvers 26 | 27 | ## (Some) Documentation 28 | 29 | ### Resolvers 30 | 31 | 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. 32 | 33 | The method has up to two arguments: 34 | 35 | - Optional `context.Context` argument. 36 | - 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. 37 | 38 | The method has up to two results: 39 | 40 | - The GraphQL field's value as determined by the resolver. 41 | - Optional `error` result. 42 | 43 | Example for a simple resolver method: 44 | 45 | ```go 46 | func (r *helloWorldResolver) Hello() string { 47 | return "Hello world!" 48 | } 49 | ``` 50 | 51 | The following signature is also allowed: 52 | 53 | ```go 54 | func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) { 55 | return "Hello world!", nil 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /introspection.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/neelance/graphql-go/internal/exec/resolvable" 8 | "github.com/neelance/graphql-go/introspection" 9 | ) 10 | 11 | // Inspect allows inspection of the given schema. 12 | func (s *Schema) Inspect() *introspection.Schema { 13 | return introspection.WrapSchema(s.schema) 14 | } 15 | 16 | // ToJSON encodes the schema in a JSON format used by tools like Relay. 17 | func (s *Schema) ToJSON() ([]byte, error) { 18 | result := s.exec(context.Background(), introspectionQuery, "", nil, &resolvable.Schema{ 19 | Query: &resolvable.Object{}, 20 | Schema: *s.schema, 21 | }) 22 | if len(result.Errors) != 0 { 23 | panic(result.Errors[0]) 24 | } 25 | return json.MarshalIndent(result.Data, "", "\t") 26 | } 27 | 28 | var introspectionQuery = ` 29 | query { 30 | __schema { 31 | queryType { name } 32 | mutationType { name } 33 | subscriptionType { name } 34 | types { 35 | ...FullType 36 | } 37 | directives { 38 | name 39 | description 40 | locations 41 | args { 42 | ...InputValue 43 | } 44 | } 45 | } 46 | } 47 | fragment FullType on __Type { 48 | kind 49 | name 50 | description 51 | fields(includeDeprecated: true) { 52 | name 53 | description 54 | args { 55 | ...InputValue 56 | } 57 | type { 58 | ...TypeRef 59 | } 60 | isDeprecated 61 | deprecationReason 62 | } 63 | inputFields { 64 | ...InputValue 65 | } 66 | interfaces { 67 | ...TypeRef 68 | } 69 | enumValues(includeDeprecated: true) { 70 | name 71 | description 72 | isDeprecated 73 | deprecationReason 74 | } 75 | possibleTypes { 76 | ...TypeRef 77 | } 78 | } 79 | fragment InputValue on __InputValue { 80 | name 81 | description 82 | type { ...TypeRef } 83 | defaultValue 84 | } 85 | fragment TypeRef on __Type { 86 | kind 87 | name 88 | ofType { 89 | kind 90 | name 91 | ofType { 92 | kind 93 | name 94 | ofType { 95 | kind 96 | name 97 | ofType { 98 | kind 99 | name 100 | ofType { 101 | kind 102 | name 103 | ofType { 104 | kind 105 | name 106 | ofType { 107 | kind 108 | name 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | ` 118 | -------------------------------------------------------------------------------- /internal/common/lexer.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "text/scanner" 6 | 7 | "github.com/neelance/graphql-go/errors" 8 | ) 9 | 10 | type syntaxError string 11 | 12 | type Lexer struct { 13 | sc *scanner.Scanner 14 | next rune 15 | descComment string 16 | } 17 | 18 | type Ident struct { 19 | Name string 20 | Loc errors.Location 21 | } 22 | 23 | func New(sc *scanner.Scanner) *Lexer { 24 | l := &Lexer{sc: sc} 25 | l.Consume() 26 | return l 27 | } 28 | 29 | func (l *Lexer) CatchSyntaxError(f func()) (errRes *errors.QueryError) { 30 | defer func() { 31 | if err := recover(); err != nil { 32 | if err, ok := err.(syntaxError); ok { 33 | errRes = errors.Errorf("syntax error: %s", err) 34 | errRes.Locations = []errors.Location{l.Location()} 35 | return 36 | } 37 | panic(err) 38 | } 39 | }() 40 | 41 | f() 42 | return 43 | } 44 | 45 | func (l *Lexer) Peek() rune { 46 | return l.next 47 | } 48 | 49 | func (l *Lexer) Consume() { 50 | l.descComment = "" 51 | for { 52 | l.next = l.sc.Scan() 53 | if l.next == ',' { 54 | continue 55 | } 56 | if l.next == '#' { 57 | if l.sc.Peek() == ' ' { 58 | l.sc.Next() 59 | } 60 | if l.descComment != "" { 61 | l.descComment += "\n" 62 | } 63 | for { 64 | next := l.sc.Next() 65 | if next == '\n' || next == scanner.EOF { 66 | break 67 | } 68 | l.descComment += string(next) 69 | } 70 | continue 71 | } 72 | break 73 | } 74 | } 75 | 76 | func (l *Lexer) ConsumeIdent() string { 77 | name := l.sc.TokenText() 78 | l.ConsumeToken(scanner.Ident) 79 | return name 80 | } 81 | 82 | func (l *Lexer) ConsumeIdentWithLoc() Ident { 83 | loc := l.Location() 84 | name := l.sc.TokenText() 85 | l.ConsumeToken(scanner.Ident) 86 | return Ident{name, loc} 87 | } 88 | 89 | func (l *Lexer) ConsumeKeyword(keyword string) { 90 | if l.next != scanner.Ident || l.sc.TokenText() != keyword { 91 | l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %q", l.sc.TokenText(), keyword)) 92 | } 93 | l.Consume() 94 | } 95 | 96 | func (l *Lexer) ConsumeLiteral() *BasicLit { 97 | lit := &BasicLit{Type: l.next, Text: l.sc.TokenText()} 98 | l.Consume() 99 | return lit 100 | } 101 | 102 | func (l *Lexer) ConsumeToken(expected rune) { 103 | if l.next != expected { 104 | l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %s", l.sc.TokenText(), scanner.TokenString(expected))) 105 | } 106 | l.Consume() 107 | } 108 | 109 | func (l *Lexer) DescComment() string { 110 | return l.descComment 111 | } 112 | 113 | func (l *Lexer) SyntaxError(message string) { 114 | panic(syntaxError(message)) 115 | } 116 | 117 | func (l *Lexer) Location() errors.Location { 118 | return errors.Location{ 119 | Line: l.sc.Line, 120 | Column: l.sc.Column, 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /trace/trace.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/neelance/graphql-go/errors" 8 | "github.com/neelance/graphql-go/introspection" 9 | opentracing "github.com/opentracing/opentracing-go" 10 | "github.com/opentracing/opentracing-go/ext" 11 | "github.com/opentracing/opentracing-go/log" 12 | ) 13 | 14 | type TraceQueryFinishFunc func([]*errors.QueryError) 15 | type TraceFieldFinishFunc func(*errors.QueryError) 16 | 17 | type Tracer interface { 18 | TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) 19 | TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) 20 | } 21 | 22 | type OpenTracingTracer struct{} 23 | 24 | func (OpenTracingTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) { 25 | span, spanCtx := opentracing.StartSpanFromContext(ctx, "GraphQL request") 26 | span.SetTag("graphql.query", queryString) 27 | 28 | if operationName != "" { 29 | span.SetTag("graphql.operationName", operationName) 30 | } 31 | 32 | if len(variables) != 0 { 33 | span.LogFields(log.Object("graphql.variables", variables)) 34 | } 35 | 36 | return spanCtx, func(errs []*errors.QueryError) { 37 | if len(errs) > 0 { 38 | msg := errs[0].Error() 39 | if len(errs) > 1 { 40 | msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1) 41 | } 42 | ext.Error.Set(span, true) 43 | span.SetTag("graphql.error", msg) 44 | } 45 | span.Finish() 46 | } 47 | } 48 | 49 | func (OpenTracingTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { 50 | if trivial { 51 | return ctx, noop 52 | } 53 | 54 | span, spanCtx := opentracing.StartSpanFromContext(ctx, label) 55 | span.SetTag("graphql.type", typeName) 56 | span.SetTag("graphql.field", fieldName) 57 | for name, value := range args { 58 | span.SetTag("graphql.args."+name, value) 59 | } 60 | 61 | return spanCtx, func(err *errors.QueryError) { 62 | if err != nil { 63 | ext.Error.Set(span, true) 64 | span.SetTag("graphql.error", err.Error()) 65 | } 66 | span.Finish() 67 | } 68 | } 69 | 70 | func noop(*errors.QueryError) {} 71 | 72 | type NoopTracer struct{} 73 | 74 | func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) { 75 | return ctx, func(errs []*errors.QueryError) {} 76 | } 77 | 78 | func (NoopTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { 79 | return ctx, func(err *errors.QueryError) {} 80 | } 81 | -------------------------------------------------------------------------------- /internal/tests/testdata/export.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Module from 'module'; 3 | import { testSchema } from './src/validation/__tests__/harness'; 4 | import { printSchema } from './src/utilities'; 5 | 6 | let schemas = []; 7 | function registerSchema(schema) { 8 | for (let i = 0; i < schemas.length; i++) { 9 | if (schemas[i] == schema) { 10 | return i; 11 | } 12 | } 13 | schemas.push(schema); 14 | return schemas.length - 1; 15 | } 16 | 17 | const harness = { 18 | expectPassesRule(rule, queryString) { 19 | harness.expectPassesRuleWithSchema(testSchema, rule, queryString); 20 | }, 21 | expectPassesRuleWithSchema(schema, rule, queryString, errors) { 22 | tests.push({ 23 | name: names.join('/'), 24 | rule: rule.name, 25 | schema: registerSchema(schema), 26 | query: queryString, 27 | errors: [], 28 | }); 29 | }, 30 | expectFailsRule(rule, queryString, errors) { 31 | harness.expectFailsRuleWithSchema(testSchema, rule, queryString, errors); 32 | }, 33 | expectFailsRuleWithSchema(schema, rule, queryString, errors) { 34 | tests.push({ 35 | name: names.join('/'), 36 | rule: rule.name, 37 | schema: registerSchema(schema), 38 | query: queryString, 39 | errors: errors, 40 | }); 41 | } 42 | }; 43 | 44 | let tests = []; 45 | let names = [] 46 | const fakeModules = { 47 | 'mocha': { 48 | describe(name, f) { 49 | switch (name) { 50 | case 'within schema language': 51 | return; 52 | } 53 | names.push(name); 54 | f(); 55 | names.pop(); 56 | }, 57 | it(name, f) { 58 | switch (name) { 59 | case 'ignores type definitions': 60 | case 'reports correctly when a non-exclusive follows an exclusive': 61 | case 'disallows differing subfields': 62 | return; 63 | } 64 | names.push(name); 65 | f(); 66 | names.pop(); 67 | }, 68 | }, 69 | './harness': harness, 70 | }; 71 | 72 | const originalLoader = Module._load; 73 | Module._load = function(request, parent, isMain) { 74 | return fakeModules[request] || originalLoader(request, parent, isMain); 75 | }; 76 | 77 | require('./src/validation/__tests__/ArgumentsOfCorrectType-test'); 78 | require('./src/validation/__tests__/DefaultValuesOfCorrectType-test'); 79 | require('./src/validation/__tests__/FieldsOnCorrectType-test'); 80 | require('./src/validation/__tests__/FragmentsOnCompositeTypes-test'); 81 | require('./src/validation/__tests__/KnownArgumentNames-test'); 82 | require('./src/validation/__tests__/KnownDirectives-test'); 83 | require('./src/validation/__tests__/KnownFragmentNames-test'); 84 | require('./src/validation/__tests__/KnownTypeNames-test'); 85 | require('./src/validation/__tests__/LoneAnonymousOperation-test'); 86 | require('./src/validation/__tests__/NoFragmentCycles-test'); 87 | require('./src/validation/__tests__/NoUndefinedVariables-test'); 88 | require('./src/validation/__tests__/NoUnusedFragments-test'); 89 | require('./src/validation/__tests__/NoUnusedVariables-test'); 90 | require('./src/validation/__tests__/OverlappingFieldsCanBeMerged-test'); 91 | require('./src/validation/__tests__/PossibleFragmentSpreads-test'); 92 | require('./src/validation/__tests__/ProvidedNonNullArguments-test'); 93 | require('./src/validation/__tests__/ScalarLeafs-test'); 94 | require('./src/validation/__tests__/UniqueArgumentNames-test'); 95 | require('./src/validation/__tests__/UniqueDirectivesPerLocation-test'); 96 | require('./src/validation/__tests__/UniqueFragmentNames-test'); 97 | require('./src/validation/__tests__/UniqueInputFieldNames-test'); 98 | require('./src/validation/__tests__/UniqueOperationNames-test'); 99 | require('./src/validation/__tests__/UniqueVariableNames-test'); 100 | require('./src/validation/__tests__/VariablesAreInputTypes-test'); 101 | require('./src/validation/__tests__/VariablesInAllowedPosition-test'); 102 | 103 | let output = JSON.stringify({ 104 | schemas: schemas.map(s => printSchema(s)), 105 | tests: tests, 106 | }, null, 2) 107 | output = output.replace(' Did you mean to use an inline fragment on \\"Dog\\" or \\"Cat\\"?', ''); 108 | output = output.replace(' Did you mean to use an inline fragment on \\"Being\\", \\"Pet\\", \\"Canine\\", \\"Dog\\", or \\"Cat\\"?', ''); 109 | output = output.replace(' Did you mean \\"Pet\\"?', ''); 110 | fs.writeFileSync("tests.json", output); 111 | -------------------------------------------------------------------------------- /internal/common/literals.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "text/scanner" 7 | 8 | "github.com/neelance/graphql-go/errors" 9 | ) 10 | 11 | type Literal interface { 12 | Value(vars map[string]interface{}) interface{} 13 | String() string 14 | Location() errors.Location 15 | } 16 | 17 | type BasicLit struct { 18 | Type rune 19 | Text string 20 | Loc errors.Location 21 | } 22 | 23 | func (lit *BasicLit) Value(vars map[string]interface{}) interface{} { 24 | switch lit.Type { 25 | case scanner.Int: 26 | value, err := strconv.ParseInt(lit.Text, 10, 32) 27 | if err != nil { 28 | panic(err) 29 | } 30 | return int32(value) 31 | 32 | case scanner.Float: 33 | value, err := strconv.ParseFloat(lit.Text, 64) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return value 38 | 39 | case scanner.String: 40 | value, err := strconv.Unquote(lit.Text) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return value 45 | 46 | case scanner.Ident: 47 | switch lit.Text { 48 | case "true": 49 | return true 50 | case "false": 51 | return false 52 | default: 53 | return lit.Text 54 | } 55 | 56 | default: 57 | panic("invalid literal") 58 | } 59 | } 60 | 61 | func (lit *BasicLit) String() string { 62 | return lit.Text 63 | } 64 | 65 | func (lit *BasicLit) Location() errors.Location { 66 | return lit.Loc 67 | } 68 | 69 | type ListLit struct { 70 | Entries []Literal 71 | Loc errors.Location 72 | } 73 | 74 | func (lit *ListLit) Value(vars map[string]interface{}) interface{} { 75 | entries := make([]interface{}, len(lit.Entries)) 76 | for i, entry := range lit.Entries { 77 | entries[i] = entry.Value(vars) 78 | } 79 | return entries 80 | } 81 | 82 | func (lit *ListLit) String() string { 83 | entries := make([]string, len(lit.Entries)) 84 | for i, entry := range lit.Entries { 85 | entries[i] = entry.String() 86 | } 87 | return "[" + strings.Join(entries, ", ") + "]" 88 | } 89 | 90 | func (lit *ListLit) Location() errors.Location { 91 | return lit.Loc 92 | } 93 | 94 | type ObjectLit struct { 95 | Fields []*ObjectLitField 96 | Loc errors.Location 97 | } 98 | 99 | type ObjectLitField struct { 100 | Name Ident 101 | Value Literal 102 | } 103 | 104 | func (lit *ObjectLit) Value(vars map[string]interface{}) interface{} { 105 | fields := make(map[string]interface{}, len(lit.Fields)) 106 | for _, f := range lit.Fields { 107 | fields[f.Name.Name] = f.Value.Value(vars) 108 | } 109 | return fields 110 | } 111 | 112 | func (lit *ObjectLit) String() string { 113 | entries := make([]string, 0, len(lit.Fields)) 114 | for _, f := range lit.Fields { 115 | entries = append(entries, f.Name.Name+": "+f.Value.String()) 116 | } 117 | return "{" + strings.Join(entries, ", ") + "}" 118 | } 119 | 120 | func (lit *ObjectLit) Location() errors.Location { 121 | return lit.Loc 122 | } 123 | 124 | type NullLit struct { 125 | Loc errors.Location 126 | } 127 | 128 | func (lit *NullLit) Value(vars map[string]interface{}) interface{} { 129 | return nil 130 | } 131 | 132 | func (lit *NullLit) String() string { 133 | return "null" 134 | } 135 | 136 | func (lit *NullLit) Location() errors.Location { 137 | return lit.Loc 138 | } 139 | 140 | type Variable struct { 141 | Name string 142 | Loc errors.Location 143 | } 144 | 145 | func (v Variable) Value(vars map[string]interface{}) interface{} { 146 | return vars[v.Name] 147 | } 148 | 149 | func (v Variable) String() string { 150 | return "$" + v.Name 151 | } 152 | 153 | func (v *Variable) Location() errors.Location { 154 | return v.Loc 155 | } 156 | 157 | func ParseLiteral(l *Lexer, constOnly bool) Literal { 158 | loc := l.Location() 159 | switch l.Peek() { 160 | case '$': 161 | if constOnly { 162 | l.SyntaxError("variable not allowed") 163 | panic("unreachable") 164 | } 165 | l.ConsumeToken('$') 166 | return &Variable{l.ConsumeIdent(), loc} 167 | 168 | case scanner.Int, scanner.Float, scanner.String, scanner.Ident: 169 | lit := l.ConsumeLiteral() 170 | if lit.Type == scanner.Ident && lit.Text == "null" { 171 | return &NullLit{loc} 172 | } 173 | lit.Loc = loc 174 | return lit 175 | case '-': 176 | l.ConsumeToken('-') 177 | lit := l.ConsumeLiteral() 178 | lit.Text = "-" + lit.Text 179 | lit.Loc = loc 180 | return lit 181 | case '[': 182 | l.ConsumeToken('[') 183 | var list []Literal 184 | for l.Peek() != ']' { 185 | list = append(list, ParseLiteral(l, constOnly)) 186 | } 187 | l.ConsumeToken(']') 188 | return &ListLit{list, loc} 189 | 190 | case '{': 191 | l.ConsumeToken('{') 192 | var fields []*ObjectLitField 193 | for l.Peek() != '}' { 194 | name := l.ConsumeIdentWithLoc() 195 | l.ConsumeToken(':') 196 | value := ParseLiteral(l, constOnly) 197 | fields = append(fields, &ObjectLitField{name, value}) 198 | } 199 | l.ConsumeToken('}') 200 | return &ObjectLit{fields, loc} 201 | 202 | default: 203 | l.SyntaxError("invalid value") 204 | panic("unreachable") 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /internal/query/query.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "text/scanner" 7 | 8 | "github.com/neelance/graphql-go/errors" 9 | "github.com/neelance/graphql-go/internal/common" 10 | ) 11 | 12 | type Document struct { 13 | Operations OperationList 14 | Fragments FragmentList 15 | } 16 | 17 | type OperationList []*Operation 18 | 19 | func (l OperationList) Get(name string) *Operation { 20 | for _, f := range l { 21 | if f.Name.Name == name { 22 | return f 23 | } 24 | } 25 | return nil 26 | } 27 | 28 | type FragmentList []*FragmentDecl 29 | 30 | func (l FragmentList) Get(name string) *FragmentDecl { 31 | for _, f := range l { 32 | if f.Name.Name == name { 33 | return f 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | type Operation struct { 40 | Type OperationType 41 | Name common.Ident 42 | Vars common.InputValueList 43 | Selections []Selection 44 | Directives common.DirectiveList 45 | Loc errors.Location 46 | } 47 | 48 | type OperationType string 49 | 50 | const ( 51 | Query OperationType = "QUERY" 52 | Mutation = "MUTATION" 53 | Subscription = "SUBSCRIPTION" 54 | ) 55 | 56 | type Fragment struct { 57 | On common.TypeName 58 | Selections []Selection 59 | } 60 | 61 | type FragmentDecl struct { 62 | Fragment 63 | Name common.Ident 64 | Directives common.DirectiveList 65 | Loc errors.Location 66 | } 67 | 68 | type Selection interface { 69 | isSelection() 70 | } 71 | 72 | type Field struct { 73 | Alias common.Ident 74 | Name common.Ident 75 | Arguments common.ArgumentList 76 | Directives common.DirectiveList 77 | Selections []Selection 78 | SelectionSetLoc errors.Location 79 | } 80 | 81 | type InlineFragment struct { 82 | Fragment 83 | Directives common.DirectiveList 84 | Loc errors.Location 85 | } 86 | 87 | type FragmentSpread struct { 88 | Name common.Ident 89 | Directives common.DirectiveList 90 | Loc errors.Location 91 | } 92 | 93 | func (Field) isSelection() {} 94 | func (InlineFragment) isSelection() {} 95 | func (FragmentSpread) isSelection() {} 96 | 97 | func Parse(queryString string) (*Document, *errors.QueryError) { 98 | sc := &scanner.Scanner{ 99 | Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings, 100 | } 101 | sc.Init(strings.NewReader(queryString)) 102 | 103 | l := common.New(sc) 104 | var doc *Document 105 | err := l.CatchSyntaxError(func() { 106 | doc = parseDocument(l) 107 | }) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | return doc, nil 113 | } 114 | 115 | func parseDocument(l *common.Lexer) *Document { 116 | d := &Document{} 117 | for l.Peek() != scanner.EOF { 118 | if l.Peek() == '{' { 119 | op := &Operation{Type: Query, Loc: l.Location()} 120 | op.Selections = parseSelectionSet(l) 121 | d.Operations = append(d.Operations, op) 122 | continue 123 | } 124 | 125 | loc := l.Location() 126 | switch x := l.ConsumeIdent(); x { 127 | case "query": 128 | op := parseOperation(l, Query) 129 | op.Loc = loc 130 | d.Operations = append(d.Operations, op) 131 | 132 | case "mutation": 133 | d.Operations = append(d.Operations, parseOperation(l, Mutation)) 134 | 135 | case "subscription": 136 | d.Operations = append(d.Operations, parseOperation(l, Subscription)) 137 | 138 | case "fragment": 139 | frag := parseFragment(l) 140 | frag.Loc = loc 141 | d.Fragments = append(d.Fragments, frag) 142 | 143 | default: 144 | l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "fragment"`, x)) 145 | } 146 | } 147 | return d 148 | } 149 | 150 | func parseOperation(l *common.Lexer, opType OperationType) *Operation { 151 | op := &Operation{Type: opType} 152 | op.Name.Loc = l.Location() 153 | if l.Peek() == scanner.Ident { 154 | op.Name = l.ConsumeIdentWithLoc() 155 | } 156 | op.Directives = common.ParseDirectives(l) 157 | if l.Peek() == '(' { 158 | l.ConsumeToken('(') 159 | for l.Peek() != ')' { 160 | loc := l.Location() 161 | l.ConsumeToken('$') 162 | iv := common.ParseInputValue(l) 163 | iv.Loc = loc 164 | op.Vars = append(op.Vars, iv) 165 | } 166 | l.ConsumeToken(')') 167 | } 168 | op.Selections = parseSelectionSet(l) 169 | return op 170 | } 171 | 172 | func parseFragment(l *common.Lexer) *FragmentDecl { 173 | f := &FragmentDecl{} 174 | f.Name = l.ConsumeIdentWithLoc() 175 | l.ConsumeKeyword("on") 176 | f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()} 177 | f.Directives = common.ParseDirectives(l) 178 | f.Selections = parseSelectionSet(l) 179 | return f 180 | } 181 | 182 | func parseSelectionSet(l *common.Lexer) []Selection { 183 | var sels []Selection 184 | l.ConsumeToken('{') 185 | for l.Peek() != '}' { 186 | sels = append(sels, parseSelection(l)) 187 | } 188 | l.ConsumeToken('}') 189 | return sels 190 | } 191 | 192 | func parseSelection(l *common.Lexer) Selection { 193 | if l.Peek() == '.' { 194 | return parseSpread(l) 195 | } 196 | return parseField(l) 197 | } 198 | 199 | func parseField(l *common.Lexer) *Field { 200 | f := &Field{} 201 | f.Alias = l.ConsumeIdentWithLoc() 202 | f.Name = f.Alias 203 | if l.Peek() == ':' { 204 | l.ConsumeToken(':') 205 | f.Name = l.ConsumeIdentWithLoc() 206 | } 207 | if l.Peek() == '(' { 208 | f.Arguments = common.ParseArguments(l) 209 | } 210 | f.Directives = common.ParseDirectives(l) 211 | if l.Peek() == '{' { 212 | f.SelectionSetLoc = l.Location() 213 | f.Selections = parseSelectionSet(l) 214 | } 215 | return f 216 | } 217 | 218 | func parseSpread(l *common.Lexer) Selection { 219 | loc := l.Location() 220 | l.ConsumeToken('.') 221 | l.ConsumeToken('.') 222 | l.ConsumeToken('.') 223 | 224 | f := &InlineFragment{Loc: loc} 225 | if l.Peek() == scanner.Ident { 226 | ident := l.ConsumeIdentWithLoc() 227 | if ident.Name != "on" { 228 | fs := &FragmentSpread{ 229 | Name: ident, 230 | Loc: loc, 231 | } 232 | fs.Directives = common.ParseDirectives(l) 233 | return fs 234 | } 235 | f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()} 236 | } 237 | f.Directives = common.ParseDirectives(l) 238 | f.Selections = parseSelectionSet(l) 239 | return f 240 | } 241 | -------------------------------------------------------------------------------- /graphql.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "encoding/json" 8 | 9 | "github.com/neelance/graphql-go/errors" 10 | "github.com/neelance/graphql-go/internal/common" 11 | "github.com/neelance/graphql-go/internal/exec" 12 | "github.com/neelance/graphql-go/internal/exec/resolvable" 13 | "github.com/neelance/graphql-go/internal/exec/selected" 14 | "github.com/neelance/graphql-go/internal/query" 15 | "github.com/neelance/graphql-go/internal/schema" 16 | "github.com/neelance/graphql-go/internal/validation" 17 | "github.com/neelance/graphql-go/introspection" 18 | "github.com/neelance/graphql-go/log" 19 | "github.com/neelance/graphql-go/trace" 20 | ) 21 | 22 | // ParseSchema parses a GraphQL schema and attaches the given root resolver. It returns an error if 23 | // the Go type signature of the resolvers does not match the schema. If nil is passed as the 24 | // resolver, then the schema can not be executed, but it may be inspected (e.g. with ToJSON). 25 | func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (*Schema, error) { 26 | s := &Schema{ 27 | schema: schema.New(), 28 | maxParallelism: 10, 29 | tracer: trace.OpenTracingTracer{}, 30 | logger: &log.DefaultLogger{}, 31 | } 32 | for _, opt := range opts { 33 | opt(s) 34 | } 35 | 36 | if err := s.schema.Parse(schemaString); err != nil { 37 | return nil, err 38 | } 39 | 40 | if resolver != nil { 41 | r, err := resolvable.ApplyResolver(s.schema, resolver) 42 | if err != nil { 43 | return nil, err 44 | } 45 | s.res = r 46 | } 47 | 48 | return s, nil 49 | } 50 | 51 | // MustParseSchema calls ParseSchema and panics on error. 52 | func MustParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) *Schema { 53 | s, err := ParseSchema(schemaString, resolver, opts...) 54 | if err != nil { 55 | panic(err) 56 | } 57 | return s 58 | } 59 | 60 | // Schema represents a GraphQL schema with an optional resolver. 61 | type Schema struct { 62 | schema *schema.Schema 63 | res *resolvable.Schema 64 | 65 | maxParallelism int 66 | tracer trace.Tracer 67 | logger log.Logger 68 | } 69 | 70 | // SchemaOpt is an option to pass to ParseSchema or MustParseSchema. 71 | type SchemaOpt func(*Schema) 72 | 73 | // MaxParallelism specifies the maximum number of resolvers per request allowed to run in parallel. The default is 10. 74 | func MaxParallelism(n int) SchemaOpt { 75 | return func(s *Schema) { 76 | s.maxParallelism = n 77 | } 78 | } 79 | 80 | // Tracer is used to trace queries and fields. It defaults to trace.OpenTracingTracer. 81 | func Tracer(tracer trace.Tracer) SchemaOpt { 82 | return func(s *Schema) { 83 | s.tracer = tracer 84 | } 85 | } 86 | 87 | // Logger is used to log panics durring query execution. It defaults to exec.DefaultLogger. 88 | func Logger(logger log.Logger) SchemaOpt { 89 | return func(s *Schema) { 90 | s.logger = logger 91 | } 92 | } 93 | 94 | // Response represents a typical response of a GraphQL server. It may be encoded to JSON directly or 95 | // it may be further processed to a custom response type, for example to include custom error data. 96 | type Response struct { 97 | Data json.RawMessage `json:"data,omitempty"` 98 | Errors []*errors.QueryError `json:"errors,omitempty"` 99 | Extensions map[string]interface{} `json:"extensions,omitempty"` 100 | } 101 | 102 | // Validate validates the given query with the schema. 103 | func (s *Schema) Validate(queryString string) []*errors.QueryError { 104 | doc, qErr := query.Parse(queryString) 105 | if qErr != nil { 106 | return []*errors.QueryError{qErr} 107 | } 108 | 109 | return validation.Validate(s.schema, doc) 110 | } 111 | 112 | // Exec executes the given query with the schema's resolver. It panics if the schema was created 113 | // without a resolver. If the context get cancelled, no further resolvers will be called and a 114 | // the context error will be returned as soon as possible (not immediately). 115 | func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response { 116 | if s.res == nil { 117 | panic("schema created without resolver, can not exec") 118 | } 119 | return s.exec(ctx, queryString, operationName, variables, s.res) 120 | } 121 | 122 | func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response { 123 | doc, qErr := query.Parse(queryString) 124 | if qErr != nil { 125 | return &Response{Errors: []*errors.QueryError{qErr}} 126 | } 127 | 128 | errs := validation.Validate(s.schema, doc) 129 | if len(errs) != 0 { 130 | return &Response{Errors: errs} 131 | } 132 | 133 | op, err := getOperation(doc, operationName) 134 | if err != nil { 135 | return &Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}} 136 | } 137 | 138 | r := &exec.Request{ 139 | Request: selected.Request{ 140 | Doc: doc, 141 | Vars: variables, 142 | Schema: s.schema, 143 | }, 144 | Limiter: make(chan struct{}, s.maxParallelism), 145 | Tracer: s.tracer, 146 | Logger: s.logger, 147 | } 148 | varTypes := make(map[string]*introspection.Type) 149 | for _, v := range op.Vars { 150 | t, err := common.ResolveType(v.Type, s.schema.Resolve) 151 | if err != nil { 152 | return &Response{Errors: []*errors.QueryError{err}} 153 | } 154 | varTypes[v.Name.Name] = introspection.WrapType(t) 155 | } 156 | traceCtx, finish := s.tracer.TraceQuery(ctx, queryString, operationName, variables, varTypes) 157 | data, errs := r.Execute(traceCtx, res, op) 158 | finish(errs) 159 | 160 | return &Response{ 161 | Data: data, 162 | Errors: errs, 163 | } 164 | } 165 | 166 | func getOperation(document *query.Document, operationName string) (*query.Operation, error) { 167 | if len(document.Operations) == 0 { 168 | return nil, fmt.Errorf("no operations in query document") 169 | } 170 | 171 | if operationName == "" { 172 | if len(document.Operations) > 1 { 173 | return nil, fmt.Errorf("more than one operation in query document and no operation name given") 174 | } 175 | for _, op := range document.Operations { 176 | return op, nil // return the one and only operation 177 | } 178 | } 179 | 180 | op := document.Operations.Get(operationName) 181 | if op == nil { 182 | return nil, fmt.Errorf("no operation with name %q", operationName) 183 | } 184 | return op, nil 185 | } 186 | -------------------------------------------------------------------------------- /internal/exec/selected/selected.go: -------------------------------------------------------------------------------- 1 | package selected 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | 8 | "github.com/neelance/graphql-go/errors" 9 | "github.com/neelance/graphql-go/internal/common" 10 | "github.com/neelance/graphql-go/internal/exec/packer" 11 | "github.com/neelance/graphql-go/internal/exec/resolvable" 12 | "github.com/neelance/graphql-go/internal/query" 13 | "github.com/neelance/graphql-go/internal/schema" 14 | "github.com/neelance/graphql-go/introspection" 15 | ) 16 | 17 | type Request struct { 18 | Schema *schema.Schema 19 | Doc *query.Document 20 | Vars map[string]interface{} 21 | Mu sync.Mutex 22 | Errs []*errors.QueryError 23 | } 24 | 25 | func (r *Request) AddError(err *errors.QueryError) { 26 | r.Mu.Lock() 27 | r.Errs = append(r.Errs, err) 28 | r.Mu.Unlock() 29 | } 30 | 31 | func ApplyOperation(r *Request, s *resolvable.Schema, op *query.Operation) []Selection { 32 | var obj *resolvable.Object 33 | switch op.Type { 34 | case query.Query: 35 | obj = s.Query.(*resolvable.Object) 36 | case query.Mutation: 37 | obj = s.Mutation.(*resolvable.Object) 38 | } 39 | return applySelectionSet(r, obj, op.Selections) 40 | } 41 | 42 | type Selection interface { 43 | isSelection() 44 | } 45 | 46 | type SchemaField struct { 47 | resolvable.Field 48 | Alias string 49 | Args map[string]interface{} 50 | PackedArgs reflect.Value 51 | Sels []Selection 52 | Async bool 53 | FixedResult reflect.Value 54 | } 55 | 56 | type TypeAssertion struct { 57 | resolvable.TypeAssertion 58 | Sels []Selection 59 | } 60 | 61 | type TypenameField struct { 62 | resolvable.Object 63 | Alias string 64 | } 65 | 66 | func (*SchemaField) isSelection() {} 67 | func (*TypeAssertion) isSelection() {} 68 | func (*TypenameField) isSelection() {} 69 | 70 | func applySelectionSet(r *Request, e *resolvable.Object, sels []query.Selection) (flattenedSels []Selection) { 71 | for _, sel := range sels { 72 | switch sel := sel.(type) { 73 | case *query.Field: 74 | field := sel 75 | if skipByDirective(r, field.Directives) { 76 | continue 77 | } 78 | 79 | switch field.Name.Name { 80 | case "__typename": 81 | flattenedSels = append(flattenedSels, &TypenameField{ 82 | Object: *e, 83 | Alias: field.Alias.Name, 84 | }) 85 | 86 | case "__schema": 87 | flattenedSels = append(flattenedSels, &SchemaField{ 88 | Field: resolvable.MetaFieldSchema, 89 | Alias: field.Alias.Name, 90 | Sels: applySelectionSet(r, resolvable.MetaSchema, field.Selections), 91 | Async: true, 92 | FixedResult: reflect.ValueOf(introspection.WrapSchema(r.Schema)), 93 | }) 94 | 95 | case "__type": 96 | p := packer.ValuePacker{ValueType: reflect.TypeOf("")} 97 | v, err := p.Pack(field.Arguments.MustGet("name").Value(r.Vars)) 98 | if err != nil { 99 | r.AddError(errors.Errorf("%s", err)) 100 | return nil 101 | } 102 | 103 | t, ok := r.Schema.Types[v.String()] 104 | if !ok { 105 | return nil 106 | } 107 | 108 | flattenedSels = append(flattenedSels, &SchemaField{ 109 | Field: resolvable.MetaFieldType, 110 | Alias: field.Alias.Name, 111 | Sels: applySelectionSet(r, resolvable.MetaType, field.Selections), 112 | Async: true, 113 | FixedResult: reflect.ValueOf(introspection.WrapType(t)), 114 | }) 115 | 116 | default: 117 | fe := e.Fields[field.Name.Name] 118 | 119 | var args map[string]interface{} 120 | var packedArgs reflect.Value 121 | if fe.ArgsPacker != nil { 122 | args = make(map[string]interface{}) 123 | for _, arg := range field.Arguments { 124 | args[arg.Name.Name] = arg.Value.Value(r.Vars) 125 | } 126 | var err error 127 | packedArgs, err = fe.ArgsPacker.Pack(args) 128 | if err != nil { 129 | r.AddError(errors.Errorf("%s", err)) 130 | return 131 | } 132 | } 133 | 134 | fieldSels := applyField(r, fe.ValueExec, field.Selections) 135 | flattenedSels = append(flattenedSels, &SchemaField{ 136 | Field: *fe, 137 | Alias: field.Alias.Name, 138 | Args: args, 139 | PackedArgs: packedArgs, 140 | Sels: fieldSels, 141 | Async: fe.HasContext || fe.ArgsPacker != nil || fe.HasError || HasAsyncSel(fieldSels), 142 | }) 143 | } 144 | 145 | case *query.InlineFragment: 146 | frag := sel 147 | if skipByDirective(r, frag.Directives) { 148 | continue 149 | } 150 | flattenedSels = append(flattenedSels, applyFragment(r, e, &frag.Fragment)...) 151 | 152 | case *query.FragmentSpread: 153 | spread := sel 154 | if skipByDirective(r, spread.Directives) { 155 | continue 156 | } 157 | flattenedSels = append(flattenedSels, applyFragment(r, e, &r.Doc.Fragments.Get(spread.Name.Name).Fragment)...) 158 | 159 | default: 160 | panic("invalid type") 161 | } 162 | } 163 | return 164 | } 165 | 166 | func applyFragment(r *Request, e *resolvable.Object, frag *query.Fragment) []Selection { 167 | if frag.On.Name != "" && frag.On.Name != e.Name { 168 | a, ok := e.TypeAssertions[frag.On.Name] 169 | if !ok { 170 | panic(fmt.Errorf("%q does not implement %q", frag.On, e.Name)) // TODO proper error handling 171 | } 172 | 173 | return []Selection{&TypeAssertion{ 174 | TypeAssertion: *a, 175 | Sels: applySelectionSet(r, a.TypeExec.(*resolvable.Object), frag.Selections), 176 | }} 177 | } 178 | return applySelectionSet(r, e, frag.Selections) 179 | } 180 | 181 | func applyField(r *Request, e resolvable.Resolvable, sels []query.Selection) []Selection { 182 | switch e := e.(type) { 183 | case *resolvable.Object: 184 | return applySelectionSet(r, e, sels) 185 | case *resolvable.List: 186 | return applyField(r, e.Elem, sels) 187 | case *resolvable.Scalar: 188 | return nil 189 | default: 190 | panic("unreachable") 191 | } 192 | } 193 | 194 | func skipByDirective(r *Request, directives common.DirectiveList) bool { 195 | if d := directives.Get("skip"); d != nil { 196 | p := packer.ValuePacker{ValueType: reflect.TypeOf(false)} 197 | v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars)) 198 | if err != nil { 199 | r.AddError(errors.Errorf("%s", err)) 200 | } 201 | if err == nil && v.Bool() { 202 | return true 203 | } 204 | } 205 | 206 | if d := directives.Get("include"); d != nil { 207 | p := packer.ValuePacker{ValueType: reflect.TypeOf(false)} 208 | v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars)) 209 | if err != nil { 210 | r.AddError(errors.Errorf("%s", err)) 211 | } 212 | if err == nil && !v.Bool() { 213 | return true 214 | } 215 | } 216 | 217 | return false 218 | } 219 | 220 | func HasAsyncSel(sels []Selection) bool { 221 | for _, sel := range sels { 222 | switch sel := sel.(type) { 223 | case *SchemaField: 224 | if sel.Async { 225 | return true 226 | } 227 | case *TypeAssertion: 228 | if HasAsyncSel(sel.Sels) { 229 | return true 230 | } 231 | case *TypenameField: 232 | // sync 233 | default: 234 | panic("unreachable") 235 | } 236 | } 237 | return false 238 | } 239 | -------------------------------------------------------------------------------- /introspection/introspection.go: -------------------------------------------------------------------------------- 1 | package introspection 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/neelance/graphql-go/internal/common" 7 | "github.com/neelance/graphql-go/internal/schema" 8 | ) 9 | 10 | type Schema struct { 11 | schema *schema.Schema 12 | } 13 | 14 | // WrapSchema is only used internally. 15 | func WrapSchema(schema *schema.Schema) *Schema { 16 | return &Schema{schema} 17 | } 18 | 19 | func (r *Schema) Types() []*Type { 20 | var names []string 21 | for name := range r.schema.Types { 22 | names = append(names, name) 23 | } 24 | sort.Strings(names) 25 | 26 | l := make([]*Type, len(names)) 27 | for i, name := range names { 28 | l[i] = &Type{r.schema.Types[name]} 29 | } 30 | return l 31 | } 32 | 33 | func (r *Schema) Directives() []*Directive { 34 | var names []string 35 | for name := range r.schema.Directives { 36 | names = append(names, name) 37 | } 38 | sort.Strings(names) 39 | 40 | l := make([]*Directive, len(names)) 41 | for i, name := range names { 42 | l[i] = &Directive{r.schema.Directives[name]} 43 | } 44 | return l 45 | } 46 | 47 | func (r *Schema) QueryType() *Type { 48 | t, ok := r.schema.EntryPoints["query"] 49 | if !ok { 50 | return nil 51 | } 52 | return &Type{t} 53 | } 54 | 55 | func (r *Schema) MutationType() *Type { 56 | t, ok := r.schema.EntryPoints["mutation"] 57 | if !ok { 58 | return nil 59 | } 60 | return &Type{t} 61 | } 62 | 63 | func (r *Schema) SubscriptionType() *Type { 64 | t, ok := r.schema.EntryPoints["subscription"] 65 | if !ok { 66 | return nil 67 | } 68 | return &Type{t} 69 | } 70 | 71 | type Type struct { 72 | typ common.Type 73 | } 74 | 75 | // WrapType is only used internally. 76 | func WrapType(typ common.Type) *Type { 77 | return &Type{typ} 78 | } 79 | 80 | func (r *Type) Kind() string { 81 | return r.typ.Kind() 82 | } 83 | 84 | func (r *Type) Name() *string { 85 | if named, ok := r.typ.(schema.NamedType); ok { 86 | name := named.TypeName() 87 | return &name 88 | } 89 | return nil 90 | } 91 | 92 | func (r *Type) Description() *string { 93 | if named, ok := r.typ.(schema.NamedType); ok { 94 | desc := named.Description() 95 | if desc == "" { 96 | return nil 97 | } 98 | return &desc 99 | } 100 | return nil 101 | } 102 | 103 | func (r *Type) Fields(args *struct{ IncludeDeprecated bool }) *[]*Field { 104 | var fields schema.FieldList 105 | switch t := r.typ.(type) { 106 | case *schema.Object: 107 | fields = t.Fields 108 | case *schema.Interface: 109 | fields = t.Fields 110 | default: 111 | return nil 112 | } 113 | 114 | var l []*Field 115 | for _, f := range fields { 116 | if d := f.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated { 117 | l = append(l, &Field{f}) 118 | } 119 | } 120 | return &l 121 | } 122 | 123 | func (r *Type) Interfaces() *[]*Type { 124 | t, ok := r.typ.(*schema.Object) 125 | if !ok { 126 | return nil 127 | } 128 | 129 | l := make([]*Type, len(t.Interfaces)) 130 | for i, intf := range t.Interfaces { 131 | l[i] = &Type{intf} 132 | } 133 | return &l 134 | } 135 | 136 | func (r *Type) PossibleTypes() *[]*Type { 137 | var possibleTypes []*schema.Object 138 | switch t := r.typ.(type) { 139 | case *schema.Interface: 140 | possibleTypes = t.PossibleTypes 141 | case *schema.Union: 142 | possibleTypes = t.PossibleTypes 143 | default: 144 | return nil 145 | } 146 | 147 | l := make([]*Type, len(possibleTypes)) 148 | for i, intf := range possibleTypes { 149 | l[i] = &Type{intf} 150 | } 151 | return &l 152 | } 153 | 154 | func (r *Type) EnumValues(args *struct{ IncludeDeprecated bool }) *[]*EnumValue { 155 | t, ok := r.typ.(*schema.Enum) 156 | if !ok { 157 | return nil 158 | } 159 | 160 | var l []*EnumValue 161 | for _, v := range t.Values { 162 | if d := v.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated { 163 | l = append(l, &EnumValue{v}) 164 | } 165 | } 166 | return &l 167 | } 168 | 169 | func (r *Type) InputFields() *[]*InputValue { 170 | t, ok := r.typ.(*schema.InputObject) 171 | if !ok { 172 | return nil 173 | } 174 | 175 | l := make([]*InputValue, len(t.Values)) 176 | for i, v := range t.Values { 177 | l[i] = &InputValue{v} 178 | } 179 | return &l 180 | } 181 | 182 | func (r *Type) OfType() *Type { 183 | switch t := r.typ.(type) { 184 | case *common.List: 185 | return &Type{t.OfType} 186 | case *common.NonNull: 187 | return &Type{t.OfType} 188 | default: 189 | return nil 190 | } 191 | } 192 | 193 | type Field struct { 194 | field *schema.Field 195 | } 196 | 197 | func (r *Field) Name() string { 198 | return r.field.Name 199 | } 200 | 201 | func (r *Field) Description() *string { 202 | if r.field.Desc == "" { 203 | return nil 204 | } 205 | return &r.field.Desc 206 | } 207 | 208 | func (r *Field) Args() []*InputValue { 209 | l := make([]*InputValue, len(r.field.Args)) 210 | for i, v := range r.field.Args { 211 | l[i] = &InputValue{v} 212 | } 213 | return l 214 | } 215 | 216 | func (r *Field) Type() *Type { 217 | return &Type{r.field.Type} 218 | } 219 | 220 | func (r *Field) IsDeprecated() bool { 221 | return r.field.Directives.Get("deprecated") != nil 222 | } 223 | 224 | func (r *Field) DeprecationReason() *string { 225 | d := r.field.Directives.Get("deprecated") 226 | if d == nil { 227 | return nil 228 | } 229 | reason := d.Args.MustGet("reason").Value(nil).(string) 230 | return &reason 231 | } 232 | 233 | type InputValue struct { 234 | value *common.InputValue 235 | } 236 | 237 | func (r *InputValue) Name() string { 238 | return r.value.Name.Name 239 | } 240 | 241 | func (r *InputValue) Description() *string { 242 | if r.value.Desc == "" { 243 | return nil 244 | } 245 | return &r.value.Desc 246 | } 247 | 248 | func (r *InputValue) Type() *Type { 249 | return &Type{r.value.Type} 250 | } 251 | 252 | func (r *InputValue) DefaultValue() *string { 253 | if r.value.Default == nil { 254 | return nil 255 | } 256 | s := r.value.Default.String() 257 | return &s 258 | } 259 | 260 | type EnumValue struct { 261 | value *schema.EnumValue 262 | } 263 | 264 | func (r *EnumValue) Name() string { 265 | return r.value.Name 266 | } 267 | 268 | func (r *EnumValue) Description() *string { 269 | if r.value.Desc == "" { 270 | return nil 271 | } 272 | return &r.value.Desc 273 | } 274 | 275 | func (r *EnumValue) IsDeprecated() bool { 276 | return r.value.Directives.Get("deprecated") != nil 277 | } 278 | 279 | func (r *EnumValue) DeprecationReason() *string { 280 | d := r.value.Directives.Get("deprecated") 281 | if d == nil { 282 | return nil 283 | } 284 | reason := d.Args.MustGet("reason").Value(nil).(string) 285 | return &reason 286 | } 287 | 288 | type Directive struct { 289 | directive *schema.DirectiveDecl 290 | } 291 | 292 | func (r *Directive) Name() string { 293 | return r.directive.Name 294 | } 295 | 296 | func (r *Directive) Description() *string { 297 | if r.directive.Desc == "" { 298 | return nil 299 | } 300 | return &r.directive.Desc 301 | } 302 | 303 | func (r *Directive) Locations() []string { 304 | return r.directive.Locs 305 | } 306 | 307 | func (r *Directive) Args() []*InputValue { 308 | l := make([]*InputValue, len(r.directive.Args)) 309 | for i, v := range r.directive.Args { 310 | l[i] = &InputValue{v} 311 | } 312 | return l 313 | } 314 | -------------------------------------------------------------------------------- /internal/schema/meta.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | var Meta *Schema 4 | 5 | func init() { 6 | Meta = &Schema{} // bootstrap 7 | Meta = New() 8 | if err := Meta.Parse(metaSrc); err != nil { 9 | panic(err) 10 | } 11 | } 12 | 13 | var metaSrc = ` 14 | # The ` + "`" + `Int` + "`" + ` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 15 | scalar Int 16 | 17 | # The ` + "`" + `Float` + "`" + ` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). 18 | scalar Float 19 | 20 | # 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. 21 | scalar String 22 | 23 | # The ` + "`" + `Boolean` + "`" + ` scalar type represents ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `. 24 | scalar Boolean 25 | 26 | # 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. 27 | scalar ID 28 | 29 | # Directs the executor to include this field or fragment only when the ` + "`" + `if` + "`" + ` argument is true. 30 | directive @include( 31 | # Included when true. 32 | if: Boolean! 33 | ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 34 | 35 | # Directs the executor to skip this field or fragment when the ` + "`" + `if` + "`" + ` argument is true. 36 | directive @skip( 37 | # Skipped when true. 38 | if: Boolean! 39 | ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 40 | 41 | # Marks an element of a GraphQL schema as no longer supported. 42 | directive @deprecated( 43 | # Explains why this element was deprecated, usually also including a suggestion 44 | # for how to access supported similar data. Formatted in 45 | # [Markdown](https://daringfireball.net/projects/markdown/). 46 | reason: String = "No longer supported" 47 | ) on FIELD_DEFINITION | ENUM_VALUE 48 | 49 | # A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. 50 | # 51 | # In some cases, you need to provide options to alter GraphQL's execution behavior 52 | # in ways field arguments will not suffice, such as conditionally including or 53 | # skipping a field. Directives provide this by describing additional information 54 | # to the executor. 55 | type __Directive { 56 | name: String! 57 | description: String 58 | locations: [__DirectiveLocation!]! 59 | args: [__InputValue!]! 60 | } 61 | 62 | # A Directive can be adjacent to many parts of the GraphQL language, a 63 | # __DirectiveLocation describes one such possible adjacencies. 64 | enum __DirectiveLocation { 65 | # Location adjacent to a query operation. 66 | QUERY 67 | # Location adjacent to a mutation operation. 68 | MUTATION 69 | # Location adjacent to a subscription operation. 70 | SUBSCRIPTION 71 | # Location adjacent to a field. 72 | FIELD 73 | # Location adjacent to a fragment definition. 74 | FRAGMENT_DEFINITION 75 | # Location adjacent to a fragment spread. 76 | FRAGMENT_SPREAD 77 | # Location adjacent to an inline fragment. 78 | INLINE_FRAGMENT 79 | # Location adjacent to a schema definition. 80 | SCHEMA 81 | # Location adjacent to a scalar definition. 82 | SCALAR 83 | # Location adjacent to an object type definition. 84 | OBJECT 85 | # Location adjacent to a field definition. 86 | FIELD_DEFINITION 87 | # Location adjacent to an argument definition. 88 | ARGUMENT_DEFINITION 89 | # Location adjacent to an interface definition. 90 | INTERFACE 91 | # Location adjacent to a union definition. 92 | UNION 93 | # Location adjacent to an enum definition. 94 | ENUM 95 | # Location adjacent to an enum value definition. 96 | ENUM_VALUE 97 | # Location adjacent to an input object type definition. 98 | INPUT_OBJECT 99 | # Location adjacent to an input object field definition. 100 | INPUT_FIELD_DEFINITION 101 | } 102 | 103 | # One possible value for a given Enum. Enum values are unique values, not a 104 | # placeholder for a string or numeric value. However an Enum value is returned in 105 | # a JSON response as a string. 106 | type __EnumValue { 107 | name: String! 108 | description: String 109 | isDeprecated: Boolean! 110 | deprecationReason: String 111 | } 112 | 113 | # Object and Interface types are described by a list of Fields, each of which has 114 | # a name, potentially a list of arguments, and a return type. 115 | type __Field { 116 | name: String! 117 | description: String 118 | args: [__InputValue!]! 119 | type: __Type! 120 | isDeprecated: Boolean! 121 | deprecationReason: String 122 | } 123 | 124 | # Arguments provided to Fields or Directives and the input fields of an 125 | # InputObject are represented as Input Values which describe their type and 126 | # optionally a default value. 127 | type __InputValue { 128 | name: String! 129 | description: String 130 | type: __Type! 131 | # A GraphQL-formatted string representing the default value for this input value. 132 | defaultValue: String 133 | } 134 | 135 | # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all 136 | # available types and directives on the server, as well as the entry points for 137 | # query, mutation, and subscription operations. 138 | type __Schema { 139 | # A list of all types supported by this server. 140 | types: [__Type!]! 141 | # The type that query operations will be rooted at. 142 | queryType: __Type! 143 | # If this server supports mutation, the type that mutation operations will be rooted at. 144 | mutationType: __Type 145 | # If this server support subscription, the type that subscription operations will be rooted at. 146 | subscriptionType: __Type 147 | # A list of all directives supported by this server. 148 | directives: [__Directive!]! 149 | } 150 | 151 | # The fundamental unit of any GraphQL Schema is the type. There are many kinds of 152 | # types in GraphQL as represented by the ` + "`" + `__TypeKind` + "`" + ` enum. 153 | # 154 | # Depending on the kind of a type, certain fields describe information about that 155 | # type. Scalar types provide no information beyond a name and description, while 156 | # Enum types provide their values. Object and Interface types provide the fields 157 | # they describe. Abstract types, Union and Interface, provide the Object types 158 | # possible at runtime. List and NonNull types compose other types. 159 | type __Type { 160 | kind: __TypeKind! 161 | name: String 162 | description: String 163 | fields(includeDeprecated: Boolean = false): [__Field!] 164 | interfaces: [__Type!] 165 | possibleTypes: [__Type!] 166 | enumValues(includeDeprecated: Boolean = false): [__EnumValue!] 167 | inputFields: [__InputValue!] 168 | ofType: __Type 169 | } 170 | 171 | # An enum describing what kind of type a given ` + "`" + `__Type` + "`" + ` is. 172 | enum __TypeKind { 173 | # Indicates this type is a scalar. 174 | SCALAR 175 | # Indicates this type is an object. ` + "`" + `fields` + "`" + ` and ` + "`" + `interfaces` + "`" + ` are valid fields. 176 | OBJECT 177 | # Indicates this type is an interface. ` + "`" + `fields` + "`" + ` and ` + "`" + `possibleTypes` + "`" + ` are valid fields. 178 | INTERFACE 179 | # Indicates this type is a union. ` + "`" + `possibleTypes` + "`" + ` is a valid field. 180 | UNION 181 | # Indicates this type is an enum. ` + "`" + `enumValues` + "`" + ` is a valid field. 182 | ENUM 183 | # Indicates this type is an input object. ` + "`" + `inputFields` + "`" + ` is a valid field. 184 | INPUT_OBJECT 185 | # Indicates this type is a list. ` + "`" + `ofType` + "`" + ` is a valid field. 186 | LIST 187 | # Indicates this type is a non-null. ` + "`" + `ofType` + "`" + ` is a valid field. 188 | NON_NULL 189 | } 190 | ` 191 | -------------------------------------------------------------------------------- /internal/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "reflect" 8 | "sync" 9 | 10 | "github.com/neelance/graphql-go/errors" 11 | "github.com/neelance/graphql-go/internal/common" 12 | "github.com/neelance/graphql-go/internal/exec/resolvable" 13 | "github.com/neelance/graphql-go/internal/exec/selected" 14 | "github.com/neelance/graphql-go/internal/query" 15 | "github.com/neelance/graphql-go/internal/schema" 16 | "github.com/neelance/graphql-go/log" 17 | "github.com/neelance/graphql-go/trace" 18 | ) 19 | 20 | type Request struct { 21 | selected.Request 22 | Limiter chan struct{} 23 | Tracer trace.Tracer 24 | Logger log.Logger 25 | } 26 | 27 | type fieldResult struct { 28 | name string 29 | value []byte 30 | } 31 | 32 | func (r *Request) handlePanic(ctx context.Context) { 33 | if value := recover(); value != nil { 34 | r.Logger.LogPanic(ctx, value) 35 | r.AddError(makePanicError(value)) 36 | } 37 | } 38 | 39 | func makePanicError(value interface{}) *errors.QueryError { 40 | return errors.Errorf("graphql: panic occurred: %v", value) 41 | } 42 | 43 | func (r *Request) Execute(ctx context.Context, s *resolvable.Schema, op *query.Operation) ([]byte, []*errors.QueryError) { 44 | var out bytes.Buffer 45 | func() { 46 | defer r.handlePanic(ctx) 47 | sels := selected.ApplyOperation(&r.Request, s, op) 48 | r.execSelections(ctx, sels, nil, s.Resolver, &out, op.Type == query.Mutation) 49 | }() 50 | 51 | if err := ctx.Err(); err != nil { 52 | return nil, []*errors.QueryError{errors.Errorf("%s", err)} 53 | } 54 | 55 | return out.Bytes(), r.Errs 56 | } 57 | 58 | type fieldToExec struct { 59 | field *selected.SchemaField 60 | sels []selected.Selection 61 | resolver reflect.Value 62 | out *bytes.Buffer 63 | } 64 | 65 | func (r *Request) execSelections(ctx context.Context, sels []selected.Selection, path *pathSegment, resolver reflect.Value, out *bytes.Buffer, serially bool) { 66 | async := !serially && selected.HasAsyncSel(sels) 67 | 68 | var fields []*fieldToExec 69 | collectFieldsToResolve(sels, resolver, &fields, make(map[string]*fieldToExec)) 70 | 71 | if async { 72 | var wg sync.WaitGroup 73 | wg.Add(len(fields)) 74 | for _, f := range fields { 75 | go func(f *fieldToExec) { 76 | defer wg.Done() 77 | defer r.handlePanic(ctx) 78 | f.out = new(bytes.Buffer) 79 | execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, true) 80 | }(f) 81 | } 82 | wg.Wait() 83 | } 84 | 85 | out.WriteByte('{') 86 | for i, f := range fields { 87 | if i > 0 { 88 | out.WriteByte(',') 89 | } 90 | out.WriteByte('"') 91 | out.WriteString(f.field.Alias) 92 | out.WriteByte('"') 93 | out.WriteByte(':') 94 | if async { 95 | out.Write(f.out.Bytes()) 96 | continue 97 | } 98 | f.out = out 99 | execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, false) 100 | } 101 | out.WriteByte('}') 102 | } 103 | 104 | func collectFieldsToResolve(sels []selected.Selection, resolver reflect.Value, fields *[]*fieldToExec, fieldByAlias map[string]*fieldToExec) { 105 | for _, sel := range sels { 106 | switch sel := sel.(type) { 107 | case *selected.SchemaField: 108 | field, ok := fieldByAlias[sel.Alias] 109 | if !ok { // validation already checked for conflict (TODO) 110 | field = &fieldToExec{field: sel, resolver: resolver} 111 | fieldByAlias[sel.Alias] = field 112 | *fields = append(*fields, field) 113 | } 114 | field.sels = append(field.sels, sel.Sels...) 115 | 116 | case *selected.TypenameField: 117 | sf := &selected.SchemaField{ 118 | Field: resolvable.MetaFieldTypename, 119 | Alias: sel.Alias, 120 | FixedResult: reflect.ValueOf(typeOf(sel, resolver)), 121 | } 122 | *fields = append(*fields, &fieldToExec{field: sf, resolver: resolver}) 123 | 124 | case *selected.TypeAssertion: 125 | out := resolver.Method(sel.MethodIndex).Call(nil) 126 | if !out[1].Bool() { 127 | continue 128 | } 129 | collectFieldsToResolve(sel.Sels, out[0], fields, fieldByAlias) 130 | 131 | default: 132 | panic("unreachable") 133 | } 134 | } 135 | } 136 | 137 | func typeOf(tf *selected.TypenameField, resolver reflect.Value) string { 138 | if len(tf.TypeAssertions) == 0 { 139 | return tf.Name 140 | } 141 | for name, a := range tf.TypeAssertions { 142 | out := resolver.Method(a.MethodIndex).Call(nil) 143 | if out[1].Bool() { 144 | return name 145 | } 146 | } 147 | return "" 148 | } 149 | 150 | func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *pathSegment, applyLimiter bool) { 151 | if applyLimiter { 152 | r.Limiter <- struct{}{} 153 | } 154 | 155 | var result reflect.Value 156 | var err *errors.QueryError 157 | 158 | traceCtx, finish := r.Tracer.TraceField(ctx, f.field.TraceLabel, f.field.TypeName, f.field.Name, !f.field.Async, f.field.Args) 159 | defer func() { 160 | finish(err) 161 | }() 162 | 163 | err = func() (err *errors.QueryError) { 164 | defer func() { 165 | if panicValue := recover(); panicValue != nil { 166 | r.Logger.LogPanic(ctx, panicValue) 167 | err = makePanicError(panicValue) 168 | err.Path = path.toSlice() 169 | } 170 | }() 171 | 172 | if f.field.FixedResult.IsValid() { 173 | result = f.field.FixedResult 174 | return nil 175 | } 176 | 177 | if err := traceCtx.Err(); err != nil { 178 | return errors.Errorf("%s", err) // don't execute any more resolvers if context got cancelled 179 | } 180 | 181 | var in []reflect.Value 182 | if f.field.HasContext { 183 | in = append(in, reflect.ValueOf(traceCtx)) 184 | } 185 | if f.field.ArgsPacker != nil { 186 | in = append(in, f.field.PackedArgs) 187 | } 188 | callOut := f.resolver.Method(f.field.MethodIndex).Call(in) 189 | result = callOut[0] 190 | if f.field.HasError && !callOut[1].IsNil() { 191 | resolverErr := callOut[1].Interface().(error) 192 | err := errors.Errorf("%s", resolverErr) 193 | err.Path = path.toSlice() 194 | err.ResolverError = resolverErr 195 | return err 196 | } 197 | return nil 198 | }() 199 | 200 | if applyLimiter { 201 | <-r.Limiter 202 | } 203 | 204 | if err != nil { 205 | r.AddError(err) 206 | f.out.WriteString("null") // TODO handle non-nil 207 | return 208 | } 209 | 210 | r.execSelectionSet(traceCtx, f.sels, f.field.Type, path, result, f.out) 211 | } 212 | 213 | func (r *Request) execSelectionSet(ctx context.Context, sels []selected.Selection, typ common.Type, path *pathSegment, resolver reflect.Value, out *bytes.Buffer) { 214 | t, nonNull := unwrapNonNull(typ) 215 | switch t := t.(type) { 216 | case *schema.Object, *schema.Interface, *schema.Union: 217 | if resolver.Kind() == reflect.Ptr && resolver.IsNil() { 218 | if nonNull { 219 | panic(errors.Errorf("got nil for non-null %q", t)) 220 | } 221 | out.WriteString("null") 222 | return 223 | } 224 | 225 | r.execSelections(ctx, sels, path, resolver, out, false) 226 | return 227 | } 228 | 229 | if !nonNull { 230 | if resolver.IsNil() { 231 | out.WriteString("null") 232 | return 233 | } 234 | resolver = resolver.Elem() 235 | } 236 | 237 | switch t := t.(type) { 238 | case *common.List: 239 | l := resolver.Len() 240 | 241 | if selected.HasAsyncSel(sels) { 242 | var wg sync.WaitGroup 243 | wg.Add(l) 244 | entryouts := make([]bytes.Buffer, l) 245 | for i := 0; i < l; i++ { 246 | go func(i int) { 247 | defer wg.Done() 248 | defer r.handlePanic(ctx) 249 | r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), &entryouts[i]) 250 | }(i) 251 | } 252 | wg.Wait() 253 | 254 | out.WriteByte('[') 255 | for i, entryout := range entryouts { 256 | if i > 0 { 257 | out.WriteByte(',') 258 | } 259 | out.Write(entryout.Bytes()) 260 | } 261 | out.WriteByte(']') 262 | return 263 | } 264 | 265 | out.WriteByte('[') 266 | for i := 0; i < l; i++ { 267 | if i > 0 { 268 | out.WriteByte(',') 269 | } 270 | r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), out) 271 | } 272 | out.WriteByte(']') 273 | 274 | case *schema.Scalar: 275 | v := resolver.Interface() 276 | data, err := json.Marshal(v) 277 | if err != nil { 278 | panic(errors.Errorf("could not marshal %v", v)) 279 | } 280 | out.Write(data) 281 | 282 | case *schema.Enum: 283 | out.WriteByte('"') 284 | out.WriteString(resolver.String()) 285 | out.WriteByte('"') 286 | 287 | default: 288 | panic("unreachable") 289 | } 290 | } 291 | 292 | func unwrapNonNull(t common.Type) (common.Type, bool) { 293 | if nn, ok := t.(*common.NonNull); ok { 294 | return nn.OfType, true 295 | } 296 | return t, false 297 | } 298 | 299 | type marshaler interface { 300 | MarshalJSON() ([]byte, error) 301 | } 302 | 303 | type pathSegment struct { 304 | parent *pathSegment 305 | value interface{} 306 | } 307 | 308 | func (p *pathSegment) toSlice() []interface{} { 309 | if p == nil { 310 | return nil 311 | } 312 | return append(p.parent.toSlice(), p.value) 313 | } 314 | -------------------------------------------------------------------------------- /internal/exec/resolvable/resolvable.go: -------------------------------------------------------------------------------- 1 | package resolvable 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/neelance/graphql-go/internal/common" 10 | "github.com/neelance/graphql-go/internal/exec/packer" 11 | "github.com/neelance/graphql-go/internal/schema" 12 | ) 13 | 14 | type Schema struct { 15 | schema.Schema 16 | Query Resolvable 17 | Mutation Resolvable 18 | Resolver reflect.Value 19 | } 20 | 21 | type Resolvable interface { 22 | isResolvable() 23 | } 24 | 25 | type Object struct { 26 | Name string 27 | Fields map[string]*Field 28 | TypeAssertions map[string]*TypeAssertion 29 | } 30 | 31 | type Field struct { 32 | schema.Field 33 | TypeName string 34 | MethodIndex int 35 | HasContext bool 36 | ArgsPacker *packer.StructPacker 37 | HasError bool 38 | ValueExec Resolvable 39 | TraceLabel string 40 | } 41 | 42 | type TypeAssertion struct { 43 | MethodIndex int 44 | TypeExec Resolvable 45 | } 46 | 47 | type List struct { 48 | Elem Resolvable 49 | } 50 | 51 | type Scalar struct{} 52 | 53 | func (*Object) isResolvable() {} 54 | func (*List) isResolvable() {} 55 | func (*Scalar) isResolvable() {} 56 | 57 | func ApplyResolver(s *schema.Schema, resolver interface{}) (*Schema, error) { 58 | b := newBuilder(s) 59 | 60 | var query, mutation Resolvable 61 | 62 | if t, ok := s.EntryPoints["query"]; ok { 63 | if err := b.assignExec(&query, t, reflect.TypeOf(resolver)); err != nil { 64 | return nil, err 65 | } 66 | } 67 | 68 | if t, ok := s.EntryPoints["mutation"]; ok { 69 | if err := b.assignExec(&mutation, t, reflect.TypeOf(resolver)); err != nil { 70 | return nil, err 71 | } 72 | } 73 | 74 | if err := b.finish(); err != nil { 75 | return nil, err 76 | } 77 | 78 | return &Schema{ 79 | Schema: *s, 80 | Resolver: reflect.ValueOf(resolver), 81 | Query: query, 82 | Mutation: mutation, 83 | }, nil 84 | } 85 | 86 | type execBuilder struct { 87 | schema *schema.Schema 88 | resMap map[typePair]*resMapEntry 89 | packerBuilder *packer.Builder 90 | } 91 | 92 | type typePair struct { 93 | graphQLType common.Type 94 | resolverType reflect.Type 95 | } 96 | 97 | type resMapEntry struct { 98 | exec Resolvable 99 | targets []*Resolvable 100 | } 101 | 102 | func newBuilder(s *schema.Schema) *execBuilder { 103 | return &execBuilder{ 104 | schema: s, 105 | resMap: make(map[typePair]*resMapEntry), 106 | packerBuilder: packer.NewBuilder(), 107 | } 108 | } 109 | 110 | func (b *execBuilder) finish() error { 111 | for _, entry := range b.resMap { 112 | for _, target := range entry.targets { 113 | *target = entry.exec 114 | } 115 | } 116 | 117 | return b.packerBuilder.Finish() 118 | } 119 | 120 | func (b *execBuilder) assignExec(target *Resolvable, t common.Type, resolverType reflect.Type) error { 121 | k := typePair{t, resolverType} 122 | ref, ok := b.resMap[k] 123 | if !ok { 124 | ref = &resMapEntry{} 125 | b.resMap[k] = ref 126 | var err error 127 | ref.exec, err = b.makeExec(t, resolverType) 128 | if err != nil { 129 | return err 130 | } 131 | } 132 | ref.targets = append(ref.targets, target) 133 | return nil 134 | } 135 | 136 | func (b *execBuilder) makeExec(t common.Type, resolverType reflect.Type) (Resolvable, error) { 137 | var nonNull bool 138 | t, nonNull = unwrapNonNull(t) 139 | 140 | switch t := t.(type) { 141 | case *schema.Object: 142 | return b.makeObjectExec(t.Name, t.Fields, nil, nonNull, resolverType) 143 | 144 | case *schema.Interface: 145 | return b.makeObjectExec(t.Name, t.Fields, t.PossibleTypes, nonNull, resolverType) 146 | 147 | case *schema.Union: 148 | return b.makeObjectExec(t.Name, nil, t.PossibleTypes, nonNull, resolverType) 149 | } 150 | 151 | if !nonNull { 152 | if resolverType.Kind() != reflect.Ptr { 153 | return nil, fmt.Errorf("%s is not a pointer", resolverType) 154 | } 155 | resolverType = resolverType.Elem() 156 | } 157 | 158 | switch t := t.(type) { 159 | case *schema.Scalar: 160 | return makeScalarExec(t, resolverType) 161 | 162 | case *schema.Enum: 163 | return &Scalar{}, nil 164 | 165 | case *common.List: 166 | if resolverType.Kind() != reflect.Slice { 167 | return nil, fmt.Errorf("%s is not a slice", resolverType) 168 | } 169 | e := &List{} 170 | if err := b.assignExec(&e.Elem, t.OfType, resolverType.Elem()); err != nil { 171 | return nil, err 172 | } 173 | return e, nil 174 | 175 | default: 176 | panic("invalid type") 177 | } 178 | } 179 | 180 | func makeScalarExec(t *schema.Scalar, resolverType reflect.Type) (Resolvable, error) { 181 | implementsType := false 182 | switch r := reflect.New(resolverType).Interface().(type) { 183 | case *int32: 184 | implementsType = (t.Name == "Int") 185 | case *float64: 186 | implementsType = (t.Name == "Float") 187 | case *string: 188 | implementsType = (t.Name == "String") 189 | case *bool: 190 | implementsType = (t.Name == "Boolean") 191 | case packer.Unmarshaler: 192 | implementsType = r.ImplementsGraphQLType(t.Name) 193 | } 194 | if !implementsType { 195 | return nil, fmt.Errorf("can not use %s as %s", resolverType, t.Name) 196 | } 197 | return &Scalar{}, nil 198 | } 199 | 200 | func (b *execBuilder) makeObjectExec(typeName string, fields schema.FieldList, possibleTypes []*schema.Object, nonNull bool, resolverType reflect.Type) (*Object, error) { 201 | if !nonNull { 202 | if resolverType.Kind() != reflect.Ptr && resolverType.Kind() != reflect.Interface { 203 | return nil, fmt.Errorf("%s is not a pointer or interface", resolverType) 204 | } 205 | } 206 | 207 | methodHasReceiver := resolverType.Kind() != reflect.Interface 208 | 209 | Fields := make(map[string]*Field) 210 | for _, f := range fields { 211 | methodIndex := findMethod(resolverType, f.Name) 212 | if methodIndex == -1 { 213 | hint := "" 214 | if findMethod(reflect.PtrTo(resolverType), f.Name) != -1 { 215 | hint = " (hint: the method exists on the pointer type)" 216 | } 217 | return nil, fmt.Errorf("%s does not resolve %q: missing method for field %q%s", resolverType, typeName, f.Name, hint) 218 | } 219 | 220 | m := resolverType.Method(methodIndex) 221 | fe, err := b.makeFieldExec(typeName, f, m, methodIndex, methodHasReceiver) 222 | if err != nil { 223 | return nil, fmt.Errorf("%s\n\treturned by (%s).%s", err, resolverType, m.Name) 224 | } 225 | Fields[f.Name] = fe 226 | } 227 | 228 | typeAssertions := make(map[string]*TypeAssertion) 229 | for _, impl := range possibleTypes { 230 | methodIndex := findMethod(resolverType, "to"+impl.Name) 231 | if methodIndex == -1 { 232 | return nil, fmt.Errorf("%s does not resolve %q: missing method %q to convert to %q", resolverType, typeName, "to"+impl.Name, impl.Name) 233 | } 234 | if resolverType.Method(methodIndex).Type.NumOut() != 2 { 235 | 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) 236 | } 237 | a := &TypeAssertion{ 238 | MethodIndex: methodIndex, 239 | } 240 | if err := b.assignExec(&a.TypeExec, impl, resolverType.Method(methodIndex).Type.Out(0)); err != nil { 241 | return nil, err 242 | } 243 | typeAssertions[impl.Name] = a 244 | } 245 | 246 | return &Object{ 247 | Name: typeName, 248 | Fields: Fields, 249 | TypeAssertions: typeAssertions, 250 | }, nil 251 | } 252 | 253 | var contextType = reflect.TypeOf((*context.Context)(nil)).Elem() 254 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 255 | 256 | func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.Method, methodIndex int, methodHasReceiver bool) (*Field, error) { 257 | in := make([]reflect.Type, m.Type.NumIn()) 258 | for i := range in { 259 | in[i] = m.Type.In(i) 260 | } 261 | if methodHasReceiver { 262 | in = in[1:] // first parameter is receiver 263 | } 264 | 265 | hasContext := len(in) > 0 && in[0] == contextType 266 | if hasContext { 267 | in = in[1:] 268 | } 269 | 270 | var argsPacker *packer.StructPacker 271 | if len(f.Args) > 0 { 272 | if len(in) == 0 { 273 | return nil, fmt.Errorf("must have parameter for field arguments") 274 | } 275 | var err error 276 | argsPacker, err = b.packerBuilder.MakeStructPacker(f.Args, in[0]) 277 | if err != nil { 278 | return nil, err 279 | } 280 | in = in[1:] 281 | } 282 | 283 | if len(in) > 0 { 284 | return nil, fmt.Errorf("too many parameters") 285 | } 286 | 287 | if m.Type.NumOut() > 2 { 288 | return nil, fmt.Errorf("too many return values") 289 | } 290 | 291 | hasError := m.Type.NumOut() == 2 292 | if hasError { 293 | if m.Type.Out(1) != errorType { 294 | return nil, fmt.Errorf(`must have "error" as its second return value`) 295 | } 296 | } 297 | 298 | fe := &Field{ 299 | Field: *f, 300 | TypeName: typeName, 301 | MethodIndex: methodIndex, 302 | HasContext: hasContext, 303 | ArgsPacker: argsPacker, 304 | HasError: hasError, 305 | TraceLabel: fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name), 306 | } 307 | if err := b.assignExec(&fe.ValueExec, f.Type, m.Type.Out(0)); err != nil { 308 | return nil, err 309 | } 310 | return fe, nil 311 | } 312 | 313 | func findMethod(t reflect.Type, name string) int { 314 | for i := 0; i < t.NumMethod(); i++ { 315 | if strings.EqualFold(stripUnderscore(name), stripUnderscore(t.Method(i).Name)) { 316 | return i 317 | } 318 | } 319 | return -1 320 | } 321 | 322 | func unwrapNonNull(t common.Type) (common.Type, bool) { 323 | if nn, ok := t.(*common.NonNull); ok { 324 | return nn.OfType, true 325 | } 326 | return t, false 327 | } 328 | 329 | func stripUnderscore(s string) string { 330 | return strings.Replace(s, "_", "", -1) 331 | } 332 | -------------------------------------------------------------------------------- /internal/exec/packer/packer.go: -------------------------------------------------------------------------------- 1 | package packer 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/neelance/graphql-go/errors" 10 | "github.com/neelance/graphql-go/internal/common" 11 | "github.com/neelance/graphql-go/internal/schema" 12 | ) 13 | 14 | type packer interface { 15 | Pack(value interface{}) (reflect.Value, error) 16 | } 17 | 18 | type Builder struct { 19 | packerMap map[typePair]*packerMapEntry 20 | structPackers []*StructPacker 21 | } 22 | 23 | type typePair struct { 24 | graphQLType common.Type 25 | resolverType reflect.Type 26 | } 27 | 28 | type packerMapEntry struct { 29 | packer packer 30 | targets []*packer 31 | } 32 | 33 | func NewBuilder() *Builder { 34 | return &Builder{ 35 | packerMap: make(map[typePair]*packerMapEntry), 36 | } 37 | } 38 | 39 | func (b *Builder) Finish() error { 40 | for _, entry := range b.packerMap { 41 | for _, target := range entry.targets { 42 | *target = entry.packer 43 | } 44 | } 45 | 46 | for _, p := range b.structPackers { 47 | p.defaultStruct = reflect.New(p.structType).Elem() 48 | for _, f := range p.fields { 49 | if defaultVal := f.field.Default; defaultVal != nil { 50 | v, err := f.fieldPacker.Pack(defaultVal.Value(nil)) 51 | if err != nil { 52 | return err 53 | } 54 | p.defaultStruct.FieldByIndex(f.fieldIndex).Set(v) 55 | } 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (b *Builder) assignPacker(target *packer, schemaType common.Type, reflectType reflect.Type) error { 63 | k := typePair{schemaType, reflectType} 64 | ref, ok := b.packerMap[k] 65 | if !ok { 66 | ref = &packerMapEntry{} 67 | b.packerMap[k] = ref 68 | var err error 69 | ref.packer, err = b.makePacker(schemaType, reflectType) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | ref.targets = append(ref.targets, target) 75 | return nil 76 | } 77 | 78 | func (b *Builder) makePacker(schemaType common.Type, reflectType reflect.Type) (packer, error) { 79 | t, nonNull := unwrapNonNull(schemaType) 80 | if !nonNull { 81 | if reflectType.Kind() != reflect.Ptr { 82 | return nil, fmt.Errorf("%s is not a pointer", reflectType) 83 | } 84 | elemType := reflectType.Elem() 85 | addPtr := true 86 | if _, ok := t.(*schema.InputObject); ok { 87 | elemType = reflectType // keep pointer for input objects 88 | addPtr = false 89 | } 90 | elem, err := b.makeNonNullPacker(t, elemType) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return &nullPacker{ 95 | elemPacker: elem, 96 | valueType: reflectType, 97 | addPtr: addPtr, 98 | }, nil 99 | } 100 | 101 | return b.makeNonNullPacker(t, reflectType) 102 | } 103 | 104 | func (b *Builder) makeNonNullPacker(schemaType common.Type, reflectType reflect.Type) (packer, error) { 105 | if u, ok := reflect.New(reflectType).Interface().(Unmarshaler); ok { 106 | if !u.ImplementsGraphQLType(schemaType.String()) { 107 | return nil, fmt.Errorf("can not unmarshal %s into %s", schemaType, reflectType) 108 | } 109 | return &unmarshalerPacker{ 110 | ValueType: reflectType, 111 | }, nil 112 | } 113 | 114 | switch t := schemaType.(type) { 115 | case *schema.Scalar: 116 | return &ValuePacker{ 117 | ValueType: reflectType, 118 | }, nil 119 | 120 | case *schema.Enum: 121 | want := reflect.TypeOf("") 122 | if reflectType != want { 123 | return nil, fmt.Errorf("wrong type, expected %s", want) 124 | } 125 | return &ValuePacker{ 126 | ValueType: reflectType, 127 | }, nil 128 | 129 | case *schema.InputObject: 130 | e, err := b.MakeStructPacker(t.Values, reflectType) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return e, nil 135 | 136 | case *common.List: 137 | if reflectType.Kind() != reflect.Slice { 138 | return nil, fmt.Errorf("expected slice, got %s", reflectType) 139 | } 140 | p := &listPacker{ 141 | sliceType: reflectType, 142 | } 143 | if err := b.assignPacker(&p.elem, t.OfType, reflectType.Elem()); err != nil { 144 | return nil, err 145 | } 146 | return p, nil 147 | 148 | case *schema.Object, *schema.Interface, *schema.Union: 149 | return nil, fmt.Errorf("type of kind %s can not be used as input", t.Kind()) 150 | 151 | default: 152 | panic("unreachable") 153 | } 154 | } 155 | 156 | func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Type) (*StructPacker, error) { 157 | structType := typ 158 | usePtr := false 159 | if typ.Kind() == reflect.Ptr { 160 | structType = typ.Elem() 161 | usePtr = true 162 | } 163 | if structType.Kind() != reflect.Struct { 164 | return nil, fmt.Errorf("expected struct or pointer to struct, got %s", typ) 165 | } 166 | 167 | var fields []*structPackerField 168 | for _, v := range values { 169 | fe := &structPackerField{field: v} 170 | fx := func(n string) bool { 171 | return strings.EqualFold(stripUnderscore(n), stripUnderscore(v.Name.Name)) 172 | } 173 | 174 | sf, ok := structType.FieldByNameFunc(fx) 175 | if !ok { 176 | return nil, fmt.Errorf("missing argument %q", v.Name) 177 | } 178 | if sf.PkgPath != "" { 179 | return nil, fmt.Errorf("field %q must be exported", sf.Name) 180 | } 181 | fe.fieldIndex = sf.Index 182 | 183 | ft := v.Type 184 | if v.Default != nil { 185 | ft, _ = unwrapNonNull(ft) 186 | ft = &common.NonNull{OfType: ft} 187 | } 188 | 189 | if err := b.assignPacker(&fe.fieldPacker, ft, sf.Type); err != nil { 190 | return nil, fmt.Errorf("field %q: %s", sf.Name, err) 191 | } 192 | 193 | fields = append(fields, fe) 194 | } 195 | 196 | p := &StructPacker{ 197 | structType: structType, 198 | usePtr: usePtr, 199 | fields: fields, 200 | } 201 | b.structPackers = append(b.structPackers, p) 202 | return p, nil 203 | } 204 | 205 | type StructPacker struct { 206 | structType reflect.Type 207 | usePtr bool 208 | defaultStruct reflect.Value 209 | fields []*structPackerField 210 | } 211 | 212 | type structPackerField struct { 213 | field *common.InputValue 214 | fieldIndex []int 215 | fieldPacker packer 216 | } 217 | 218 | func (p *StructPacker) Pack(value interface{}) (reflect.Value, error) { 219 | if value == nil { 220 | return reflect.Value{}, errors.Errorf("got null for non-null") 221 | } 222 | 223 | values := value.(map[string]interface{}) 224 | v := reflect.New(p.structType) 225 | v.Elem().Set(p.defaultStruct) 226 | for _, f := range p.fields { 227 | if value, ok := values[f.field.Name.Name]; ok { 228 | packed, err := f.fieldPacker.Pack(value) 229 | if err != nil { 230 | return reflect.Value{}, err 231 | } 232 | v.Elem().FieldByIndex(f.fieldIndex).Set(packed) 233 | } 234 | } 235 | if !p.usePtr { 236 | return v.Elem(), nil 237 | } 238 | return v, nil 239 | } 240 | 241 | type listPacker struct { 242 | sliceType reflect.Type 243 | elem packer 244 | } 245 | 246 | func (e *listPacker) Pack(value interface{}) (reflect.Value, error) { 247 | list, ok := value.([]interface{}) 248 | if !ok { 249 | list = []interface{}{value} 250 | } 251 | 252 | v := reflect.MakeSlice(e.sliceType, len(list), len(list)) 253 | for i := range list { 254 | packed, err := e.elem.Pack(list[i]) 255 | if err != nil { 256 | return reflect.Value{}, err 257 | } 258 | v.Index(i).Set(packed) 259 | } 260 | return v, nil 261 | } 262 | 263 | type nullPacker struct { 264 | elemPacker packer 265 | valueType reflect.Type 266 | addPtr bool 267 | } 268 | 269 | func (p *nullPacker) Pack(value interface{}) (reflect.Value, error) { 270 | if value == nil { 271 | return reflect.Zero(p.valueType), nil 272 | } 273 | 274 | v, err := p.elemPacker.Pack(value) 275 | if err != nil { 276 | return reflect.Value{}, err 277 | } 278 | 279 | if p.addPtr { 280 | ptr := reflect.New(p.valueType.Elem()) 281 | ptr.Elem().Set(v) 282 | return ptr, nil 283 | } 284 | 285 | return v, nil 286 | } 287 | 288 | type ValuePacker struct { 289 | ValueType reflect.Type 290 | } 291 | 292 | func (p *ValuePacker) Pack(value interface{}) (reflect.Value, error) { 293 | if value == nil { 294 | return reflect.Value{}, errors.Errorf("got null for non-null") 295 | } 296 | 297 | coerced, err := unmarshalInput(p.ValueType, value) 298 | if err != nil { 299 | return reflect.Value{}, fmt.Errorf("could not unmarshal %#v (%T) into %s: %s", value, value, p.ValueType, err) 300 | } 301 | return reflect.ValueOf(coerced), nil 302 | } 303 | 304 | type unmarshalerPacker struct { 305 | ValueType reflect.Type 306 | } 307 | 308 | func (p *unmarshalerPacker) Pack(value interface{}) (reflect.Value, error) { 309 | if value == nil { 310 | return reflect.Value{}, errors.Errorf("got null for non-null") 311 | } 312 | 313 | v := reflect.New(p.ValueType) 314 | if err := v.Interface().(Unmarshaler).UnmarshalGraphQL(value); err != nil { 315 | return reflect.Value{}, err 316 | } 317 | return v.Elem(), nil 318 | } 319 | 320 | type Unmarshaler interface { 321 | ImplementsGraphQLType(name string) bool 322 | UnmarshalGraphQL(input interface{}) error 323 | } 324 | 325 | func unmarshalInput(typ reflect.Type, input interface{}) (interface{}, error) { 326 | if reflect.TypeOf(input) == typ { 327 | return input, nil 328 | } 329 | 330 | switch typ.Kind() { 331 | case reflect.Int32: 332 | switch input := input.(type) { 333 | case int: 334 | if input < math.MinInt32 || input > math.MaxInt32 { 335 | return nil, fmt.Errorf("not a 32-bit integer") 336 | } 337 | return int32(input), nil 338 | case float64: 339 | coerced := int32(input) 340 | if input < math.MinInt32 || input > math.MaxInt32 || float64(coerced) != input { 341 | return nil, fmt.Errorf("not a 32-bit integer") 342 | } 343 | return coerced, nil 344 | } 345 | 346 | case reflect.Float64: 347 | switch input := input.(type) { 348 | case int32: 349 | return float64(input), nil 350 | case int: 351 | return float64(input), nil 352 | } 353 | } 354 | 355 | return nil, fmt.Errorf("incompatible type") 356 | } 357 | 358 | func unwrapNonNull(t common.Type) (common.Type, bool) { 359 | if nn, ok := t.(*common.NonNull); ok { 360 | return nn.OfType, true 361 | } 362 | return t, false 363 | } 364 | 365 | func stripUnderscore(s string) string { 366 | return strings.Replace(s, "_", "", -1) 367 | } 368 | -------------------------------------------------------------------------------- /internal/schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "text/scanner" 7 | 8 | "github.com/neelance/graphql-go/errors" 9 | "github.com/neelance/graphql-go/internal/common" 10 | ) 11 | 12 | type Schema struct { 13 | EntryPoints map[string]NamedType 14 | Types map[string]NamedType 15 | Directives map[string]*DirectiveDecl 16 | 17 | entryPointNames map[string]string 18 | objects []*Object 19 | unions []*Union 20 | enums []*Enum 21 | } 22 | 23 | func (s *Schema) Resolve(name string) common.Type { 24 | return s.Types[name] 25 | } 26 | 27 | type NamedType interface { 28 | common.Type 29 | TypeName() string 30 | Description() string 31 | } 32 | 33 | type Scalar struct { 34 | Name string 35 | Desc string 36 | } 37 | 38 | type Object struct { 39 | Name string 40 | Interfaces []*Interface 41 | Fields FieldList 42 | Desc string 43 | 44 | interfaceNames []string 45 | } 46 | 47 | type Interface struct { 48 | Name string 49 | PossibleTypes []*Object 50 | Fields FieldList 51 | Desc string 52 | } 53 | 54 | type Union struct { 55 | Name string 56 | PossibleTypes []*Object 57 | Desc string 58 | 59 | typeNames []string 60 | } 61 | 62 | type Enum struct { 63 | Name string 64 | Values []*EnumValue 65 | Desc string 66 | } 67 | 68 | type EnumValue struct { 69 | Name string 70 | Directives common.DirectiveList 71 | Desc string 72 | } 73 | 74 | type InputObject struct { 75 | Name string 76 | Desc string 77 | Values common.InputValueList 78 | } 79 | 80 | type FieldList []*Field 81 | 82 | func (l FieldList) Get(name string) *Field { 83 | for _, f := range l { 84 | if f.Name == name { 85 | return f 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | func (l FieldList) Names() []string { 92 | names := make([]string, len(l)) 93 | for i, f := range l { 94 | names[i] = f.Name 95 | } 96 | return names 97 | } 98 | 99 | type DirectiveDecl struct { 100 | Name string 101 | Desc string 102 | Locs []string 103 | Args common.InputValueList 104 | } 105 | 106 | func (*Scalar) Kind() string { return "SCALAR" } 107 | func (*Object) Kind() string { return "OBJECT" } 108 | func (*Interface) Kind() string { return "INTERFACE" } 109 | func (*Union) Kind() string { return "UNION" } 110 | func (*Enum) Kind() string { return "ENUM" } 111 | func (*InputObject) Kind() string { return "INPUT_OBJECT" } 112 | 113 | func (t *Scalar) String() string { return t.Name } 114 | func (t *Object) String() string { return t.Name } 115 | func (t *Interface) String() string { return t.Name } 116 | func (t *Union) String() string { return t.Name } 117 | func (t *Enum) String() string { return t.Name } 118 | func (t *InputObject) String() string { return t.Name } 119 | 120 | func (t *Scalar) TypeName() string { return t.Name } 121 | func (t *Object) TypeName() string { return t.Name } 122 | func (t *Interface) TypeName() string { return t.Name } 123 | func (t *Union) TypeName() string { return t.Name } 124 | func (t *Enum) TypeName() string { return t.Name } 125 | func (t *InputObject) TypeName() string { return t.Name } 126 | 127 | func (t *Scalar) Description() string { return t.Desc } 128 | func (t *Object) Description() string { return t.Desc } 129 | func (t *Interface) Description() string { return t.Desc } 130 | func (t *Union) Description() string { return t.Desc } 131 | func (t *Enum) Description() string { return t.Desc } 132 | func (t *InputObject) Description() string { return t.Desc } 133 | 134 | type Field struct { 135 | Name string 136 | Args common.InputValueList 137 | Type common.Type 138 | Directives common.DirectiveList 139 | Desc string 140 | } 141 | 142 | func New() *Schema { 143 | s := &Schema{ 144 | entryPointNames: make(map[string]string), 145 | Types: make(map[string]NamedType), 146 | Directives: make(map[string]*DirectiveDecl), 147 | } 148 | for n, t := range Meta.Types { 149 | s.Types[n] = t 150 | } 151 | for n, d := range Meta.Directives { 152 | s.Directives[n] = d 153 | } 154 | return s 155 | } 156 | 157 | func (s *Schema) Parse(schemaString string) error { 158 | sc := &scanner.Scanner{ 159 | Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings, 160 | } 161 | sc.Init(strings.NewReader(schemaString)) 162 | 163 | l := common.New(sc) 164 | err := l.CatchSyntaxError(func() { 165 | parseSchema(s, l) 166 | }) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | for _, t := range s.Types { 172 | if err := resolveNamedType(s, t); err != nil { 173 | return err 174 | } 175 | } 176 | for _, d := range s.Directives { 177 | for _, arg := range d.Args { 178 | t, err := common.ResolveType(arg.Type, s.Resolve) 179 | if err != nil { 180 | return err 181 | } 182 | arg.Type = t 183 | } 184 | } 185 | 186 | s.EntryPoints = make(map[string]NamedType) 187 | for key, name := range s.entryPointNames { 188 | t, ok := s.Types[name] 189 | if !ok { 190 | if !ok { 191 | return errors.Errorf("type %q not found", name) 192 | } 193 | } 194 | s.EntryPoints[key] = t 195 | } 196 | 197 | for _, obj := range s.objects { 198 | obj.Interfaces = make([]*Interface, len(obj.interfaceNames)) 199 | for i, intfName := range obj.interfaceNames { 200 | t, ok := s.Types[intfName] 201 | if !ok { 202 | return errors.Errorf("interface %q not found", intfName) 203 | } 204 | intf, ok := t.(*Interface) 205 | if !ok { 206 | return errors.Errorf("type %q is not an interface", intfName) 207 | } 208 | obj.Interfaces[i] = intf 209 | intf.PossibleTypes = append(intf.PossibleTypes, obj) 210 | } 211 | } 212 | 213 | for _, union := range s.unions { 214 | union.PossibleTypes = make([]*Object, len(union.typeNames)) 215 | for i, name := range union.typeNames { 216 | t, ok := s.Types[name] 217 | if !ok { 218 | return errors.Errorf("object type %q not found", name) 219 | } 220 | obj, ok := t.(*Object) 221 | if !ok { 222 | return errors.Errorf("type %q is not an object", name) 223 | } 224 | union.PossibleTypes[i] = obj 225 | } 226 | } 227 | 228 | for _, enum := range s.enums { 229 | for _, value := range enum.Values { 230 | if err := resolveDirectives(s, value.Directives); err != nil { 231 | return err 232 | } 233 | } 234 | } 235 | 236 | return nil 237 | } 238 | 239 | func resolveNamedType(s *Schema, t NamedType) error { 240 | switch t := t.(type) { 241 | case *Object: 242 | for _, f := range t.Fields { 243 | if err := resolveField(s, f); err != nil { 244 | return err 245 | } 246 | } 247 | case *Interface: 248 | for _, f := range t.Fields { 249 | if err := resolveField(s, f); err != nil { 250 | return err 251 | } 252 | } 253 | case *InputObject: 254 | if err := resolveInputObject(s, t.Values); err != nil { 255 | return err 256 | } 257 | } 258 | return nil 259 | } 260 | 261 | func resolveField(s *Schema, f *Field) error { 262 | t, err := common.ResolveType(f.Type, s.Resolve) 263 | if err != nil { 264 | return err 265 | } 266 | f.Type = t 267 | if err := resolveDirectives(s, f.Directives); err != nil { 268 | return err 269 | } 270 | return resolveInputObject(s, f.Args) 271 | } 272 | 273 | func resolveDirectives(s *Schema, directives common.DirectiveList) error { 274 | for _, d := range directives { 275 | dirName := d.Name.Name 276 | dd, ok := s.Directives[dirName] 277 | if !ok { 278 | return errors.Errorf("directive %q not found", dirName) 279 | } 280 | for _, arg := range d.Args { 281 | if dd.Args.Get(arg.Name.Name) == nil { 282 | return errors.Errorf("invalid argument %q for directive %q", arg.Name.Name, dirName) 283 | } 284 | } 285 | for _, arg := range dd.Args { 286 | if _, ok := d.Args.Get(arg.Name.Name); !ok { 287 | d.Args = append(d.Args, common.Argument{Name: arg.Name, Value: arg.Default}) 288 | } 289 | } 290 | } 291 | return nil 292 | } 293 | 294 | func resolveInputObject(s *Schema, values common.InputValueList) error { 295 | for _, v := range values { 296 | t, err := common.ResolveType(v.Type, s.Resolve) 297 | if err != nil { 298 | return err 299 | } 300 | v.Type = t 301 | } 302 | return nil 303 | } 304 | 305 | func parseSchema(s *Schema, l *common.Lexer) { 306 | for l.Peek() != scanner.EOF { 307 | desc := l.DescComment() 308 | switch x := l.ConsumeIdent(); x { 309 | case "schema": 310 | l.ConsumeToken('{') 311 | for l.Peek() != '}' { 312 | name := l.ConsumeIdent() 313 | l.ConsumeToken(':') 314 | typ := l.ConsumeIdent() 315 | s.entryPointNames[name] = typ 316 | } 317 | l.ConsumeToken('}') 318 | case "type": 319 | obj := parseObjectDecl(l) 320 | obj.Desc = desc 321 | s.Types[obj.Name] = obj 322 | s.objects = append(s.objects, obj) 323 | case "interface": 324 | intf := parseInterfaceDecl(l) 325 | intf.Desc = desc 326 | s.Types[intf.Name] = intf 327 | case "union": 328 | union := parseUnionDecl(l) 329 | union.Desc = desc 330 | s.Types[union.Name] = union 331 | s.unions = append(s.unions, union) 332 | case "enum": 333 | enum := parseEnumDecl(l) 334 | enum.Desc = desc 335 | s.Types[enum.Name] = enum 336 | s.enums = append(s.enums, enum) 337 | case "input": 338 | input := parseInputDecl(l) 339 | input.Desc = desc 340 | s.Types[input.Name] = input 341 | case "scalar": 342 | name := l.ConsumeIdent() 343 | s.Types[name] = &Scalar{Name: name, Desc: desc} 344 | case "directive": 345 | directive := parseDirectiveDecl(l) 346 | directive.Desc = desc 347 | s.Directives[directive.Name] = directive 348 | default: 349 | l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "schema", "type", "enum", "interface", "union", "input", "scalar" or "directive"`, x)) 350 | } 351 | } 352 | } 353 | 354 | func parseObjectDecl(l *common.Lexer) *Object { 355 | o := &Object{} 356 | o.Name = l.ConsumeIdent() 357 | if l.Peek() == scanner.Ident { 358 | l.ConsumeKeyword("implements") 359 | for { 360 | o.interfaceNames = append(o.interfaceNames, l.ConsumeIdent()) 361 | if l.Peek() == '{' { 362 | break 363 | } 364 | } 365 | } 366 | l.ConsumeToken('{') 367 | o.Fields = parseFields(l) 368 | l.ConsumeToken('}') 369 | return o 370 | } 371 | 372 | func parseInterfaceDecl(l *common.Lexer) *Interface { 373 | i := &Interface{} 374 | i.Name = l.ConsumeIdent() 375 | l.ConsumeToken('{') 376 | i.Fields = parseFields(l) 377 | l.ConsumeToken('}') 378 | return i 379 | } 380 | 381 | func parseUnionDecl(l *common.Lexer) *Union { 382 | union := &Union{} 383 | union.Name = l.ConsumeIdent() 384 | l.ConsumeToken('=') 385 | union.typeNames = []string{l.ConsumeIdent()} 386 | for l.Peek() == '|' { 387 | l.ConsumeToken('|') 388 | union.typeNames = append(union.typeNames, l.ConsumeIdent()) 389 | } 390 | return union 391 | } 392 | 393 | func parseInputDecl(l *common.Lexer) *InputObject { 394 | i := &InputObject{} 395 | i.Name = l.ConsumeIdent() 396 | l.ConsumeToken('{') 397 | for l.Peek() != '}' { 398 | i.Values = append(i.Values, common.ParseInputValue(l)) 399 | } 400 | l.ConsumeToken('}') 401 | return i 402 | } 403 | 404 | func parseEnumDecl(l *common.Lexer) *Enum { 405 | enum := &Enum{} 406 | enum.Name = l.ConsumeIdent() 407 | l.ConsumeToken('{') 408 | for l.Peek() != '}' { 409 | v := &EnumValue{} 410 | v.Desc = l.DescComment() 411 | v.Name = l.ConsumeIdent() 412 | v.Directives = common.ParseDirectives(l) 413 | enum.Values = append(enum.Values, v) 414 | } 415 | l.ConsumeToken('}') 416 | return enum 417 | } 418 | 419 | func parseDirectiveDecl(l *common.Lexer) *DirectiveDecl { 420 | d := &DirectiveDecl{} 421 | l.ConsumeToken('@') 422 | d.Name = l.ConsumeIdent() 423 | if l.Peek() == '(' { 424 | l.ConsumeToken('(') 425 | for l.Peek() != ')' { 426 | v := common.ParseInputValue(l) 427 | d.Args = append(d.Args, v) 428 | } 429 | l.ConsumeToken(')') 430 | } 431 | l.ConsumeKeyword("on") 432 | for { 433 | loc := l.ConsumeIdent() 434 | d.Locs = append(d.Locs, loc) 435 | if l.Peek() != '|' { 436 | break 437 | } 438 | l.ConsumeToken('|') 439 | } 440 | return d 441 | } 442 | 443 | func parseFields(l *common.Lexer) FieldList { 444 | var fields FieldList 445 | for l.Peek() != '}' { 446 | f := &Field{} 447 | f.Desc = l.DescComment() 448 | f.Name = l.ConsumeIdent() 449 | if l.Peek() == '(' { 450 | l.ConsumeToken('(') 451 | for l.Peek() != ')' { 452 | f.Args = append(f.Args, common.ParseInputValue(l)) 453 | } 454 | l.ConsumeToken(')') 455 | } 456 | l.ConsumeToken(':') 457 | f.Type = common.ParseType(l) 458 | f.Directives = common.ParseDirectives(l) 459 | fields = append(fields, f) 460 | } 461 | return fields 462 | } 463 | -------------------------------------------------------------------------------- /example/starwars/starwars.go: -------------------------------------------------------------------------------- 1 | // Package starwars provides a example schema and resolver based on Star Wars characters. 2 | // 3 | // Source: https://github.com/graphql/graphql.github.io/blob/source/site/_core/swapiSchema.js 4 | package starwars 5 | 6 | import ( 7 | "encoding/base64" 8 | "fmt" 9 | "strconv" 10 | "strings" 11 | 12 | graphql "github.com/neelance/graphql-go" 13 | ) 14 | 15 | var Schema = ` 16 | schema { 17 | query: Query 18 | mutation: Mutation 19 | } 20 | # The query type, represents all of the entry points into our object graph 21 | type Query { 22 | hero(episode: Episode = NEWHOPE): Character 23 | reviews(episode: Episode!): [Review]! 24 | search(text: String!): [SearchResult]! 25 | character(id: ID!): Character 26 | droid(id: ID!): Droid 27 | human(id: ID!): Human 28 | starship(id: ID!): Starship 29 | } 30 | # The mutation type, represents all updates we can make to our data 31 | type Mutation { 32 | createReview(episode: Episode!, review: ReviewInput!): Review 33 | } 34 | # The episodes in the Star Wars trilogy 35 | enum Episode { 36 | # Star Wars Episode IV: A New Hope, released in 1977. 37 | NEWHOPE 38 | # Star Wars Episode V: The Empire Strikes Back, released in 1980. 39 | EMPIRE 40 | # Star Wars Episode VI: Return of the Jedi, released in 1983. 41 | JEDI 42 | } 43 | # A character from the Star Wars universe 44 | interface Character { 45 | # The ID of the character 46 | id: ID! 47 | # The name of the character 48 | name: String! 49 | # The friends of the character, or an empty list if they have none 50 | friends: [Character] 51 | # The friends of the character exposed as a connection with edges 52 | friendsConnection(first: Int, after: ID): FriendsConnection! 53 | # The movies this character appears in 54 | appearsIn: [Episode!]! 55 | } 56 | # Units of height 57 | enum LengthUnit { 58 | # The standard unit around the world 59 | METER 60 | # Primarily used in the United States 61 | FOOT 62 | } 63 | # A humanoid creature from the Star Wars universe 64 | type Human implements Character { 65 | # The ID of the human 66 | id: ID! 67 | # What this human calls themselves 68 | name: String! 69 | # Height in the preferred unit, default is meters 70 | height(unit: LengthUnit = METER): Float! 71 | # Mass in kilograms, or null if unknown 72 | mass: Float 73 | # This human's friends, or an empty list if they have none 74 | friends: [Character] 75 | # The friends of the human exposed as a connection with edges 76 | friendsConnection(first: Int, after: ID): FriendsConnection! 77 | # The movies this human appears in 78 | appearsIn: [Episode!]! 79 | # A list of starships this person has piloted, or an empty list if none 80 | starships: [Starship] 81 | } 82 | # An autonomous mechanical character in the Star Wars universe 83 | type Droid implements Character { 84 | # The ID of the droid 85 | id: ID! 86 | # What others call this droid 87 | name: String! 88 | # This droid's friends, or an empty list if they have none 89 | friends: [Character] 90 | # The friends of the droid exposed as a connection with edges 91 | friendsConnection(first: Int, after: ID): FriendsConnection! 92 | # The movies this droid appears in 93 | appearsIn: [Episode!]! 94 | # This droid's primary function 95 | primaryFunction: String 96 | } 97 | # A connection object for a character's friends 98 | type FriendsConnection { 99 | # The total number of friends 100 | totalCount: Int! 101 | # The edges for each of the character's friends. 102 | edges: [FriendsEdge] 103 | # A list of the friends, as a convenience when edges are not needed. 104 | friends: [Character] 105 | # Information for paginating this connection 106 | pageInfo: PageInfo! 107 | } 108 | # An edge object for a character's friends 109 | type FriendsEdge { 110 | # A cursor used for pagination 111 | cursor: ID! 112 | # The character represented by this friendship edge 113 | node: Character 114 | } 115 | # Information for paginating this connection 116 | type PageInfo { 117 | startCursor: ID 118 | endCursor: ID 119 | hasNextPage: Boolean! 120 | } 121 | # Represents a review for a movie 122 | type Review { 123 | # The number of stars this review gave, 1-5 124 | stars: Int! 125 | # Comment about the movie 126 | commentary: String 127 | } 128 | # The input object sent when someone is creating a new review 129 | input ReviewInput { 130 | # 0-5 stars 131 | stars: Int! 132 | # Comment about the movie, optional 133 | commentary: String 134 | } 135 | type Starship { 136 | # The ID of the starship 137 | id: ID! 138 | # The name of the starship 139 | name: String! 140 | # Length of the starship, along the longest axis 141 | length(unit: LengthUnit = METER): Float! 142 | } 143 | union SearchResult = Human | Droid | Starship 144 | ` 145 | 146 | type human struct { 147 | ID graphql.ID 148 | Name string 149 | Friends []graphql.ID 150 | AppearsIn []string 151 | Height float64 152 | Mass int 153 | Starships []graphql.ID 154 | } 155 | 156 | var humans = []*human{ 157 | { 158 | ID: "1000", 159 | Name: "Luke Skywalker", 160 | Friends: []graphql.ID{"1002", "1003", "2000", "2001"}, 161 | AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, 162 | Height: 1.72, 163 | Mass: 77, 164 | Starships: []graphql.ID{"3001", "3003"}, 165 | }, 166 | { 167 | ID: "1001", 168 | Name: "Darth Vader", 169 | Friends: []graphql.ID{"1004"}, 170 | AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, 171 | Height: 2.02, 172 | Mass: 136, 173 | Starships: []graphql.ID{"3002"}, 174 | }, 175 | { 176 | ID: "1002", 177 | Name: "Han Solo", 178 | Friends: []graphql.ID{"1000", "1003", "2001"}, 179 | AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, 180 | Height: 1.8, 181 | Mass: 80, 182 | Starships: []graphql.ID{"3000", "3003"}, 183 | }, 184 | { 185 | ID: "1003", 186 | Name: "Leia Organa", 187 | Friends: []graphql.ID{"1000", "1002", "2000", "2001"}, 188 | AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, 189 | Height: 1.5, 190 | Mass: 49, 191 | }, 192 | { 193 | ID: "1004", 194 | Name: "Wilhuff Tarkin", 195 | Friends: []graphql.ID{"1001"}, 196 | AppearsIn: []string{"NEWHOPE"}, 197 | Height: 1.8, 198 | Mass: 0, 199 | }, 200 | } 201 | 202 | var humanData = make(map[graphql.ID]*human) 203 | 204 | func init() { 205 | for _, h := range humans { 206 | humanData[h.ID] = h 207 | } 208 | } 209 | 210 | type droid struct { 211 | ID graphql.ID 212 | Name string 213 | Friends []graphql.ID 214 | AppearsIn []string 215 | PrimaryFunction string 216 | } 217 | 218 | var droids = []*droid{ 219 | { 220 | ID: "2000", 221 | Name: "C-3PO", 222 | Friends: []graphql.ID{"1000", "1002", "1003", "2001"}, 223 | AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, 224 | PrimaryFunction: "Protocol", 225 | }, 226 | { 227 | ID: "2001", 228 | Name: "R2-D2", 229 | Friends: []graphql.ID{"1000", "1002", "1003"}, 230 | AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, 231 | PrimaryFunction: "Astromech", 232 | }, 233 | } 234 | 235 | var droidData = make(map[graphql.ID]*droid) 236 | 237 | func init() { 238 | for _, d := range droids { 239 | droidData[d.ID] = d 240 | } 241 | } 242 | 243 | type starship struct { 244 | ID graphql.ID 245 | Name string 246 | Length float64 247 | } 248 | 249 | var starships = []*starship{ 250 | { 251 | ID: "3000", 252 | Name: "Millennium Falcon", 253 | Length: 34.37, 254 | }, 255 | { 256 | ID: "3001", 257 | Name: "X-Wing", 258 | Length: 12.5, 259 | }, 260 | { 261 | ID: "3002", 262 | Name: "TIE Advanced x1", 263 | Length: 9.2, 264 | }, 265 | { 266 | ID: "3003", 267 | Name: "Imperial shuttle", 268 | Length: 20, 269 | }, 270 | } 271 | 272 | var starshipData = make(map[graphql.ID]*starship) 273 | 274 | func init() { 275 | for _, s := range starships { 276 | starshipData[s.ID] = s 277 | } 278 | } 279 | 280 | type review struct { 281 | stars int32 282 | commentary *string 283 | } 284 | 285 | var reviews = make(map[string][]*review) 286 | 287 | type Resolver struct{} 288 | 289 | func (r *Resolver) Hero(args struct{ Episode string }) *characterResolver { 290 | if args.Episode == "EMPIRE" { 291 | return &characterResolver{&humanResolver{humanData["1000"]}} 292 | } 293 | return &characterResolver{&droidResolver{droidData["2001"]}} 294 | } 295 | 296 | func (r *Resolver) Reviews(args struct{ Episode string }) []*reviewResolver { 297 | var l []*reviewResolver 298 | for _, review := range reviews[args.Episode] { 299 | l = append(l, &reviewResolver{review}) 300 | } 301 | return l 302 | } 303 | 304 | func (r *Resolver) Search(args struct{ Text string }) []*searchResultResolver { 305 | var l []*searchResultResolver 306 | for _, h := range humans { 307 | if strings.Contains(h.Name, args.Text) { 308 | l = append(l, &searchResultResolver{&humanResolver{h}}) 309 | } 310 | } 311 | for _, d := range droids { 312 | if strings.Contains(d.Name, args.Text) { 313 | l = append(l, &searchResultResolver{&droidResolver{d}}) 314 | } 315 | } 316 | for _, s := range starships { 317 | if strings.Contains(s.Name, args.Text) { 318 | l = append(l, &searchResultResolver{&starshipResolver{s}}) 319 | } 320 | } 321 | return l 322 | } 323 | 324 | func (r *Resolver) Character(args struct{ ID graphql.ID }) *characterResolver { 325 | if h := humanData[args.ID]; h != nil { 326 | return &characterResolver{&humanResolver{h}} 327 | } 328 | if d := droidData[args.ID]; d != nil { 329 | return &characterResolver{&droidResolver{d}} 330 | } 331 | return nil 332 | } 333 | 334 | func (r *Resolver) Human(args struct{ ID graphql.ID }) *humanResolver { 335 | if h := humanData[args.ID]; h != nil { 336 | return &humanResolver{h} 337 | } 338 | return nil 339 | } 340 | 341 | func (r *Resolver) Droid(args struct{ ID graphql.ID }) *droidResolver { 342 | if d := droidData[args.ID]; d != nil { 343 | return &droidResolver{d} 344 | } 345 | return nil 346 | } 347 | 348 | func (r *Resolver) Starship(args struct{ ID graphql.ID }) *starshipResolver { 349 | if s := starshipData[args.ID]; s != nil { 350 | return &starshipResolver{s} 351 | } 352 | return nil 353 | } 354 | 355 | func (r *Resolver) CreateReview(args *struct { 356 | Episode string 357 | Review *reviewInput 358 | }) *reviewResolver { 359 | review := &review{ 360 | stars: args.Review.Stars, 361 | commentary: args.Review.Commentary, 362 | } 363 | reviews[args.Episode] = append(reviews[args.Episode], review) 364 | return &reviewResolver{review} 365 | } 366 | 367 | type friendsConnectionArgs struct { 368 | First *int32 369 | After *graphql.ID 370 | } 371 | 372 | type character interface { 373 | ID() graphql.ID 374 | Name() string 375 | Friends() *[]*characterResolver 376 | FriendsConnection(friendsConnectionArgs) (*friendsConnectionResolver, error) 377 | AppearsIn() []string 378 | } 379 | 380 | type characterResolver struct { 381 | character 382 | } 383 | 384 | func (r *characterResolver) ToHuman() (*humanResolver, bool) { 385 | c, ok := r.character.(*humanResolver) 386 | return c, ok 387 | } 388 | 389 | func (r *characterResolver) ToDroid() (*droidResolver, bool) { 390 | c, ok := r.character.(*droidResolver) 391 | return c, ok 392 | } 393 | 394 | type humanResolver struct { 395 | h *human 396 | } 397 | 398 | func (r *humanResolver) ID() graphql.ID { 399 | return r.h.ID 400 | } 401 | 402 | func (r *humanResolver) Name() string { 403 | return r.h.Name 404 | } 405 | 406 | func (r *humanResolver) Height(args struct{ Unit string }) float64 { 407 | return convertLength(r.h.Height, args.Unit) 408 | } 409 | 410 | func (r *humanResolver) Mass() *float64 { 411 | if r.h.Mass == 0 { 412 | return nil 413 | } 414 | f := float64(r.h.Mass) 415 | return &f 416 | } 417 | 418 | func (r *humanResolver) Friends() *[]*characterResolver { 419 | return resolveCharacters(r.h.Friends) 420 | } 421 | 422 | func (r *humanResolver) FriendsConnection(args friendsConnectionArgs) (*friendsConnectionResolver, error) { 423 | return newFriendsConnectionResolver(r.h.Friends, args) 424 | } 425 | 426 | func (r *humanResolver) AppearsIn() []string { 427 | return r.h.AppearsIn 428 | } 429 | 430 | func (r *humanResolver) Starships() *[]*starshipResolver { 431 | l := make([]*starshipResolver, len(r.h.Starships)) 432 | for i, id := range r.h.Starships { 433 | l[i] = &starshipResolver{starshipData[id]} 434 | } 435 | return &l 436 | } 437 | 438 | type droidResolver struct { 439 | d *droid 440 | } 441 | 442 | func (r *droidResolver) ID() graphql.ID { 443 | return r.d.ID 444 | } 445 | 446 | func (r *droidResolver) Name() string { 447 | return r.d.Name 448 | } 449 | 450 | func (r *droidResolver) Friends() *[]*characterResolver { 451 | return resolveCharacters(r.d.Friends) 452 | } 453 | 454 | func (r *droidResolver) FriendsConnection(args friendsConnectionArgs) (*friendsConnectionResolver, error) { 455 | return newFriendsConnectionResolver(r.d.Friends, args) 456 | } 457 | 458 | func (r *droidResolver) AppearsIn() []string { 459 | return r.d.AppearsIn 460 | } 461 | 462 | func (r *droidResolver) PrimaryFunction() *string { 463 | if r.d.PrimaryFunction == "" { 464 | return nil 465 | } 466 | return &r.d.PrimaryFunction 467 | } 468 | 469 | type starshipResolver struct { 470 | s *starship 471 | } 472 | 473 | func (r *starshipResolver) ID() graphql.ID { 474 | return r.s.ID 475 | } 476 | 477 | func (r *starshipResolver) Name() string { 478 | return r.s.Name 479 | } 480 | 481 | func (r *starshipResolver) Length(args struct{ Unit string }) float64 { 482 | return convertLength(r.s.Length, args.Unit) 483 | } 484 | 485 | type searchResultResolver struct { 486 | result interface{} 487 | } 488 | 489 | func (r *searchResultResolver) ToHuman() (*humanResolver, bool) { 490 | res, ok := r.result.(*humanResolver) 491 | return res, ok 492 | } 493 | 494 | func (r *searchResultResolver) ToDroid() (*droidResolver, bool) { 495 | res, ok := r.result.(*droidResolver) 496 | return res, ok 497 | } 498 | 499 | func (r *searchResultResolver) ToStarship() (*starshipResolver, bool) { 500 | res, ok := r.result.(*starshipResolver) 501 | return res, ok 502 | } 503 | 504 | func convertLength(meters float64, unit string) float64 { 505 | switch unit { 506 | case "METER": 507 | return meters 508 | case "FOOT": 509 | return meters * 3.28084 510 | default: 511 | panic("invalid unit") 512 | } 513 | } 514 | 515 | func resolveCharacters(ids []graphql.ID) *[]*characterResolver { 516 | var characters []*characterResolver 517 | for _, id := range ids { 518 | if c := resolveCharacter(id); c != nil { 519 | characters = append(characters, c) 520 | } 521 | } 522 | return &characters 523 | } 524 | 525 | func resolveCharacter(id graphql.ID) *characterResolver { 526 | if h, ok := humanData[id]; ok { 527 | return &characterResolver{&humanResolver{h}} 528 | } 529 | if d, ok := droidData[id]; ok { 530 | return &characterResolver{&droidResolver{d}} 531 | } 532 | return nil 533 | } 534 | 535 | type reviewResolver struct { 536 | r *review 537 | } 538 | 539 | func (r *reviewResolver) Stars() int32 { 540 | return r.r.stars 541 | } 542 | 543 | func (r *reviewResolver) Commentary() *string { 544 | return r.r.commentary 545 | } 546 | 547 | type friendsConnectionResolver struct { 548 | ids []graphql.ID 549 | from int 550 | to int 551 | } 552 | 553 | func newFriendsConnectionResolver(ids []graphql.ID, args friendsConnectionArgs) (*friendsConnectionResolver, error) { 554 | from := 0 555 | if args.After != nil { 556 | b, err := base64.StdEncoding.DecodeString(string(*args.After)) 557 | if err != nil { 558 | return nil, err 559 | } 560 | i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor")) 561 | if err != nil { 562 | return nil, err 563 | } 564 | from = i 565 | } 566 | 567 | to := len(ids) 568 | if args.First != nil { 569 | to = from + int(*args.First) 570 | if to > len(ids) { 571 | to = len(ids) 572 | } 573 | } 574 | 575 | return &friendsConnectionResolver{ 576 | ids: ids, 577 | from: from, 578 | to: to, 579 | }, nil 580 | } 581 | 582 | func (r *friendsConnectionResolver) TotalCount() int32 { 583 | return int32(len(r.ids)) 584 | } 585 | 586 | func (r *friendsConnectionResolver) Edges() *[]*friendsEdgeResolver { 587 | l := make([]*friendsEdgeResolver, r.to-r.from) 588 | for i := range l { 589 | l[i] = &friendsEdgeResolver{ 590 | cursor: encodeCursor(r.from + i), 591 | id: r.ids[r.from+i], 592 | } 593 | } 594 | return &l 595 | } 596 | 597 | func (r *friendsConnectionResolver) Friends() *[]*characterResolver { 598 | return resolveCharacters(r.ids[r.from:r.to]) 599 | } 600 | 601 | func (r *friendsConnectionResolver) PageInfo() *pageInfoResolver { 602 | return &pageInfoResolver{ 603 | startCursor: encodeCursor(r.from), 604 | endCursor: encodeCursor(r.to - 1), 605 | hasNextPage: r.to < len(r.ids), 606 | } 607 | } 608 | 609 | func encodeCursor(i int) graphql.ID { 610 | return graphql.ID(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("cursor%d", i+1)))) 611 | } 612 | 613 | type friendsEdgeResolver struct { 614 | cursor graphql.ID 615 | id graphql.ID 616 | } 617 | 618 | func (r *friendsEdgeResolver) Cursor() graphql.ID { 619 | return r.cursor 620 | } 621 | 622 | func (r *friendsEdgeResolver) Node() *characterResolver { 623 | return resolveCharacter(r.id) 624 | } 625 | 626 | type pageInfoResolver struct { 627 | startCursor graphql.ID 628 | endCursor graphql.ID 629 | hasNextPage bool 630 | } 631 | 632 | func (r *pageInfoResolver) StartCursor() *graphql.ID { 633 | return &r.startCursor 634 | } 635 | 636 | func (r *pageInfoResolver) EndCursor() *graphql.ID { 637 | return &r.endCursor 638 | } 639 | 640 | func (r *pageInfoResolver) HasNextPage() bool { 641 | return r.hasNextPage 642 | } 643 | 644 | type reviewInput struct { 645 | Stars int32 646 | Commentary *string 647 | } 648 | -------------------------------------------------------------------------------- /internal/validation/validation.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "text/scanner" 10 | 11 | "github.com/neelance/graphql-go/errors" 12 | "github.com/neelance/graphql-go/internal/common" 13 | "github.com/neelance/graphql-go/internal/query" 14 | "github.com/neelance/graphql-go/internal/schema" 15 | ) 16 | 17 | type varSet map[*common.InputValue]struct{} 18 | 19 | type selectionPair struct{ a, b query.Selection } 20 | 21 | type fieldInfo struct { 22 | sf *schema.Field 23 | parent schema.NamedType 24 | } 25 | 26 | type context struct { 27 | schema *schema.Schema 28 | doc *query.Document 29 | errs []*errors.QueryError 30 | opErrs map[*query.Operation][]*errors.QueryError 31 | usedVars map[*query.Operation]varSet 32 | fieldMap map[*query.Field]fieldInfo 33 | overlapValidated map[selectionPair]struct{} 34 | } 35 | 36 | func (c *context) addErr(loc errors.Location, rule string, format string, a ...interface{}) { 37 | c.addErrMultiLoc([]errors.Location{loc}, rule, format, a...) 38 | } 39 | 40 | func (c *context) addErrMultiLoc(locs []errors.Location, rule string, format string, a ...interface{}) { 41 | c.errs = append(c.errs, &errors.QueryError{ 42 | Message: fmt.Sprintf(format, a...), 43 | Locations: locs, 44 | Rule: rule, 45 | }) 46 | } 47 | 48 | type opContext struct { 49 | *context 50 | ops []*query.Operation 51 | } 52 | 53 | func Validate(s *schema.Schema, doc *query.Document) []*errors.QueryError { 54 | c := &context{ 55 | schema: s, 56 | doc: doc, 57 | opErrs: make(map[*query.Operation][]*errors.QueryError), 58 | usedVars: make(map[*query.Operation]varSet), 59 | fieldMap: make(map[*query.Field]fieldInfo), 60 | overlapValidated: make(map[selectionPair]struct{}), 61 | } 62 | 63 | opNames := make(nameSet) 64 | fragUsedBy := make(map[*query.FragmentDecl][]*query.Operation) 65 | for _, op := range doc.Operations { 66 | c.usedVars[op] = make(varSet) 67 | opc := &opContext{c, []*query.Operation{op}} 68 | 69 | if op.Name.Name == "" && len(doc.Operations) != 1 { 70 | c.addErr(op.Loc, "LoneAnonymousOperation", "This anonymous operation must be the only defined operation.") 71 | } 72 | if op.Name.Name != "" { 73 | validateName(c, opNames, op.Name, "UniqueOperationNames", "operation") 74 | } 75 | 76 | validateDirectives(opc, string(op.Type), op.Directives) 77 | 78 | varNames := make(nameSet) 79 | for _, v := range op.Vars { 80 | validateName(c, varNames, v.Name, "UniqueVariableNames", "variable") 81 | 82 | t := resolveType(c, v.Type) 83 | if !canBeInput(t) { 84 | c.addErr(v.TypeLoc, "VariablesAreInputTypes", "Variable %q cannot be non-input type %q.", "$"+v.Name.Name, t) 85 | } 86 | 87 | if v.Default != nil { 88 | validateLiteral(opc, v.Default) 89 | 90 | if t != nil { 91 | if nn, ok := t.(*common.NonNull); ok { 92 | 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) 93 | } 94 | 95 | if ok, reason := validateValueType(opc, v.Default, t); !ok { 96 | 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) 97 | } 98 | } 99 | } 100 | } 101 | 102 | var entryPoint schema.NamedType 103 | switch op.Type { 104 | case query.Query: 105 | entryPoint = s.EntryPoints["query"] 106 | case query.Mutation: 107 | entryPoint = s.EntryPoints["mutation"] 108 | case query.Subscription: 109 | entryPoint = s.EntryPoints["subscription"] 110 | default: 111 | panic("unreachable") 112 | } 113 | 114 | validateSelectionSet(opc, op.Selections, entryPoint) 115 | 116 | fragUsed := make(map[*query.FragmentDecl]struct{}) 117 | markUsedFragments(c, op.Selections, fragUsed) 118 | for frag := range fragUsed { 119 | fragUsedBy[frag] = append(fragUsedBy[frag], op) 120 | } 121 | } 122 | 123 | fragNames := make(nameSet) 124 | fragVisited := make(map[*query.FragmentDecl]struct{}) 125 | for _, frag := range doc.Fragments { 126 | opc := &opContext{c, fragUsedBy[frag]} 127 | 128 | validateName(c, fragNames, frag.Name, "UniqueFragmentNames", "fragment") 129 | validateDirectives(opc, "FRAGMENT_DEFINITION", frag.Directives) 130 | 131 | t := unwrapType(resolveType(c, &frag.On)) 132 | // continue even if t is nil 133 | if t != nil && !canBeFragment(t) { 134 | c.addErr(frag.On.Loc, "FragmentsOnCompositeTypes", "Fragment %q cannot condition on non composite type %q.", frag.Name.Name, t) 135 | continue 136 | } 137 | 138 | validateSelectionSet(opc, frag.Selections, t) 139 | 140 | if _, ok := fragVisited[frag]; !ok { 141 | detectFragmentCycle(c, frag.Selections, fragVisited, nil, map[string]int{frag.Name.Name: 0}) 142 | } 143 | } 144 | 145 | for _, frag := range doc.Fragments { 146 | if len(fragUsedBy[frag]) == 0 { 147 | c.addErr(frag.Loc, "NoUnusedFragments", "Fragment %q is never used.", frag.Name.Name) 148 | } 149 | } 150 | 151 | for _, op := range doc.Operations { 152 | c.errs = append(c.errs, c.opErrs[op]...) 153 | 154 | opUsedVars := c.usedVars[op] 155 | for _, v := range op.Vars { 156 | if _, ok := opUsedVars[v]; !ok { 157 | opSuffix := "" 158 | if op.Name.Name != "" { 159 | opSuffix = fmt.Sprintf(" in operation %q", op.Name.Name) 160 | } 161 | c.addErr(v.Loc, "NoUnusedVariables", "Variable %q is never used%s.", "$"+v.Name.Name, opSuffix) 162 | } 163 | } 164 | } 165 | 166 | return c.errs 167 | } 168 | 169 | func validateSelectionSet(c *opContext, sels []query.Selection, t schema.NamedType) { 170 | for _, sel := range sels { 171 | validateSelection(c, sel, t) 172 | } 173 | 174 | for i, a := range sels { 175 | for _, b := range sels[i+1:] { 176 | c.validateOverlap(a, b, nil, nil) 177 | } 178 | } 179 | } 180 | 181 | func validateSelection(c *opContext, sel query.Selection, t schema.NamedType) { 182 | switch sel := sel.(type) { 183 | case *query.Field: 184 | validateDirectives(c, "FIELD", sel.Directives) 185 | 186 | fieldName := sel.Name.Name 187 | var f *schema.Field 188 | switch fieldName { 189 | case "__typename": 190 | f = &schema.Field{ 191 | Name: "__typename", 192 | Type: c.schema.Types["String"], 193 | } 194 | case "__schema": 195 | f = &schema.Field{ 196 | Name: "__schema", 197 | Type: c.schema.Types["__Schema"], 198 | } 199 | case "__type": 200 | f = &schema.Field{ 201 | Name: "__type", 202 | Args: common.InputValueList{ 203 | &common.InputValue{ 204 | Name: common.Ident{Name: "name"}, 205 | Type: &common.NonNull{OfType: c.schema.Types["String"]}, 206 | }, 207 | }, 208 | Type: c.schema.Types["__Type"], 209 | } 210 | default: 211 | f = fields(t).Get(fieldName) 212 | if f == nil && t != nil { 213 | suggestion := makeSuggestion("Did you mean", fields(t).Names(), fieldName) 214 | c.addErr(sel.Alias.Loc, "FieldsOnCorrectType", "Cannot query field %q on type %q.%s", fieldName, t, suggestion) 215 | } 216 | } 217 | c.fieldMap[sel] = fieldInfo{sf: f, parent: t} 218 | 219 | validateArgumentLiterals(c, sel.Arguments) 220 | if f != nil { 221 | validateArgumentTypes(c, sel.Arguments, f.Args, sel.Alias.Loc, 222 | func() string { return fmt.Sprintf("field %q of type %q", fieldName, t) }, 223 | func() string { return fmt.Sprintf("Field %q", fieldName) }, 224 | ) 225 | } 226 | 227 | var ft common.Type 228 | if f != nil { 229 | ft = f.Type 230 | sf := hasSubfields(ft) 231 | if sf && sel.Selections == nil { 232 | c.addErr(sel.Alias.Loc, "ScalarLeafs", "Field %q of type %q must have a selection of subfields. Did you mean \"%s { ... }\"?", fieldName, ft, fieldName) 233 | } 234 | if !sf && sel.Selections != nil { 235 | c.addErr(sel.SelectionSetLoc, "ScalarLeafs", "Field %q must not have a selection since type %q has no subfields.", fieldName, ft) 236 | } 237 | } 238 | if sel.Selections != nil { 239 | validateSelectionSet(c, sel.Selections, unwrapType(ft)) 240 | } 241 | 242 | case *query.InlineFragment: 243 | validateDirectives(c, "INLINE_FRAGMENT", sel.Directives) 244 | if sel.On.Name != "" { 245 | fragTyp := unwrapType(resolveType(c.context, &sel.On)) 246 | if fragTyp != nil && !compatible(t, fragTyp) { 247 | c.addErr(sel.Loc, "PossibleFragmentSpreads", "Fragment cannot be spread here as objects of type %q can never be of type %q.", t, fragTyp) 248 | } 249 | t = fragTyp 250 | // continue even if t is nil 251 | } 252 | if t != nil && !canBeFragment(t) { 253 | c.addErr(sel.On.Loc, "FragmentsOnCompositeTypes", "Fragment cannot condition on non composite type %q.", t) 254 | return 255 | } 256 | validateSelectionSet(c, sel.Selections, unwrapType(t)) 257 | 258 | case *query.FragmentSpread: 259 | validateDirectives(c, "FRAGMENT_SPREAD", sel.Directives) 260 | frag := c.doc.Fragments.Get(sel.Name.Name) 261 | if frag == nil { 262 | c.addErr(sel.Name.Loc, "KnownFragmentNames", "Unknown fragment %q.", sel.Name.Name) 263 | return 264 | } 265 | fragTyp := c.schema.Types[frag.On.Name] 266 | if !compatible(t, fragTyp) { 267 | 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) 268 | } 269 | 270 | default: 271 | panic("unreachable") 272 | } 273 | } 274 | 275 | func compatible(a, b common.Type) bool { 276 | for _, pta := range possibleTypes(a) { 277 | for _, ptb := range possibleTypes(b) { 278 | if pta == ptb { 279 | return true 280 | } 281 | } 282 | } 283 | return false 284 | } 285 | 286 | func possibleTypes(t common.Type) []*schema.Object { 287 | switch t := t.(type) { 288 | case *schema.Object: 289 | return []*schema.Object{t} 290 | case *schema.Interface: 291 | return t.PossibleTypes 292 | case *schema.Union: 293 | return t.PossibleTypes 294 | default: 295 | return nil 296 | } 297 | } 298 | 299 | func markUsedFragments(c *context, sels []query.Selection, fragUsed map[*query.FragmentDecl]struct{}) { 300 | for _, sel := range sels { 301 | switch sel := sel.(type) { 302 | case *query.Field: 303 | if sel.Selections != nil { 304 | markUsedFragments(c, sel.Selections, fragUsed) 305 | } 306 | 307 | case *query.InlineFragment: 308 | markUsedFragments(c, sel.Selections, fragUsed) 309 | 310 | case *query.FragmentSpread: 311 | frag := c.doc.Fragments.Get(sel.Name.Name) 312 | if frag == nil { 313 | return 314 | } 315 | 316 | if _, ok := fragUsed[frag]; ok { 317 | return 318 | } 319 | fragUsed[frag] = struct{}{} 320 | markUsedFragments(c, frag.Selections, fragUsed) 321 | 322 | default: 323 | panic("unreachable") 324 | } 325 | } 326 | } 327 | 328 | func detectFragmentCycle(c *context, sels []query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) { 329 | for _, sel := range sels { 330 | detectFragmentCycleSel(c, sel, fragVisited, spreadPath, spreadPathIndex) 331 | } 332 | } 333 | 334 | func detectFragmentCycleSel(c *context, sel query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) { 335 | switch sel := sel.(type) { 336 | case *query.Field: 337 | if sel.Selections != nil { 338 | detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex) 339 | } 340 | 341 | case *query.InlineFragment: 342 | detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex) 343 | 344 | case *query.FragmentSpread: 345 | frag := c.doc.Fragments.Get(sel.Name.Name) 346 | if frag == nil { 347 | return 348 | } 349 | 350 | spreadPath = append(spreadPath, sel) 351 | if i, ok := spreadPathIndex[frag.Name.Name]; ok { 352 | cyclePath := spreadPath[i:] 353 | via := "" 354 | if len(cyclePath) > 1 { 355 | names := make([]string, len(cyclePath)-1) 356 | for i, frag := range cyclePath[:len(cyclePath)-1] { 357 | names[i] = frag.Name.Name 358 | } 359 | via = " via " + strings.Join(names, ", ") 360 | } 361 | 362 | locs := make([]errors.Location, len(cyclePath)) 363 | for i, frag := range cyclePath { 364 | locs[i] = frag.Loc 365 | } 366 | c.addErrMultiLoc(locs, "NoFragmentCycles", "Cannot spread fragment %q within itself%s.", frag.Name.Name, via) 367 | return 368 | } 369 | 370 | if _, ok := fragVisited[frag]; ok { 371 | return 372 | } 373 | fragVisited[frag] = struct{}{} 374 | 375 | spreadPathIndex[frag.Name.Name] = len(spreadPath) 376 | detectFragmentCycle(c, frag.Selections, fragVisited, spreadPath, spreadPathIndex) 377 | delete(spreadPathIndex, frag.Name.Name) 378 | 379 | default: 380 | panic("unreachable") 381 | } 382 | } 383 | 384 | func (c *context) validateOverlap(a, b query.Selection, reasons *[]string, locs *[]errors.Location) { 385 | if a == b { 386 | return 387 | } 388 | 389 | if _, ok := c.overlapValidated[selectionPair{a, b}]; ok { 390 | return 391 | } 392 | c.overlapValidated[selectionPair{a, b}] = struct{}{} 393 | c.overlapValidated[selectionPair{b, a}] = struct{}{} 394 | 395 | switch a := a.(type) { 396 | case *query.Field: 397 | switch b := b.(type) { 398 | case *query.Field: 399 | if b.Alias.Loc.Before(a.Alias.Loc) { 400 | a, b = b, a 401 | } 402 | if reasons2, locs2 := c.validateFieldOverlap(a, b); len(reasons2) != 0 { 403 | locs2 = append(locs2, a.Alias.Loc, b.Alias.Loc) 404 | if reasons == nil { 405 | 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 ")) 406 | return 407 | } 408 | for _, r := range reasons2 { 409 | *reasons = append(*reasons, fmt.Sprintf("subfields %q conflict because %s", a.Alias.Name, r)) 410 | } 411 | *locs = append(*locs, locs2...) 412 | } 413 | 414 | case *query.InlineFragment: 415 | for _, sel := range b.Selections { 416 | c.validateOverlap(a, sel, reasons, locs) 417 | } 418 | 419 | case *query.FragmentSpread: 420 | if frag := c.doc.Fragments.Get(b.Name.Name); frag != nil { 421 | for _, sel := range frag.Selections { 422 | c.validateOverlap(a, sel, reasons, locs) 423 | } 424 | } 425 | 426 | default: 427 | panic("unreachable") 428 | } 429 | 430 | case *query.InlineFragment: 431 | for _, sel := range a.Selections { 432 | c.validateOverlap(sel, b, reasons, locs) 433 | } 434 | 435 | case *query.FragmentSpread: 436 | if frag := c.doc.Fragments.Get(a.Name.Name); frag != nil { 437 | for _, sel := range frag.Selections { 438 | c.validateOverlap(sel, b, reasons, locs) 439 | } 440 | } 441 | 442 | default: 443 | panic("unreachable") 444 | } 445 | } 446 | 447 | func (c *context) validateFieldOverlap(a, b *query.Field) ([]string, []errors.Location) { 448 | if a.Alias.Name != b.Alias.Name { 449 | return nil, nil 450 | } 451 | 452 | if asf := c.fieldMap[a].sf; asf != nil { 453 | if bsf := c.fieldMap[b].sf; bsf != nil { 454 | if !typesCompatible(asf.Type, bsf.Type) { 455 | return []string{fmt.Sprintf("they return conflicting types %s and %s", asf.Type, bsf.Type)}, nil 456 | } 457 | } 458 | } 459 | 460 | at := c.fieldMap[a].parent 461 | bt := c.fieldMap[b].parent 462 | if at == nil || bt == nil || at == bt { 463 | if a.Name.Name != b.Name.Name { 464 | return []string{fmt.Sprintf("%s and %s are different fields", a.Name.Name, b.Name.Name)}, nil 465 | } 466 | 467 | if argumentsConflict(a.Arguments, b.Arguments) { 468 | return []string{"they have differing arguments"}, nil 469 | } 470 | } 471 | 472 | var reasons []string 473 | var locs []errors.Location 474 | for _, a2 := range a.Selections { 475 | for _, b2 := range b.Selections { 476 | c.validateOverlap(a2, b2, &reasons, &locs) 477 | } 478 | } 479 | return reasons, locs 480 | } 481 | 482 | func argumentsConflict(a, b common.ArgumentList) bool { 483 | if len(a) != len(b) { 484 | return true 485 | } 486 | for _, argA := range a { 487 | valB, ok := b.Get(argA.Name.Name) 488 | if !ok || !reflect.DeepEqual(argA.Value.Value(nil), valB.Value(nil)) { 489 | return true 490 | } 491 | } 492 | return false 493 | } 494 | 495 | func fields(t common.Type) schema.FieldList { 496 | switch t := t.(type) { 497 | case *schema.Object: 498 | return t.Fields 499 | case *schema.Interface: 500 | return t.Fields 501 | default: 502 | return nil 503 | } 504 | } 505 | 506 | func unwrapType(t common.Type) schema.NamedType { 507 | if t == nil { 508 | return nil 509 | } 510 | for { 511 | switch t2 := t.(type) { 512 | case schema.NamedType: 513 | return t2 514 | case *common.List: 515 | t = t2.OfType 516 | case *common.NonNull: 517 | t = t2.OfType 518 | default: 519 | panic("unreachable") 520 | } 521 | } 522 | } 523 | 524 | func resolveType(c *context, t common.Type) common.Type { 525 | t2, err := common.ResolveType(t, c.schema.Resolve) 526 | if err != nil { 527 | c.errs = append(c.errs, err) 528 | } 529 | return t2 530 | } 531 | 532 | func validateDirectives(c *opContext, loc string, directives common.DirectiveList) { 533 | directiveNames := make(nameSet) 534 | for _, d := range directives { 535 | dirName := d.Name.Name 536 | validateNameCustomMsg(c.context, directiveNames, d.Name, "UniqueDirectivesPerLocation", func() string { 537 | return fmt.Sprintf("The directive %q can only be used once at this location.", dirName) 538 | }) 539 | 540 | validateArgumentLiterals(c, d.Args) 541 | 542 | dd, ok := c.schema.Directives[dirName] 543 | if !ok { 544 | c.addErr(d.Name.Loc, "KnownDirectives", "Unknown directive %q.", dirName) 545 | continue 546 | } 547 | 548 | locOK := false 549 | for _, allowedLoc := range dd.Locs { 550 | if loc == allowedLoc { 551 | locOK = true 552 | break 553 | } 554 | } 555 | if !locOK { 556 | c.addErr(d.Name.Loc, "KnownDirectives", "Directive %q may not be used on %s.", dirName, loc) 557 | } 558 | 559 | validateArgumentTypes(c, d.Args, dd.Args, d.Name.Loc, 560 | func() string { return fmt.Sprintf("directive %q", "@"+dirName) }, 561 | func() string { return fmt.Sprintf("Directive %q", "@"+dirName) }, 562 | ) 563 | } 564 | return 565 | } 566 | 567 | type nameSet map[string]errors.Location 568 | 569 | func validateName(c *context, set nameSet, name common.Ident, rule string, kind string) { 570 | validateNameCustomMsg(c, set, name, rule, func() string { 571 | return fmt.Sprintf("There can be only one %s named %q.", kind, name.Name) 572 | }) 573 | } 574 | 575 | func validateNameCustomMsg(c *context, set nameSet, name common.Ident, rule string, msg func() string) { 576 | if loc, ok := set[name.Name]; ok { 577 | c.addErrMultiLoc([]errors.Location{loc, name.Loc}, rule, msg()) 578 | return 579 | } 580 | set[name.Name] = name.Loc 581 | return 582 | } 583 | 584 | func validateArgumentTypes(c *opContext, args common.ArgumentList, argDecls common.InputValueList, loc errors.Location, owner1, owner2 func() string) { 585 | for _, selArg := range args { 586 | arg := argDecls.Get(selArg.Name.Name) 587 | if arg == nil { 588 | c.addErr(selArg.Name.Loc, "KnownArgumentNames", "Unknown argument %q on %s.", selArg.Name.Name, owner1()) 589 | continue 590 | } 591 | value := selArg.Value 592 | if ok, reason := validateValueType(c, value, arg.Type); !ok { 593 | c.addErr(value.Location(), "ArgumentsOfCorrectType", "Argument %q has invalid value %s.\n%s", arg.Name.Name, value, reason) 594 | } 595 | } 596 | for _, decl := range argDecls { 597 | if _, ok := decl.Type.(*common.NonNull); ok { 598 | if _, ok := args.Get(decl.Name.Name); !ok { 599 | c.addErr(loc, "ProvidedNonNullArguments", "%s argument %q of type %q is required but not provided.", owner2(), decl.Name.Name, decl.Type) 600 | } 601 | } 602 | } 603 | } 604 | 605 | func validateArgumentLiterals(c *opContext, args common.ArgumentList) { 606 | argNames := make(nameSet) 607 | for _, arg := range args { 608 | validateName(c.context, argNames, arg.Name, "UniqueArgumentNames", "argument") 609 | validateLiteral(c, arg.Value) 610 | } 611 | } 612 | 613 | func validateLiteral(c *opContext, l common.Literal) { 614 | switch l := l.(type) { 615 | case *common.ObjectLit: 616 | fieldNames := make(nameSet) 617 | for _, f := range l.Fields { 618 | validateName(c.context, fieldNames, f.Name, "UniqueInputFieldNames", "input field") 619 | validateLiteral(c, f.Value) 620 | } 621 | case *common.ListLit: 622 | for _, entry := range l.Entries { 623 | validateLiteral(c, entry) 624 | } 625 | case *common.Variable: 626 | for _, op := range c.ops { 627 | v := op.Vars.Get(l.Name) 628 | if v == nil { 629 | byOp := "" 630 | if op.Name.Name != "" { 631 | byOp = fmt.Sprintf(" by operation %q", op.Name.Name) 632 | } 633 | c.opErrs[op] = append(c.opErrs[op], &errors.QueryError{ 634 | Message: fmt.Sprintf("Variable %q is not defined%s.", "$"+l.Name, byOp), 635 | Locations: []errors.Location{l.Loc, op.Loc}, 636 | Rule: "NoUndefinedVariables", 637 | }) 638 | continue 639 | } 640 | c.usedVars[op][v] = struct{}{} 641 | } 642 | } 643 | } 644 | 645 | func validateValueType(c *opContext, v common.Literal, t common.Type) (bool, string) { 646 | if v, ok := v.(*common.Variable); ok { 647 | for _, op := range c.ops { 648 | if v2 := op.Vars.Get(v.Name); v2 != nil { 649 | t2, err := common.ResolveType(v2.Type, c.schema.Resolve) 650 | if _, ok := t2.(*common.NonNull); !ok && v2.Default != nil { 651 | t2 = &common.NonNull{OfType: t2} 652 | } 653 | if err == nil && !typeCanBeUsedAs(t2, t) { 654 | c.addErrMultiLoc([]errors.Location{v2.Loc, v.Loc}, "VariablesInAllowedPosition", "Variable %q of type %q used in position expecting type %q.", "$"+v.Name, t2, t) 655 | } 656 | } 657 | } 658 | return true, "" 659 | } 660 | 661 | if nn, ok := t.(*common.NonNull); ok { 662 | if isNull(v) { 663 | return false, fmt.Sprintf("Expected %q, found null.", t) 664 | } 665 | t = nn.OfType 666 | } 667 | if isNull(v) { 668 | return true, "" 669 | } 670 | 671 | switch t := t.(type) { 672 | case *schema.Scalar, *schema.Enum: 673 | if lit, ok := v.(*common.BasicLit); ok { 674 | if validateBasicLit(lit, t) { 675 | return true, "" 676 | } 677 | } 678 | 679 | case *common.List: 680 | list, ok := v.(*common.ListLit) 681 | if !ok { 682 | return validateValueType(c, v, t.OfType) // single value instead of list 683 | } 684 | for i, entry := range list.Entries { 685 | if ok, reason := validateValueType(c, entry, t.OfType); !ok { 686 | return false, fmt.Sprintf("In element #%d: %s", i, reason) 687 | } 688 | } 689 | return true, "" 690 | 691 | case *schema.InputObject: 692 | v, ok := v.(*common.ObjectLit) 693 | if !ok { 694 | return false, fmt.Sprintf("Expected %q, found not an object.", t) 695 | } 696 | for _, f := range v.Fields { 697 | name := f.Name.Name 698 | iv := t.Values.Get(name) 699 | if iv == nil { 700 | return false, fmt.Sprintf("In field %q: Unknown field.", name) 701 | } 702 | if ok, reason := validateValueType(c, f.Value, iv.Type); !ok { 703 | return false, fmt.Sprintf("In field %q: %s", name, reason) 704 | } 705 | } 706 | for _, iv := range t.Values { 707 | found := false 708 | for _, f := range v.Fields { 709 | if f.Name.Name == iv.Name.Name { 710 | found = true 711 | break 712 | } 713 | } 714 | if !found { 715 | if _, ok := iv.Type.(*common.NonNull); ok && iv.Default == nil { 716 | return false, fmt.Sprintf("In field %q: Expected %q, found null.", iv.Name.Name, iv.Type) 717 | } 718 | } 719 | } 720 | return true, "" 721 | } 722 | 723 | return false, fmt.Sprintf("Expected type %q, found %s.", t, v) 724 | } 725 | 726 | func validateBasicLit(v *common.BasicLit, t common.Type) bool { 727 | switch t := t.(type) { 728 | case *schema.Scalar: 729 | switch t.Name { 730 | case "Int": 731 | if v.Type != scanner.Int { 732 | return false 733 | } 734 | f, err := strconv.ParseFloat(v.Text, 64) 735 | if err != nil { 736 | panic(err) 737 | } 738 | return f >= math.MinInt32 && f <= math.MaxInt32 739 | case "Float": 740 | return v.Type == scanner.Int || v.Type == scanner.Float 741 | case "String": 742 | return v.Type == scanner.String 743 | case "Boolean": 744 | return v.Type == scanner.Ident && (v.Text == "true" || v.Text == "false") 745 | case "ID": 746 | return v.Type == scanner.Int || v.Type == scanner.String 747 | default: 748 | //TODO: Type-check against expected type by Unmarshalling 749 | return true 750 | } 751 | 752 | case *schema.Enum: 753 | if v.Type != scanner.Ident { 754 | return false 755 | } 756 | for _, option := range t.Values { 757 | if option.Name == v.Text { 758 | return true 759 | } 760 | } 761 | return false 762 | } 763 | 764 | return false 765 | } 766 | 767 | func canBeFragment(t common.Type) bool { 768 | switch t.(type) { 769 | case *schema.Object, *schema.Interface, *schema.Union: 770 | return true 771 | default: 772 | return false 773 | } 774 | } 775 | 776 | func canBeInput(t common.Type) bool { 777 | switch t := t.(type) { 778 | case *schema.InputObject, *schema.Scalar, *schema.Enum: 779 | return true 780 | case *common.List: 781 | return canBeInput(t.OfType) 782 | case *common.NonNull: 783 | return canBeInput(t.OfType) 784 | default: 785 | return false 786 | } 787 | } 788 | 789 | func hasSubfields(t common.Type) bool { 790 | switch t := t.(type) { 791 | case *schema.Object, *schema.Interface, *schema.Union: 792 | return true 793 | case *common.List: 794 | return hasSubfields(t.OfType) 795 | case *common.NonNull: 796 | return hasSubfields(t.OfType) 797 | default: 798 | return false 799 | } 800 | } 801 | 802 | func isLeaf(t common.Type) bool { 803 | switch t.(type) { 804 | case *schema.Scalar, *schema.Enum: 805 | return true 806 | default: 807 | return false 808 | } 809 | } 810 | 811 | func isNull(lit interface{}) bool { 812 | _, ok := lit.(*common.NullLit) 813 | return ok 814 | } 815 | 816 | func typesCompatible(a, b common.Type) bool { 817 | al, aIsList := a.(*common.List) 818 | bl, bIsList := b.(*common.List) 819 | if aIsList || bIsList { 820 | return aIsList && bIsList && typesCompatible(al.OfType, bl.OfType) 821 | } 822 | 823 | ann, aIsNN := a.(*common.NonNull) 824 | bnn, bIsNN := b.(*common.NonNull) 825 | if aIsNN || bIsNN { 826 | return aIsNN && bIsNN && typesCompatible(ann.OfType, bnn.OfType) 827 | } 828 | 829 | if isLeaf(a) || isLeaf(b) { 830 | return a == b 831 | } 832 | 833 | return true 834 | } 835 | 836 | func typeCanBeUsedAs(t, as common.Type) bool { 837 | nnT, okT := t.(*common.NonNull) 838 | if okT { 839 | t = nnT.OfType 840 | } 841 | 842 | nnAs, okAs := as.(*common.NonNull) 843 | if okAs { 844 | as = nnAs.OfType 845 | if !okT { 846 | return false // nullable can not be used as non-null 847 | } 848 | } 849 | 850 | if t == as { 851 | return true 852 | } 853 | 854 | if lT, ok := t.(*common.List); ok { 855 | if lAs, ok := as.(*common.List); ok { 856 | return typeCanBeUsedAs(lT.OfType, lAs.OfType) 857 | } 858 | } 859 | return false 860 | } 861 | -------------------------------------------------------------------------------- /graphql_test.go: -------------------------------------------------------------------------------- 1 | package graphql_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/neelance/graphql-go" 9 | "github.com/neelance/graphql-go/example/starwars" 10 | "github.com/neelance/graphql-go/gqltesting" 11 | ) 12 | 13 | type helloWorldResolver1 struct{} 14 | 15 | func (r *helloWorldResolver1) Hello() string { 16 | return "Hello world!" 17 | } 18 | 19 | type helloWorldResolver2 struct{} 20 | 21 | func (r *helloWorldResolver2) Hello(ctx context.Context) (string, error) { 22 | return "Hello world!", nil 23 | } 24 | 25 | type helloSnakeResolver1 struct{} 26 | 27 | func (r *helloSnakeResolver1) HelloHTML() string { 28 | return "Hello snake!" 29 | } 30 | 31 | func (r *helloSnakeResolver1) SayHello(args struct{ FullName string }) string { 32 | return "Hello " + args.FullName + "!" 33 | } 34 | 35 | type helloSnakeResolver2 struct{} 36 | 37 | func (r *helloSnakeResolver2) HelloHTML(ctx context.Context) (string, error) { 38 | return "Hello snake!", nil 39 | } 40 | 41 | func (r *helloSnakeResolver2) SayHello(ctx context.Context, args struct{ FullName string }) (string, error) { 42 | return "Hello " + args.FullName + "!", nil 43 | } 44 | 45 | type theNumberResolver struct { 46 | number int32 47 | } 48 | 49 | func (r *theNumberResolver) TheNumber() int32 { 50 | return r.number 51 | } 52 | 53 | func (r *theNumberResolver) ChangeTheNumber(args struct{ NewNumber int32 }) *theNumberResolver { 54 | r.number = args.NewNumber 55 | return r 56 | } 57 | 58 | type timeResolver struct{} 59 | 60 | func (r *timeResolver) AddHour(args struct{ Time graphql.Time }) graphql.Time { 61 | return graphql.Time{Time: args.Time.Add(time.Hour)} 62 | } 63 | 64 | var starwarsSchema = graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{}) 65 | 66 | func TestHelloWorld(t *testing.T) { 67 | gqltesting.RunTests(t, []*gqltesting.Test{ 68 | { 69 | Schema: graphql.MustParseSchema(` 70 | schema { 71 | query: Query 72 | } 73 | 74 | type Query { 75 | hello: String! 76 | } 77 | `, &helloWorldResolver1{}), 78 | Query: ` 79 | { 80 | hello 81 | } 82 | `, 83 | ExpectedResult: ` 84 | { 85 | "hello": "Hello world!" 86 | } 87 | `, 88 | }, 89 | 90 | { 91 | Schema: graphql.MustParseSchema(` 92 | schema { 93 | query: Query 94 | } 95 | 96 | type Query { 97 | hello: String! 98 | } 99 | `, &helloWorldResolver2{}), 100 | Query: ` 101 | { 102 | hello 103 | } 104 | `, 105 | ExpectedResult: ` 106 | { 107 | "hello": "Hello world!" 108 | } 109 | `, 110 | }, 111 | }) 112 | } 113 | 114 | func TestHelloSnake(t *testing.T) { 115 | gqltesting.RunTests(t, []*gqltesting.Test{ 116 | { 117 | Schema: graphql.MustParseSchema(` 118 | schema { 119 | query: Query 120 | } 121 | 122 | type Query { 123 | hello_html: String! 124 | } 125 | `, &helloSnakeResolver1{}), 126 | Query: ` 127 | { 128 | hello_html 129 | } 130 | `, 131 | ExpectedResult: ` 132 | { 133 | "hello_html": "Hello snake!" 134 | } 135 | `, 136 | }, 137 | 138 | { 139 | Schema: graphql.MustParseSchema(` 140 | schema { 141 | query: Query 142 | } 143 | 144 | type Query { 145 | hello_html: String! 146 | } 147 | `, &helloSnakeResolver2{}), 148 | Query: ` 149 | { 150 | hello_html 151 | } 152 | `, 153 | ExpectedResult: ` 154 | { 155 | "hello_html": "Hello snake!" 156 | } 157 | `, 158 | }, 159 | }) 160 | } 161 | 162 | func TestHelloSnakeArguments(t *testing.T) { 163 | gqltesting.RunTests(t, []*gqltesting.Test{ 164 | { 165 | Schema: graphql.MustParseSchema(` 166 | schema { 167 | query: Query 168 | } 169 | 170 | type Query { 171 | say_hello(full_name: String!): String! 172 | } 173 | `, &helloSnakeResolver1{}), 174 | Query: ` 175 | { 176 | say_hello(full_name: "Rob Pike") 177 | } 178 | `, 179 | ExpectedResult: ` 180 | { 181 | "say_hello": "Hello Rob Pike!" 182 | } 183 | `, 184 | }, 185 | 186 | { 187 | Schema: graphql.MustParseSchema(` 188 | schema { 189 | query: Query 190 | } 191 | 192 | type Query { 193 | say_hello(full_name: String!): String! 194 | } 195 | `, &helloSnakeResolver2{}), 196 | Query: ` 197 | { 198 | say_hello(full_name: "Rob Pike") 199 | } 200 | `, 201 | ExpectedResult: ` 202 | { 203 | "say_hello": "Hello Rob Pike!" 204 | } 205 | `, 206 | }, 207 | }) 208 | } 209 | 210 | func TestBasic(t *testing.T) { 211 | gqltesting.RunTests(t, []*gqltesting.Test{ 212 | { 213 | Schema: starwarsSchema, 214 | Query: ` 215 | { 216 | hero { 217 | id 218 | name 219 | friends { 220 | name 221 | } 222 | } 223 | } 224 | `, 225 | ExpectedResult: ` 226 | { 227 | "hero": { 228 | "id": "2001", 229 | "name": "R2-D2", 230 | "friends": [ 231 | { 232 | "name": "Luke Skywalker" 233 | }, 234 | { 235 | "name": "Han Solo" 236 | }, 237 | { 238 | "name": "Leia Organa" 239 | } 240 | ] 241 | } 242 | } 243 | `, 244 | }, 245 | }) 246 | } 247 | 248 | func TestArguments(t *testing.T) { 249 | gqltesting.RunTests(t, []*gqltesting.Test{ 250 | { 251 | Schema: starwarsSchema, 252 | Query: ` 253 | { 254 | human(id: "1000") { 255 | name 256 | height 257 | } 258 | } 259 | `, 260 | ExpectedResult: ` 261 | { 262 | "human": { 263 | "name": "Luke Skywalker", 264 | "height": 1.72 265 | } 266 | } 267 | `, 268 | }, 269 | 270 | { 271 | Schema: starwarsSchema, 272 | Query: ` 273 | { 274 | human(id: "1000") { 275 | name 276 | height(unit: FOOT) 277 | } 278 | } 279 | `, 280 | ExpectedResult: ` 281 | { 282 | "human": { 283 | "name": "Luke Skywalker", 284 | "height": 5.6430448 285 | } 286 | } 287 | `, 288 | }, 289 | }) 290 | } 291 | 292 | func TestAliases(t *testing.T) { 293 | gqltesting.RunTests(t, []*gqltesting.Test{ 294 | { 295 | Schema: starwarsSchema, 296 | Query: ` 297 | { 298 | empireHero: hero(episode: EMPIRE) { 299 | name 300 | } 301 | jediHero: hero(episode: JEDI) { 302 | name 303 | } 304 | } 305 | `, 306 | ExpectedResult: ` 307 | { 308 | "empireHero": { 309 | "name": "Luke Skywalker" 310 | }, 311 | "jediHero": { 312 | "name": "R2-D2" 313 | } 314 | } 315 | `, 316 | }, 317 | }) 318 | } 319 | 320 | func TestFragments(t *testing.T) { 321 | gqltesting.RunTests(t, []*gqltesting.Test{ 322 | { 323 | Schema: starwarsSchema, 324 | Query: ` 325 | { 326 | leftComparison: hero(episode: EMPIRE) { 327 | ...comparisonFields 328 | ...height 329 | } 330 | rightComparison: hero(episode: JEDI) { 331 | ...comparisonFields 332 | ...height 333 | } 334 | } 335 | 336 | fragment comparisonFields on Character { 337 | name 338 | appearsIn 339 | friends { 340 | name 341 | } 342 | } 343 | 344 | fragment height on Human { 345 | height 346 | } 347 | `, 348 | ExpectedResult: ` 349 | { 350 | "leftComparison": { 351 | "name": "Luke Skywalker", 352 | "appearsIn": [ 353 | "NEWHOPE", 354 | "EMPIRE", 355 | "JEDI" 356 | ], 357 | "friends": [ 358 | { 359 | "name": "Han Solo" 360 | }, 361 | { 362 | "name": "Leia Organa" 363 | }, 364 | { 365 | "name": "C-3PO" 366 | }, 367 | { 368 | "name": "R2-D2" 369 | } 370 | ], 371 | "height": 1.72 372 | }, 373 | "rightComparison": { 374 | "name": "R2-D2", 375 | "appearsIn": [ 376 | "NEWHOPE", 377 | "EMPIRE", 378 | "JEDI" 379 | ], 380 | "friends": [ 381 | { 382 | "name": "Luke Skywalker" 383 | }, 384 | { 385 | "name": "Han Solo" 386 | }, 387 | { 388 | "name": "Leia Organa" 389 | } 390 | ] 391 | } 392 | } 393 | `, 394 | }, 395 | }) 396 | } 397 | 398 | func TestVariables(t *testing.T) { 399 | gqltesting.RunTests(t, []*gqltesting.Test{ 400 | { 401 | Schema: starwarsSchema, 402 | Query: ` 403 | query HeroNameAndFriends($episode: Episode) { 404 | hero(episode: $episode) { 405 | name 406 | } 407 | } 408 | `, 409 | Variables: map[string]interface{}{ 410 | "episode": "JEDI", 411 | }, 412 | ExpectedResult: ` 413 | { 414 | "hero": { 415 | "name": "R2-D2" 416 | } 417 | } 418 | `, 419 | }, 420 | 421 | { 422 | Schema: starwarsSchema, 423 | Query: ` 424 | query HeroNameAndFriends($episode: Episode) { 425 | hero(episode: $episode) { 426 | name 427 | } 428 | } 429 | `, 430 | Variables: map[string]interface{}{ 431 | "episode": "EMPIRE", 432 | }, 433 | ExpectedResult: ` 434 | { 435 | "hero": { 436 | "name": "Luke Skywalker" 437 | } 438 | } 439 | `, 440 | }, 441 | }) 442 | } 443 | 444 | func TestSkipDirective(t *testing.T) { 445 | gqltesting.RunTests(t, []*gqltesting.Test{ 446 | { 447 | Schema: starwarsSchema, 448 | Query: ` 449 | query Hero($episode: Episode, $withoutFriends: Boolean!) { 450 | hero(episode: $episode) { 451 | name 452 | friends @skip(if: $withoutFriends) { 453 | name 454 | } 455 | } 456 | } 457 | `, 458 | Variables: map[string]interface{}{ 459 | "episode": "JEDI", 460 | "withoutFriends": true, 461 | }, 462 | ExpectedResult: ` 463 | { 464 | "hero": { 465 | "name": "R2-D2" 466 | } 467 | } 468 | `, 469 | }, 470 | 471 | { 472 | Schema: starwarsSchema, 473 | Query: ` 474 | query Hero($episode: Episode, $withoutFriends: Boolean!) { 475 | hero(episode: $episode) { 476 | name 477 | friends @skip(if: $withoutFriends) { 478 | name 479 | } 480 | } 481 | } 482 | `, 483 | Variables: map[string]interface{}{ 484 | "episode": "JEDI", 485 | "withoutFriends": false, 486 | }, 487 | ExpectedResult: ` 488 | { 489 | "hero": { 490 | "name": "R2-D2", 491 | "friends": [ 492 | { 493 | "name": "Luke Skywalker" 494 | }, 495 | { 496 | "name": "Han Solo" 497 | }, 498 | { 499 | "name": "Leia Organa" 500 | } 501 | ] 502 | } 503 | } 504 | `, 505 | }, 506 | }) 507 | } 508 | 509 | func TestIncludeDirective(t *testing.T) { 510 | gqltesting.RunTests(t, []*gqltesting.Test{ 511 | { 512 | Schema: starwarsSchema, 513 | Query: ` 514 | query Hero($episode: Episode, $withFriends: Boolean!) { 515 | hero(episode: $episode) { 516 | name 517 | ...friendsFragment @include(if: $withFriends) 518 | } 519 | } 520 | 521 | fragment friendsFragment on Character { 522 | friends { 523 | name 524 | } 525 | } 526 | `, 527 | Variables: map[string]interface{}{ 528 | "episode": "JEDI", 529 | "withFriends": false, 530 | }, 531 | ExpectedResult: ` 532 | { 533 | "hero": { 534 | "name": "R2-D2" 535 | } 536 | } 537 | `, 538 | }, 539 | 540 | { 541 | Schema: starwarsSchema, 542 | Query: ` 543 | query Hero($episode: Episode, $withFriends: Boolean!) { 544 | hero(episode: $episode) { 545 | name 546 | ...friendsFragment @include(if: $withFriends) 547 | } 548 | } 549 | 550 | fragment friendsFragment on Character { 551 | friends { 552 | name 553 | } 554 | } 555 | `, 556 | Variables: map[string]interface{}{ 557 | "episode": "JEDI", 558 | "withFriends": true, 559 | }, 560 | ExpectedResult: ` 561 | { 562 | "hero": { 563 | "name": "R2-D2", 564 | "friends": [ 565 | { 566 | "name": "Luke Skywalker" 567 | }, 568 | { 569 | "name": "Han Solo" 570 | }, 571 | { 572 | "name": "Leia Organa" 573 | } 574 | ] 575 | } 576 | } 577 | `, 578 | }, 579 | }) 580 | } 581 | 582 | type testDeprecatedDirectiveResolver struct{} 583 | 584 | func (r *testDeprecatedDirectiveResolver) A() int32 { 585 | return 0 586 | } 587 | 588 | func (r *testDeprecatedDirectiveResolver) B() int32 { 589 | return 0 590 | } 591 | 592 | func (r *testDeprecatedDirectiveResolver) C() int32 { 593 | return 0 594 | } 595 | 596 | func TestDeprecatedDirective(t *testing.T) { 597 | gqltesting.RunTests(t, []*gqltesting.Test{ 598 | { 599 | Schema: graphql.MustParseSchema(` 600 | schema { 601 | query: Query 602 | } 603 | 604 | type Query { 605 | a: Int! 606 | b: Int! @deprecated 607 | c: Int! @deprecated(reason: "We don't like it") 608 | } 609 | `, &testDeprecatedDirectiveResolver{}), 610 | Query: ` 611 | { 612 | __type(name: "Query") { 613 | fields { 614 | name 615 | } 616 | allFields: fields(includeDeprecated: true) { 617 | name 618 | isDeprecated 619 | deprecationReason 620 | } 621 | } 622 | } 623 | `, 624 | ExpectedResult: ` 625 | { 626 | "__type": { 627 | "fields": [ 628 | { "name": "a" } 629 | ], 630 | "allFields": [ 631 | { "name": "a", "isDeprecated": false, "deprecationReason": null }, 632 | { "name": "b", "isDeprecated": true, "deprecationReason": "No longer supported" }, 633 | { "name": "c", "isDeprecated": true, "deprecationReason": "We don't like it" } 634 | ] 635 | } 636 | } 637 | `, 638 | }, 639 | { 640 | Schema: graphql.MustParseSchema(` 641 | schema { 642 | query: Query 643 | } 644 | 645 | type Query { 646 | } 647 | 648 | enum Test { 649 | A 650 | B @deprecated 651 | C @deprecated(reason: "We don't like it") 652 | } 653 | `, &testDeprecatedDirectiveResolver{}), 654 | Query: ` 655 | { 656 | __type(name: "Test") { 657 | enumValues { 658 | name 659 | } 660 | allEnumValues: enumValues(includeDeprecated: true) { 661 | name 662 | isDeprecated 663 | deprecationReason 664 | } 665 | } 666 | } 667 | `, 668 | ExpectedResult: ` 669 | { 670 | "__type": { 671 | "enumValues": [ 672 | { "name": "A" } 673 | ], 674 | "allEnumValues": [ 675 | { "name": "A", "isDeprecated": false, "deprecationReason": null }, 676 | { "name": "B", "isDeprecated": true, "deprecationReason": "No longer supported" }, 677 | { "name": "C", "isDeprecated": true, "deprecationReason": "We don't like it" } 678 | ] 679 | } 680 | } 681 | `, 682 | }, 683 | }) 684 | } 685 | 686 | func TestInlineFragments(t *testing.T) { 687 | gqltesting.RunTests(t, []*gqltesting.Test{ 688 | { 689 | Schema: starwarsSchema, 690 | Query: ` 691 | query HeroForEpisode($episode: Episode!) { 692 | hero(episode: $episode) { 693 | name 694 | ... on Droid { 695 | primaryFunction 696 | } 697 | ... on Human { 698 | height 699 | } 700 | } 701 | } 702 | `, 703 | Variables: map[string]interface{}{ 704 | "episode": "JEDI", 705 | }, 706 | ExpectedResult: ` 707 | { 708 | "hero": { 709 | "name": "R2-D2", 710 | "primaryFunction": "Astromech" 711 | } 712 | } 713 | `, 714 | }, 715 | 716 | { 717 | Schema: starwarsSchema, 718 | Query: ` 719 | query HeroForEpisode($episode: Episode!) { 720 | hero(episode: $episode) { 721 | name 722 | ... on Droid { 723 | primaryFunction 724 | } 725 | ... on Human { 726 | height 727 | } 728 | } 729 | } 730 | `, 731 | Variables: map[string]interface{}{ 732 | "episode": "EMPIRE", 733 | }, 734 | ExpectedResult: ` 735 | { 736 | "hero": { 737 | "name": "Luke Skywalker", 738 | "height": 1.72 739 | } 740 | } 741 | `, 742 | }, 743 | }) 744 | } 745 | 746 | func TestTypeName(t *testing.T) { 747 | gqltesting.RunTests(t, []*gqltesting.Test{ 748 | { 749 | Schema: starwarsSchema, 750 | Query: ` 751 | { 752 | search(text: "an") { 753 | __typename 754 | ... on Human { 755 | name 756 | } 757 | ... on Droid { 758 | name 759 | } 760 | ... on Starship { 761 | name 762 | } 763 | } 764 | } 765 | `, 766 | ExpectedResult: ` 767 | { 768 | "search": [ 769 | { 770 | "__typename": "Human", 771 | "name": "Han Solo" 772 | }, 773 | { 774 | "__typename": "Human", 775 | "name": "Leia Organa" 776 | }, 777 | { 778 | "__typename": "Starship", 779 | "name": "TIE Advanced x1" 780 | } 781 | ] 782 | } 783 | `, 784 | }, 785 | 786 | { 787 | Schema: starwarsSchema, 788 | Query: ` 789 | { 790 | human(id: "1000") { 791 | __typename 792 | name 793 | } 794 | } 795 | `, 796 | ExpectedResult: ` 797 | { 798 | "human": { 799 | "__typename": "Human", 800 | "name": "Luke Skywalker" 801 | } 802 | } 803 | `, 804 | }, 805 | }) 806 | } 807 | 808 | func TestConnections(t *testing.T) { 809 | gqltesting.RunTests(t, []*gqltesting.Test{ 810 | { 811 | Schema: starwarsSchema, 812 | Query: ` 813 | { 814 | hero { 815 | name 816 | friendsConnection { 817 | totalCount 818 | pageInfo { 819 | startCursor 820 | endCursor 821 | hasNextPage 822 | } 823 | edges { 824 | cursor 825 | node { 826 | name 827 | } 828 | } 829 | } 830 | } 831 | } 832 | `, 833 | ExpectedResult: ` 834 | { 835 | "hero": { 836 | "name": "R2-D2", 837 | "friendsConnection": { 838 | "totalCount": 3, 839 | "pageInfo": { 840 | "startCursor": "Y3Vyc29yMQ==", 841 | "endCursor": "Y3Vyc29yMw==", 842 | "hasNextPage": false 843 | }, 844 | "edges": [ 845 | { 846 | "cursor": "Y3Vyc29yMQ==", 847 | "node": { 848 | "name": "Luke Skywalker" 849 | } 850 | }, 851 | { 852 | "cursor": "Y3Vyc29yMg==", 853 | "node": { 854 | "name": "Han Solo" 855 | } 856 | }, 857 | { 858 | "cursor": "Y3Vyc29yMw==", 859 | "node": { 860 | "name": "Leia Organa" 861 | } 862 | } 863 | ] 864 | } 865 | } 866 | } 867 | `, 868 | }, 869 | 870 | { 871 | Schema: starwarsSchema, 872 | Query: ` 873 | { 874 | hero { 875 | name 876 | friendsConnection(first: 1, after: "Y3Vyc29yMQ==") { 877 | totalCount 878 | pageInfo { 879 | startCursor 880 | endCursor 881 | hasNextPage 882 | } 883 | edges { 884 | cursor 885 | node { 886 | name 887 | } 888 | } 889 | } 890 | }, 891 | moreFriends: hero { 892 | name 893 | friendsConnection(first: 1, after: "Y3Vyc29yMg==") { 894 | totalCount 895 | pageInfo { 896 | startCursor 897 | endCursor 898 | hasNextPage 899 | } 900 | edges { 901 | cursor 902 | node { 903 | name 904 | } 905 | } 906 | } 907 | } 908 | } 909 | `, 910 | ExpectedResult: ` 911 | { 912 | "hero": { 913 | "name": "R2-D2", 914 | "friendsConnection": { 915 | "totalCount": 3, 916 | "pageInfo": { 917 | "startCursor": "Y3Vyc29yMg==", 918 | "endCursor": "Y3Vyc29yMg==", 919 | "hasNextPage": true 920 | }, 921 | "edges": [ 922 | { 923 | "cursor": "Y3Vyc29yMg==", 924 | "node": { 925 | "name": "Han Solo" 926 | } 927 | } 928 | ] 929 | } 930 | }, 931 | "moreFriends": { 932 | "name": "R2-D2", 933 | "friendsConnection": { 934 | "totalCount": 3, 935 | "pageInfo": { 936 | "startCursor": "Y3Vyc29yMw==", 937 | "endCursor": "Y3Vyc29yMw==", 938 | "hasNextPage": false 939 | }, 940 | "edges": [ 941 | { 942 | "cursor": "Y3Vyc29yMw==", 943 | "node": { 944 | "name": "Leia Organa" 945 | } 946 | } 947 | ] 948 | } 949 | } 950 | } 951 | `, 952 | }, 953 | }) 954 | } 955 | 956 | func TestMutation(t *testing.T) { 957 | gqltesting.RunTests(t, []*gqltesting.Test{ 958 | { 959 | Schema: starwarsSchema, 960 | Query: ` 961 | { 962 | reviews(episode: JEDI) { 963 | stars 964 | commentary 965 | } 966 | } 967 | `, 968 | ExpectedResult: ` 969 | { 970 | "reviews": [] 971 | } 972 | `, 973 | }, 974 | 975 | { 976 | Schema: starwarsSchema, 977 | Query: ` 978 | mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { 979 | createReview(episode: $ep, review: $review) { 980 | stars 981 | commentary 982 | } 983 | } 984 | `, 985 | Variables: map[string]interface{}{ 986 | "ep": "JEDI", 987 | "review": map[string]interface{}{ 988 | "stars": 5, 989 | "commentary": "This is a great movie!", 990 | }, 991 | }, 992 | ExpectedResult: ` 993 | { 994 | "createReview": { 995 | "stars": 5, 996 | "commentary": "This is a great movie!" 997 | } 998 | } 999 | `, 1000 | }, 1001 | 1002 | { 1003 | Schema: starwarsSchema, 1004 | Query: ` 1005 | mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { 1006 | createReview(episode: $ep, review: $review) { 1007 | stars 1008 | commentary 1009 | } 1010 | } 1011 | `, 1012 | Variables: map[string]interface{}{ 1013 | "ep": "EMPIRE", 1014 | "review": map[string]interface{}{ 1015 | "stars": float64(4), 1016 | }, 1017 | }, 1018 | ExpectedResult: ` 1019 | { 1020 | "createReview": { 1021 | "stars": 4, 1022 | "commentary": null 1023 | } 1024 | } 1025 | `, 1026 | }, 1027 | 1028 | { 1029 | Schema: starwarsSchema, 1030 | Query: ` 1031 | { 1032 | reviews(episode: JEDI) { 1033 | stars 1034 | commentary 1035 | } 1036 | } 1037 | `, 1038 | ExpectedResult: ` 1039 | { 1040 | "reviews": [{ 1041 | "stars": 5, 1042 | "commentary": "This is a great movie!" 1043 | }] 1044 | } 1045 | `, 1046 | }, 1047 | }) 1048 | } 1049 | 1050 | func TestIntrospection(t *testing.T) { 1051 | gqltesting.RunTests(t, []*gqltesting.Test{ 1052 | { 1053 | Schema: starwarsSchema, 1054 | Query: ` 1055 | { 1056 | __schema { 1057 | types { 1058 | name 1059 | } 1060 | } 1061 | } 1062 | `, 1063 | ExpectedResult: ` 1064 | { 1065 | "__schema": { 1066 | "types": [ 1067 | { "name": "Boolean" }, 1068 | { "name": "Character" }, 1069 | { "name": "Droid" }, 1070 | { "name": "Episode" }, 1071 | { "name": "Float" }, 1072 | { "name": "FriendsConnection" }, 1073 | { "name": "FriendsEdge" }, 1074 | { "name": "Human" }, 1075 | { "name": "ID" }, 1076 | { "name": "Int" }, 1077 | { "name": "LengthUnit" }, 1078 | { "name": "Mutation" }, 1079 | { "name": "PageInfo" }, 1080 | { "name": "Query" }, 1081 | { "name": "Review" }, 1082 | { "name": "ReviewInput" }, 1083 | { "name": "SearchResult" }, 1084 | { "name": "Starship" }, 1085 | { "name": "String" }, 1086 | { "name": "__Directive" }, 1087 | { "name": "__DirectiveLocation" }, 1088 | { "name": "__EnumValue" }, 1089 | { "name": "__Field" }, 1090 | { "name": "__InputValue" }, 1091 | { "name": "__Schema" }, 1092 | { "name": "__Type" }, 1093 | { "name": "__TypeKind" } 1094 | ] 1095 | } 1096 | } 1097 | `, 1098 | }, 1099 | 1100 | { 1101 | Schema: starwarsSchema, 1102 | Query: ` 1103 | { 1104 | __schema { 1105 | queryType { 1106 | name 1107 | } 1108 | } 1109 | } 1110 | `, 1111 | ExpectedResult: ` 1112 | { 1113 | "__schema": { 1114 | "queryType": { 1115 | "name": "Query" 1116 | } 1117 | } 1118 | } 1119 | `, 1120 | }, 1121 | 1122 | { 1123 | Schema: starwarsSchema, 1124 | Query: ` 1125 | { 1126 | a: __type(name: "Droid") { 1127 | name 1128 | kind 1129 | interfaces { 1130 | name 1131 | } 1132 | possibleTypes { 1133 | name 1134 | } 1135 | }, 1136 | b: __type(name: "Character") { 1137 | name 1138 | kind 1139 | interfaces { 1140 | name 1141 | } 1142 | possibleTypes { 1143 | name 1144 | } 1145 | } 1146 | c: __type(name: "SearchResult") { 1147 | name 1148 | kind 1149 | interfaces { 1150 | name 1151 | } 1152 | possibleTypes { 1153 | name 1154 | } 1155 | } 1156 | } 1157 | `, 1158 | ExpectedResult: ` 1159 | { 1160 | "a": { 1161 | "name": "Droid", 1162 | "kind": "OBJECT", 1163 | "interfaces": [ 1164 | { 1165 | "name": "Character" 1166 | } 1167 | ], 1168 | "possibleTypes": null 1169 | }, 1170 | "b": { 1171 | "name": "Character", 1172 | "kind": "INTERFACE", 1173 | "interfaces": null, 1174 | "possibleTypes": [ 1175 | { 1176 | "name": "Human" 1177 | }, 1178 | { 1179 | "name": "Droid" 1180 | } 1181 | ] 1182 | }, 1183 | "c": { 1184 | "name": "SearchResult", 1185 | "kind": "UNION", 1186 | "interfaces": null, 1187 | "possibleTypes": [ 1188 | { 1189 | "name": "Human" 1190 | }, 1191 | { 1192 | "name": "Droid" 1193 | }, 1194 | { 1195 | "name": "Starship" 1196 | } 1197 | ] 1198 | } 1199 | } 1200 | `, 1201 | }, 1202 | 1203 | { 1204 | Schema: starwarsSchema, 1205 | Query: ` 1206 | { 1207 | __type(name: "Droid") { 1208 | name 1209 | fields { 1210 | name 1211 | args { 1212 | name 1213 | type { 1214 | name 1215 | } 1216 | defaultValue 1217 | } 1218 | type { 1219 | name 1220 | kind 1221 | } 1222 | } 1223 | } 1224 | } 1225 | `, 1226 | ExpectedResult: ` 1227 | { 1228 | "__type": { 1229 | "name": "Droid", 1230 | "fields": [ 1231 | { 1232 | "name": "id", 1233 | "args": [], 1234 | "type": { 1235 | "name": null, 1236 | "kind": "NON_NULL" 1237 | } 1238 | }, 1239 | { 1240 | "name": "name", 1241 | "args": [], 1242 | "type": { 1243 | "name": null, 1244 | "kind": "NON_NULL" 1245 | } 1246 | }, 1247 | { 1248 | "name": "friends", 1249 | "args": [], 1250 | "type": { 1251 | "name": null, 1252 | "kind": "LIST" 1253 | } 1254 | }, 1255 | { 1256 | "name": "friendsConnection", 1257 | "args": [ 1258 | { 1259 | "name": "first", 1260 | "type": { 1261 | "name": "Int" 1262 | }, 1263 | "defaultValue": null 1264 | }, 1265 | { 1266 | "name": "after", 1267 | "type": { 1268 | "name": "ID" 1269 | }, 1270 | "defaultValue": null 1271 | } 1272 | ], 1273 | "type": { 1274 | "name": null, 1275 | "kind": "NON_NULL" 1276 | } 1277 | }, 1278 | { 1279 | "name": "appearsIn", 1280 | "args": [], 1281 | "type": { 1282 | "name": null, 1283 | "kind": "NON_NULL" 1284 | } 1285 | }, 1286 | { 1287 | "name": "primaryFunction", 1288 | "args": [], 1289 | "type": { 1290 | "name": "String", 1291 | "kind": "SCALAR" 1292 | } 1293 | } 1294 | ] 1295 | } 1296 | } 1297 | `, 1298 | }, 1299 | 1300 | { 1301 | Schema: starwarsSchema, 1302 | Query: ` 1303 | { 1304 | __type(name: "Episode") { 1305 | enumValues { 1306 | name 1307 | } 1308 | } 1309 | } 1310 | `, 1311 | ExpectedResult: ` 1312 | { 1313 | "__type": { 1314 | "enumValues": [ 1315 | { 1316 | "name": "NEWHOPE" 1317 | }, 1318 | { 1319 | "name": "EMPIRE" 1320 | }, 1321 | { 1322 | "name": "JEDI" 1323 | } 1324 | ] 1325 | } 1326 | } 1327 | `, 1328 | }, 1329 | 1330 | { 1331 | Schema: starwarsSchema, 1332 | Query: ` 1333 | { 1334 | __schema { 1335 | directives { 1336 | name 1337 | description 1338 | locations 1339 | args { 1340 | name 1341 | description 1342 | type { 1343 | kind 1344 | ofType { 1345 | kind 1346 | name 1347 | } 1348 | } 1349 | } 1350 | } 1351 | } 1352 | } 1353 | `, 1354 | ExpectedResult: ` 1355 | { 1356 | "__schema": { 1357 | "directives": [ 1358 | { 1359 | "name": "deprecated", 1360 | "description": "Marks an element of a GraphQL schema as no longer supported.", 1361 | "locations": [ 1362 | "FIELD_DEFINITION", 1363 | "ENUM_VALUE" 1364 | ], 1365 | "args": [ 1366 | { 1367 | "name": "reason", 1368 | "description": "Explains why this element was deprecated, usually also including a suggestion\nfor how to access supported similar data. Formatted in\n[Markdown](https://daringfireball.net/projects/markdown/).", 1369 | "type": { 1370 | "kind": "SCALAR", 1371 | "ofType": null 1372 | } 1373 | } 1374 | ] 1375 | }, 1376 | { 1377 | "name": "include", 1378 | "description": "Directs the executor to include this field or fragment only when the ` + "`" + `if` + "`" + ` argument is true.", 1379 | "locations": [ 1380 | "FIELD", 1381 | "FRAGMENT_SPREAD", 1382 | "INLINE_FRAGMENT" 1383 | ], 1384 | "args": [ 1385 | { 1386 | "name": "if", 1387 | "description": "Included when true.", 1388 | "type": { 1389 | "kind": "NON_NULL", 1390 | "ofType": { 1391 | "kind": "SCALAR", 1392 | "name": "Boolean" 1393 | } 1394 | } 1395 | } 1396 | ] 1397 | }, 1398 | { 1399 | "name": "skip", 1400 | "description": "Directs the executor to skip this field or fragment when the ` + "`" + `if` + "`" + ` argument is true.", 1401 | "locations": [ 1402 | "FIELD", 1403 | "FRAGMENT_SPREAD", 1404 | "INLINE_FRAGMENT" 1405 | ], 1406 | "args": [ 1407 | { 1408 | "name": "if", 1409 | "description": "Skipped when true.", 1410 | "type": { 1411 | "kind": "NON_NULL", 1412 | "ofType": { 1413 | "kind": "SCALAR", 1414 | "name": "Boolean" 1415 | } 1416 | } 1417 | } 1418 | ] 1419 | } 1420 | ] 1421 | } 1422 | } 1423 | `, 1424 | }, 1425 | }) 1426 | } 1427 | 1428 | func TestMutationOrder(t *testing.T) { 1429 | gqltesting.RunTests(t, []*gqltesting.Test{ 1430 | { 1431 | Schema: graphql.MustParseSchema(` 1432 | schema { 1433 | query: Query 1434 | mutation: Mutation 1435 | } 1436 | 1437 | type Query { 1438 | theNumber: Int! 1439 | } 1440 | 1441 | type Mutation { 1442 | changeTheNumber(newNumber: Int!): Query 1443 | } 1444 | `, &theNumberResolver{}), 1445 | Query: ` 1446 | mutation { 1447 | first: changeTheNumber(newNumber: 1) { 1448 | theNumber 1449 | } 1450 | second: changeTheNumber(newNumber: 3) { 1451 | theNumber 1452 | } 1453 | third: changeTheNumber(newNumber: 2) { 1454 | theNumber 1455 | } 1456 | } 1457 | `, 1458 | ExpectedResult: ` 1459 | { 1460 | "first": { 1461 | "theNumber": 1 1462 | }, 1463 | "second": { 1464 | "theNumber": 3 1465 | }, 1466 | "third": { 1467 | "theNumber": 2 1468 | } 1469 | } 1470 | `, 1471 | }, 1472 | }) 1473 | } 1474 | 1475 | func TestTime(t *testing.T) { 1476 | gqltesting.RunTests(t, []*gqltesting.Test{ 1477 | { 1478 | Schema: graphql.MustParseSchema(` 1479 | schema { 1480 | query: Query 1481 | } 1482 | 1483 | type Query { 1484 | addHour(time: Time = "2001-02-03T04:05:06Z"): Time! 1485 | } 1486 | 1487 | scalar Time 1488 | `, &timeResolver{}), 1489 | Query: ` 1490 | query($t: Time!) { 1491 | a: addHour(time: $t) 1492 | b: addHour 1493 | } 1494 | `, 1495 | Variables: map[string]interface{}{ 1496 | "t": time.Date(2000, 2, 3, 4, 5, 6, 0, time.UTC), 1497 | }, 1498 | ExpectedResult: ` 1499 | { 1500 | "a": "2000-02-03T05:05:06Z", 1501 | "b": "2001-02-03T05:05:06Z" 1502 | } 1503 | `, 1504 | }, 1505 | }) 1506 | } 1507 | 1508 | type resolverWithUnexportedMethod struct{} 1509 | 1510 | func (r *resolverWithUnexportedMethod) changeTheNumber(args struct{ NewNumber int32 }) int32 { 1511 | return args.NewNumber 1512 | } 1513 | 1514 | func TestUnexportedMethod(t *testing.T) { 1515 | _, err := graphql.ParseSchema(` 1516 | schema { 1517 | mutation: Mutation 1518 | } 1519 | 1520 | type Mutation { 1521 | changeTheNumber(newNumber: Int!): Int! 1522 | } 1523 | `, &resolverWithUnexportedMethod{}) 1524 | if err == nil { 1525 | t.Error("error expected") 1526 | } 1527 | } 1528 | 1529 | type resolverWithUnexportedField struct{} 1530 | 1531 | func (r *resolverWithUnexportedField) ChangeTheNumber(args struct{ newNumber int32 }) int32 { 1532 | return args.newNumber 1533 | } 1534 | 1535 | func TestUnexportedField(t *testing.T) { 1536 | _, err := graphql.ParseSchema(` 1537 | schema { 1538 | mutation: Mutation 1539 | } 1540 | 1541 | type Mutation { 1542 | changeTheNumber(newNumber: Int!): Int! 1543 | } 1544 | `, &resolverWithUnexportedField{}) 1545 | if err == nil { 1546 | t.Error("error expected") 1547 | } 1548 | } 1549 | 1550 | type inputResolver struct{} 1551 | 1552 | func (r *inputResolver) Int(args struct{ Value int32 }) int32 { 1553 | return args.Value 1554 | } 1555 | 1556 | func (r *inputResolver) Float(args struct{ Value float64 }) float64 { 1557 | return args.Value 1558 | } 1559 | 1560 | func (r *inputResolver) String(args struct{ Value string }) string { 1561 | return args.Value 1562 | } 1563 | 1564 | func (r *inputResolver) Boolean(args struct{ Value bool }) bool { 1565 | return args.Value 1566 | } 1567 | 1568 | func (r *inputResolver) Nullable(args struct{ Value *int32 }) *int32 { 1569 | return args.Value 1570 | } 1571 | 1572 | func (r *inputResolver) List(args struct{ Value []*struct{ V int32 } }) []int32 { 1573 | l := make([]int32, len(args.Value)) 1574 | for i, entry := range args.Value { 1575 | l[i] = entry.V 1576 | } 1577 | return l 1578 | } 1579 | 1580 | func (r *inputResolver) NullableList(args struct{ Value *[]*struct{ V int32 } }) *[]*int32 { 1581 | if args.Value == nil { 1582 | return nil 1583 | } 1584 | l := make([]*int32, len(*args.Value)) 1585 | for i, entry := range *args.Value { 1586 | if entry != nil { 1587 | l[i] = &entry.V 1588 | } 1589 | } 1590 | return &l 1591 | } 1592 | 1593 | func (r *inputResolver) Enum(args struct{ Value string }) string { 1594 | return args.Value 1595 | } 1596 | 1597 | func (r *inputResolver) NullableEnum(args struct{ Value *string }) *string { 1598 | return args.Value 1599 | } 1600 | 1601 | type recursive struct { 1602 | Next *recursive 1603 | } 1604 | 1605 | func (r *inputResolver) Recursive(args struct{ Value *recursive }) int32 { 1606 | n := int32(0) 1607 | v := args.Value 1608 | for v != nil { 1609 | v = v.Next 1610 | n++ 1611 | } 1612 | return n 1613 | } 1614 | 1615 | func (r *inputResolver) ID(args struct{ Value graphql.ID }) graphql.ID { 1616 | return args.Value 1617 | } 1618 | 1619 | func TestInput(t *testing.T) { 1620 | coercionSchema := graphql.MustParseSchema(` 1621 | schema { 1622 | query: Query 1623 | } 1624 | 1625 | type Query { 1626 | int(value: Int!): Int! 1627 | float(value: Float!): Float! 1628 | string(value: String!): String! 1629 | boolean(value: Boolean!): Boolean! 1630 | nullable(value: Int): Int 1631 | list(value: [Input!]!): [Int!]! 1632 | nullableList(value: [Input]): [Int] 1633 | enum(value: Enum!): Enum! 1634 | nullableEnum(value: Enum): Enum 1635 | recursive(value: RecursiveInput!): Int! 1636 | id(value: ID!): ID! 1637 | } 1638 | 1639 | input Input { 1640 | v: Int! 1641 | } 1642 | 1643 | input RecursiveInput { 1644 | next: RecursiveInput 1645 | } 1646 | 1647 | enum Enum { 1648 | Option1 1649 | Option2 1650 | } 1651 | `, &inputResolver{}) 1652 | gqltesting.RunTests(t, []*gqltesting.Test{ 1653 | { 1654 | Schema: coercionSchema, 1655 | Query: ` 1656 | { 1657 | int(value: 42) 1658 | float1: float(value: 42) 1659 | float2: float(value: 42.5) 1660 | string(value: "foo") 1661 | boolean(value: true) 1662 | nullable1: nullable(value: 42) 1663 | nullable2: nullable(value: null) 1664 | list1: list(value: [{v: 41}, {v: 42}, {v: 43}]) 1665 | list2: list(value: {v: 42}) 1666 | nullableList1: nullableList(value: [{v: 41}, null, {v: 43}]) 1667 | nullableList2: nullableList(value: null) 1668 | enum(value: Option2) 1669 | nullableEnum1: nullableEnum(value: Option2) 1670 | nullableEnum2: nullableEnum(value: null) 1671 | recursive(value: {next: {next: {}}}) 1672 | intID: id(value: 1234) 1673 | strID: id(value: "1234") 1674 | } 1675 | `, 1676 | ExpectedResult: ` 1677 | { 1678 | "int": 42, 1679 | "float1": 42, 1680 | "float2": 42.5, 1681 | "string": "foo", 1682 | "boolean": true, 1683 | "nullable1": 42, 1684 | "nullable2": null, 1685 | "list1": [41, 42, 43], 1686 | "list2": [42], 1687 | "nullableList1": [41, null, 43], 1688 | "nullableList2": null, 1689 | "enum": "Option2", 1690 | "nullableEnum1": "Option2", 1691 | "nullableEnum2": null, 1692 | "recursive": 3, 1693 | "intID": "1234", 1694 | "strID": "1234" 1695 | } 1696 | `, 1697 | }, 1698 | }) 1699 | } 1700 | 1701 | func TestComposedFragments(t *testing.T) { 1702 | gqltesting.RunTests(t, []*gqltesting.Test{ 1703 | { 1704 | Schema: starwarsSchema, 1705 | Query: ` 1706 | { 1707 | composed: hero(episode: EMPIRE) { 1708 | name 1709 | ...friendsNames 1710 | ...friendsIds 1711 | } 1712 | } 1713 | 1714 | fragment friendsNames on Character { 1715 | name 1716 | friends { 1717 | name 1718 | } 1719 | } 1720 | 1721 | fragment friendsIds on Character { 1722 | name 1723 | friends { 1724 | id 1725 | } 1726 | } 1727 | `, 1728 | ExpectedResult: ` 1729 | { 1730 | "composed": { 1731 | "name": "Luke Skywalker", 1732 | "friends": [ 1733 | { 1734 | "id": "1002", 1735 | "name": "Han Solo" 1736 | }, 1737 | { 1738 | "id": "1003", 1739 | "name": "Leia Organa" 1740 | }, 1741 | { 1742 | "id": "2000", 1743 | "name": "C-3PO" 1744 | }, 1745 | { 1746 | "id": "2001", 1747 | "name": "R2-D2" 1748 | } 1749 | ] 1750 | } 1751 | } 1752 | `, 1753 | }, 1754 | }) 1755 | } 1756 | --------------------------------------------------------------------------------