├── .gitignore ├── errors └── errors.go ├── internal ├── playground │ └── main.go ├── validation │ ├── testdata │ │ ├── gen.go │ │ ├── package.json │ │ ├── LICENSE │ │ └── export.js │ ├── suggestion.go │ └── validation_test.go ├── introspection │ ├── new_schema_test.go │ └── resolvers.go ├── example │ ├── starwars │ │ └── server │ │ │ └── server.go │ ├── relay │ │ └── main.go │ ├── subscription │ │ └── subscription.go │ └── directiveresolver │ │ └── main.go ├── lexer │ ├── lexer_test.go │ └── lexer.go ├── linkedmap │ └── linkedmap.go ├── gqltesting │ ├── asserts.go │ └── testing.go ├── generator │ ├── main.go │ └── assets_vfsdata.go ├── filesystem │ └── file_info_mapping_fs.go ├── deprecated │ └── graphql.go └── assets │ └── meta.graphql ├── resolvers ├── nil_resolver.go ├── resolver_func.go ├── type_resolver.go ├── type_and_field_resolver.go ├── dynamic_resolver.go ├── resolver_list.go ├── map_resolver.go ├── value.go ├── cache.go ├── field_resolver.go ├── async.go ├── directive_resolver.go ├── cast.go ├── resolvers.go ├── metadata_resolver.go ├── method_resolver.go ├── method_resolver_reflect.go └── field_resolver_reflect.go ├── error.go ├── schema ├── meta.go ├── query_test.go ├── operation.go ├── directive.go ├── types.go ├── query_format.go ├── schema_test.go ├── values.go ├── type_add_if_missing.go ├── schema_internal_test.go ├── deep_copy.go ├── schema_format_test.go ├── literals.go ├── query.go └── schema_format.go ├── benchmark-pprof-mem.sh ├── deprecations.go ├── customtypes ├── id.go └── time.go ├── log └── log.go ├── benchmark.sh ├── text └── text.go ├── go.mod ├── response.go ├── exec ├── directive.go └── field_selection.go ├── graphql_stream.go ├── httpgql ├── marshal.go ├── ws_test.go ├── http_test.go ├── http-client.go ├── http-server.go ├── ws-server.go └── ws-client.go ├── LICENSE ├── graphql.go ├── request.go ├── inputconv └── inputconv.go ├── trace ├── trace_test.go └── trace.go ├── graphiql └── graphiql.go ├── benchmark_test.go ├── engine.go ├── go.sum └── qerrors └── qerrors.go /.gitignore: -------------------------------------------------------------------------------- 1 | /internal/validation/testdata/graphql-js 2 | /internal/validation/testdata/node_modules 3 | /vendor 4 | /.idea 5 | /*.iml 6 | /benchmark-results -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/chirino/graphql/qerrors" 5 | ) 6 | 7 | // Deprecated: Use graphql.Error instead. 8 | type QueryError = qerrors.Error 9 | -------------------------------------------------------------------------------- /internal/playground/main.go: -------------------------------------------------------------------------------- 1 | package playground 2 | 3 | func main() { 4 | for i := 0; i < 1000; i++ { 5 | profileMe() 6 | } 7 | } 8 | 9 | func profileMe() { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /resolvers/nil_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | type nilResolver byte 4 | 5 | var NilResolver = nilResolver(0) 6 | 7 | func (this nilResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /resolvers/resolver_func.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | type Func func(request *ResolveRequest, next Resolution) Resolution 4 | 5 | func (f Func) Resolve(request *ResolveRequest, next Resolution) Resolution { 6 | return f(request, next) 7 | } 8 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "github.com/chirino/graphql/qerrors" 5 | ) 6 | 7 | type Error = qerrors.Error 8 | type ErrorList = qerrors.ErrorList 9 | 10 | func NewError(message string) *Error { 11 | return qerrors.New(message) 12 | } 13 | 14 | func Errorf(format string, a ...interface{}) *Error { 15 | return qerrors.Errorf(format, a...) 16 | } 17 | -------------------------------------------------------------------------------- /internal/validation/testdata/gen.go: -------------------------------------------------------------------------------- 1 | // Package testdata copies validation test cases from the reference implementation at 2 | // github.com/graphql/graphql-js. 3 | // 4 | // Pre-requisites: 5 | // - nodejs 6 | // - npm >= 5.2.0 (for use of npx) 7 | // 8 | // Usage: 9 | // $ git clone https://github.com/graphql/graphql-js 10 | // $ go generate . 11 | package testdata 12 | 13 | //go:generate npm install 14 | //go:generate cp export.js graphql-js/export.js 15 | //go:generate npx babel-node graphql-js/export.js 16 | -------------------------------------------------------------------------------- /resolvers/type_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | type TypeResolver map[string]Resolver 4 | 5 | func (this TypeResolver) Set(typeName string, factory Resolver) { 6 | this[typeName] = factory 7 | } 8 | 9 | func (this TypeResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 10 | if request.ParentType == nil { 11 | return next 12 | } 13 | resolverFunc := this[request.ParentType.String()] 14 | if resolverFunc == nil { 15 | return next 16 | } 17 | return resolverFunc.Resolve(request, next) 18 | } 19 | -------------------------------------------------------------------------------- /schema/meta.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/chirino/graphql/internal/assets" 5 | "io/ioutil" 6 | ) 7 | 8 | var Meta *Schema 9 | 10 | func init() { 11 | Meta = &Schema{} // bootstrap 12 | Meta = New() 13 | file, err := assets.FileSystem.Open("/meta.graphql") 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer file.Close() 18 | data, err := ioutil.ReadAll(file) 19 | if err != nil { 20 | panic(err) 21 | } 22 | if err := Meta.Parse(string(data)); err != nil { 23 | panic(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/introspection/new_schema_test.go: -------------------------------------------------------------------------------- 1 | package introspection_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chirino/graphql" 7 | "github.com/chirino/graphql/internal/example/starwars" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGetSchema(t *testing.T) { 13 | engine := graphql.New() 14 | engine.Schema.Parse(starwars.Schema) 15 | s, err := graphql.GetSchema(engine.ServeGraphQL) 16 | require.NoError(t, err) 17 | assert.Equal(t, engine.Schema.String(), s.String()) 18 | } 19 | -------------------------------------------------------------------------------- /schema/query_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chirino/graphql/internal/introspection" 7 | "github.com/chirino/graphql/schema" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNoLossOnQueryFormating(t *testing.T) { 13 | q1 := &schema.QueryDocument{} 14 | err := q1.Parse(introspection.Query) 15 | require.NoError(t, err) 16 | 17 | q2 := &schema.QueryDocument{} 18 | err = q2.Parse(q1.String()) 19 | require.NoError(t, err) 20 | 21 | assert.Equal(t, q1.String(), q2.String()) 22 | } 23 | -------------------------------------------------------------------------------- /benchmark-pprof-mem.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir benchmark-results 2> /dev/null 4 | go test -c -o benchmark-results/test 5 | 6 | #export GODEBUG=allocfreetrace=1,gctrace=1 7 | #BENCHMARK=BenchmarkParallelParseStarwarsQuery 8 | BENCHMARK=${1:-BenchmarkParallelParseStarwarsQuery} 9 | 10 | ./benchmark-results/test -test.bench=$BENCHMARK -test.memprofile benchmark-results/latest.mem | tee b benchmark-results/latest-memprofile.txt 11 | 12 | # go tool pprof -alloc_objects -lines -top benchmark-results/latest.mem | head -n 25 13 | go tool pprof -alloc_space -lines -top benchmark-results/latest.mem | head -n 25 -------------------------------------------------------------------------------- /schema/operation.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "strings" 4 | 5 | type OperationType string 6 | 7 | const ( 8 | InvalidOperation OperationType = "" 9 | Query = "query" 10 | Mutation = "mutation" 11 | Subscription = "subscription" 12 | ) 13 | 14 | func GetOperationType(v string) OperationType { 15 | switch strings.ToLower(v) { 16 | case string(Query): 17 | return Query 18 | case string(Mutation): 19 | return Mutation 20 | case string(Subscription): 21 | return Subscription 22 | default: 23 | return InvalidOperation 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /deprecations.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | // Deprecated: use Request instead 4 | type EngineRequest = Request 5 | 6 | // Deprecated: use Response instead 7 | type EngineResponse = Response 8 | 9 | // Deprecated: use ServeGraphQL instead. 10 | func (engine *Engine) ExecuteOne(request *EngineRequest) *EngineResponse { 11 | stream := ServeGraphQLStreamFunc(engine.ServeGraphQLStream) 12 | return stream.ServeGraphQL(request) 13 | } 14 | 15 | // Deprecated: use ServeGraphQLStream instead. 16 | func (engine *Engine) Execute(request *EngineRequest) (ResponseStream, error) { 17 | return engine.ServeGraphQLStream(request), nil 18 | } 19 | -------------------------------------------------------------------------------- /resolvers/type_and_field_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | type TypeAndFieldResolver map[TypeAndFieldKey]Func 4 | 5 | type TypeAndFieldKey struct { 6 | Type string 7 | Field string 8 | } 9 | 10 | func (r TypeAndFieldResolver) Set(typeName string, field string, f Func) { 11 | r[TypeAndFieldKey{Type: typeName, Field: field}] = f 12 | } 13 | 14 | func (r TypeAndFieldResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 15 | if request.ParentType == nil { 16 | return next 17 | } 18 | f := r[TypeAndFieldKey{ 19 | Type: request.ParentType.String(), 20 | Field: request.Field.Name, 21 | }] 22 | if f == nil { 23 | return next 24 | } 25 | return f(request, next) 26 | } 27 | -------------------------------------------------------------------------------- /customtypes/id.go: -------------------------------------------------------------------------------- 1 | package customtypes 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 | -------------------------------------------------------------------------------- /resolvers/dynamic_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | type dynamicResolver struct { 4 | } 5 | 6 | func DynamicResolverFactory() Resolver { 7 | return &dynamicResolver{} 8 | } 9 | 10 | func (this *dynamicResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 11 | resolver := MetadataResolver.Resolve(request, next) 12 | if resolver != nil { 13 | return resolver 14 | } 15 | resolver = MethodResolver.Resolve(request, next) 16 | if resolver != nil { 17 | return resolver 18 | } 19 | resolver = FieldResolver.Resolve(request, next) 20 | if resolver != nil { 21 | return resolver 22 | } 23 | resolver = MapResolver.Resolve(request, next) 24 | if resolver != nil { 25 | return resolver 26 | } 27 | return next 28 | } 29 | -------------------------------------------------------------------------------- /resolvers/resolver_list.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | /////////////////////////////////////////////////////////////////////// 4 | // 5 | // ResolverFactoryList uses a list of other resolvers to resolve 6 | // requests. Last resolver wins. 7 | // 8 | /////////////////////////////////////////////////////////////////////// 9 | type ResolverList []Resolver 10 | 11 | func List(factories ...Resolver) *ResolverList { 12 | list := ResolverList(factories) 13 | return &list 14 | } 15 | 16 | func (this *ResolverList) Add(factory Resolver) { 17 | *this = append(*this, factory) 18 | } 19 | 20 | func (this *ResolverList) Resolve(request *ResolveRequest, next Resolution) Resolution { 21 | for _, f := range *this { 22 | next = f.Resolve(request, next) 23 | } 24 | return next 25 | } 26 | -------------------------------------------------------------------------------- /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 during query execution. It is settable on the graphql.Engine 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 during query execution 15 | type DefaultLogger struct{} 16 | 17 | // LogPanic is used to log recovered panic values that occur during 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 | -------------------------------------------------------------------------------- /benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir benchmark-results 2> /dev/null 4 | mv benchmark-results/latest.txt benchmark-results/previous.txt 2> /dev/null 5 | 6 | go test -c -o benchmark-results/test 7 | 8 | #export GODEBUG=allocfreetrace=1,gctrace=1 9 | 10 | BENCHMARK=${1:-.} 11 | PREV_BENCHMARK=${2:-benchmark-results/previous.txt} 12 | 13 | ./benchmark-results/test -test.bench=$BENCHMARK | tee benchmark-results/latest.txt 14 | ./benchmark-results/test -test.bench=$BENCHMARK | tee -a benchmark-results/latest.txt 15 | ./benchmark-results/test -test.bench=$BENCHMARK | tee -a benchmark-results/latest.txt 16 | benchcmp "$PREV_BENCHMARK" benchmark-results/latest.txt 17 | benchstat "$PREV_BENCHMARK" benchmark-results/latest.txt 18 | 19 | cp "benchmark-results/latest.txt" "benchmark-results/$(date).txt" 20 | 21 | -------------------------------------------------------------------------------- /internal/example/starwars/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/chirino/graphql/httpgql" 9 | "github.com/chirino/graphql/internal/deprecated" 10 | "github.com/friendsofgo/graphiql" 11 | 12 | "github.com/chirino/graphql/internal/example/starwars" 13 | ) 14 | 15 | var schema *deprecated.Schema 16 | 17 | func init() { 18 | schema = deprecated.MustParseSchema(starwars.Schema, &starwars.Resolver{}) 19 | } 20 | 21 | func main() { 22 | http.Handle("/query", &httpgql.Handler{ServeGraphQLStream: schema.Engine.ServeGraphQLStream}) 23 | graphiql, _ := graphiql.NewGraphiqlHandler("/query") 24 | http.Handle("/", graphiql) 25 | 26 | fmt.Println("GraphiQL UI running at http://localhost:8085/") 27 | log.Fatal(http.ListenAndServe(":8085", nil)) 28 | } 29 | -------------------------------------------------------------------------------- /resolvers/map_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import "reflect" 4 | 5 | /////////////////////////////////////////////////////////////////////// 6 | // 7 | // MapResolverFactory resolves fields using entries in a map 8 | // 9 | /////////////////////////////////////////////////////////////////////// 10 | type mapResolver byte 11 | 12 | const MapResolver = mapResolver(0) 13 | 14 | func (this mapResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 15 | 16 | parentValue := Dereference(request.Parent) 17 | if parentValue.Kind() != reflect.Map || parentValue.Type().Key().Kind() != reflect.String { 18 | return next 19 | } 20 | field := reflect.ValueOf(request.Field.Name) 21 | value := parentValue.MapIndex(field) 22 | return func() (reflect.Value, error) { 23 | return value, nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /text/text.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import "strings" 4 | 5 | func BulletIndent(bullet string, text string) string { 6 | indentBytes := make([]byte, len(bullet), len(bullet)) 7 | for i, _ := range bullet { 8 | indentBytes[i] = ' ' 9 | } 10 | indentText := string(indentBytes) 11 | text = Indent(text, indentText) 12 | text = strings.TrimPrefix(text, indentText) 13 | return bullet + text 14 | } 15 | 16 | func Indent(text, indent string) string { 17 | if len(text) != 0 && text[len(text)-1:] == "\n" { 18 | result := "" 19 | for _, j := range strings.Split(text[:len(text)-1], "\n") { 20 | result += indent + j + "\n" 21 | } 22 | return result 23 | } 24 | result := "" 25 | for _, j := range strings.Split(strings.TrimRight(text, "\n"), "\n") { 26 | result += indent + j + "\n" 27 | } 28 | return result[:len(result)-1] 29 | } 30 | -------------------------------------------------------------------------------- /resolvers/value.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import "reflect" 4 | 5 | // MapValue allows you to convert map a resolved result to something new. 6 | func MapValue(mapper func(value reflect.Value) reflect.Value) Resolver { 7 | return Func(func(request *ResolveRequest, next Resolution) Resolution { 8 | if next == nil { 9 | return nil 10 | } 11 | return func() (value reflect.Value, err error) { 12 | value, err = next() 13 | if err != nil { 14 | return 15 | } 16 | value = mapper(value) 17 | return 18 | } 19 | }) 20 | } 21 | 22 | // Sniff allows you see resolution requests, but does not impact them 23 | func Sniff(sniffer func(request *ResolveRequest, next Resolution)) Resolver { 24 | return Func(func(request *ResolveRequest, next Resolution) Resolution { 25 | sniffer(request, next) 26 | return next 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /resolvers/cache.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | type Cache struct { 9 | value atomic.Value 10 | mu sync.Mutex 11 | } 12 | 13 | func (cache *Cache) GetOrElseUpdate(key interface{}, create func() interface{}) (x interface{}) { 14 | lastCacheMap, _ := cache.value.Load().(map[interface{}]interface{}) 15 | value, found := lastCacheMap[key] 16 | if found { 17 | return value 18 | } 19 | 20 | // Compute fields without lock. 21 | // Might duplicate effort but won't hold other computations back. 22 | value = create() 23 | 24 | // Update 25 | cache.mu.Lock() 26 | lastCacheMap, _ = cache.value.Load().(map[interface{}]interface{}) 27 | nextCacheMap := make(map[interface{}]interface{}, len(lastCacheMap)+1) 28 | for k, v := range lastCacheMap { 29 | nextCacheMap[k] = v 30 | } 31 | nextCacheMap[key] = value 32 | cache.value.Store(nextCacheMap) 33 | cache.mu.Unlock() 34 | return value 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chirino/graphql 2 | 3 | require ( 4 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect 5 | github.com/davecgh/go-spew v1.1.1 // indirect 6 | github.com/friendsofgo/graphiql v0.2.2 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/josharian/intern v1.0.0 9 | github.com/opentracing/opentracing-go v1.2.0 10 | github.com/pkg/errors v0.9.1 11 | github.com/segmentio/ksuid v1.0.2 12 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 13 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd 14 | github.com/stretchr/testify v1.5.1 15 | github.com/uber-go/atomic v1.3.2 // indirect 16 | github.com/uber/jaeger-client-go v2.14.1-0.20180928181052-40fb3b2c4120+incompatible 17 | github.com/uber/jaeger-lib v1.5.0 // indirect 18 | go.uber.org/atomic v1.3.2 // indirect 19 | golang.org/x/tools v0.0.0-20200128220307-520188d60f50 // indirect 20 | gopkg.in/yaml.v2 v2.2.8 // indirect 21 | ) 22 | 23 | go 1.13 24 | -------------------------------------------------------------------------------- /resolvers/field_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import "reflect" 4 | 5 | /////////////////////////////////////////////////////////////////////// 6 | // 7 | // FieldResolverFactory resolves fields using struct fields on the parent 8 | // value. 9 | // 10 | /////////////////////////////////////////////////////////////////////// 11 | type fieldResolver byte 12 | 13 | const FieldResolver = fieldResolver(0) 14 | 15 | func (this fieldResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 16 | parentValue := Dereference(request.Parent) 17 | if parentValue.Kind() != reflect.Struct { 18 | return next 19 | } 20 | childValue, found := getChildField(&parentValue, request.Field.Name) 21 | if !found { 22 | return next 23 | } 24 | return func() (reflect.Value, error) { 25 | return *childValue, nil 26 | } 27 | } 28 | 29 | func Dereference(value reflect.Value) reflect.Value { 30 | for value.Kind() == reflect.Ptr || value.Kind() == reflect.Interface { 31 | value = value.Elem() 32 | } 33 | return value 34 | } 35 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/chirino/graphql/qerrors" 8 | ) 9 | 10 | // Response represents a typical response of a GraphQL server. It may be encoded to JSON directly or 11 | // it may be further processed to a custom response type, for example to include custom error data. 12 | type Response struct { 13 | Data json.RawMessage `json:"data,omitempty"` 14 | Errors ErrorList `json:"errors,omitempty"` 15 | Extensions interface{} `json:"extensions,omitempty"` 16 | Details map[string]interface{} `json:"-"` 17 | } 18 | 19 | func NewResponse() *Response { 20 | return &Response{} 21 | } 22 | 23 | func (r *Response) Error() error { 24 | return r.Errors.Error() 25 | } 26 | 27 | func (r *Response) String() string { 28 | return fmt.Sprintf("{Data: %s, Errors: %v}", string(r.Data), r.Errors) 29 | } 30 | 31 | func (r *Response) AddError(err error) *Response { 32 | r.Errors = qerrors.AppendErrors(r.Errors, err) 33 | return r 34 | } 35 | -------------------------------------------------------------------------------- /internal/lexer/lexer_test.go: -------------------------------------------------------------------------------- 1 | package lexer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chirino/graphql/internal/lexer" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConsumeDescription(t *testing.T) { 11 | lex := lexer.Get(` 12 | # comment 13 | "Comment line 1" 14 | ,,,,,, # Commas are insignificant 15 | type Hello { 16 | world: String! 17 | }`) 18 | lex.Consume() 19 | //err := lex.CatchSyntaxError(lex.Consume) 20 | //require.NoError(t, err) 21 | assert.Equal(t, 22 | lexer.Description{Text: "Comment line 1", ShowType: lexer.ShowStringDescription, Loc: lexer.Location{Line: 3, Column: 1}}, 23 | lex.ConsumeDescription()) 24 | } 25 | 26 | func TestConsumeBlockDescription(t *testing.T) { 27 | lex := lexer.Get(` 28 | """ 29 | Comment line 1 30 | """ 31 | type Hello { 32 | world: String! 33 | }`) 34 | lex.Consume() 35 | assert.Equal(t, 36 | lexer.Description{Text: "\nComment line 1\n", ShowType: lexer.ShowBlockDescription, Loc: lexer.Location{Line: 2, Column: 1}}, 37 | lex.ConsumeDescription()) 38 | } 39 | -------------------------------------------------------------------------------- /exec/directive.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/chirino/graphql/internal/exec/packer" 7 | "github.com/chirino/graphql/qerrors" 8 | "github.com/chirino/graphql/schema" 9 | ) 10 | 11 | func SkipByDirective(l schema.DirectiveList, vars map[string]interface{}) (bool, *qerrors.Error) { 12 | if d := l.Get("skip"); d != nil { 13 | return evaluateIf(d, vars) 14 | } 15 | if d := l.Get("include"); d != nil { 16 | b, err := evaluateIf(d, vars) 17 | if err != nil { 18 | return false, err 19 | } 20 | return !b, nil 21 | } 22 | return false, nil 23 | } 24 | 25 | func evaluateIf(d *schema.Directive, vars map[string]interface{}) (bool, *qerrors.Error) { 26 | arg := d.Args.MustGet("if") 27 | if arg == nil { 28 | return false, qerrors.New("@skip if argument missing").WithLocations(d.NameLoc).WithStack() 29 | } 30 | p := packer.ValuePacker{ValueType: reflect.TypeOf(false)} 31 | v, err := p.Pack(arg.Evaluate(vars)) 32 | if err != nil { 33 | return false, qerrors.New(err.Error()).WithLocations(d.NameLoc).WithStack() 34 | } 35 | return v.Bool(), nil 36 | } 37 | -------------------------------------------------------------------------------- /graphql_stream.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chirino/graphql/qerrors" 7 | ) 8 | 9 | type ResponseStream = <-chan *Response 10 | 11 | func NewErrStream(err error) ResponseStream { 12 | rc := make(chan *Response, 1) 13 | rc <- NewResponse().AddError(err) 14 | close(rc) 15 | return rc 16 | } 17 | 18 | type StreamingHandler interface { 19 | ServeGraphQLStream(request *Request) ResponseStream 20 | } 21 | 22 | type ServeGraphQLStreamFunc func(request *Request) ResponseStream 23 | 24 | func (f ServeGraphQLStreamFunc) ServeGraphQLStream(request *Request) ResponseStream { 25 | return f(request) 26 | } 27 | 28 | func (f ServeGraphQLStreamFunc) ServeGraphQL(request *Request) *Response { 29 | ctx, cancel := context.WithCancel(request.GetContext()) 30 | requestCp := *request 31 | requestCp.Context = ctx 32 | 33 | stream := f(request) 34 | request.GetContext() 35 | 36 | response := <-stream 37 | if response == nil { 38 | response = &Response{} 39 | response.AddError(qerrors.New("response stream closed.").WithStack()) 40 | } 41 | cancel() 42 | return response 43 | } 44 | -------------------------------------------------------------------------------- /internal/example/relay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/chirino/graphql" 9 | "github.com/chirino/graphql/graphiql" 10 | "github.com/chirino/graphql/httpgql" 11 | ) 12 | 13 | type query struct { 14 | Name string `json:"name"` 15 | } 16 | 17 | func (q *query) Hello() string { return "Hello, " + q.Name } 18 | 19 | func main() { 20 | engine := graphql.New() 21 | engine.Root = &query{ 22 | Name: "World!", 23 | } 24 | err := engine.Schema.Parse(` 25 | schema { 26 | query: Query 27 | } 28 | type Query { 29 | name: String! 30 | hello: String! 31 | } 32 | `) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | http.Handle("/graphql", &httpgql.Handler{ServeGraphQLStream: engine.ServeGraphQLStream}) 38 | fmt.Println("GraphQL service running at http://localhost:8080/graphql") 39 | 40 | http.Handle("/", graphiql.New("ws://localhost:8080/graphql", true)) 41 | fmt.Println("GraphiQL UI running at http://localhost:8080/") 42 | 43 | log.Fatal(http.ListenAndServe(":8080", nil)) 44 | } 45 | -------------------------------------------------------------------------------- /httpgql/marshal.go: -------------------------------------------------------------------------------- 1 | package httpgql 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/chirino/graphql/customtypes" 10 | "github.com/chirino/graphql/qerrors" 11 | ) 12 | 13 | func MarshalID(kind string, spec interface{}) customtypes.ID { 14 | d, err := json.Marshal(spec) 15 | if err != nil { 16 | panic(fmt.Errorf("relay.MarshalID: %s", err)) 17 | } 18 | return customtypes.ID(base64.URLEncoding.EncodeToString(append([]byte(kind+":"), d...))) 19 | } 20 | 21 | func UnmarshalKind(id customtypes.ID) string { 22 | s, err := base64.URLEncoding.DecodeString(string(id)) 23 | if err != nil { 24 | return "" 25 | } 26 | i := strings.IndexByte(string(s), ':') 27 | if i == -1 { 28 | return "" 29 | } 30 | return string(s[:i]) 31 | } 32 | 33 | func UnmarshalSpec(id customtypes.ID, v interface{}) error { 34 | s, err := base64.URLEncoding.DecodeString(string(id)) 35 | if err != nil { 36 | return err 37 | } 38 | i := strings.IndexByte(string(s), ':') 39 | if i == -1 { 40 | return qerrors.Errorf("invalid graphql.ID") 41 | } 42 | return json.Unmarshal([]byte(s[i+1:]), v) 43 | } 44 | -------------------------------------------------------------------------------- /resolvers/async.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | func (this *ResolveRequest) RunAsync(resolver Resolution) Resolution { 9 | 10 | type resolution struct { 11 | result reflect.Value 12 | err error 13 | } 14 | 15 | channel := make(chan *resolution, 1) 16 | r := resolution{ 17 | result: reflect.Value{}, 18 | err: errors.New("Unknown"), 19 | } 20 | 21 | // Limit the number of concurrent go routines that we startup. 22 | *this.ExecutionContext.GetLimiter() <- 1 23 | go func() { 24 | 25 | // Setup some post processing 26 | defer func() { 27 | err := this.ExecutionContext.HandlePanic(this.SelectionPath()) 28 | if err != nil { 29 | r.err = err 30 | } 31 | <-*this.ExecutionContext.GetLimiter() 32 | channel <- &r // we do this in defer since the resolver() could panic. 33 | }() 34 | 35 | // Stash the results, then get sent back in the defer. 36 | r.result, r.err = resolver() 37 | }() 38 | 39 | // Return a resolver that waits for the async results 40 | return func() (reflect.Value, error) { 41 | r := <-channel 42 | return r.result, r.err 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /schema/directive.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/chirino/graphql/internal/lexer" 5 | ) 6 | 7 | type Directive struct { 8 | Name string 9 | NameLoc Location 10 | Args ArgumentList 11 | } 12 | 13 | func (s *Directive) String() string { 14 | return FormatterToString(s) 15 | } 16 | 17 | func ParseDirectives(l *lexer.Lexer) DirectiveList { 18 | var directives DirectiveList 19 | for l.Peek() == '@' { 20 | l.ConsumeToken('@') 21 | d := &Directive{} 22 | d.Name, d.NameLoc = l.ConsumeIdentInternWithLoc() 23 | d.NameLoc.Column-- 24 | if l.Peek() == '(' { 25 | d.Args = ParseArguments(l) 26 | } 27 | directives = append(directives, d) 28 | } 29 | return directives 30 | } 31 | 32 | type DirectiveList []*Directive 33 | 34 | func (l DirectiveList) Get(name string) *Directive { 35 | for _, d := range l { 36 | if d.Name == name { 37 | return d 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (l DirectiveList) Select(keep func(d *Directive) bool) DirectiveList { 44 | rc := DirectiveList{} 45 | for _, d := range l { 46 | if keep(d) { 47 | rc = append(rc, d) 48 | } 49 | } 50 | return rc 51 | } 52 | -------------------------------------------------------------------------------- /internal/linkedmap/linkedmap.go: -------------------------------------------------------------------------------- 1 | package linkedmap 2 | 3 | type LinkedMapEntry struct { 4 | Value interface{} 5 | Next *LinkedMapEntry 6 | } 7 | type LinkedMap struct { 8 | ValuesByKey map[interface{}]*LinkedMapEntry 9 | First *LinkedMapEntry 10 | Last *LinkedMapEntry 11 | } 12 | 13 | func CreateLinkedMap(size int) *LinkedMap { 14 | return &LinkedMap{ 15 | ValuesByKey: make(map[interface{}]*LinkedMapEntry, size), 16 | } 17 | } 18 | 19 | func (this *LinkedMap) Get(key interface{}) interface{} { 20 | entry := this.ValuesByKey[key] 21 | if entry == nil { 22 | return nil 23 | } 24 | return entry.Value 25 | } 26 | 27 | func (this *LinkedMap) Set(key interface{}, value interface{}) interface{} { 28 | if previousEntry, found := this.ValuesByKey[key]; found { 29 | prevValue := previousEntry.Value 30 | entry := this.ValuesByKey[key] 31 | entry.Value = value 32 | return prevValue 33 | } 34 | entry := &LinkedMapEntry{ 35 | Value: value, 36 | } 37 | if this.First == nil { 38 | this.First = entry 39 | this.Last = entry 40 | } else { 41 | this.Last.Next = entry 42 | this.Last = entry 43 | } 44 | this.ValuesByKey[key] = entry 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/gqltesting/asserts.go: -------------------------------------------------------------------------------- 1 | package gqltesting 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/chirino/graphql" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func AssertQuery(t *testing.T, engine *graphql.Engine, query string, expected string) { 11 | request := graphql.Request{} 12 | request.Query = query 13 | AssertRequest(t, engine, request, expected) 14 | } 15 | 16 | func AssertRequestString(t *testing.T, engine *graphql.Engine, req string, expected string) { 17 | request := graphql.Request{} 18 | jsonUnmarshal(t, req, &request) 19 | AssertRequest(t, engine, request, expected) 20 | } 21 | 22 | func AssertRequest(t *testing.T, engine *graphql.Engine, request graphql.Request, expected string) { 23 | response := engine.ServeGraphQL(&request) 24 | actual := jsonMarshal(t, response) 25 | assert.Equal(t, expected, actual) 26 | } 27 | 28 | func jsonMarshal(t *testing.T, value interface{}) string { 29 | data, err := json.Marshal(value) 30 | assert.NoError(t, err) 31 | return string(data) 32 | } 33 | 34 | func jsonUnmarshal(t *testing.T, from string, target interface{}) { 35 | err := json.Unmarshal([]byte(from), target) 36 | assert.NoError(t, err) 37 | } 38 | -------------------------------------------------------------------------------- /internal/validation/testdata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testdata", 3 | "version": "0.0.0", 4 | "description": "Extracts test data from github.com/graphql/graphql-js", 5 | "main": "", 6 | "devDependencies": { 7 | "babel-cli": "^6.26.0", 8 | "babel-plugin-syntax-async-functions": "^6.13.0", 9 | "babel-plugin-syntax-async-generators": "^6.13.0", 10 | "babel-plugin-transform-class-properties": "^6.24.1", 11 | "babel-plugin-transform-es2015-classes": "^6.24.1", 12 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 13 | "babel-plugin-transform-es2015-spread": "^6.22.0", 14 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 15 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 16 | "babel-preset-env": "^1.6.1", 17 | "chai": "^4.1.2", 18 | "iterall": "^1.2.2" 19 | }, 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/chirino/graphql.git" 26 | }, 27 | "author": "", 28 | "license": "BSD-3-Clause", 29 | "bugs": { 30 | "url": "https://github.com/chirino/graphql/issues" 31 | }, 32 | "homepage": "https://github.com/chirino/graphql#readme" 33 | } 34 | -------------------------------------------------------------------------------- /resolvers/directive_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import "github.com/chirino/graphql/schema" 4 | 5 | /////////////////////////////////////////////////////////////////////// 6 | // 7 | // DirectiveResolver gets applied to fields or types that have a given 8 | // directive 9 | // 10 | /////////////////////////////////////////////////////////////////////// 11 | type DirectiveResolver struct { 12 | Directive string 13 | Create func(request *ResolveRequest, next Resolution, args map[string]interface{}) Resolution 14 | } 15 | 16 | func (factory DirectiveResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 17 | if args := matchingArgList(request, factory.Directive); args != nil { 18 | args := args.Value(nil) 19 | return factory.Create(request, next, args) 20 | } 21 | return next 22 | } 23 | 24 | func matchingArgList(request *ResolveRequest, directive string) schema.ArgumentList { 25 | for _, d := range request.Field.Directives { 26 | if d.Name == directive { 27 | return d.Args 28 | } 29 | } 30 | if parentType, ok := request.ParentType.(schema.HasDirectives); ok { 31 | for _, d := range parentType.GetDirectives() { 32 | if d.Name == directive { 33 | return d.Args 34 | } 35 | } 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /httpgql/ws_test.go: -------------------------------------------------------------------------------- 1 | package httpgql_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/chirino/graphql" 9 | "github.com/chirino/graphql/httpgql" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type testStream struct { 14 | responses chan *graphql.Response 15 | } 16 | 17 | func (t testStream) Close() { 18 | } 19 | func (t testStream) Responses() <-chan *graphql.Response { 20 | return t.responses 21 | } 22 | 23 | func TestClientServeGraphQLStream(t *testing.T) { 24 | 25 | s := httptest.NewServer(&httpgql.Handler{ 26 | ServeGraphQLStream: func(request *graphql.Request) graphql.ResponseStream { 27 | result := make(chan *graphql.Response, 2) 28 | result <- &graphql.Response{ 29 | Data: json.RawMessage(`{"hello":"world"}`), 30 | } 31 | result <- &graphql.Response{ 32 | Data: json.RawMessage(`{"bye":"world"}`), 33 | } 34 | close(result) 35 | return result 36 | }, 37 | }) 38 | 39 | defer s.Close() 40 | client := httpgql.NewClient(s.URL) 41 | rs := client.ServeGraphQLStream(&graphql.Request{Query: "{hello}"}) 42 | response := <-rs 43 | assert.Equal(t, `{"hello":"world"}`, string(response.Data)) 44 | response = <-rs 45 | assert.Equal(t, `{"bye":"world"}`, string(response.Data)) 46 | response = <-rs 47 | assert.Nil(t, response) 48 | } 49 | -------------------------------------------------------------------------------- /internal/generator/main.go: -------------------------------------------------------------------------------- 1 | //go:generate go run . 2 | 3 | // This whole directory is used to generate the assets_vfsdata.go file. It's not compiled into the binary. 4 | // 5 | package main 6 | 7 | import ( 8 | "github.com/chirino/graphql/internal/filesystem" 9 | "github.com/shurcooL/httpfs/filter" 10 | "github.com/shurcooL/vfsgen" 11 | "log" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | func main() { 20 | err := vfsgen.Generate(GetAssetsFS(), vfsgen.Options{ 21 | Filename: filepath.Join("..", "assets", "assets.go"), 22 | PackageName: "assets", 23 | BuildTags: "", 24 | VariableName: "FileSystem", 25 | VariableComment: "", 26 | }) 27 | if err != nil { 28 | log.Fatalln(err) 29 | } 30 | } 31 | 32 | func GetAssetsFS() http.FileSystem { 33 | assetsDir := filepath.Join("..", "assets") 34 | return filesystem.NewFileInfoMappingFS(filter.Keep(http.Dir(assetsDir), func(path string, fi os.FileInfo) bool { 35 | if fi.Name() == ".DS_Store" { 36 | return false 37 | } 38 | if strings.HasSuffix(fi.Name(), ".go") { 39 | return false 40 | } 41 | return true 42 | }), func(fi os.FileInfo) (os.FileInfo, error) { 43 | return &zeroTimeFileInfo{fi}, nil 44 | }) 45 | } 46 | 47 | type zeroTimeFileInfo struct { 48 | os.FileInfo 49 | } 50 | 51 | func (*zeroTimeFileInfo) ModTime() time.Time { 52 | return time.Time{} 53 | } 54 | -------------------------------------------------------------------------------- /customtypes/time.go: -------------------------------------------------------------------------------- 1 | package customtypes 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // Time is a custom GraphQL type to represent an instant in time. It has to be added to a schema 10 | // via "scalar Time" since it is not a predeclared GraphQL type like "ID". 11 | type Time struct { 12 | time.Time 13 | } 14 | 15 | // ImplementsGraphQLType maps this custom Go type 16 | // to the graphql scalar type in the schema. 17 | func (Time) ImplementsGraphQLType(name string) bool { 18 | return name == "Time" 19 | } 20 | 21 | // UnmarshalGraphQL is a custom unmarshaler for Time 22 | // 23 | // This function will be called whenever you use the 24 | // time scalar as an input 25 | func (t *Time) UnmarshalGraphQL(input interface{}) error { 26 | switch input := input.(type) { 27 | case time.Time: 28 | t.Time = input 29 | return nil 30 | case string: 31 | var err error 32 | t.Time, err = time.Parse(time.RFC3339, input) 33 | return err 34 | case int: 35 | t.Time = time.Unix(int64(input), 0) 36 | return nil 37 | case float64: 38 | t.Time = time.Unix(int64(input), 0) 39 | return nil 40 | default: 41 | return fmt.Errorf("wrong type") 42 | } 43 | } 44 | 45 | // MarshalJSON is a custom marshaler for Time 46 | // 47 | // This function will be called whenever you 48 | // query for fields that use the Time type 49 | func (t Time) MarshalJSON() ([]byte, error) { 50 | return json.Marshal(t.Time) 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Richard Musiol. All rights reserved. 2 | Copyright (c) 2018 Red Hat, Inc. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /httpgql/http_test.go: -------------------------------------------------------------------------------- 1 | package httpgql_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/chirino/graphql/httpgql" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/chirino/graphql" 14 | "github.com/chirino/graphql/internal/example/starwars" 15 | ) 16 | 17 | func TestEngineAPIServeHTTP(t *testing.T) { 18 | w := httptest.NewRecorder() 19 | r := httptest.NewRequest("POST", "/some/path/here", strings.NewReader(`{"query":"{ hero { name } }", "operationName":"", "variables": null}`)) 20 | 21 | engine := graphql.New() 22 | err := engine.Schema.Parse(starwars.Schema) 23 | 24 | require.NoError(t, err) 25 | engine.Root = &starwars.Resolver{} 26 | h := httpgql.Handler{ServeGraphQLStream: engine.ServeGraphQLStream} 27 | 28 | h.ServeHTTP(w, r) 29 | 30 | assert.Equal(t, 200, w.Code) 31 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 32 | assert.Equal(t, `{"data":{"hero":{"name":"R2-D2"}}} 33 | `, w.Body.String()) 34 | } 35 | 36 | func TestClientServeGraphQL(t *testing.T) { 37 | 38 | s := httptest.NewServer(&httpgql.Handler{ 39 | ServeGraphQL: func(request *graphql.Request) *graphql.Response { 40 | return &graphql.Response{ 41 | Data: json.RawMessage(`{"hello":"world"}`), 42 | } 43 | }, 44 | }) 45 | defer s.Close() 46 | client := httpgql.NewClient(s.URL) 47 | response := client.ServeGraphQL(&graphql.Request{Query: "{hello}"}) 48 | assert.Equal(t, `{"hello":"world"}`, string(response.Data)) 49 | } 50 | -------------------------------------------------------------------------------- /internal/filesystem/file_info_mapping_fs.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | ) 7 | 8 | type FileInfoMappingFunction func(os.FileInfo) (os.FileInfo, error) 9 | 10 | func NewFileInfoMappingFS(source http.FileSystem, mapper FileInfoMappingFunction) http.FileSystem { 11 | return &fileInfoMappingFS{source, mapper} 12 | } 13 | 14 | type fileInfoMappingFS struct { 15 | source http.FileSystem 16 | mapper FileInfoMappingFunction 17 | } 18 | 19 | func (fs *fileInfoMappingFS) Open(path string) (http.File, error) { 20 | f, err := fs.source.Open(path) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return &fileInfoMappingFile{fs, f}, nil 25 | } 26 | 27 | type fileInfoMappingFile struct { 28 | fs *fileInfoMappingFS 29 | source http.File 30 | } 31 | 32 | func (this *fileInfoMappingFile) Close() error { 33 | return this.source.Close() 34 | } 35 | 36 | func (this *fileInfoMappingFile) Read(p []byte) (n int, err error) { 37 | return this.source.Read(p) 38 | } 39 | 40 | func (this *fileInfoMappingFile) Seek(offset int64, whence int) (int64, error) { 41 | return this.source.Seek(offset, whence) 42 | } 43 | 44 | func (this *fileInfoMappingFile) Readdir(count int) ([]os.FileInfo, error) { 45 | infos, err := this.source.Readdir(count) 46 | for i, info := range infos { 47 | infos[i], err = this.fs.mapper(info) 48 | if err != nil { 49 | return nil, err 50 | } 51 | } 52 | return infos, err 53 | } 54 | 55 | func (this *fileInfoMappingFile) Stat() (os.FileInfo, error) { 56 | info, err := this.source.Stat() 57 | if err != nil { 58 | return info, err 59 | } 60 | return this.fs.mapper(info) 61 | } 62 | -------------------------------------------------------------------------------- /graphql.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/chirino/graphql/internal/introspection" 8 | "github.com/chirino/graphql/schema" 9 | ) 10 | 11 | type Handler interface { 12 | ServeGraphQL(request *Request) *Response 13 | } 14 | type ServeGraphQLFunc func(request *Request) *Response 15 | 16 | func (f ServeGraphQLFunc) ServeGraphQL(request *Request) *Response { 17 | return f(request) 18 | } 19 | 20 | func GetSchema(serveGraphQL ServeGraphQLFunc) (*schema.Schema, error) { 21 | json, err := GetSchemaIntrospectionJSON(serveGraphQL) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return introspection.NewSchema(json) 26 | } 27 | 28 | func GetSchemaIntrospectionJSON(serveGraphQL ServeGraphQLFunc) ([]byte, error) { 29 | result := serveGraphQL(&Request{ 30 | Query: introspection.Query, 31 | }) 32 | return result.Data, result.Error() 33 | } 34 | 35 | func Exec(serveGraphQL ServeGraphQLFunc, ctx context.Context, result interface{}, query string, args ...interface{}) error { 36 | variables := map[string]interface{}{} 37 | for i := 0; i+1 < len(args); i += 2 { 38 | variables[args[i].(string)] = args[i+1] 39 | } 40 | response := serveGraphQL(&Request{ 41 | Context: ctx, 42 | Query: query, 43 | Variables: variables, 44 | }) 45 | 46 | if result != nil && response != nil { 47 | switch result := result.(type) { 48 | case *[]byte: 49 | *result = response.Data 50 | case *string: 51 | *result = string(response.Data) 52 | default: 53 | err := json.Unmarshal(response.Data, result) 54 | if err != nil { 55 | response.AddError(err) 56 | } 57 | } 58 | } 59 | return response.Error() 60 | } 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resolvers/cast.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | func normalizeMethodName(method string) string { 9 | //method = strings.Replace(method, "_", "", -1) 10 | method = strings.ToLower(method) 11 | return method 12 | } 13 | 14 | var castMethodCache Cache 15 | 16 | func TryCastFunction(value reflect.Value, toType string) (v reflect.Value, ok bool) { 17 | for { 18 | v, ok = tryCastFunction(value, toType) 19 | if ok { 20 | return 21 | } 22 | if value.Kind() == reflect.Interface || value.Kind() == reflect.Ptr { 23 | value = value.Elem() 24 | } else { 25 | return 26 | } 27 | } 28 | } 29 | 30 | func tryCastFunction(parentValue reflect.Value, toType string) (reflect.Value, bool) { 31 | var key struct { 32 | fromType reflect.Type 33 | toType string 34 | } 35 | 36 | key.fromType = parentValue.Type() 37 | key.toType = toType 38 | 39 | methodIndex := childMethodTypeCache.GetOrElseUpdate(key, func() interface{} { 40 | needle := normalizeMethodName("To" + toType) 41 | for methodIndex := 0; methodIndex < key.fromType.NumMethod(); methodIndex++ { 42 | method := normalizeMethodName(key.fromType.Method(methodIndex).Name) 43 | if needle == method { 44 | if key.fromType.Method(methodIndex).Type.NumIn() != 1 { 45 | continue 46 | } 47 | if key.fromType.Method(methodIndex).Type.NumOut() != 2 { 48 | continue 49 | } 50 | if key.fromType.Method(methodIndex).Type.Out(1) != reflect.TypeOf(true) { 51 | continue 52 | } 53 | return methodIndex 54 | } 55 | } 56 | return -1 57 | }).(int) 58 | if methodIndex == -1 { 59 | return reflect.Value{}, false 60 | } 61 | out := parentValue.Method(methodIndex).Call(nil) 62 | return out[0], out[1].Bool() 63 | } 64 | -------------------------------------------------------------------------------- /internal/validation/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/example/subscription/subscription.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "reflect" 8 | "time" 9 | 10 | "github.com/chirino/graphql" 11 | "github.com/chirino/graphql/graphiql" 12 | "github.com/chirino/graphql/httpgql" 13 | "github.com/chirino/graphql/resolvers" 14 | ) 15 | 16 | type root struct { 17 | Test string `json:"test"` 18 | } 19 | 20 | func (m *root) Hello(ctx resolvers.ExecutionContext, args struct{ Duration int }) { 21 | go func() { 22 | counter := args.Duration 23 | for { 24 | select { 25 | case <-ctx.GetContext().Done(): 26 | // We could close any resources held open for the subscription here. 27 | return 28 | case <-time.After(time.Duration(args.Duration) * time.Millisecond): 29 | // every few duration ms.. fire a subscription event. 30 | ctx.FireSubscriptionEvent(reflect.ValueOf(fmt.Sprintf("Hello: %d", counter)), nil) 31 | counter += args.Duration 32 | } 33 | } 34 | }() 35 | } 36 | 37 | func main() { 38 | engine := graphql.New() 39 | engine.Root = &root{Test: "Hi!"} 40 | err := engine.Schema.Parse(` 41 | schema { 42 | query: MyQuery 43 | subscription: MySubscription 44 | } 45 | type MyQuery { 46 | test: String 47 | } 48 | type MySubscription { 49 | hello(duration: Int!): String 50 | } 51 | `) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | http.Handle("/graphql", &httpgql.Handler{ServeGraphQLStream: engine.ServeGraphQLStream}) 57 | fmt.Println("GraphQL service running at http://localhost:8080/graphql") 58 | 59 | http.Handle("/", graphiql.New("ws://localhost:8080/graphql", true)) 60 | fmt.Println("GraphiQL UI running at http://localhost:8080/") 61 | log.Fatal(http.ListenAndServe(":8080", nil)) 62 | } 63 | -------------------------------------------------------------------------------- /schema/types.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/chirino/graphql/internal/lexer" 5 | "github.com/chirino/graphql/qerrors" 6 | ) 7 | 8 | func DeepestType(t Type) Type { 9 | switch t := (t).(type) { 10 | case *NonNull: 11 | return DeepestType(t.OfType) 12 | case *List: 13 | return DeepestType(t.OfType) 14 | } 15 | return t 16 | } 17 | 18 | func OfType(t Type) Type { 19 | switch t := (t).(type) { 20 | case *NonNull: 21 | return t.OfType 22 | case *List: 23 | return t.OfType 24 | default: 25 | return nil 26 | } 27 | } 28 | 29 | func ParseType(l *lexer.Lexer) Type { 30 | t := parseNullType(l) 31 | if l.Peek() == '!' { 32 | l.ConsumeToken('!') 33 | return &NonNull{OfType: t} 34 | } 35 | return t 36 | } 37 | 38 | func parseNullType(l *lexer.Lexer) Type { 39 | if l.Peek() == '[' { 40 | l.ConsumeToken('[') 41 | ofType := ParseType(l) 42 | l.ConsumeToken(']') 43 | return &List{OfType: ofType} 44 | } 45 | 46 | name := parseTypeName(l) 47 | return &name 48 | } 49 | 50 | type Resolver func(name string) Type 51 | 52 | func ResolveType(t Type, resolver Resolver) (Type, *qerrors.Error) { 53 | switch t := t.(type) { 54 | case *List: 55 | ofType, err := ResolveType(t.OfType, resolver) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return &List{OfType: ofType}, nil 60 | case *NonNull: 61 | ofType, err := ResolveType(t.OfType, resolver) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return &NonNull{OfType: ofType}, nil 66 | case *TypeName: 67 | refT := resolver(t.Name) 68 | if refT == nil { 69 | err := qerrors.Errorf("Unknown type %q.", t.Name) 70 | err.Rule = "KnownTypeNames" 71 | err.Locations = []qerrors.Location{t.NameLoc} 72 | return nil, err 73 | } 74 | return refT, nil 75 | default: 76 | return t, nil 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | ) 9 | 10 | type Request struct { 11 | // Optional Context to use on the request. Required if caller wants to cancel long-running \ 12 | // operations like subscriptions. 13 | Context context.Context `json:"-"` 14 | // Query is the graphql query document to execute 15 | Query string `json:"query,omitempty"` 16 | // OperationName is the name of the operation in the query document to execute 17 | OperationName string `json:"operationName,omitempty"` 18 | // Variables can be set to a json.RawMessage or a map[string]interface{} 19 | Variables interface{} `json:"variables,omitempty"` 20 | } 21 | 22 | func (r Request) GetContext() (ctx context.Context) { 23 | ctx = r.Context 24 | if ctx == nil { 25 | ctx = context.Background() 26 | } 27 | return 28 | } 29 | 30 | func (r *Request) VariablesAsMap() (map[string]interface{}, error) { 31 | if r.Variables == nil { 32 | return nil, nil 33 | } 34 | switch variables := r.Variables.(type) { 35 | case map[string]interface{}: 36 | return variables, nil 37 | case json.RawMessage: 38 | if len(variables) == 0 { 39 | return nil, nil 40 | } 41 | x := map[string]interface{}{} 42 | err := json.Unmarshal(variables, &x) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return x, nil 47 | } 48 | return nil, fmt.Errorf("unsupported type: %s", reflect.TypeOf(r.Variables)) 49 | } 50 | 51 | func (r *Request) VariablesAsJson() (json.RawMessage, error) { 52 | if r.Variables == nil { 53 | return nil, nil 54 | } 55 | switch variables := r.Variables.(type) { 56 | case map[string]interface{}: 57 | return json.Marshal(variables) 58 | case json.RawMessage: 59 | return variables, nil 60 | } 61 | return nil, fmt.Errorf("unsupported type: %s", reflect.TypeOf(r.Variables)) 62 | } 63 | -------------------------------------------------------------------------------- /resolvers/resolvers.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "reflect" 7 | 8 | "github.com/chirino/graphql/schema" 9 | ) 10 | 11 | type ExecutionContext interface { 12 | GetRoot() interface{} 13 | FireSubscriptionEvent(value reflect.Value, err error) 14 | FireSubscriptionClose() 15 | GetSchema() *schema.Schema 16 | GetContext() context.Context 17 | GetLimiter() *chan byte 18 | HandlePanic(selectionPath []string) error 19 | GetQuery() string 20 | GetDocument() *schema.QueryDocument 21 | GetOperation() *schema.Operation 22 | GetVars() map[string]interface{} 23 | } 24 | 25 | type ResolveRequest struct { 26 | Context context.Context 27 | ExecutionContext ExecutionContext 28 | ParentResolve *ResolveRequest 29 | SelectionPath func() []string 30 | ParentType schema.Type 31 | Parent reflect.Value 32 | Field *schema.Field 33 | Args map[string]interface{} 34 | Selection *schema.FieldSelection 35 | } 36 | 37 | type Resolution func() (reflect.Value, error) 38 | 39 | type Resolver interface { 40 | // Resolve allows you to inspect a ResolveRequest to see if your resolver can resolve it. 41 | // If you can resolve it, return a new Resolution that computes the value of the field. 42 | // If you don't know how to resolve that request, return next. 43 | // 44 | // The next variable hold the Resolution of the previous resolver, this allows your resolver 45 | // to filter it's results. next may be nil if no resolution has been found yet. 46 | Resolve(request *ResolveRequest, next Resolution) Resolution 47 | } 48 | 49 | // When RawMessage is the result of a Resolution, for a field, no sub field resolution performed. 50 | type RawMessage json.RawMessage 51 | 52 | type ValueWithContext struct { 53 | Value reflect.Value 54 | Context context.Context 55 | } 56 | -------------------------------------------------------------------------------- /resolvers/metadata_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chirino/graphql/internal/introspection" 6 | "github.com/chirino/graphql/schema" 7 | "reflect" 8 | ) 9 | 10 | /////////////////////////////////////////////////////////////////////// 11 | // 12 | // MetadataResolverFactory resolves fields using schema metadata 13 | // 14 | /////////////////////////////////////////////////////////////////////// 15 | type metadataResolver byte 16 | 17 | const MetadataResolver = metadataResolver(0) 18 | 19 | func (this metadataResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 20 | s := request.ExecutionContext.GetSchema() 21 | switch request.Field.Name { 22 | case "__typename": 23 | return func() (reflect.Value, error) { 24 | 25 | switch schemaType := request.ParentType.(type) { 26 | case *schema.Union: 27 | for _, pt := range schemaType.PossibleTypes { 28 | if _, ok := TryCastFunction(request.Parent, pt.Name); ok { 29 | return reflect.ValueOf(pt.Name), nil 30 | } 31 | } 32 | case *schema.Interface: 33 | for _, pt := range schemaType.PossibleTypes { 34 | if _, ok := TryCastFunction(request.Parent, pt.Name); ok { 35 | return reflect.ValueOf(pt.Name), nil 36 | } 37 | } 38 | default: 39 | return reflect.ValueOf(schemaType.String()), nil 40 | } 41 | return reflect.ValueOf(""), nil 42 | } 43 | 44 | case "__schema": 45 | return func() (reflect.Value, error) { 46 | return reflect.ValueOf(introspection.WrapSchema(s)), nil 47 | } 48 | 49 | case "__type": 50 | return func() (reflect.Value, error) { 51 | t, ok := s.Types[request.Args["name"].(string)] 52 | if !ok { 53 | return reflect.Value{}, fmt.Errorf("Could not find the type") 54 | } 55 | return reflect.ValueOf(introspection.WrapType(t)), nil 56 | } 57 | } 58 | return next 59 | } 60 | -------------------------------------------------------------------------------- /inputconv/inputconv.go: -------------------------------------------------------------------------------- 1 | package inputconv 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chirino/graphql/schema" 6 | ) 7 | 8 | type TypeConverters map[string]func(t schema.Type, value interface{}) (interface{}, error) 9 | 10 | func (tc TypeConverters) Convert(t schema.Type, value interface{}, path string) (interface{}, error) { 11 | var result interface{} = nil 12 | // Dive into nested children first using recursion... 13 | switch t := t.(type) { 14 | case *schema.Scalar: // no children... 15 | result = value 16 | case *schema.NonNull: 17 | if value == nil { 18 | panic(fmt.Sprintf("Expecting non null value, but got one. (field path: %s)", path)) 19 | } 20 | cv, err := tc.Convert(t.OfType, value, path) 21 | if err != nil { 22 | return nil, err 23 | } 24 | result = cv 25 | case *schema.List: 26 | if value == nil { 27 | break 28 | } 29 | value := value.([]interface{}) 30 | for i, cv := range value { 31 | cv, err := tc.Convert(t.OfType, cv, path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | value[i] = cv 36 | } 37 | result = value 38 | case *schema.InputObject: 39 | if value == nil { 40 | break 41 | } 42 | value := value.(map[string]interface{}) 43 | converted := make(map[string]interface{}, len(value)) 44 | for _, field := range t.Fields { 45 | fieldName := field.Name 46 | cv := value[fieldName] 47 | cv, err := tc.Convert(field.Type, cv, path+"/"+fieldName) 48 | if err != nil { 49 | return nil, err 50 | } 51 | if cv != nil { 52 | converted[fieldName] = cv 53 | } 54 | } 55 | result = converted 56 | default: 57 | panic(fmt.Sprintf("convert not implemented for type %T: ", t)) 58 | } 59 | // then convert values on the way out... 60 | converter := tc[t.String()] 61 | if converter != nil { 62 | v, err := converter(t, result) 63 | if err != nil { 64 | return nil, err 65 | } 66 | result = v 67 | } 68 | return result, nil 69 | } 70 | -------------------------------------------------------------------------------- /httpgql/http-client.go: -------------------------------------------------------------------------------- 1 | package httpgql 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/chirino/graphql" 14 | "github.com/chirino/graphql/qerrors" 15 | ) 16 | 17 | type Client struct { 18 | URL string 19 | HTTPClient *http.Client 20 | connections map[string]*wsConnection 21 | RequestHeader http.Header 22 | mu sync.Mutex 23 | } 24 | 25 | func NewClient(url string) *Client { 26 | return &Client{ 27 | URL: url, 28 | connections: map[string]*wsConnection{}, 29 | RequestHeader: http.Header{}, 30 | } 31 | } 32 | 33 | func (client *Client) ServeGraphQL(request *graphql.Request) *graphql.Response { 34 | c := client.HTTPClient 35 | if c == nil { 36 | c = &http.Client{} 37 | } 38 | 39 | response := graphql.NewResponse() 40 | body, err := json.Marshal(request) 41 | if err != nil { 42 | return response.AddError(err) 43 | } 44 | 45 | req, err := http.NewRequestWithContext(request.GetContext(), http.MethodPost, client.URL, bytes.NewReader(body)) 46 | if err != nil { 47 | return response.AddError(err) 48 | } 49 | req.Header.Set("Accept", "application/json") 50 | req.Header.Set("Content-Type", "application/json") 51 | 52 | for k, h := range client.RequestHeader { 53 | req.Header[k] = h 54 | } 55 | 56 | resp, err := c.Do(req) 57 | if err != nil { 58 | return response.AddError(err) 59 | } 60 | defer resp.Body.Close() 61 | 62 | contentType := resp.Header.Get("Content-Type") 63 | if strings.HasPrefix(contentType, "application/json") { 64 | err = json.NewDecoder(resp.Body).Decode(&response) 65 | if err != nil { 66 | return response.AddError(err) 67 | } 68 | return response 69 | } 70 | 71 | preview, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1024)) 72 | return response.AddError(qerrors.Errorf("invalid content type: %s", contentType).WithCause(errors.New(string(preview)))) 73 | } 74 | -------------------------------------------------------------------------------- /resolvers/method_resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "github.com/chirino/graphql/internal/exec/packer" 5 | "reflect" 6 | ) 7 | 8 | /////////////////////////////////////////////////////////////////////// 9 | // 10 | // MethodResolverFactory resolves fields using the method 11 | // implemented by a receiver type. 12 | // 13 | /////////////////////////////////////////////////////////////////////// 14 | type methodResolver byte 15 | 16 | const MethodResolver = methodResolver(0) 17 | 18 | func (this methodResolver) Resolve(request *ResolveRequest, next Resolution) Resolution { 19 | if !request.Parent.IsValid() { 20 | return nil 21 | } 22 | childMethod := getChildMethod(&request.Parent, request.Field.Name) 23 | if childMethod == nil { 24 | return next 25 | } 26 | 27 | var structPacker *packer.StructPacker = nil 28 | if childMethod.argumentsType != nil { 29 | p := packer.NewBuilder() 30 | sp, err := p.MakeStructPacker(request.Field.Args, *childMethod.argumentsType) 31 | if err != nil { 32 | return nil 33 | } 34 | structPacker = sp 35 | p.Finish() 36 | } 37 | 38 | return func() (reflect.Value, error) { 39 | var in []reflect.Value 40 | if childMethod.hasContext { 41 | in = append(in, reflect.ValueOf(request.ExecutionContext.GetContext())) 42 | } 43 | if childMethod.hasExecutionContext { 44 | in = append(in, reflect.ValueOf(request.ExecutionContext)) 45 | } 46 | 47 | if childMethod.argumentsType != nil { 48 | 49 | argValue, err := structPacker.Pack(request.Args) 50 | if err != nil { 51 | return reflect.Value{}, err 52 | } 53 | in = append(in, argValue) 54 | 55 | } 56 | result := request.Parent.Method(childMethod.Index).Call(in) 57 | if childMethod.hasError && !result[1].IsNil() { 58 | return reflect.Value{}, result[1].Interface().(error) 59 | } 60 | if len(result) > 0 { 61 | return result[0], nil 62 | } else { 63 | return reflect.ValueOf(nil), nil 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/example/directiveresolver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "reflect" 8 | 9 | "github.com/chirino/graphql" 10 | "github.com/chirino/graphql/graphiql" 11 | "github.com/chirino/graphql/httpgql" 12 | "github.com/chirino/graphql/resolvers" 13 | ) 14 | 15 | type query struct { 16 | Name string `json:"name"` 17 | } 18 | 19 | func main() { 20 | engine := graphql.New() 21 | engine.Root = &query{ 22 | Name: "Hiram", 23 | } 24 | err := engine.Schema.Parse(` 25 | directive @static(prefix: String) on FIELD 26 | schema { 27 | query: Query 28 | } 29 | type Query { 30 | name: String! @static(prefix: "Hello ") 31 | } 32 | `) 33 | 34 | // Lets register a resolver that is applied to fields with the @static directive 35 | engine.Resolver = resolvers.List(engine.Resolver, resolvers.DirectiveResolver{ 36 | Directive: "static", 37 | Create: func(request *resolvers.ResolveRequest, next resolvers.Resolution, args map[string]interface{}) resolvers.Resolution { 38 | 39 | // This resolver just filters the next result bu applying a prefix.. 40 | // so we need the next resolver to be valid. 41 | if next == nil { 42 | return nil 43 | } 44 | return func() (reflect.Value, error) { 45 | value, err := next() 46 | if err != nil { 47 | return value, err 48 | } 49 | v := fmt.Sprintf("%s%s", args["prefix"], value.String()) 50 | return reflect.ValueOf(v), nil 51 | } 52 | }, 53 | }) 54 | 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | http.Handle("/graphql", &httpgql.Handler{ServeGraphQLStream: engine.ServeGraphQLStream}) 60 | fmt.Println("GraphQL service running at http://localhost:8080/graphql") 61 | 62 | http.Handle("/", graphiql.New("ws://localhost:8080/graphql", true)) 63 | fmt.Println("GraphiQL UI running at http://localhost:8080/") 64 | 65 | log.Fatal(http.ListenAndServe(":8080", nil)) 66 | } 67 | -------------------------------------------------------------------------------- /schema/query_format.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | func (s *QueryDocument) String() string { 8 | return FormatterToString(s) 9 | } 10 | 11 | func (s *QueryDocument) WriteTo(out io.StringWriter) { 12 | for _, value := range s.Operations { 13 | value.WriteTo(out) 14 | out.WriteString("\n") 15 | } 16 | for _, value := range s.Fragments { 17 | value.WriteTo(out) 18 | out.WriteString("\n") 19 | } 20 | } 21 | 22 | func (o *FragmentDecl) WriteTo(out io.StringWriter) { 23 | out.WriteString("fragment") 24 | if o.Name != "" { 25 | out.WriteString(" ") 26 | out.WriteString(o.Name) 27 | } 28 | out.WriteString(" on ") 29 | out.WriteString(o.On.Name) 30 | o.Directives.WriteTo(out) 31 | o.Selections.WriteTo(out) 32 | } 33 | 34 | func (o *Operation) WriteTo(out io.StringWriter) { 35 | out.WriteString(string(o.Type)) 36 | if o.Name != "" { 37 | out.WriteString(" ") 38 | out.WriteString(o.Name) 39 | } 40 | o.Directives.WriteTo(out) 41 | o.Vars.WriteTo(out) 42 | o.Selections.WriteTo(out) 43 | } 44 | 45 | func (o SelectionList) WriteTo(out io.StringWriter) { 46 | if len(o) > 0 { 47 | out.WriteString(" {\n") 48 | i := &indent{} 49 | for _, value := range o { 50 | value.WriteTo(i) 51 | } 52 | i.Done(out) 53 | out.WriteString("}") 54 | } 55 | } 56 | 57 | func (t *FieldSelection) WriteTo(out io.StringWriter) { 58 | out.WriteString(t.Alias) 59 | if t.Name != t.Alias { 60 | out.WriteString(":") 61 | out.WriteString(t.Name) 62 | } 63 | t.Arguments.WriteTo(out) 64 | t.Directives.WriteTo(out) 65 | t.Selections.WriteTo(out) 66 | out.WriteString("\n") 67 | } 68 | 69 | func (t *FragmentSpread) WriteTo(out io.StringWriter) { 70 | out.WriteString("...") 71 | out.WriteString(t.Name) 72 | t.Directives.WriteTo(out) 73 | out.WriteString("\n") 74 | } 75 | 76 | func (t *InlineFragment) WriteTo(out io.StringWriter) { 77 | out.WriteString("... on ") 78 | out.WriteString(t.On.Name) 79 | t.Directives.WriteTo(out) 80 | t.Selections.WriteTo(out) 81 | out.WriteString("\n") 82 | } 83 | -------------------------------------------------------------------------------- /internal/validation/validation_test.go: -------------------------------------------------------------------------------- 1 | package validation_test 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "sort" 7 | "testing" 8 | 9 | "encoding/json" 10 | 11 | "github.com/chirino/graphql/internal/validation" 12 | "github.com/chirino/graphql/qerrors" 13 | "github.com/chirino/graphql/schema" 14 | ) 15 | 16 | type Test struct { 17 | Name string 18 | Rule string 19 | Schema int 20 | Query string 21 | Errors qerrors.ErrorList 22 | } 23 | 24 | func TestValidate(t *testing.T) { 25 | f, err := os.Open("testdata/tests.json") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | var testData struct { 31 | Schemas []string 32 | Tests []*Test 33 | } 34 | if err := json.NewDecoder(f).Decode(&testData); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | schemas := make([]*schema.Schema, len(testData.Schemas)) 39 | for i, schemaStr := range testData.Schemas { 40 | schemas[i] = schema.New() 41 | if err := schemas[i].Parse(schemaStr); err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | 46 | for _, test := range testData.Tests { 47 | if test.Name != "Validate: Variables are in allowed positions/String over Boolean" { 48 | continue 49 | } 50 | t.Run(test.Name, func(t *testing.T) { 51 | d := &schema.QueryDocument{} 52 | err := d.Parse(test.Query) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | errs := validation.Validate(schemas[test.Schema], d, 0) 57 | got := qerrors.ErrorList{} 58 | for _, err := range errs { 59 | if err.Rule == test.Rule { 60 | err.Rule = "" 61 | err.ClearStack() 62 | got = append(got, err) 63 | } 64 | } 65 | sortLocations(test.Errors) 66 | sortLocations(got) 67 | if !reflect.DeepEqual(test.Errors, got) { 68 | t.Errorf("wrong errors\nexpected: %v\ngot: %v", test.Errors, got) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func sortLocations(errs qerrors.ErrorList) { 75 | for _, err := range errs { 76 | locs := err.Locations 77 | sort.Slice(locs, func(i, j int) bool { return locs[i].Before(locs[j]) }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /schema/schema_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chirino/graphql/schema" 7 | ) 8 | 9 | type parseTestCase struct { 10 | description string 11 | sdl string 12 | expected *schema.Schema 13 | err error 14 | } 15 | 16 | var parseTests = []parseTestCase{{ 17 | description: "Parses interface definition", 18 | sdl: "interface Greeting { message: String! }", 19 | expected: &schema.Schema{ 20 | Types: map[string]schema.NamedType{ 21 | "Greeting": &schema.Interface{ 22 | Name: "Greeting", 23 | Fields: []*schema.Field{{Name: "message"}}, 24 | }}, 25 | }}, { 26 | description: "Parses type with description string", 27 | sdl: ` 28 | "Single line description." 29 | type Type { 30 | field: String 31 | }`, 32 | expected: &schema.Schema{ 33 | Types: map[string]schema.NamedType{ 34 | "Type": &schema.Object{ 35 | Name: "Type", 36 | Desc: schema.Description{Text: "Single line description."}, 37 | }}, 38 | }}, { 39 | description: "Parses type with multi-line description string", 40 | sdl: ` 41 | """ 42 | Multi-line description. 43 | """ 44 | type Type { 45 | field: String 46 | }`, 47 | expected: &schema.Schema{ 48 | Types: map[string]schema.NamedType{ 49 | "Type": &schema.Object{ 50 | Name: "Type", 51 | Desc: schema.Description{Text: "Multi-line description."}, 52 | }}, 53 | }}, { 54 | description: "Parses type with multi-line description and ignores comments", 55 | sdl: ` 56 | """ 57 | Multi-line description with ignored comments. 58 | """ 59 | # This comment should be ignored. 60 | type Type { 61 | field: String 62 | }`, 63 | expected: &schema.Schema{ 64 | Types: map[string]schema.NamedType{ 65 | "Type": &schema.Object{ 66 | Name: "Type", 67 | Desc: schema.Description{Text: "Multi-line description with ignored comments."}, 68 | }}, 69 | }}, 70 | } 71 | 72 | func TestParse(t *testing.T) { 73 | setup := func(t *testing.T) *schema.Schema { 74 | t.Helper() 75 | return schema.New() 76 | } 77 | 78 | for _, test := range parseTests { 79 | t.Run(test.description, func(t *testing.T) { 80 | t.Skip("TODO: add support for descriptions") 81 | schema := setup(t) 82 | 83 | err := schema.Parse(test.sdl) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | // TODO: verify schema is the same as expected. 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /schema/values.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/chirino/graphql/internal/lexer" 7 | "github.com/chirino/graphql/qerrors" 8 | ) 9 | 10 | // http://facebook.github.io/graphql/draft/#InputValueDefinition 11 | type InputValue struct { 12 | Loc qerrors.Location 13 | Name string 14 | NameLoc Location 15 | Type Type 16 | TypeLoc qerrors.Location 17 | Default Literal 18 | Desc Description 19 | Directives DirectiveList 20 | } 21 | 22 | type InputValueList []*InputValue 23 | 24 | func (l InputValueList) Get(name string) *InputValue { 25 | for _, v := range l { 26 | if v.Name == name { 27 | return v 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func ParseInputValue(l *lexer.Lexer) *InputValue { 34 | p := &InputValue{} 35 | p.Loc = l.Location() 36 | p.Desc = l.ConsumeDescription() 37 | p.Name, p.NameLoc = l.ConsumeIdentInternWithLoc() 38 | l.ConsumeToken(':') 39 | p.TypeLoc = l.Location() 40 | p.Type = ParseType(l) 41 | if l.Peek() == '=' { 42 | l.ConsumeToken('=') 43 | p.Default = ParseLiteral(l, true) 44 | } 45 | p.Directives = ParseDirectives(l) 46 | return p 47 | } 48 | 49 | type Argument struct { 50 | Name string 51 | NameLoc Location 52 | Value Literal 53 | } 54 | 55 | type ArgumentList []Argument 56 | 57 | func (l ArgumentList) Get(name string) (Literal, bool) { 58 | for _, arg := range l { 59 | if arg.Name == name { 60 | return arg.Value, true 61 | } 62 | } 63 | return nil, false 64 | } 65 | 66 | func (l ArgumentList) MustGet(name string) Literal { 67 | value, ok := l.Get(name) 68 | if !ok { 69 | panic("argument not found") 70 | } 71 | return value 72 | } 73 | 74 | func (l ArgumentList) Value(vars map[string]interface{}) map[string]interface{} { 75 | result := make(map[string]interface{}, len(l)) 76 | for _, v := range l { 77 | result[v.Name] = v.Value.Evaluate(vars) 78 | } 79 | return result 80 | } 81 | 82 | // ScanValue stores the values of the argument list 83 | // in the value pointed to by v. If v is nil or not a pointer, 84 | // ScanValue returns an error. 85 | func (l ArgumentList) ScanValue(vars map[string]interface{}, v interface{}) error { 86 | value := l.Value(vars) 87 | marshaled, err := json.Marshal(value) 88 | if err != nil { 89 | return err 90 | } 91 | return json.Unmarshal(marshaled, v) 92 | } 93 | 94 | func ParseArguments(l *lexer.Lexer) ArgumentList { 95 | var args ArgumentList 96 | l.ConsumeToken('(') 97 | for l.Peek() != ')' { 98 | arg := Argument{} 99 | arg.Name, arg.NameLoc = l.ConsumeIdentInternWithLoc() 100 | l.ConsumeToken(':') 101 | arg.Value = ParseLiteral(l, false) 102 | args = append(args, arg) 103 | } 104 | l.ConsumeToken(')') 105 | return args 106 | } 107 | -------------------------------------------------------------------------------- /internal/gqltesting/testing.go: -------------------------------------------------------------------------------- 1 | package gqltesting 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/chirino/graphql/internal/deprecated" 10 | "github.com/chirino/graphql/qerrors" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | // Test is a GraphQL test case to be used with RunTest(s). 15 | type Test struct { 16 | Context context.Context 17 | Schema *deprecated.Schema 18 | Query string 19 | OperationName string 20 | Variables map[string]interface{} 21 | ExpectedResult string 22 | ExpectedErrors qerrors.ErrorList 23 | } 24 | 25 | // RunTests runs the given GraphQL test cases as subtests. 26 | func RunTests(t *testing.T, tests []*Test) { 27 | if len(tests) == 1 { 28 | RunTest(t, tests[0]) 29 | return 30 | } 31 | 32 | for i, test := range tests { 33 | t.Run(strconv.Itoa(i+1), func(t *testing.T) { 34 | RunTest(t, test) 35 | }) 36 | } 37 | } 38 | 39 | // RunTest runs a single GraphQL test case. 40 | func RunTest(t *testing.T, test *Test) { 41 | if test.Context == nil { 42 | test.Context = context.Background() 43 | } 44 | result := test.Schema.Exec(test.Context, test.Query, test.OperationName, test.Variables) 45 | 46 | checkErrors(t, test.ExpectedErrors, result.Errors) 47 | 48 | if test.ExpectedResult != "" { 49 | 50 | // Verify JSON to avoid red herring errors. 51 | got, err := formatJSON(result.Data) 52 | if err != nil { 53 | t.Fatalf("got: invalid JSON: %s, json was: %s", err, result.Data) 54 | } 55 | 56 | want, err := formatJSON([]byte(test.ExpectedResult)) 57 | if err != nil { 58 | t.Fatalf("want: invalid JSON: %s", err) 59 | } 60 | require.Equal(t, string(want), string(got)) 61 | } 62 | 63 | } 64 | 65 | func formatJSON(data []byte) ([]byte, error) { 66 | var v interface{} 67 | if err := json.Unmarshal(data, &v); err != nil { 68 | return nil, err 69 | } 70 | formatted, err := json.MarshalIndent(v, "", " ") 71 | if err != nil { 72 | return nil, err 73 | } 74 | return formatted, nil 75 | } 76 | 77 | func checkErrors(t *testing.T, expected, actual qerrors.ErrorList) { 78 | expectedCount, actualCount := len(expected), len(actual) 79 | 80 | if expectedCount != actualCount { 81 | for i, err := range actual { 82 | t.Errorf("Actual Error %d: '%#v'", i, err) 83 | } 84 | t.Fatalf("unexpected number of errors: got %d, want %d", actualCount, expectedCount) 85 | } 86 | 87 | if expectedCount > 0 { 88 | for i, want := range expected { 89 | got := actual[i] 90 | got.ClearStack() 91 | want.ClearStack() 92 | require.Equal(t, want, got) 93 | //if !reflect.DeepEqual(got, want) { 94 | // t.Fatalf("unexpected error: got %+v, want %+v", got, want) 95 | //} 96 | } 97 | 98 | // Return because we're done checking. 99 | return 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /trace/trace_test.go: -------------------------------------------------------------------------------- 1 | package trace_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/chirino/graphql" 7 | "github.com/chirino/graphql/internal/gqltesting" 8 | "github.com/chirino/graphql/trace" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/segmentio/ksuid" 11 | "github.com/stretchr/testify/require" 12 | "github.com/uber/jaeger-client-go" 13 | "github.com/uber/jaeger-client-go/log" 14 | "io/ioutil" 15 | "net/http" 16 | "os" 17 | "time" 18 | 19 | "github.com/stretchr/testify/assert" 20 | jaegercfg "github.com/uber/jaeger-client-go/config" 21 | "testing" 22 | ) 23 | 24 | func TestJaegerTracing(t *testing.T) { 25 | 26 | cfg, err := jaegercfg.FromEnv() 27 | if err != nil { 28 | t.Skipf("skipping test; Could not initialize jaeger: %s", err) 29 | return 30 | } 31 | queryAPI := os.Getenv("JAEGER_QUERY_ENDPOINT") 32 | if queryAPI == "" { 33 | t.Skipf("skipping test; JAEGER_QUERY_ENDPOINT env not defined.") 34 | return 35 | } 36 | 37 | svcName := t.Name() + "-" + ksuid.New().String() 38 | queryURL := fmt.Sprintf( 39 | "%s?lookback=1h&limit=1&service=%s", 40 | queryAPI, 41 | svcName, 42 | ) 43 | 44 | cfg.ServiceName = svcName 45 | cfg.Sampler.Type = jaeger.SamplerTypeConst 46 | cfg.Sampler.Param = 1 47 | cfg.Reporter.LogSpans = true 48 | 49 | tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(log.StdLogger)) 50 | if err != nil { 51 | t.Skipf("skipping test; Could not initialize jaeger: %s", err) 52 | return 53 | } 54 | defer closer.Close() 55 | opentracing.SetGlobalTracer(tracer) 56 | 57 | engine := graphql.New() 58 | engine.Root = map[string]interface{}{ 59 | "hello": "World", 60 | } 61 | 62 | err = engine.Schema.Parse(` 63 | schema { 64 | query: Query 65 | } 66 | type Query { 67 | hello: String 68 | }`) 69 | if !assert.NoError(t, err) { 70 | t.FailNow() 71 | } 72 | engine.Tracer = trace.OpenTracingTracer{} 73 | 74 | // No traces should be in the system yet.. 75 | assertTraceCount(t, queryURL, 0) 76 | 77 | gqltesting.AssertRequestString(t, engine, 78 | `{"query":"{ hello }"}`, 79 | `{"data":{"hello":"World"}}`, 80 | ) 81 | 82 | time.Sleep(1 * time.Second) 83 | assertTraceCount(t, queryURL, 1) 84 | 85 | } 86 | 87 | func assertTraceCount(t *testing.T, queryURL string, count int) { 88 | data := map[string]interface{}{} 89 | httpGetJson(t, queryURL, &data) 90 | datas := data["data"].([]interface{}) 91 | assert.Equal(t, count, len(datas)) 92 | } 93 | 94 | func httpGetJson(t *testing.T, url string, target interface{}) { 95 | httpClient := &http.Client{} 96 | req, err := http.NewRequest("GET", url, nil) 97 | require.NoError(t, err) 98 | 99 | resp, err := httpClient.Do(req) 100 | require.NoError(t, err) 101 | defer resp.Body.Close() 102 | 103 | bodyBytes, err := ioutil.ReadAll(resp.Body) 104 | require.NoError(t, err) 105 | 106 | err = json.Unmarshal(bodyBytes, target) 107 | assert.NoError(t, err) 108 | } 109 | -------------------------------------------------------------------------------- /httpgql/http-server.go: -------------------------------------------------------------------------------- 1 | package httpgql 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/chirino/graphql" 11 | ) 12 | 13 | type Handler struct { 14 | ServeGraphQL graphql.ServeGraphQLFunc 15 | ServeGraphQLStream graphql.ServeGraphQLStreamFunc 16 | MaxRequestSizeBytes int64 17 | Indent string 18 | } 19 | 20 | type OperationMessage struct { 21 | Id interface{} `json:"id,omitempty"` 22 | Type string `json:"type,omitempty"` 23 | Payload json.RawMessage `json:"payload,omitempty"` 24 | } 25 | 26 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 27 | 28 | handlerFunc := h.ServeGraphQL 29 | streamingHandlerFunc := h.ServeGraphQLStream 30 | 31 | if handlerFunc == nil && streamingHandlerFunc != nil { 32 | handlerFunc = streamingHandlerFunc.ServeGraphQL 33 | } 34 | 35 | if handlerFunc == nil { 36 | panic("either HandlerFunc or StreamingHandlerFunc must be configured") 37 | } 38 | 39 | if streamingHandlerFunc != nil { 40 | u := strings.ToLower(r.Header.Get("Upgrade")) 41 | if u == "websocket" { 42 | Upgrade(w, r, streamingHandlerFunc) 43 | return 44 | } 45 | } 46 | 47 | defer r.Body.Close() 48 | var request graphql.Request 49 | 50 | switch r.Method { 51 | case http.MethodGet: 52 | request.Query = r.URL.Query().Get("query") 53 | request.Variables = json.RawMessage(r.URL.Query().Get("variables")) 54 | request.OperationName = r.URL.Query().Get("operationName") 55 | case http.MethodPost: 56 | 57 | reader := r.Body.(io.Reader) 58 | if h.MaxRequestSizeBytes > 0 { 59 | reader = io.LimitReader(reader, h.MaxRequestSizeBytes) 60 | } 61 | 62 | if err := json.NewDecoder(reader).Decode(&request); err != nil { 63 | http.Error(w, err.Error(), http.StatusBadRequest) 64 | return 65 | } 66 | 67 | // Fallback to using query parameters 68 | if request.Query == "" { 69 | request.Query = r.URL.Query().Get("query") 70 | } 71 | if request.Variables == nil { 72 | request.Variables = json.RawMessage(r.URL.Query().Get("variables")) 73 | } 74 | if request.OperationName == "" { 75 | request.OperationName = r.URL.Query().Get("operationName") 76 | } 77 | 78 | default: 79 | w.WriteHeader(http.StatusBadRequest) 80 | return 81 | } 82 | 83 | // Attach the response and request to the context, in case a resolver wants to 84 | // work at the the http level. 85 | ctx := r.Context() 86 | ctx = context.WithValue(ctx, "net/http.ResponseWriter", w) 87 | ctx = context.WithValue(ctx, "*net/http.Request", r) 88 | 89 | request.Context = ctx 90 | response := handlerFunc(&request) 91 | 92 | w.Header().Set("Content-Type", "application/json") 93 | encoder := json.NewEncoder(w) 94 | encoder.SetIndent("", h.Indent) 95 | err := encoder.Encode(response) 96 | if err != nil { 97 | http.Error(w, err.Error(), http.StatusInternalServerError) 98 | return 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/generator/assets_vfsdata.go: -------------------------------------------------------------------------------- 1 | // Code generated by vfsgen; DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "compress/gzip" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "os" 13 | pathpkg "path" 14 | "time" 15 | ) 16 | 17 | // assets statically implements the virtual filesystem provided to vfsgen. 18 | var assets = func() http.FileSystem { 19 | fs := vfsgen۰FS{ 20 | "/": &vfsgen۰DirInfo{ 21 | name: "/", 22 | modTime: time.Time{}, 23 | }, 24 | } 25 | 26 | return fs 27 | }() 28 | 29 | type vfsgen۰FS map[string]interface{} 30 | 31 | func (fs vfsgen۰FS) Open(path string) (http.File, error) { 32 | path = pathpkg.Clean("/" + path) 33 | f, ok := fs[path] 34 | if !ok { 35 | return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist} 36 | } 37 | 38 | switch f := f.(type) { 39 | case *vfsgen۰DirInfo: 40 | return &vfsgen۰Dir{ 41 | vfsgen۰DirInfo: f, 42 | }, nil 43 | default: 44 | // This should never happen because we generate only the above types. 45 | panic(fmt.Sprintf("unexpected type %T", f)) 46 | } 47 | } 48 | 49 | // We already imported "compress/gzip" and "io/ioutil", but ended up not using them. Avoid unused import error. 50 | var _ = gzip.Reader{} 51 | var _ = ioutil.Discard 52 | 53 | // We already imported "bytes", but ended up not using it. Avoid unused import error. 54 | var _ = bytes.Reader{} 55 | 56 | // vfsgen۰DirInfo is a static definition of a directory. 57 | type vfsgen۰DirInfo struct { 58 | name string 59 | modTime time.Time 60 | entries []os.FileInfo 61 | } 62 | 63 | func (d *vfsgen۰DirInfo) Read([]byte) (int, error) { 64 | return 0, fmt.Errorf("cannot Read from directory %s", d.name) 65 | } 66 | func (d *vfsgen۰DirInfo) Close() error { return nil } 67 | func (d *vfsgen۰DirInfo) Stat() (os.FileInfo, error) { return d, nil } 68 | 69 | func (d *vfsgen۰DirInfo) Name() string { return d.name } 70 | func (d *vfsgen۰DirInfo) Size() int64 { return 0 } 71 | func (d *vfsgen۰DirInfo) Mode() os.FileMode { return 0755 | os.ModeDir } 72 | func (d *vfsgen۰DirInfo) ModTime() time.Time { return d.modTime } 73 | func (d *vfsgen۰DirInfo) IsDir() bool { return true } 74 | func (d *vfsgen۰DirInfo) Sys() interface{} { return nil } 75 | 76 | // vfsgen۰Dir is an opened dir instance. 77 | type vfsgen۰Dir struct { 78 | *vfsgen۰DirInfo 79 | pos int // Position within entries for Seek and Readdir. 80 | } 81 | 82 | func (d *vfsgen۰Dir) Seek(offset int64, whence int) (int64, error) { 83 | if offset == 0 && whence == io.SeekStart { 84 | d.pos = 0 85 | return 0, nil 86 | } 87 | return 0, fmt.Errorf("unsupported Seek in directory %s", d.name) 88 | } 89 | 90 | func (d *vfsgen۰Dir) Readdir(count int) ([]os.FileInfo, error) { 91 | if d.pos >= len(d.entries) && count > 0 { 92 | return nil, io.EOF 93 | } 94 | if count <= 0 || count > len(d.entries)-d.pos { 95 | count = len(d.entries) - d.pos 96 | } 97 | e := d.entries[d.pos : d.pos+count] 98 | d.pos += count 99 | return e, nil 100 | } 101 | -------------------------------------------------------------------------------- /resolvers/method_resolver_reflect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Most of this file was reused from the ecoding/json package, 6 | // since we want to use the same json encoding. 7 | package resolvers 8 | 9 | import ( 10 | "context" 11 | "reflect" 12 | "strings" 13 | ) 14 | 15 | var childMethodTypeCache Cache 16 | 17 | type methodInfo struct { 18 | Index int 19 | hasContext bool 20 | hasExecutionContext bool 21 | argumentsType *reflect.Type 22 | hasError bool 23 | } 24 | 25 | func getChildMethod(parent *reflect.Value, fieldName string) *methodInfo { 26 | 27 | var key struct { 28 | parentType reflect.Type 29 | fieldName string 30 | } 31 | 32 | key.parentType = parent.Type() 33 | key.fieldName = fieldName 34 | 35 | // use a cache to make subsequent lookups cheap 36 | method := childMethodTypeCache.GetOrElseUpdate(key, func() interface{} { 37 | methods := typeMethods(key.parentType) 38 | return methods[strings.Replace(strings.ToLower(fieldName), "_", "", -1)] 39 | }).(*methodInfo) 40 | 41 | return method 42 | } 43 | 44 | var contextType = reflect.TypeOf((*context.Context)(nil)).Elem() 45 | var executionContextType = reflect.TypeOf((*ExecutionContext)(nil)).Elem() 46 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 47 | 48 | func typeMethods(t reflect.Type) map[string]*methodInfo { 49 | methods := map[string]*methodInfo{} 50 | for i := 0; i < t.NumMethod(); i++ { 51 | methodInfo := methodInfo{} 52 | methodInfo.Index = i 53 | typeMethod := t.Method(i) 54 | 55 | in := make([]reflect.Type, typeMethod.Type.NumIn()) 56 | for i := range in { 57 | in[i] = typeMethod.Type.In(i) 58 | } 59 | 60 | methodHasReceiver := unwrapIfPtr(t).Kind() != reflect.Interface 61 | if methodHasReceiver { 62 | in = in[1:] // first parameter is receiver 63 | } 64 | 65 | methodInfo.hasContext = len(in) > 0 && in[0] == contextType 66 | if methodInfo.hasContext { 67 | in = in[1:] 68 | } 69 | 70 | methodInfo.hasExecutionContext = len(in) > 0 && in[0] == executionContextType 71 | if methodInfo.hasExecutionContext { 72 | in = in[1:] 73 | } 74 | 75 | if len(in) > 0 && (in[0].Kind() == reflect.Struct || (in[0].Kind() == reflect.Ptr && in[0].Elem().Kind() == reflect.Struct)) { 76 | methodInfo.argumentsType = &in[0] 77 | in = in[1:] 78 | } 79 | 80 | if len(in) > 0 { 81 | continue 82 | } 83 | 84 | if typeMethod.Type.NumOut() > 2 { 85 | continue 86 | } 87 | 88 | methodInfo.hasError = typeMethod.Type.NumOut() == 2 89 | if methodInfo.hasError { 90 | if typeMethod.Type.Out(1) != errorType { 91 | continue 92 | } 93 | } 94 | methods[strings.ToLower(typeMethod.Name)] = &methodInfo 95 | } 96 | 97 | return methods 98 | } 99 | func unwrapIfPtr(t reflect.Type) reflect.Type { 100 | if t.Kind() == reflect.Ptr { 101 | return t.Elem() 102 | } 103 | return t 104 | } 105 | -------------------------------------------------------------------------------- /trace/trace.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/chirino/graphql/internal/introspection" 8 | "github.com/chirino/graphql/qerrors" 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 TraceQueryResponse func(qerrors.ErrorList) 15 | type TraceQueryFinishFunc func() 16 | type TraceFieldFinishFunc func(*qerrors.Error) 17 | 18 | type Tracer interface { 19 | TraceQuery(ctx context.Context, queryString string, operationName string, variables interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryResponse, TraceQueryFinishFunc) 20 | TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) 21 | } 22 | 23 | type OpenTracingTracer struct{} 24 | 25 | func (OpenTracingTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryResponse, TraceQueryFinishFunc) { 26 | span, spanCtx := opentracing.StartSpanFromContext(ctx, "GraphQL request") 27 | span.SetTag("graphql.query", queryString) 28 | 29 | if operationName != "" { 30 | span.SetTag("graphql.operationName", operationName) 31 | } 32 | 33 | if variables != nil { 34 | span.LogFields(log.Object("graphql.variables", variables)) 35 | } 36 | 37 | return spanCtx, func(errs qerrors.ErrorList) { 38 | if len(errs) > 0 { 39 | msg := errs[0].Error() 40 | if len(errs) > 1 { 41 | msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1) 42 | } 43 | ext.Error.Set(span, true) 44 | span.SetTag("graphql.error", msg) 45 | } 46 | }, func() { 47 | span.Finish() 48 | } 49 | } 50 | 51 | func (OpenTracingTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { 52 | if trivial { 53 | return ctx, noop 54 | } 55 | 56 | span, spanCtx := opentracing.StartSpanFromContext(ctx, label) 57 | span.SetTag("graphql.type", typeName) 58 | span.SetTag("graphql.field", fieldName) 59 | for name, value := range args { 60 | span.SetTag("graphql.args."+name, value) 61 | } 62 | 63 | return spanCtx, func(err *qerrors.Error) { 64 | if err != nil { 65 | ext.Error.Set(span, true) 66 | span.SetTag("graphql.error", err.Error()) 67 | } 68 | span.Finish() 69 | } 70 | } 71 | 72 | func noop(*qerrors.Error) {} 73 | 74 | type NoopTracer struct{} 75 | 76 | func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryResponse, TraceQueryFinishFunc) { 77 | return ctx, func(errs qerrors.ErrorList) {}, func() {} 78 | } 79 | 80 | func (NoopTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { 81 | return ctx, func(err *qerrors.Error) {} 82 | } 83 | -------------------------------------------------------------------------------- /schema/type_add_if_missing.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | func (t *Schema) AddIfMissing(to *Schema, from *Schema) { 4 | if t != from { 5 | panic("receiver should match the from argument") 6 | } 7 | for k, v := range t.EntryPoints { 8 | if to.EntryPoints[k] == nil { 9 | to.EntryPoints[k] = v 10 | } 11 | } 12 | for k, v := range t.DeclaredDirectives { 13 | if to.DeclaredDirectives[k] == nil { 14 | to.DeclaredDirectives[k] = v 15 | } 16 | } 17 | for k, v := range t.Types { 18 | if to.Types[k] == nil { 19 | to.Types[k] = v 20 | } 21 | } 22 | } 23 | 24 | func (t *InputObject) AddIfMissing(to *Schema, from *Schema) { 25 | if to.Types[t.Name] == nil { 26 | to.Types[t.Name] = t 27 | t.Directives.AddIfMissing(to, from) 28 | t.Fields.AddIfMissing(to, from) 29 | } 30 | } 31 | 32 | func (t DirectiveList) AddIfMissing(to *Schema, from *Schema) { 33 | for _, d := range t { 34 | k := d.Name 35 | v := from.DeclaredDirectives[k] 36 | if to.DeclaredDirectives[k] == nil { 37 | to.DeclaredDirectives[k] = v 38 | } 39 | } 40 | } 41 | 42 | func (t *Object) AddIfMissing(to *Schema, from *Schema) { 43 | if to.Types[t.Name] == nil { 44 | to.Types[t.Name] = t 45 | t.Directives.AddIfMissing(to, from) 46 | t.Fields.AddIfMissing(to, from) 47 | } 48 | } 49 | 50 | func (t FieldList) AddIfMissing(to *Schema, from *Schema) { 51 | for _, t := range t { 52 | t.AddIfMissing(to, from) 53 | } 54 | } 55 | 56 | func (t *Field) AddIfMissing(to *Schema, from *Schema) { 57 | t.Directives.AddIfMissing(to, from) 58 | t.Type.AddIfMissing(to, from) 59 | t.Args.AddIfMissing(to, from) 60 | } 61 | 62 | func (t InputValueList) AddIfMissing(to *Schema, from *Schema) { 63 | for _, t := range t { 64 | t.AddIfMissing(to, from) 65 | } 66 | } 67 | 68 | func (t *InputValue) AddIfMissing(to *Schema, from *Schema) { 69 | t.Type.AddIfMissing(to, from) 70 | t.Directives.AddIfMissing(to, from) 71 | } 72 | 73 | func (t *List) AddIfMissing(to *Schema, from *Schema) { 74 | t.OfType.AddIfMissing(to, from) 75 | } 76 | func (t *NonNull) AddIfMissing(to *Schema, from *Schema) { 77 | t.OfType.AddIfMissing(to, from) 78 | } 79 | func (t *TypeName) AddIfMissing(to *Schema, from *Schema) { 80 | } 81 | func (t *Scalar) AddIfMissing(to *Schema, from *Schema) { 82 | if to.Types[t.Name] == nil { 83 | to.Types[t.Name] = t 84 | } 85 | } 86 | func (t *Interface) AddIfMissing(to *Schema, from *Schema) { 87 | if to.Types[t.Name] == nil { 88 | to.Types[t.Name] = t 89 | t.Directives.AddIfMissing(to, from) 90 | t.Fields.AddIfMissing(to, from) 91 | } 92 | } 93 | func (t *Union) AddIfMissing(to *Schema, from *Schema) { 94 | if to.Types[t.Name] == nil { 95 | to.Types[t.Name] = t 96 | t.Directives.AddIfMissing(to, from) 97 | for _, t := range t.PossibleTypes { 98 | t.AddIfMissing(to, from) 99 | } 100 | } 101 | } 102 | func (t *Enum) AddIfMissing(to *Schema, from *Schema) { 103 | if to.Types[t.Name] == nil { 104 | to.Types[t.Name] = t 105 | t.Directives.AddIfMissing(to, from) 106 | for _, t := range t.Values { 107 | t.AddIfMissing(to, from) 108 | } 109 | } 110 | } 111 | func (t *EnumValue) AddIfMissing(to *Schema, from *Schema) { 112 | t.Directives.AddIfMissing(to, from) 113 | } 114 | -------------------------------------------------------------------------------- /httpgql/ws-server.go: -------------------------------------------------------------------------------- 1 | package httpgql 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "sync" 9 | 10 | "github.com/chirino/graphql" 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | type wsStream struct { 15 | cancel context.CancelFunc 16 | responses graphql.ResponseStream 17 | } 18 | 19 | func Upgrade(w http.ResponseWriter, r *http.Request, streamingHandlerFunc graphql.ServeGraphQLStreamFunc) { 20 | 21 | var upgrader = websocket.Upgrader{ 22 | ReadBufferSize: 1024, 23 | WriteBufferSize: 1024, 24 | } 25 | header := http.Header{} 26 | 27 | subprotocol := r.Header.Get("Sec-WebSocket-Protocol") 28 | switch subprotocol { 29 | case "graphql-ws": 30 | fallthrough 31 | case "graphql-subscriptions": 32 | header.Set("Sec-WebSocket-Protocol", subprotocol) 33 | } 34 | 35 | mu := sync.Mutex{} 36 | streams := map[interface{}]wsStream{} 37 | conn, err := upgrader.Upgrade(w, r, header) // error ignored for sake of simplicity 38 | if err != nil { 39 | http.Error(w, err.Error(), http.StatusBadRequest) 40 | return 41 | } 42 | 43 | defer func() { 44 | mu.Lock() 45 | for _, stream := range streams { 46 | stream.cancel() 47 | } 48 | mu.Unlock() 49 | conn.Close() 50 | }() 51 | 52 | // websocket connections do not support concurrent write access.. protect with a mutex. 53 | writeJSON := func(json interface{}) error { 54 | mu.Lock() 55 | err := conn.WriteJSON(json) 56 | mu.Unlock() 57 | return err 58 | } 59 | 60 | op := OperationMessage{} 61 | err = conn.ReadJSON(&op) 62 | if err != nil { 63 | return 64 | } 65 | if op.Type != "connection_init" { 66 | r := graphql.NewResponse().AddError(fmt.Errorf("protocol violation: expected an init message, but received: %v", op.Type)) 67 | payload, _ := json.Marshal(r) 68 | writeJSON(OperationMessage{Type: "connection_error", Payload: payload}) 69 | return 70 | } 71 | 72 | writeJSON(OperationMessage{Type: "connection_ack"}) 73 | for { 74 | 75 | msg := OperationMessage{} 76 | err := conn.ReadJSON(&msg) 77 | if err != nil { 78 | return 79 | } 80 | 81 | switch msg.Type { 82 | case "start": 83 | 84 | var request graphql.Request 85 | err := json.Unmarshal(msg.Payload, &request) 86 | if err != nil { 87 | return 88 | } 89 | 90 | ctx := r.Context() 91 | ctx = context.WithValue(ctx, "net/http.ResponseWriter", w) 92 | ctx = context.WithValue(ctx, "*net/http.Request", r) 93 | 94 | stream := wsStream{} 95 | request.Context, stream.cancel = context.WithCancel(ctx) 96 | stream.responses = streamingHandlerFunc(&request) 97 | 98 | // save it.. so that client can later cancel it... 99 | mu.Lock() 100 | streams[msg.Id] = stream 101 | mu.Unlock() 102 | 103 | // Start a goroutine ot handle the events.... 104 | go func() { 105 | for { 106 | r := <-stream.responses 107 | if r != nil { 108 | payload, err := json.Marshal(r) 109 | if err != nil { 110 | panic(fmt.Sprintf("could not marshal payload: %v\n", err)) 111 | } 112 | writeJSON(OperationMessage{Type: "data", Id: msg.Id, Payload: payload}) 113 | } else { 114 | 115 | mu.Lock() 116 | delete(streams, msg.Id) 117 | mu.Unlock() 118 | 119 | writeJSON(OperationMessage{Type: "complete", Id: msg.Id}) 120 | stream.cancel() 121 | return 122 | } 123 | } 124 | }() 125 | 126 | case "stop": 127 | mu.Lock() 128 | stream, ok := streams[msg.Id] 129 | mu.Unlock() 130 | if ok { 131 | stream.cancel() 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /graphiql/graphiql.go: -------------------------------------------------------------------------------- 1 | package graphiql 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | type Handler struct { 12 | url *url.URL 13 | ws bool 14 | template *template.Template 15 | } 16 | 17 | func New(urlPath string, ws bool) *Handler { 18 | html := ` 19 | 20 | 21 | GraphiQL 22 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
Loading...
40 | 58 | 59 | 60 | ` 61 | u, err := url.Parse(urlPath) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | t, err := template.New("index.html").Parse(html) 67 | if err != nil { 68 | panic(err) 69 | } 70 | return &Handler{ 71 | url: u, 72 | ws: ws, 73 | template: t, 74 | } 75 | } 76 | 77 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 78 | u := *h.url 79 | 80 | scheme, host, _ := originalSchemeHostClient(r) 81 | if u.Scheme == "" { 82 | u.Scheme = scheme 83 | } 84 | if u.Host == "" { 85 | u.Host = host 86 | } 87 | 88 | if h.ws { 89 | switch u.Scheme { 90 | case "http": 91 | u.Scheme = "ws" 92 | case "https": 93 | u.Scheme = "wss" 94 | } 95 | } 96 | 97 | w.Header().Set("Content-Type", "text/html") 98 | buf := &bytes.Buffer{} 99 | err := h.template.Execute(buf, struct { 100 | WebSocket bool 101 | URL string 102 | }{ 103 | WebSocket: h.ws, 104 | URL: u.String(), 105 | }) 106 | if err != nil { 107 | panic(err) 108 | } 109 | w.Write(buf.Bytes()) 110 | } 111 | 112 | func originalSchemeHostClient(r *http.Request) (scheme string, host string, client string) { 113 | 114 | h := r.Header.Get("Forwarded") 115 | if h != "" { 116 | for _, kv := range strings.Split(h, ";") { 117 | if pair := strings.SplitN(kv, "=", 2); len(pair) == 2 { 118 | switch strings.ToLower(pair[0]) { 119 | case "for": 120 | client = pair[1] 121 | case "host": 122 | host = pair[1] 123 | case "proto": 124 | scheme = pair[1] 125 | } 126 | } 127 | } 128 | } 129 | 130 | if scheme == "" { 131 | scheme = r.Header.Get("X-Forwarded-Proto") 132 | } 133 | if host == "" { 134 | host = r.Header.Get("X-Forwarded-Host") 135 | } 136 | if client == "" { 137 | client = r.Header.Get("X-Forwarded-For") 138 | } 139 | 140 | if scheme == "" { 141 | if r.TLS != nil { 142 | scheme = "https" 143 | } else { 144 | scheme = "http" 145 | } 146 | } 147 | if host == "" { 148 | host = r.Host 149 | } 150 | if client == "" { 151 | client = r.RemoteAddr 152 | } 153 | return 154 | } 155 | -------------------------------------------------------------------------------- /exec/field_selection.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "reflect" 5 | 6 | qerrors2 "github.com/chirino/graphql/qerrors" 7 | "github.com/chirino/graphql/schema" 8 | ) 9 | 10 | type FieldSelectionContext struct { 11 | Schema *schema.Schema 12 | QueryDocument *schema.QueryDocument 13 | // Used to provide path context in errors 14 | Path []string 15 | // The current type that is being selected against 16 | OnType schema.Type 17 | // CanCast is optional. Used to evaluate if the current OnType can be casted. 18 | CanCast func(castTo schema.Type) bool 19 | // Vars are used to evaluate @skip directives 20 | Vars map[string]interface{} 21 | } 22 | 23 | type FieldSelection struct { 24 | OnType schema.Type 25 | Selection *schema.FieldSelection 26 | Field *schema.Field 27 | } 28 | 29 | func (c FieldSelectionContext) Apply(selections []schema.Selection) (result []FieldSelection, errs qerrors2.ErrorList) { 30 | c.OnType = schema.DeepestType(c.OnType) 31 | selectedFieldAliases := map[string]bool{} 32 | for _, selection := range selections { 33 | switch selection := selection.(type) { 34 | case *schema.FieldSelection: 35 | skip, err := SkipByDirective(selection.Directives, c.Vars) 36 | if err != nil { 37 | errs = append(errs, err) 38 | } 39 | if skip { 40 | continue 41 | } 42 | 43 | fields := schema.FieldList{} 44 | switch o := c.OnType.(type) { 45 | case *schema.Object: 46 | fields = o.Fields 47 | case *schema.Interface: 48 | fields = o.Fields 49 | default: 50 | panic("unexpected value type: " + reflect.TypeOf(c.OnType).String()) 51 | } 52 | 53 | field := fields.Get(selection.Name) 54 | if field == nil { 55 | errs = append(errs, qerrors2.Errorf("field '%s' not found on '%s': ", selection.Name, c.OnType.String())) 56 | } else { 57 | selection.Schema = &schema.FieldSchema{ 58 | Field: field, 59 | Parent: c.OnType.(schema.NamedType), 60 | } 61 | if !selectedFieldAliases[selection.Alias] { 62 | result = append(result, FieldSelection{ 63 | OnType: c.OnType, 64 | Selection: selection, 65 | Field: field, 66 | }) 67 | } 68 | } 69 | 70 | case *schema.InlineFragment: 71 | skip, err := SkipByDirective(selection.Directives, c.Vars) 72 | if err != nil { 73 | errs = append(errs, err) 74 | } 75 | if skip { 76 | continue 77 | } 78 | 79 | rs, es := c.applyFragment(selection.Fragment) 80 | for _, r := range rs { 81 | if !selectedFieldAliases[r.Selection.Alias] { 82 | result = append(result, r) 83 | } 84 | } 85 | errs = append(errs, es...) 86 | 87 | case *schema.FragmentSpread: 88 | skip, err := SkipByDirective(selection.Directives, c.Vars) 89 | if err != nil { 90 | errs = append(errs, err) 91 | } 92 | if skip { 93 | continue 94 | } 95 | 96 | rs, es := c.applyFragment(c.QueryDocument.Fragments.Get(selection.Name).Fragment) 97 | for _, r := range rs { 98 | if !selectedFieldAliases[r.Selection.Alias] { 99 | result = append(result, r) 100 | } 101 | } 102 | errs = append(errs, es...) 103 | 104 | default: 105 | panic("unexpected selection type: " + reflect.TypeOf(selection).String()) 106 | } 107 | } 108 | return 109 | } 110 | 111 | func (c FieldSelectionContext) applyFragment(fragment schema.Fragment) ([]FieldSelection, qerrors2.ErrorList) { 112 | if fragment.On.Name != "" && fragment.On.Name != c.OnType.String() { 113 | 114 | castType := c.Schema.Types[fragment.On.Name] 115 | if c.CanCast == nil || !c.CanCast(castType) { 116 | return []FieldSelection{}, qerrors2.ErrorList{} 117 | } 118 | 119 | castedContext := c 120 | castedContext.OnType = castType 121 | return castedContext.Apply(fragment.Selections) 122 | } else { 123 | return c.Apply(fragment.Selections) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package graphql_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chirino/graphql" 7 | "github.com/chirino/graphql/internal/example/starwars" 8 | "github.com/chirino/graphql/schema" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var query = ` 13 | query HeroNameAndFriends($episode: Episode, $withoutFriends: Boolean!, $withFriends: Boolean!) { 14 | hero { 15 | id 16 | name 17 | friends { 18 | name 19 | } 20 | } 21 | empireHerhero: hero(episode: EMPIRE) { 22 | name 23 | } 24 | jediHero: hero(episode: JEDI) { 25 | name 26 | } 27 | human(id: "1000") { 28 | name 29 | height(unit: FOOT) 30 | } 31 | leftComparison: hero(episode: EMPIRE) { 32 | ...comparisonFields 33 | ...height 34 | } 35 | rightComparison: hero(episode: JEDI) { 36 | ...comparisonFields 37 | ...height 38 | } 39 | heroNameAndFriends: hero(episode: $episode) { 40 | name 41 | } 42 | heroSkip: hero(episode: $episode) { 43 | name 44 | friends @skip(if: $withoutFriends) { 45 | name 46 | } 47 | } 48 | 49 | heroInclude: hero(episode: $episode) { 50 | name 51 | ...friendsFragment @include(if: $withFriends) 52 | } 53 | inlineFragments: hero(episode: $episode) { 54 | name 55 | ... on Droid { 56 | primaryFunction 57 | } 58 | ... on Human { 59 | height 60 | } 61 | } 62 | search(text: "an") { 63 | __typename 64 | ... on Human { 65 | name 66 | } 67 | ... on Droid { 68 | name 69 | } 70 | ... on Starship { 71 | name 72 | } 73 | } 74 | heroConnections: hero { 75 | name 76 | friendsConnection { 77 | totalCount 78 | pageInfo { 79 | startCursor 80 | endCursor 81 | hasNextPage 82 | } 83 | edges { 84 | cursor 85 | node { 86 | name 87 | } 88 | } 89 | } 90 | } 91 | reviews(episode: JEDI) { 92 | stars 93 | commentary 94 | } 95 | __schema { 96 | types { 97 | name 98 | } 99 | } 100 | __type(name: "Droid") { 101 | name 102 | fields { 103 | name 104 | args { 105 | name 106 | type { 107 | name 108 | } 109 | defaultValue 110 | } 111 | type { 112 | name 113 | kind 114 | } 115 | } 116 | } 117 | } 118 | 119 | fragment comparisonFields on Character { 120 | name 121 | appearsIn 122 | friends { 123 | name 124 | } 125 | } 126 | fragment height on Human { 127 | height 128 | } 129 | fragment friendsFragment on Character { 130 | friends { 131 | name 132 | } 133 | } 134 | ` 135 | 136 | func BenchmarkParallelExecuteStarwarsQuery(b *testing.B) { 137 | engine := graphql.New() 138 | engine.Root = &starwars.Resolver{} 139 | err := engine.Schema.Parse(starwars.Schema) 140 | require.NoError(b, err) 141 | 142 | // Lets build a query that throws the kitchen skink at the query engine. 143 | // (we grab a little bit of all the tests we have so far) 144 | request := &graphql.Request{ 145 | OperationName: "", 146 | Query: query, 147 | Variables: map[string]interface{}{ 148 | "episode": "JEDI", 149 | "withoutFriends": true, 150 | "withFriends": false, 151 | "review": map[string]interface{}{ 152 | "stars": 5, 153 | "commentary": "This is a great movie!", 154 | }, 155 | }, 156 | } 157 | 158 | b.ReportAllocs() 159 | b.ResetTimer() 160 | b.RunParallel(func(pb *testing.PB) { 161 | for pb.Next() { 162 | engine.ServeGraphQL(request) 163 | } 164 | }) 165 | 166 | } 167 | func BenchmarkParallelParseStarwarsQuery(b *testing.B) { 168 | b.ReportAllocs() 169 | b.ResetTimer() 170 | b.RunParallel(func(pb *testing.PB) { 171 | for pb.Next() { 172 | document := schema.QueryDocument{} 173 | err := document.Parse(query) 174 | if err != nil { 175 | panic(err) 176 | } 177 | document.Close() 178 | } 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /internal/validation/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 | // TODO: Fix test failures. 78 | // require('./src/validation/__tests__/ExecutableDefinitions-test'); 79 | require('./src/validation/__tests__/FieldsOnCorrectType-test'); 80 | require('./src/validation/__tests__/FragmentsOnCompositeTypes-test'); 81 | // TODO: Fix test failures. 82 | // require('./src/validation/__tests__/KnownArgumentNames-test'); 83 | require('./src/validation/__tests__/KnownDirectives-test'); 84 | require('./src/validation/__tests__/KnownFragmentNames-test'); 85 | require('./src/validation/__tests__/KnownTypeNames-test'); 86 | require('./src/validation/__tests__/LoneAnonymousOperation-test'); 87 | require('./src/validation/__tests__/NoFragmentCycles-test'); 88 | require('./src/validation/__tests__/NoUndefinedVariables-test'); 89 | require('./src/validation/__tests__/NoUnusedFragments-test'); 90 | require('./src/validation/__tests__/NoUnusedVariables-test'); 91 | require('./src/validation/__tests__/OverlappingFieldsCanBeMerged-test'); 92 | // TODO: Fix test failures. 93 | // require('./src/validation/__tests__/PossibleFragmentSpreads-test'); 94 | require('./src/validation/__tests__/ProvidedNonNullArguments-test'); 95 | require('./src/validation/__tests__/ScalarLeafs-test'); 96 | // TODO: Add support for subscriptions. 97 | // require('./src/validation/__tests__/SingleFieldSubscriptions-test.js'); 98 | require('./src/validation/__tests__/UniqueArgumentNames-test'); 99 | require('./src/validation/__tests__/UniqueDirectivesPerLocation-test'); 100 | require('./src/validation/__tests__/UniqueFragmentNames-test'); 101 | require('./src/validation/__tests__/UniqueInputFieldNames-test'); 102 | require('./src/validation/__tests__/UniqueOperationNames-test'); 103 | require('./src/validation/__tests__/UniqueVariableNames-test'); 104 | // TODO: Fix test failures. 105 | // require('./src/validation/__tests__/ValuesofCorrectType-test'); 106 | require('./src/validation/__tests__/VariablesAreInputTypes-test'); 107 | // TODO: Fix test failures. 108 | // require('./src/validation/__tests__/VariablesDefaultValueAllowed-test'); 109 | require('./src/validation/__tests__/VariablesInAllowedPosition-test'); 110 | 111 | let output = JSON.stringify({ 112 | schemas: schemas.map(s => printSchema(s)), 113 | tests: tests, 114 | }, null, 2) 115 | output = output.replace(' Did you mean to use an inline fragment on \\"Dog\\" or \\"Cat\\"?', ''); 116 | output = output.replace(' Did you mean to use an inline fragment on \\"Being\\", \\"Pet\\", \\"Canine\\", \\"Dog\\", or \\"Cat\\"?', ''); 117 | output = output.replace(' Did you mean \\"Pet\\"?', ''); 118 | fs.writeFileSync("tests.json", output); 119 | -------------------------------------------------------------------------------- /schema/schema_internal_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/chirino/graphql/internal/lexer" 5 | "github.com/stretchr/testify/assert" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestParseInterfaceDef(t *testing.T) { 11 | type testCase struct { 12 | description string 13 | definition string 14 | expected *Interface 15 | err error 16 | } 17 | 18 | tests := []testCase{{ 19 | description: "Parses simple interface", 20 | definition: "Greeting { field: String }", 21 | expected: &Interface{Name: "Greeting", Fields: []*Field{&Field{Name: "field"}}}, 22 | }} 23 | 24 | for _, test := range tests { 25 | t.Run(test.description, func(t *testing.T) { 26 | var actual *Interface 27 | lex := setup(t, test.definition) 28 | 29 | parse := func() { actual = parseInterfaceDef(lex) } 30 | err := lex.CatchSyntaxError(parse) 31 | 32 | compareErrors(t, test.err, err) 33 | compareInterfaces(t, test.expected, actual) 34 | }) 35 | } 36 | } 37 | 38 | // TestParseObjectDef tests the logic for parsing object types from the schema definition as 39 | // written in `parseObjectDef()`. 40 | func TestParseObjectDef(t *testing.T) { 41 | type testCase struct { 42 | description string 43 | definition string 44 | expected *Object 45 | err error 46 | } 47 | 48 | tests := []testCase{{ 49 | description: "Parses type inheriting single interface", 50 | definition: "Hello implements World { field: String }", 51 | expected: &Object{Name: "Hello", InterfaceNames: []string{"World"}}, 52 | }, { 53 | description: "Parses type inheriting multiple interfaces", 54 | definition: "Hello implements Wo & rld { field: String }", 55 | expected: &Object{Name: "Hello", InterfaceNames: []string{"Wo", "rld"}}, 56 | }, { 57 | description: "Parses type inheriting multiple interfaces with leading ampersand", 58 | definition: "Hello implements & Wo & rld { field: String }", 59 | expected: &Object{Name: "Hello", InterfaceNames: []string{"Wo", "rld"}}, 60 | }, { 61 | description: "Allows legacy SDL interfaces", 62 | definition: "Hello implements Wo, rld { field: String }", 63 | expected: &Object{Name: "Hello", InterfaceNames: []string{"Wo", "rld"}}, 64 | }} 65 | 66 | for _, test := range tests { 67 | t.Run(test.description, func(t *testing.T) { 68 | var actual *Object 69 | lex := setup(t, test.definition) 70 | 71 | parse := func() { actual = parseObjectDef(lex) } 72 | err := lex.CatchSyntaxError(parse) 73 | 74 | compareErrors(t, test.err, err) 75 | compareObjects(t, test.expected, actual) 76 | }) 77 | } 78 | } 79 | 80 | func compareErrors(t *testing.T, expected, actual error) { 81 | t.Helper() 82 | assert.Equal(t, expected, actual) 83 | } 84 | 85 | func compareInterfaces(t *testing.T, expected, actual *Interface) { 86 | t.Helper() 87 | 88 | // TODO: We can probably extract this switch statement into its own function. 89 | switch { 90 | case expected == nil && actual == nil: 91 | return 92 | case expected == nil && actual != nil: 93 | t.Fatalf("wanted nil, got an unexpected result: %#v", actual) 94 | case expected != nil && actual == nil: 95 | t.Fatalf("wanted non-nil result, got nil") 96 | } 97 | 98 | if expected.Name != actual.Name { 99 | t.Errorf("wrong interface name: want %q, got %q", expected.Name, actual.Name) 100 | } 101 | 102 | if len(expected.Fields) != len(actual.Fields) { 103 | t.Fatalf("wanted %d field definitions, got %d", len(expected.Fields), len(actual.Fields)) 104 | } 105 | 106 | for i, f := range expected.Fields { 107 | if f.Name != actual.Fields[i].Name { 108 | t.Errorf("fields[%d]: wrong field name: want %q, got %q", i, f.Name, actual.Fields[i].Name) 109 | } 110 | } 111 | } 112 | 113 | func compareObjects(t *testing.T, expected, actual *Object) { 114 | t.Helper() 115 | 116 | switch { 117 | case expected == nil && expected == actual: 118 | return 119 | case expected == nil && actual != nil: 120 | t.Fatalf("wanted nil, got an unexpected result: %#v", actual) 121 | case expected != nil && actual == nil: 122 | t.Fatalf("wanted non-nil result, got nil") 123 | } 124 | 125 | if expected.Name != actual.Name { 126 | t.Errorf("wrong object name: want %q, got %q", expected.Name, actual.Name) 127 | } 128 | 129 | if len(expected.InterfaceNames) != len(actual.InterfaceNames) { 130 | t.Fatalf( 131 | "wrong number of interface names: want %s, got %s", 132 | expected.InterfaceNames, 133 | actual.InterfaceNames, 134 | ) 135 | } 136 | 137 | for i, expectedName := range expected.InterfaceNames { 138 | actualName := actual.InterfaceNames[i] 139 | if expectedName != actualName { 140 | t.Errorf("wrong interface name: want %q, got %q", expectedName, actualName) 141 | } 142 | } 143 | } 144 | 145 | func setup(t *testing.T, def string) *lexer.Lexer { 146 | t.Helper() 147 | 148 | lex := lexer.Get(def) 149 | lex.Consume() 150 | 151 | return lex 152 | } 153 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "reflect" 7 | 8 | "github.com/chirino/graphql/internal/exec" 9 | "github.com/chirino/graphql/internal/introspection" 10 | "github.com/chirino/graphql/internal/validation" 11 | "github.com/chirino/graphql/log" 12 | "github.com/chirino/graphql/qerrors" 13 | "github.com/chirino/graphql/resolvers" 14 | "github.com/chirino/graphql/schema" 15 | "github.com/chirino/graphql/trace" 16 | ) 17 | 18 | type Engine struct { 19 | Schema *schema.Schema 20 | MaxDepth int 21 | MaxParallelism int 22 | Tracer trace.Tracer 23 | Logger log.Logger 24 | Resolver resolvers.Resolver 25 | Root interface{} 26 | // Validate can be set to nil to disable validation. 27 | Validate func(doc *schema.QueryDocument, maxDepth int) error 28 | // OnRequest is called after the query is parsed but before the request is validated. 29 | OnRequestHook func(request *Request, doc *schema.QueryDocument, op *schema.Operation) error 30 | TryCast func(value reflect.Value, toType string) (v reflect.Value, ok bool) 31 | } 32 | 33 | func CreateEngine(schema string) (*Engine, error) { 34 | engine := New() 35 | err := engine.Schema.Parse(schema) 36 | return engine, err 37 | } 38 | 39 | func New() *Engine { 40 | e := &Engine{ 41 | Schema: schema.New(), 42 | Tracer: trace.NoopTracer{}, 43 | MaxParallelism: 10, 44 | MaxDepth: 50, 45 | Logger: &log.DefaultLogger{}, 46 | Resolver: resolvers.DynamicResolverFactory(), 47 | TryCast: resolvers.TryCastFunction, 48 | } 49 | e.Validate = e.validate 50 | return e 51 | } 52 | 53 | func (engine *Engine) GetSchemaIntrospectionJSON() ([]byte, error) { 54 | return GetSchemaIntrospectionJSON(engine.ServeGraphQL) 55 | } 56 | 57 | func (engine *Engine) Exec(ctx context.Context, result interface{}, query string, args ...interface{}) error { 58 | return Exec(engine.ServeGraphQL, ctx, result, query, args...) 59 | } 60 | 61 | func (engine *Engine) ServeGraphQL(request *Request) *Response { 62 | return ServeGraphQLStreamFunc(engine.ServeGraphQLStream).ServeGraphQL(request) 63 | } 64 | 65 | type responseStream struct { 66 | cancel context.CancelFunc 67 | responses chan *Response 68 | } 69 | 70 | func (r responseStream) Close() { 71 | r.cancel() 72 | } 73 | 74 | func (r responseStream) Responses() <-chan *Response { 75 | return r.responses 76 | } 77 | 78 | func (r responseStream) CloseWithErr(err error) responseStream { 79 | r.responses <- NewResponse().AddError(err) 80 | close(r.responses) 81 | return r 82 | } 83 | 84 | func (engine *Engine) ServeGraphQLStream(request *Request) ResponseStream { 85 | 86 | doc := &schema.QueryDocument{} 87 | err := doc.Parse(request.Query) 88 | if err != nil { 89 | return NewErrStream(err) 90 | } 91 | 92 | op, err := doc.GetOperation(request.OperationName) 93 | if err != nil { 94 | return NewErrStream(err) 95 | } 96 | 97 | if engine.OnRequestHook != nil { 98 | err := engine.OnRequestHook(request, doc, op) 99 | if err != nil { 100 | return NewErrStream(err) 101 | } 102 | } 103 | 104 | if engine.Validate != nil { 105 | err = engine.Validate(doc, engine.MaxDepth) 106 | if err != nil { 107 | return NewErrStream(err) 108 | } 109 | } 110 | 111 | varTypes := make(map[string]*introspection.Type) 112 | for _, v := range op.Vars { 113 | t, err := schema.ResolveType(v.Type, engine.Schema.Resolve) 114 | if err != nil { 115 | return NewErrStream(err) 116 | } 117 | varTypes[v.Name] = introspection.WrapType(t) 118 | } 119 | 120 | ctx := request.GetContext() 121 | traceContext, traceResponse, traceFinish := engine.Tracer.TraceQuery(ctx, request.Query, request.OperationName, request.Variables, varTypes) 122 | 123 | variables, err := request.VariablesAsMap() 124 | if err != nil { 125 | return NewErrStream(err) 126 | } 127 | 128 | responses := make(chan *Response, 1) 129 | r := exec.Execution{ 130 | Context: traceContext, 131 | Query: request.Query, 132 | Vars: variables, 133 | Schema: engine.Schema, 134 | Tracer: engine.Tracer, 135 | Logger: engine.Logger, 136 | Resolver: engine.Resolver, 137 | Doc: doc, 138 | Operation: op, 139 | VarTypes: varTypes, 140 | MaxParallelism: engine.MaxParallelism, 141 | Root: engine.Root, 142 | TryCast: engine.TryCast, 143 | FireSubscriptionEventFunc: func(d json.RawMessage, e qerrors.ErrorList) { 144 | responses <- &Response{ 145 | Data: d, 146 | Errors: e, 147 | } 148 | traceResponse(e) 149 | }, 150 | FireSubscriptionCloseFunc: func() { 151 | close(responses) 152 | doc.Close() 153 | traceFinish() 154 | }, 155 | } 156 | 157 | err = r.Execute() 158 | if err != nil { 159 | return NewErrStream(err) 160 | } 161 | return responses 162 | } 163 | 164 | func (engine *Engine) validate(doc *schema.QueryDocument, maxDepth int) error { 165 | errs := validation.Validate(engine.Schema, doc, maxDepth) 166 | if len(errs) != 0 { 167 | return errs.Error() 168 | } 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= 2 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/friendsofgo/graphiql v0.2.2 h1:ccnuxpjgIkB+Lr9YB2ZouiZm7wvciSfqwpa9ugWzmn0= 7 | github.com/friendsofgo/graphiql v0.2.2/go.mod h1:8Y2kZ36AoTGWs78+VRpvATyt3LJBx0SZXmay80ZTRWo= 8 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 9 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 10 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 11 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 12 | github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= 13 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 14 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 15 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 16 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/segmentio/ksuid v1.0.2 h1:9yBfKyw4ECGTdALaF09Snw3sLJmYIX6AbPJrAy6MrDc= 21 | github.com/segmentio/ksuid v1.0.2/go.mod h1:BXuJDr2byAiHuQaQtSKoXh1J0YmUDurywOXgB2w+OSU= 22 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= 23 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 24 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0= 25 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= 26 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 29 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 30 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 31 | github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= 32 | github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= 33 | github.com/uber/jaeger-client-go v2.14.1-0.20180928181052-40fb3b2c4120+incompatible h1:Dw0AFQs6RGO8RxMPGP2LknN/VtHolVH82P9PP0Ni+9w= 34 | github.com/uber/jaeger-client-go v2.14.1-0.20180928181052-40fb3b2c4120+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 35 | github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo= 36 | github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 37 | go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= 38 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 39 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 40 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 41 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 42 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 43 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 44 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 45 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 49 | golang.org/x/tools v0.0.0-20200128220307-520188d60f50 h1:0qnG0gwzB6QPiLDow10WJDdB38c+hQ7ArxO26Qc1boM= 50 | golang.org/x/tools v0.0.0-20200128220307-520188d60f50/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 51 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 55 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 56 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 57 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 58 | -------------------------------------------------------------------------------- /schema/deep_copy.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | func (from *QueryDocument) DeepCopy() *QueryDocument { 4 | if from == nil { 5 | return nil 6 | } 7 | return &QueryDocument{ 8 | Operations: from.Operations.DeepCopy(), 9 | Fragments: from.Fragments.DeepCopy(), 10 | } 11 | } 12 | 13 | func (from *Operation) DeepCopy() *Operation { 14 | if from == nil { 15 | return nil 16 | } 17 | to := *from 18 | to.Vars = from.Vars.DeepCopy() 19 | to.Selections = from.Selections.DeepCopy() 20 | to.Directives = from.Directives.DeepCopy() 21 | return &to 22 | } 23 | 24 | func (from *Directive) DeepCopy() *Directive { 25 | if from == nil { 26 | return nil 27 | } 28 | to := *from 29 | to.Args = from.Args.DeepCopy() 30 | return &to 31 | } 32 | 33 | func (from Argument) DeepCopy() Argument { 34 | to := from 35 | to.Value = DeepCopyLiteral(from.Value) 36 | return to 37 | } 38 | 39 | func DeepCopyLiteral(from Literal) Literal { 40 | if from == nil { 41 | return nil 42 | } 43 | switch from := from.(type) { 44 | case *BasicLit: 45 | return from.DeepCopy() 46 | case *ListLit: 47 | return from.DeepCopy() 48 | case *NullLit: 49 | return from.DeepCopy() 50 | case *ObjectLit: 51 | return from.DeepCopy() 52 | case *Variable: 53 | return from.DeepCopy() 54 | default: 55 | panic("unreachable") 56 | } 57 | } 58 | 59 | func (from *BasicLit) DeepCopy() Literal { 60 | if from == nil { 61 | return nil 62 | } 63 | to := *from 64 | return &to 65 | } 66 | func (from *ListLit) DeepCopy() Literal { 67 | if from == nil { 68 | return nil 69 | } 70 | to := *from 71 | to.Entries = make([]Literal, len(from.Entries)) 72 | for i, v := range from.Entries { 73 | to.Entries[i] = DeepCopyLiteral(v) 74 | } 75 | return &to 76 | } 77 | func (from *NullLit) DeepCopy() Literal { 78 | if from == nil { 79 | return nil 80 | } 81 | to := *from 82 | return &to 83 | } 84 | func (from *ObjectLit) DeepCopy() Literal { 85 | if from == nil { 86 | return nil 87 | } 88 | to := *from 89 | to.Fields = make([]*ObjectLitField, len(from.Fields)) 90 | for i, v := range from.Fields { 91 | to.Fields[i] = v.DeepCopy() 92 | } 93 | return &to 94 | } 95 | 96 | func (from *ObjectLitField) DeepCopy() *ObjectLitField { 97 | if from == nil { 98 | return nil 99 | } 100 | to := *from 101 | to.Value = DeepCopyLiteral(from.Value) 102 | return &to 103 | } 104 | 105 | func (from *Variable) DeepCopy() Literal { 106 | if from == nil { 107 | return nil 108 | } 109 | to := *from 110 | return &to 111 | } 112 | 113 | func (from *FieldSelection) DeepCopy() Selection { 114 | if from == nil { 115 | return nil 116 | } 117 | to := *from 118 | to.Arguments = from.Arguments.DeepCopy() 119 | to.Directives = from.Directives.DeepCopy() 120 | to.Selections = from.Selections.DeepCopy() 121 | return &to 122 | } 123 | 124 | func (from *FragmentSpread) DeepCopy() Selection { 125 | if from == nil { 126 | return nil 127 | } 128 | to := *from 129 | to.Directives = from.Directives.DeepCopy() 130 | return &to 131 | } 132 | 133 | func (from *InlineFragment) DeepCopy() Selection { 134 | if from == nil { 135 | return nil 136 | } 137 | to := *from 138 | to.Directives = from.Directives.DeepCopy() 139 | to.Selections = from.Selections.DeepCopy() 140 | return &to 141 | } 142 | 143 | func (from *InputValue) DeepCopy() *InputValue { 144 | if from == nil { 145 | return nil 146 | } 147 | to := *from 148 | to.Directives = from.Directives.DeepCopy() 149 | to.Default = DeepCopyLiteral(from.Default) 150 | return &to 151 | } 152 | 153 | func (from *FragmentDecl) DeepCopy() *FragmentDecl { 154 | if from == nil { 155 | return nil 156 | } 157 | to := *from 158 | to.Directives = from.Directives.DeepCopy() 159 | to.Selections = from.Selections.DeepCopy() 160 | return &to 161 | } 162 | 163 | func DeepCopySelection(from Selection) Selection { 164 | if from == nil { 165 | return nil 166 | } 167 | switch from := from.(type) { 168 | case *FieldSelection: 169 | return from.DeepCopy() 170 | case *InlineFragment: 171 | return from.DeepCopy() 172 | case *FragmentSpread: 173 | return from.DeepCopy() 174 | default: 175 | panic("unreachable") 176 | } 177 | } 178 | 179 | func (from SelectionList) DeepCopy() (to SelectionList) { 180 | if len(from) == 0 { 181 | return 182 | } 183 | to = make(SelectionList, len(from)) 184 | for i, v := range from { 185 | to[i] = DeepCopySelection(v) 186 | } 187 | return 188 | } 189 | 190 | func (from InputValueList) DeepCopy() (to InputValueList) { 191 | if len(from) == 0 { 192 | return 193 | } 194 | to = make(InputValueList, len(from)) 195 | for i, v := range from { 196 | to[i] = v.DeepCopy() 197 | } 198 | return 199 | } 200 | 201 | func (from FragmentList) DeepCopy() (to FragmentList) { 202 | if len(from) == 0 { 203 | return 204 | } 205 | to = make(FragmentList, len(from)) 206 | for i, v := range from { 207 | to[i] = v.DeepCopy() 208 | } 209 | return 210 | 211 | } 212 | 213 | func (from OperationList) DeepCopy() (to OperationList) { 214 | if len(from) == 0 { 215 | return 216 | } 217 | to = make(OperationList, len(from)) 218 | for i, v := range from { 219 | to[i] = v.DeepCopy() 220 | } 221 | return 222 | } 223 | 224 | func (from ArgumentList) DeepCopy() (to ArgumentList) { 225 | if len(from) == 0 { 226 | return 227 | } 228 | to = make(ArgumentList, len(from)) 229 | for i, v := range from { 230 | to[i] = v.DeepCopy() 231 | } 232 | return 233 | } 234 | 235 | func (from DirectiveList) DeepCopy() (to DirectiveList) { 236 | if len(from) == 0 { 237 | return 238 | } 239 | to = make(DirectiveList, len(from)) 240 | for i, v := range from { 241 | to[i] = v.DeepCopy() 242 | } 243 | return 244 | } 245 | -------------------------------------------------------------------------------- /qerrors/qerrors.go: -------------------------------------------------------------------------------- 1 | package qerrors 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/chirino/graphql/text" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | ///////////////////////////////////////////////////////////////////////////// 15 | // Section: Error 16 | ///////////////////////////////////////////////////////////////////////////// 17 | 18 | type Error struct { 19 | Message string `json:"message"` 20 | Locations []Location `json:"locations,omitempty"` 21 | Path []string `json:"path,omitempty"` 22 | Extensions map[string]interface{} `json:"extensions,omitempty"` 23 | Rule string `json:"-"` 24 | cause error 25 | stack errors.StackTrace 26 | } 27 | 28 | // asserts that *Error implements the error interface. 29 | var _ error = &Error{} 30 | 31 | func Errorf(format string, a ...interface{}) *Error { 32 | return New(fmt.Sprintf(format, a...)).WithStack() 33 | } 34 | 35 | func New(message string) *Error { 36 | return (&Error{ 37 | Message: message, 38 | }).WithStack() 39 | } 40 | 41 | func WrapError(err error, message string) *Error { 42 | return &Error{ 43 | Message: message, 44 | cause: err, 45 | } 46 | } 47 | 48 | func (e *Error) WithPath(path ...string) *Error { 49 | e.Path = path 50 | return e 51 | } 52 | 53 | func (e *Error) WithCause(err error) *Error { 54 | e.cause = err 55 | return e 56 | } 57 | 58 | func (e *Error) WithRule(rule string) *Error { 59 | e.Rule = rule 60 | return e 61 | } 62 | func (e *Error) WithExtensions(details map[string]interface{}) *Error { 63 | e.Extensions = details 64 | return e 65 | } 66 | 67 | func (e *Error) WithLocations(locations ...Location) *Error { 68 | e.Locations = locations 69 | return e 70 | } 71 | 72 | func (err *Error) WithStack() *Error { 73 | const depth = 32 74 | var pcs [depth]uintptr 75 | n := runtime.Callers(3, pcs[:]) 76 | f := make([]errors.Frame, n) 77 | for i := 0; i < n; i++ { 78 | f[i] = errors.Frame(pcs[i]) 79 | } 80 | 81 | err.stack = f 82 | return err 83 | } 84 | 85 | func (err *Error) ClearStack() *Error { 86 | err.stack = nil 87 | return err 88 | } 89 | 90 | func (err *Error) Error() string { 91 | if err == nil { 92 | return "" 93 | } 94 | str := fmt.Sprintf("graphql: %s", err.Message) 95 | for _, loc := range err.Locations { 96 | str += fmt.Sprintf(" %s", loc) 97 | } 98 | 99 | if len(err.Path) > 0 { 100 | str += fmt.Sprintf(" (path %s)", strings.Join(err.Path, "/")) 101 | } 102 | return str 103 | } 104 | 105 | type state struct { 106 | fmt.State 107 | buf bytes.Buffer 108 | } 109 | 110 | func (s *state) Write(b []byte) (n int, err error) { 111 | return s.buf.Write(b) 112 | } 113 | 114 | func (w *Error) Format(s fmt.State, verb rune) { 115 | type stackTracer interface { 116 | StackTrace() errors.StackTrace 117 | } 118 | 119 | switch verb { 120 | case 'v': 121 | io.WriteString(s, w.Error()) 122 | if s.Flag('+') { 123 | stack := w.stack 124 | if w.Cause() != nil { 125 | if cause, ok := w.Cause().(stackTracer); ok { 126 | //fmt.Fprintf(s, "%+v", c) 127 | stack = cause.StackTrace() 128 | } 129 | } 130 | if stack != nil { 131 | tempState := &state{State: s} 132 | stack.Format(tempState, verb) 133 | stackText := tempState.buf.String() 134 | io.WriteString(s, "\n"+text.BulletIndent(" stack: ", stackText[1:])) 135 | io.WriteString(s, "\n") 136 | } 137 | return 138 | } 139 | fallthrough 140 | case 's': 141 | io.WriteString(s, w.Error()) 142 | case 'q': 143 | fmt.Fprintf(s, "%q", w.Error()) 144 | } 145 | } 146 | 147 | func (err *Error) Cause() error { 148 | return err.cause 149 | } 150 | 151 | ///////////////////////////////////////////////////////////////////////////// 152 | // Section: Location 153 | ///////////////////////////////////////////////////////////////////////////// 154 | 155 | type Location struct { 156 | Line int `json:"line"` 157 | Column int `json:"column"` 158 | } 159 | 160 | func (l Location) String() string { 161 | return fmt.Sprintf("(line %d, column %d)", l.Line, l.Column) 162 | } 163 | 164 | func (a Location) Before(b Location) bool { 165 | return a.Line < b.Line || (a.Line == b.Line && a.Column < b.Column) 166 | } 167 | 168 | ///////////////////////////////////////////////////////////////////////////// 169 | // Section: ErrorList 170 | ///////////////////////////////////////////////////////////////////////////// 171 | 172 | type ErrorList []*Error 173 | 174 | func AppendErrors(items ErrorList, values ...error) ErrorList { 175 | for _, err := range values { 176 | if err == nil { 177 | continue 178 | } 179 | switch err := err.(type) { 180 | case *Error: 181 | items = append(items, err) 182 | case asError: 183 | for _, e := range err { 184 | items = AppendErrors(items, e) 185 | } 186 | default: 187 | items = append(items, WrapError(err, err.Error())) 188 | } 189 | } 190 | return items 191 | } 192 | 193 | func (items ErrorList) Error() error { 194 | if len(items) == 0 { 195 | return nil 196 | } else { 197 | return asError(items) 198 | } 199 | } 200 | 201 | ///////////////////////////////////////////////////////////////////////////// 202 | // Section: asError 203 | ///////////////////////////////////////////////////////////////////////////// 204 | 205 | type asError ErrorList 206 | 207 | var _ error = &asError{} 208 | 209 | func (es asError) Error() string { 210 | points := make([]string, len(es)) 211 | for i, err := range es { 212 | points[i] = text.BulletIndent(" * ", err.Error()) 213 | } 214 | return fmt.Sprintf( 215 | "%d errors occurred:\n\t%s\n\n", 216 | len(es), strings.Join(points, "\n\t")) 217 | } 218 | -------------------------------------------------------------------------------- /internal/lexer/lexer.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/chirino/graphql/internal/scanner" 8 | "github.com/chirino/graphql/qerrors" 9 | 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | type syntaxError string 15 | type Location = qerrors.Location 16 | 17 | type Lexer struct { 18 | sc scanner.Scanner 19 | next rune 20 | SkipDescriptions bool 21 | } 22 | 23 | var lexerPool = sync.Pool{ 24 | New: func() interface{} { return new(Lexer) }, 25 | } 26 | 27 | func Put(l *Lexer) { 28 | l.sc.TokBuf.Reset() 29 | l.SkipDescriptions = false 30 | lexerPool.Put(l) 31 | } 32 | 33 | func Get(s string) *Lexer { 34 | l := lexerPool.Get().(*Lexer) 35 | l.sc.Init(strings.NewReader(s)) 36 | return l 37 | } 38 | 39 | func (l *Lexer) CatchSyntaxError(f func()) (errRes error) { 40 | defer func() { 41 | if err := recover(); err != nil { 42 | if err, ok := err.(syntaxError); ok { 43 | errRes = qerrors.Errorf("syntax error: %s", err).WithLocations(l.Location()) 44 | return 45 | } 46 | panic(err) 47 | } 48 | }() 49 | 50 | f() 51 | return 52 | } 53 | 54 | func (l *Lexer) Peek() rune { 55 | return l.next 56 | } 57 | 58 | // Consume whitespace and tokens equivalent to whitespace (e.g. commas and comments). 59 | // 60 | // Consumed comment characters will build the description for the next type or field encountered. 61 | // The description is available from `DescComment()`, and will be reset every time `Consume()` is 62 | // executed. 63 | func (l *Lexer) Consume() { 64 | for { 65 | l.next = l.sc.Scan() 66 | if l.next == ',' { 67 | // Similar to white space and line terminators, commas (',') are used to improve the 68 | // legibility of source text and separate lexical tokens but are otherwise syntactically and 69 | // semantically insignificant within GraphQL documents. 70 | // 71 | // http://facebook.github.io/graphql/draft/#sec-Insignificant-Commas 72 | continue 73 | } 74 | break 75 | } 76 | } 77 | 78 | func (l *Lexer) ConsumeIdentIntern() string { 79 | name := l.sc.TokenTextIntern() 80 | l.ConsumeToken(scanner.Ident) 81 | return name 82 | } 83 | 84 | func (l *Lexer) ConsumeIdentInternWithLoc() (string, Location) { 85 | loc := l.Location() 86 | name := l.sc.TokenTextIntern() 87 | l.ConsumeToken(scanner.Ident) 88 | return name, loc 89 | } 90 | 91 | func (l *Lexer) PeekKeyword(keyword string) bool { 92 | return l.next == scanner.Ident && l.sc.TokenTextIntern() == keyword 93 | } 94 | 95 | func (l *Lexer) ConsumeKeyword(keywords ...string) string { 96 | if l.next != scanner.Ident || !isOneOf(l.sc.TokenTextIntern(), keywords...) { 97 | l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %q", l.sc.TokenTextIntern(), keywords)) 98 | } 99 | result := l.sc.TokenTextIntern() 100 | l.Consume() 101 | return result 102 | } 103 | 104 | func isOneOf(one string, of ...string) bool { 105 | for _, v := range of { 106 | if one == v { 107 | return true 108 | } 109 | } 110 | return false 111 | } 112 | 113 | func (l *Lexer) ConsumeLiteral() string { 114 | switch l.next { 115 | case scanner.Int, scanner.Float, scanner.String, scanner.BlockString, scanner.Ident: 116 | lit := l.sc.TokenText() 117 | l.Consume() 118 | return lit 119 | default: 120 | l.SyntaxError(fmt.Sprintf("unexpected %q, expecting literal", l.next)) 121 | panic("unreachable") 122 | } 123 | } 124 | 125 | func (l *Lexer) ConsumeToken(expected rune) { 126 | if l.next != expected { 127 | l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %s", l.sc.TokenText(), scanner.TokenString(expected))) 128 | } 129 | l.Consume() 130 | } 131 | 132 | type ShowType byte 133 | 134 | var PossibleDescription = ShowType(0) 135 | var ShowStringDescription = ShowType(1) 136 | var ShowBlockDescription = ShowType(2) 137 | var NoDescription = ShowType(3) 138 | 139 | type Description struct { 140 | ShowType ShowType 141 | Text string 142 | Loc Location 143 | } 144 | 145 | func (d Description) String() string { 146 | return d.Text 147 | } 148 | 149 | func (l *Lexer) ConsumeDescription() (d Description) { 150 | d.Loc = l.Location() 151 | if l.Peek() == scanner.String { 152 | if l.SkipDescriptions { 153 | d.ShowType = NoDescription 154 | l.ConsumeToken(scanner.String) 155 | } else { 156 | d.ShowType = ShowStringDescription 157 | d.Text = l.ConsumeString() 158 | } 159 | } else if l.Peek() == scanner.BlockString { 160 | if l.SkipDescriptions { 161 | d.ShowType = NoDescription 162 | l.ConsumeToken(scanner.BlockString) 163 | } else { 164 | d.ShowType = ShowBlockDescription 165 | text := l.sc.TokenText() 166 | text = text[3 : len(text)-3] 167 | 168 | col := l.sc.Column - 1 169 | prefix := l.sc.TextAt(l.sc.Offset-col, col) 170 | text = trimLinePrefixes(text, prefix) 171 | 172 | l.ConsumeToken(scanner.BlockString) 173 | d.Text = text 174 | } 175 | } else { 176 | d.ShowType = NoDescription 177 | } 178 | return 179 | } 180 | 181 | func trimLinePrefixes(text string, prefix string) string { 182 | if prefix == "" { 183 | return text 184 | } 185 | 186 | lines := strings.Split(text, "\n") 187 | for i := range lines { 188 | lines[i] = strings.TrimPrefix(lines[i], prefix) 189 | } 190 | return strings.Join(lines, "\n") 191 | } 192 | 193 | func (l *Lexer) ConsumeString() string { 194 | loc := l.Location() 195 | unquoted, err := strconv.Unquote(l.sc.TokenText()) 196 | if err != nil { 197 | panic(fmt.Sprintf("Invalid string literal at %s: %s ", loc, err)) 198 | } 199 | l.ConsumeToken(scanner.String) 200 | return unquoted 201 | } 202 | 203 | func (l *Lexer) SyntaxError(message string) { 204 | panic(syntaxError(message)) 205 | } 206 | 207 | func (l *Lexer) Location() Location { 208 | return Location{ 209 | Line: l.sc.Line, 210 | Column: l.sc.Column, 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /schema/schema_format_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/chirino/graphql/internal/example/starwars" 8 | "github.com/chirino/graphql/schema" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestWriteSchemaFormatForStarwars(t *testing.T) { 13 | s := schema.New() 14 | s.Parse(starwars.Schema) 15 | buf := &bytes.Buffer{} 16 | s.WriteTo(buf) 17 | assert.Equal(t, `"A character from the Star Wars universe" 18 | interface Character { 19 | "The movies this character appears in" 20 | appearsIn:[Episode!]! 21 | "The friends of the character, or an empty list if they have none" 22 | friends:[Character] 23 | "The friends of the character exposed as a connection with edges" 24 | friendsConnection(after:ID, first:Int):FriendsConnection! 25 | "The ID of the character" 26 | id:ID! 27 | "The name of the character" 28 | name:String! 29 | } 30 | "An autonomous mechanical character in the Star Wars universe" 31 | type Droid implements Character { 32 | "The movies this droid appears in" 33 | appearsIn:[Episode!]! 34 | "This droid's friends, or an empty list if they have none" 35 | friends:[Character] 36 | "The friends of the droid exposed as a connection with edges" 37 | friendsConnection(after:ID, first:Int):FriendsConnection! 38 | "The ID of the droid" 39 | id:ID! 40 | "What others call this droid" 41 | name:String! 42 | "This droid's primary function" 43 | primaryFunction:String 44 | } 45 | "The episodes in the Star Wars trilogy" 46 | enum Episode { 47 | "Star Wars Episode V: The Empire Strikes Back, released in 1980." 48 | EMPIRE 49 | "Star Wars Episode VI: Return of the Jedi, released in 1983." 50 | JEDI 51 | "Star Wars Episode IV: A New Hope, released in 1977." 52 | NEWHOPE 53 | } 54 | "A connection object for a character's friends" 55 | type FriendsConnection { 56 | "The edges for each of the character's friends." 57 | edges:[FriendsEdge] 58 | "A list of the friends, as a convenience when edges are not needed." 59 | friends:[Character] 60 | "Information for paginating this connection" 61 | pageInfo:PageInfo! 62 | "The total number of friends" 63 | totalCount:Int! 64 | } 65 | "An edge object for a character's friends" 66 | type FriendsEdge { 67 | "A cursor used for pagination" 68 | cursor:ID! 69 | "The character represented by this friendship edge" 70 | node:Character 71 | } 72 | "A humanoid creature from the Star Wars universe" 73 | type Human implements Character { 74 | "The movies this human appears in" 75 | appearsIn:[Episode!]! 76 | "This human's friends, or an empty list if they have none" 77 | friends:[Character] 78 | "The friends of the human exposed as a connection with edges" 79 | friendsConnection(after:ID, first:Int):FriendsConnection! 80 | "Height in the preferred unit, default is meters" 81 | height(unit:LengthUnit=METER):Float! 82 | "The ID of the human" 83 | id:ID! 84 | "Mass in kilograms, or null if unknown" 85 | mass:Float 86 | "What this human calls themselves" 87 | name:String! 88 | "A list of starships this person has piloted, or an empty list if none" 89 | starships:[Starship] 90 | } 91 | "Units of height" 92 | enum LengthUnit { 93 | "Primarily used in the United States" 94 | FOOT 95 | "The standard unit around the world" 96 | METER 97 | } 98 | "The mutation type, represents all updates we can make to our data" 99 | type Mutation { 100 | createReview(episode:Episode!, review:ReviewInput!):Review 101 | } 102 | "Information for paginating this connection" 103 | type PageInfo { 104 | endCursor:ID 105 | hasNextPage:Boolean! 106 | startCursor:ID 107 | } 108 | "The query type, represents all of the entry points into our object graph" 109 | type Query { 110 | character(id:ID!):Character 111 | droid(id:ID!):Droid 112 | hero(episode:Episode=NEWHOPE):Character 113 | human(id:ID!):Human 114 | reviews(episode:Episode!):[Review]! 115 | search(text:String!):[SearchResult]! 116 | starship(id:ID!):Starship 117 | } 118 | "Represents a review for a movie" 119 | type Review { 120 | "Comment about the movie" 121 | commentary:String 122 | "The number of stars this review gave, 1-5" 123 | stars:Int! 124 | } 125 | "The input object sent when someone is creating a new review" 126 | input ReviewInput { 127 | "Comment about the movie, optional" 128 | commentary:String 129 | "0-5 stars" 130 | stars:Int! 131 | } 132 | union SearchResult = Droid | Human | Starship 133 | type Starship { 134 | "The ID of the starship" 135 | id:ID! 136 | "Length of the starship, along the longest axis" 137 | length(unit:LengthUnit=METER):Float! 138 | "The name of the starship" 139 | name:String! 140 | } 141 | schema { 142 | mutation: Mutation 143 | query: Query 144 | } 145 | `, buf.String()) 146 | } 147 | 148 | func TestWriteSchemaFormatEdgeCases(t *testing.T) { 149 | s := schema.New() 150 | s.Parse(` 151 | """ 152 | Multi 153 | Line Description 154 | """ 155 | directive @db_table( 156 | name: String 157 | ) on OBJECT 158 | 159 | scalar Revision 160 | 161 | schema { 162 | query: Query 163 | } 164 | 165 | type Query @db_table(name:"Hello") { 166 | """ 167 | a long 168 | message 169 | """ 170 | hi: Revision 171 | args( 172 | a:String=null, 173 | "test" 174 | b:Int=5, 175 | c:String="Hi", d:[String]=["a", "b"]): String 176 | } 177 | 178 | `) 179 | buf := &bytes.Buffer{} 180 | s.WriteTo(buf) 181 | assert.Equal(t, `""" 182 | Multi 183 | Line Description 184 | """ 185 | directive @db_table(name:String) on OBJECT 186 | type Query @db_table(name:"Hello") { 187 | args( 188 | a:String=null, 189 | "test" 190 | b:Int=5, 191 | c:String="Hi", 192 | d:[String]=["a", "b"] 193 | ):String 194 | """ 195 | a long 196 | message 197 | """ 198 | hi:Revision 199 | } 200 | scalar Revision 201 | schema { 202 | query: Query 203 | } 204 | `, buf.String()) 205 | } 206 | -------------------------------------------------------------------------------- /internal/deprecated/graphql.go: -------------------------------------------------------------------------------- 1 | package deprecated 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/chirino/graphql" 7 | "github.com/chirino/graphql/qerrors" 8 | "github.com/chirino/graphql/schema" 9 | 10 | "github.com/chirino/graphql/internal/validation" 11 | "github.com/chirino/graphql/log" 12 | "github.com/chirino/graphql/trace" 13 | ) 14 | 15 | // ParseSchema parses a GraphQL schema and attaches the given root resolver. It returns an error if 16 | // the Go type signature of the resolvers does not match the schema. If nil is passed as the 17 | // resolver, then the schema can not be executed, but it may be inspected (e.g. with ToJSON). 18 | func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (*Schema, error) { 19 | 20 | engine := graphql.New() 21 | err := engine.Schema.Parse(schemaString) 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | s := &Schema{ 27 | Engine: engine, 28 | } 29 | for _, opt := range opts { 30 | opt(s) 31 | } 32 | if resolver != nil { 33 | s.resolver = resolver 34 | } 35 | 36 | //s := &Schema{ 37 | // schema: schema.New(), 38 | // maxParallelism: 10, 39 | // tracer: trace.OpenTracingTracer{}, 40 | // validationTracer: trace.NoopValidationTracer{}, 41 | // logger: &log.DefaultLogger{}, 42 | //} 43 | //for _, opt := range opts { 44 | // opt(s) 45 | //} 46 | // 47 | //if err := s.schema.Parse(schemaString); err != nil { 48 | // return nil, err 49 | //} 50 | // 51 | //if resolver != nil { 52 | // r, err := resolvable.ApplyResolver(s.schema, resolver) 53 | // if err != nil { 54 | // return nil, err 55 | // } 56 | // s.res = r 57 | //} 58 | 59 | return s, nil 60 | } 61 | 62 | // MustParseSchema calls ParseSchema and panics on error. 63 | func MustParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) *Schema { 64 | s, err := ParseSchema(schemaString, resolver, opts...) 65 | if err != nil { 66 | panic(err) 67 | } 68 | return s 69 | } 70 | 71 | // Schema represents a GraphQL schema with an optional resolver. 72 | type Schema struct { 73 | //schema *schema.Schema 74 | //res *resolvable.Schema 75 | // 76 | //maxDepth int 77 | //maxParallelism int 78 | //tracer trace.Tracer 79 | //validationTracer trace.ValidationTracer 80 | //logger log.Logger 81 | 82 | Engine *graphql.Engine 83 | resolver interface{} 84 | } 85 | 86 | // SchemaOpt is an option to pass to ParseSchema or MustParseSchema. 87 | type SchemaOpt func(*Schema) 88 | 89 | // MaxDepth specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking. 90 | func MaxDepth(n int) SchemaOpt { 91 | return func(s *Schema) { 92 | s.Engine.MaxDepth = n 93 | } 94 | } 95 | 96 | // MaxParallelism specifies the maximum number of resolvers per request allowed to run in parallel. The default is 10. 97 | func MaxParallelism(n int) SchemaOpt { 98 | return func(s *Schema) { 99 | s.Engine.MaxParallelism = n 100 | } 101 | } 102 | 103 | // Tracer is used to trace queries and fields. It defaults to trace.OpenTracingTracer. 104 | func Tracer(tracer trace.Tracer) SchemaOpt { 105 | return func(s *Schema) { 106 | s.Engine.Tracer = tracer 107 | } 108 | } 109 | 110 | // Logger is used to log panics during query execution. It defaults to exec.DefaultLogger. 111 | func Logger(logger log.Logger) SchemaOpt { 112 | return func(s *Schema) { 113 | s.Engine.Logger = logger 114 | } 115 | } 116 | 117 | // Validate validates the given query with the schema. 118 | func (s *Schema) Validate(queryString string) qerrors.ErrorList { 119 | doc := &schema.QueryDocument{} 120 | qErr := doc.Parse(queryString) 121 | if qErr != nil { 122 | return qerrors.ErrorList{qErr.(*qerrors.Error)} 123 | } 124 | 125 | return validation.Validate(s.Engine.Schema, doc, s.Engine.MaxDepth) 126 | } 127 | 128 | // Exec executes the given query with the schema's resolver. It panics if the schema was created 129 | // without a resolver. If the context get cancelled, no further resolvers will be called and a 130 | // the context error will be returned as soon as possible (not immediately). 131 | func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *graphql.Response { 132 | if s.resolver == nil { 133 | panic("schema created without resolver, can not exec") 134 | } 135 | s.Engine.Root = s.resolver 136 | return s.Engine.ServeGraphQL(&graphql.Request{ 137 | Context: ctx, 138 | Query: queryString, 139 | OperationName: operationName, 140 | Variables: variables, 141 | }) 142 | } 143 | 144 | //func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response { 145 | // doc, qErr := query.Parse(queryString) 146 | // if qErr != nil { 147 | // return &Response{Errors: errors.ErrorList{qErr}} 148 | // } 149 | // 150 | // validationFinish := s.validationTracer.TraceValidation() 151 | // errs := validation.Validate(s.schema, doc, s.maxDepth) 152 | // validationFinish(errs) 153 | // if len(errs) != 0 { 154 | // return &Response{Errors: errs} 155 | // } 156 | // 157 | // op, err := getOperation(doc, operationName) 158 | // if err != nil { 159 | // return &Response{Errors: errors.ErrorList{errors.Errorf("%s", err)}} 160 | // } 161 | // 162 | // r := &exec.Request{ 163 | // Request: selected.Request{ 164 | // Doc: doc, 165 | // Vars: variables, 166 | // Schema: s.schema, 167 | // }, 168 | // Limiter: make(chan struct{}, s.maxParallelism), 169 | // Tracer: s.tracer, 170 | // Logger: s.logger, 171 | // } 172 | // varTypes := make(map[string]*introspection.Type) 173 | // for _, v := range op.Vars { 174 | // t, err := common.ResolveType(v.Type, s.schema.Resolve) 175 | // if err != nil { 176 | // return &Response{Errors: errors.ErrorList{err}} 177 | // } 178 | // varTypes[v.Name.Name] = introspection.WrapType(t) 179 | // } 180 | // traceCtx, finish := s.tracer.TraceQuery(ctx, queryString, operationName, variables, varTypes) 181 | // data, errs := r.Execute(traceCtx, res, op) 182 | // finish(errs) 183 | // 184 | // return &Response{ 185 | // Data: data, 186 | // Errors: errs, 187 | // } 188 | //} 189 | -------------------------------------------------------------------------------- /schema/literals.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/chirino/graphql/internal/lexer" 7 | "github.com/chirino/graphql/internal/scanner" 8 | "github.com/chirino/graphql/qerrors" 9 | 10 | "io" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | type Literal interface { 16 | Evaluate(vars map[string]interface{}) interface{} 17 | String() string 18 | Location() qerrors.Location 19 | WriteTo(out io.StringWriter) 20 | } 21 | 22 | type BasicLit struct { 23 | Type rune 24 | Text string 25 | Loc qerrors.Location 26 | } 27 | 28 | func ToLiteral(v interface{}) Literal { 29 | switch v := v.(type) { 30 | case nil: 31 | return &NullLit{} 32 | case string: 33 | return &BasicLit{ 34 | Type: scanner.String, 35 | Text: strconv.Quote(v), 36 | } 37 | case int: 38 | return &BasicLit{ 39 | Type: scanner.Int, 40 | Text: strconv.FormatInt(int64(v), 10), 41 | } 42 | case int32: 43 | return &BasicLit{ 44 | Type: scanner.Int, 45 | Text: strconv.FormatInt(int64(v), 10), 46 | } 47 | case int64: 48 | return &BasicLit{ 49 | Type: scanner.Int, 50 | Text: strconv.FormatInt(v, 10), 51 | } 52 | case uint: 53 | return &BasicLit{ 54 | Type: scanner.Int, 55 | Text: strconv.FormatUint(uint64(v), 10), 56 | } 57 | case uint32: 58 | return &BasicLit{ 59 | Type: scanner.Int, 60 | Text: strconv.FormatUint(uint64(v), 10), 61 | } 62 | case uint64: 63 | return &BasicLit{ 64 | Type: scanner.Int, 65 | Text: strconv.FormatUint(v, 10), 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | func (lit *BasicLit) Evaluate(vars map[string]interface{}) interface{} { 72 | switch lit.Type { 73 | case scanner.Int: 74 | value, err := strconv.ParseInt(lit.Text, 10, 32) 75 | if err != nil { 76 | panic(err) 77 | } 78 | return int32(value) 79 | 80 | case scanner.Float: 81 | value, err := strconv.ParseFloat(lit.Text, 64) 82 | if err != nil { 83 | panic(err) 84 | } 85 | return value 86 | 87 | case scanner.String: 88 | value, err := strconv.Unquote(lit.Text) 89 | if err != nil { 90 | panic(err) 91 | } 92 | return value 93 | 94 | case scanner.BlockString: 95 | return lit.Text[3 : len(lit.Text)-3] 96 | 97 | case scanner.Ident: 98 | switch lit.Text { 99 | case "true": 100 | return true 101 | case "false": 102 | return false 103 | default: 104 | return lit.Text 105 | } 106 | 107 | default: 108 | panic("invalid literal") 109 | } 110 | } 111 | 112 | func (lit *BasicLit) String() string { 113 | return lit.Text 114 | } 115 | 116 | func (lit *BasicLit) Location() qerrors.Location { 117 | return lit.Loc 118 | } 119 | 120 | type ListLit struct { 121 | Entries []Literal 122 | Loc qerrors.Location 123 | } 124 | 125 | func (lit *ListLit) Evaluate(vars map[string]interface{}) interface{} { 126 | entries := make([]interface{}, len(lit.Entries)) 127 | for i, entry := range lit.Entries { 128 | entries[i] = entry.Evaluate(vars) 129 | } 130 | return entries 131 | } 132 | 133 | func (lit *ListLit) String() string { 134 | entries := make([]string, len(lit.Entries)) 135 | for i, entry := range lit.Entries { 136 | entries[i] = entry.String() 137 | } 138 | return "[" + strings.Join(entries, ", ") + "]" 139 | } 140 | 141 | func (lit *ListLit) Location() qerrors.Location { 142 | return lit.Loc 143 | } 144 | 145 | type ObjectLit struct { 146 | Fields []*ObjectLitField 147 | Loc qerrors.Location 148 | } 149 | 150 | type ObjectLitField struct { 151 | Name string 152 | NameLoc Location 153 | Value Literal 154 | } 155 | 156 | func (lit *ObjectLit) Evaluate(vars map[string]interface{}) interface{} { 157 | fields := make(map[string]interface{}, len(lit.Fields)) 158 | for _, f := range lit.Fields { 159 | fields[f.Name] = f.Value.Evaluate(vars) 160 | } 161 | return fields 162 | } 163 | 164 | func (lit *ObjectLit) String() string { 165 | entries := make([]string, 0, len(lit.Fields)) 166 | for _, f := range lit.Fields { 167 | entries = append(entries, f.Name+": "+f.Value.String()) 168 | } 169 | return "{" + strings.Join(entries, ", ") + "}" 170 | } 171 | 172 | func (lit *ObjectLit) Location() qerrors.Location { 173 | return lit.Loc 174 | } 175 | 176 | type NullLit struct { 177 | Loc qerrors.Location 178 | } 179 | 180 | func (lit *NullLit) Evaluate(vars map[string]interface{}) interface{} { 181 | return nil 182 | } 183 | 184 | func (lit *NullLit) String() string { 185 | return "null" 186 | } 187 | 188 | func (lit *NullLit) Location() qerrors.Location { 189 | return lit.Loc 190 | } 191 | 192 | type Variable struct { 193 | Name string 194 | Loc qerrors.Location 195 | } 196 | 197 | func (v Variable) Evaluate(vars map[string]interface{}) interface{} { 198 | return vars[v.Name] 199 | } 200 | 201 | func (v Variable) String() string { 202 | return "$" + v.Name 203 | } 204 | 205 | func (v *Variable) Location() qerrors.Location { 206 | return v.Loc 207 | } 208 | 209 | func ParseLiteral(l *lexer.Lexer, constOnly bool) Literal { 210 | loc := l.Location() 211 | switch l.Peek() { 212 | case '$': 213 | if constOnly { 214 | l.SyntaxError("variable not allowed") 215 | panic("unreachable") 216 | } 217 | l.ConsumeToken('$') 218 | return &Variable{l.ConsumeIdentIntern(), loc} 219 | 220 | case scanner.Int, scanner.Float, scanner.String, scanner.BlockString, scanner.Ident: 221 | lit := ConsumeLiteral(l) 222 | if lit.Type == scanner.Ident && lit.Text == "null" { 223 | return &NullLit{loc} 224 | } 225 | lit.Loc = loc 226 | return lit 227 | case '-': 228 | l.ConsumeToken('-') 229 | lit := ConsumeLiteral(l) 230 | lit.Text = "-" + lit.Text 231 | lit.Loc = loc 232 | return lit 233 | case '[': 234 | l.ConsumeToken('[') 235 | var list []Literal 236 | for l.Peek() != ']' { 237 | list = append(list, ParseLiteral(l, constOnly)) 238 | } 239 | l.ConsumeToken(']') 240 | return &ListLit{list, loc} 241 | 242 | case '{': 243 | l.ConsumeToken('{') 244 | var fields []*ObjectLitField 245 | for l.Peek() != '}' { 246 | name, loc := l.ConsumeIdentInternWithLoc() 247 | l.ConsumeToken(':') 248 | value := ParseLiteral(l, constOnly) 249 | fields = append(fields, &ObjectLitField{Name: name, NameLoc: loc, Value: value}) 250 | } 251 | l.ConsumeToken('}') 252 | sort.Slice(fields, func(i, j int) bool { 253 | return fields[i].Name < fields[j].Name 254 | }) 255 | return &ObjectLit{fields, loc} 256 | 257 | default: 258 | peek := l.Peek() 259 | l.SyntaxError("invalid value: " + string(peek)) 260 | panic("unreachable") 261 | } 262 | } 263 | 264 | func ConsumeLiteral(l *lexer.Lexer) *BasicLit { 265 | return &BasicLit{ 266 | Loc: l.Location(), 267 | Type: l.Peek(), 268 | Text: l.ConsumeLiteral(), 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /internal/introspection/resolvers.go: -------------------------------------------------------------------------------- 1 | package introspection 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/chirino/graphql/internal/lexer" 7 | "github.com/chirino/graphql/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.DeclaredDirectives { 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.DeclaredDirectives[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 schema.Type 73 | } 74 | 75 | // WrapType is only used internally. 76 | func WrapType(typ schema.Type) *Type { 77 | return &Type{typ} 78 | } 79 | 80 | func (t *Type) To__Type() (*Type, bool) { 81 | return t, true 82 | } 83 | 84 | func (r *Type) Kind() string { 85 | return r.typ.Kind() 86 | } 87 | 88 | func (r *Type) Name() *string { 89 | if named, ok := r.typ.(schema.NamedType); ok { 90 | name := named.TypeName() 91 | return &name 92 | } 93 | return nil 94 | } 95 | 96 | func (r *Type) Description() *string { 97 | if named, ok := r.typ.(schema.NamedType); ok { 98 | desc := named.Description() 99 | if desc == "" { 100 | return nil 101 | } 102 | return &desc 103 | } 104 | return nil 105 | } 106 | 107 | func (r *Type) Fields(args *struct{ IncludeDeprecated bool }) *[]*Field { 108 | var fields schema.FieldList 109 | switch t := r.typ.(type) { 110 | case *schema.Object: 111 | fields = t.Fields 112 | case *schema.Interface: 113 | fields = t.Fields 114 | default: 115 | return nil 116 | } 117 | 118 | var l []*Field 119 | for _, f := range fields { 120 | if d := f.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated { 121 | l = append(l, &Field{f}) 122 | } 123 | } 124 | return &l 125 | } 126 | 127 | func (r *Type) Interfaces() *[]*Type { 128 | t, ok := r.typ.(*schema.Object) 129 | if !ok { 130 | return nil 131 | } 132 | 133 | l := make([]*Type, len(t.Interfaces)) 134 | for i, intf := range t.Interfaces { 135 | l[i] = &Type{intf} 136 | } 137 | return &l 138 | } 139 | 140 | func (r *Type) PossibleTypes() *[]*Type { 141 | var possibleTypes []*schema.Object 142 | switch t := r.typ.(type) { 143 | case *schema.Interface: 144 | possibleTypes = t.PossibleTypes 145 | case *schema.Union: 146 | possibleTypes = t.PossibleTypes 147 | default: 148 | return nil 149 | } 150 | 151 | sort.Slice(possibleTypes, func(i, j int) bool { 152 | return possibleTypes[i].Name < possibleTypes[j].Name 153 | }) 154 | 155 | l := make([]*Type, len(possibleTypes)) 156 | for i, intf := range possibleTypes { 157 | l[i] = &Type{intf} 158 | } 159 | return &l 160 | } 161 | 162 | func (r *Type) EnumValues(args *struct{ IncludeDeprecated bool }) *[]*EnumValue { 163 | t, ok := r.typ.(*schema.Enum) 164 | if !ok { 165 | return nil 166 | } 167 | 168 | var l []*EnumValue 169 | for _, v := range t.Values { 170 | if d := v.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated { 171 | l = append(l, &EnumValue{v}) 172 | } 173 | } 174 | return &l 175 | } 176 | 177 | func (r *Type) InputFields() *[]*InputValue { 178 | t, ok := r.typ.(*schema.InputObject) 179 | if !ok { 180 | return nil 181 | } 182 | 183 | l := make([]*InputValue, len(t.Fields)) 184 | for i, v := range t.Fields { 185 | l[i] = &InputValue{v} 186 | } 187 | return &l 188 | } 189 | 190 | func (r *Type) OfType() *Type { 191 | switch t := r.typ.(type) { 192 | case *schema.List: 193 | return &Type{t.OfType} 194 | case *schema.NonNull: 195 | return &Type{t.OfType} 196 | default: 197 | return nil 198 | } 199 | } 200 | 201 | type Field struct { 202 | field *schema.Field 203 | } 204 | 205 | func (r *Field) Name() string { 206 | return r.field.Name 207 | } 208 | 209 | func (r *Field) Description() *string { 210 | return Description(r.field.Desc) 211 | } 212 | 213 | func Description(desc schema.Description) *string { 214 | if desc.ShowType == lexer.NoDescription { 215 | return nil 216 | } 217 | if desc.ShowType == lexer.PossibleDescription { 218 | if desc.Text == "" { 219 | return nil 220 | } 221 | } 222 | return &desc.Text 223 | } 224 | 225 | func (r *Field) Args() []*InputValue { 226 | l := make([]*InputValue, len(r.field.Args)) 227 | for i, v := range r.field.Args { 228 | l[i] = &InputValue{v} 229 | } 230 | return l 231 | } 232 | 233 | func (r *Field) Type() *Type { 234 | return &Type{r.field.Type} 235 | } 236 | 237 | func (r *Field) IsDeprecated() bool { 238 | return r.field.Directives.Get("deprecated") != nil 239 | } 240 | 241 | func (r *Field) DeprecationReason() *string { 242 | d := r.field.Directives.Get("deprecated") 243 | if d == nil { 244 | return nil 245 | } 246 | reason := d.Args.MustGet("reason").Evaluate(nil).(string) 247 | return &reason 248 | } 249 | 250 | type InputValue struct { 251 | value *schema.InputValue 252 | } 253 | 254 | func (r *InputValue) To__InputValue() (*InputValue, bool) { 255 | return r, true 256 | } 257 | 258 | func (r *InputValue) Name() string { 259 | return r.value.Name 260 | } 261 | 262 | func (r *InputValue) Description() *string { 263 | return Description(r.value.Desc) 264 | } 265 | 266 | func (r *InputValue) Type() *Type { 267 | return &Type{r.value.Type} 268 | } 269 | 270 | func (r *InputValue) DefaultValue() *string { 271 | if r.value.Default == nil { 272 | return nil 273 | } 274 | s := r.value.Default.String() 275 | return &s 276 | } 277 | 278 | type EnumValue struct { 279 | value *schema.EnumValue 280 | } 281 | 282 | func (r *EnumValue) Name() string { 283 | return r.value.Name 284 | } 285 | 286 | func (r *EnumValue) Description() *string { 287 | return Description(r.value.Desc) 288 | } 289 | 290 | func (r *EnumValue) IsDeprecated() bool { 291 | return r.value.Directives.Get("deprecated") != nil 292 | } 293 | 294 | func (r *EnumValue) DeprecationReason() *string { 295 | d := r.value.Directives.Get("deprecated") 296 | if d == nil { 297 | return nil 298 | } 299 | reason := d.Args.MustGet("reason").Evaluate(nil).(string) 300 | return &reason 301 | } 302 | 303 | type Directive struct { 304 | directive *schema.DirectiveDecl 305 | } 306 | 307 | func (r *Directive) Name() string { 308 | return r.directive.Name 309 | } 310 | 311 | func (r *Directive) Description() *string { 312 | return Description(r.directive.Desc) 313 | } 314 | 315 | func (r *Directive) Locations() []string { 316 | return r.directive.Locs 317 | } 318 | 319 | func (r *Directive) Args() []*InputValue { 320 | l := make([]*InputValue, len(r.directive.Args)) 321 | for i, v := range r.directive.Args { 322 | l[i] = &InputValue{v} 323 | } 324 | return l 325 | } 326 | -------------------------------------------------------------------------------- /internal/assets/meta.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent 3 | values between -(2^31) and 2^31 - 1. 4 | """ 5 | scalar Int 6 | 7 | """ 8 | The `Float` scalar type represents signed double-precision fractional values as specified by 9 | [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). 10 | """ 11 | scalar Float 12 | 13 | """ 14 | The `String` scalar type represents textual data, represented as UTF-8 character sequences. 15 | The String type is most often used by GraphQL to represent free-form human-readable text. 16 | """ 17 | scalar String 18 | 19 | """ 20 | The `Boolean` scalar type represents `true` or `false`. 21 | """ 22 | scalar Boolean 23 | 24 | """ 25 | The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. 26 | The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. 27 | When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted 28 | as an ID. 29 | """ 30 | scalar ID 31 | 32 | """ 33 | Directs the executor to include this field or fragment only when the `if` argument is true. 34 | """ 35 | directive @include( 36 | "Included when true." 37 | if: Boolean! 38 | ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 39 | 40 | """ 41 | Directs the executor to skip this field or fragment when the `if` argument is true. 42 | """ 43 | directive @skip( 44 | "Skipped when true." 45 | if: Boolean! 46 | ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 47 | 48 | """ 49 | Marks an element of a GraphQL schema as no longer supported. 50 | """ 51 | directive @deprecated( 52 | """ 53 | Explains why this element was deprecated, usually also including a suggestion 54 | for how to access supported similar data. Formatted in 55 | [Markdown](https://daringfireball.net/projects/markdown/). 56 | """ 57 | reason: String = "No longer supported" 58 | ) on FIELD_DEFINITION | ENUM_VALUE 59 | 60 | """ 61 | A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. 62 | 63 | In some cases, you need to provide options to alter GraphQL's execution behavior 64 | in ways field arguments will not suffice, such as conditionally including or 65 | skipping a field. Directives provide this by describing additional information 66 | to the executor. 67 | """ 68 | type __Directive { 69 | name: String! 70 | description: String 71 | locations: [__DirectiveLocation!]! 72 | args: [__InputValue!]! 73 | } 74 | 75 | """ 76 | A Directive can be adjacent to many parts of the GraphQL language, a 77 | __DirectiveLocation describes one such possible adjacencies. 78 | """ 79 | enum __DirectiveLocation { 80 | "Location adjacent to a query operation." 81 | QUERY 82 | "Location adjacent to a mutation operation." 83 | MUTATION 84 | "Location adjacent to a subscription operation." 85 | SUBSCRIPTION 86 | "Location adjacent to a field." 87 | FIELD 88 | "Location adjacent to a fragment definition." 89 | FRAGMENT_DEFINITION 90 | "Location adjacent to a fragment spread." 91 | FRAGMENT_SPREAD 92 | "Location adjacent to an inline fragment." 93 | INLINE_FRAGMENT 94 | "Location adjacent to a schema definition." 95 | SCHEMA 96 | "Location adjacent to a scalar definition." 97 | SCALAR 98 | "Location adjacent to an object type definition." 99 | OBJECT 100 | "Location adjacent to a field definition." 101 | FIELD_DEFINITION 102 | "Location adjacent to an argument definition." 103 | ARGUMENT_DEFINITION 104 | "Location adjacent to an interface definition." 105 | INTERFACE 106 | "Location adjacent to a union definition." 107 | UNION 108 | "Location adjacent to an enum definition." 109 | ENUM 110 | "Location adjacent to an enum value definition." 111 | ENUM_VALUE 112 | "Location adjacent to an input object type definition." 113 | INPUT_OBJECT 114 | "Location adjacent to an input object field definition." 115 | INPUT_FIELD_DEFINITION 116 | } 117 | 118 | """ 119 | One possible value for a given Enum. Enum values are unique values, not a 120 | placeholder for a string or numeric value. However an Enum value is returned in 121 | a JSON response as a string. 122 | """ 123 | type __EnumValue { 124 | name: String! 125 | description: String 126 | isDeprecated: Boolean! 127 | deprecationReason: String 128 | } 129 | 130 | """ 131 | Object and Interface types are described by a list of Fields, each of which has 132 | a name, potentially a list of arguments, and a return type. 133 | """ 134 | type __Field { 135 | name: String! 136 | description: String 137 | args: [__InputValue!]! 138 | type: __Type! 139 | isDeprecated: Boolean! 140 | deprecationReason: String 141 | } 142 | 143 | """ 144 | Arguments provided to Fields or Directives and the input fields of an 145 | InputObject are represented as Input Values which describe their type and 146 | optionally a default value. 147 | """ 148 | type __InputValue { 149 | name: String! 150 | description: String 151 | type: __Type! 152 | "A GraphQL-formatted string representing the default value for this input value." 153 | defaultValue: String 154 | } 155 | 156 | """ 157 | A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all 158 | available types and directives on the server, as well as the entry points for 159 | query, mutation, and subscription operations. 160 | """ 161 | type __Schema { 162 | "A list of all types supported by this server." 163 | types: [__Type!]! 164 | "The type that query operations will be rooted at." 165 | queryType: __Type! 166 | "If this server supports mutation, the type that mutation operations will be rooted at." 167 | mutationType: __Type 168 | "If this server support subscription, the type that subscription operations will be rooted at." 169 | subscriptionType: __Type 170 | "A list of all directives supported by this server." 171 | directives: [__Directive!]! 172 | } 173 | 174 | """ 175 | The fundamental unit of any GraphQL Schema is the type. There are many kinds of 176 | types in GraphQL as represented by the `__TypeKind` enum. 177 | 178 | Depending on the kind of a type, certain fields describe information about that 179 | type. Scalar types provide no information beyond a name and description, while 180 | Enum types provide their values. Object and Interface types provide the fields 181 | they describe. Abstract types, Union and Interface, provide the Object types 182 | possible at runtime. List and NonNull types compose other types. 183 | """ 184 | type __Type { 185 | kind: __TypeKind! 186 | name: String 187 | description: String 188 | fields(includeDeprecated: Boolean = false): [__Field!] 189 | interfaces: [__Type!] 190 | possibleTypes: [__Type!] 191 | enumValues(includeDeprecated: Boolean = false): [__EnumValue!] 192 | inputFields: [__InputValue!] 193 | ofType: __Type 194 | } 195 | 196 | """ 197 | An enum describing what kind of type a given `__Type` is. 198 | """ 199 | enum __TypeKind { 200 | "Indicates this type is a scalar." 201 | SCALAR 202 | "Indicates this type is an object. `fields` and `interfaces` are valid fields." 203 | OBJECT 204 | "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields." 205 | INTERFACE 206 | "Indicates this type is a union. possibleTypes` is a valid field." 207 | UNION 208 | "Indicates this type is an enum. `enumValues` is a valid field." 209 | ENUM 210 | "Indicates this type is an input object. `inputFields` is a valid field." 211 | INPUT_OBJECT 212 | "Indicates this type is a list. `ofType` is a valid field." 213 | LIST 214 | "Indicates this type is a non-null. `ofType` is a valid field." 215 | NON_NULL 216 | } 217 | -------------------------------------------------------------------------------- /httpgql/ws-client.go: -------------------------------------------------------------------------------- 1 | package httpgql 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | url2 "net/url" 8 | "time" 9 | 10 | "github.com/chirino/graphql" 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | func (client *Client) Exec(ctx context.Context, result interface{}, query string, args ...interface{}) error { 15 | return graphql.Exec(client.ServeGraphQL, ctx, result, query, args...) 16 | } 17 | 18 | func (client *Client) ServeGraphQLStream(request *graphql.Request) graphql.ResponseStream { 19 | url := client.URL 20 | 21 | client.mu.Lock() 22 | c := client.connections[url] 23 | client.mu.Unlock() 24 | 25 | if c == nil { 26 | c = &wsConnection{ 27 | client: client, 28 | url: url, 29 | serviceCommands: make(chan interface{}), 30 | streams: map[int64]*wsOperation{}, 31 | idleFlag: false, 32 | } 33 | 34 | wsUrl, err := ToWsURL(client.URL) 35 | if err != nil { 36 | return graphql.NewErrStream(err) 37 | } 38 | 39 | headers := client.RequestHeader.Clone() 40 | headers.Set("Sec-WebSocket-Protocol", "graphql-ws") 41 | 42 | c.websocket, _, err = websocket.DefaultDialer.Dial(wsUrl, headers) 43 | if err != nil { 44 | return graphql.NewErrStream(err) 45 | } 46 | 47 | err = c.websocket.WriteJSON(OperationMessage{Type: "connection_init"}) 48 | if err != nil { 49 | c.websocket.Close() 50 | return graphql.NewErrStream(err) 51 | } 52 | 53 | op := OperationMessage{} 54 | err = c.websocket.ReadJSON(&op) 55 | if err != nil { 56 | c.websocket.Close() 57 | return graphql.NewErrStream(err) 58 | } 59 | if op.Type != "connection_ack" { 60 | c.websocket.Close() 61 | return graphql.NewErrStream(fmt.Errorf("protocol violation: expected an init message, but received: %v\n", op.Type)) 62 | } 63 | go service(c) 64 | 65 | client.mu.Lock() 66 | client.connections[url] = c 67 | client.mu.Unlock() 68 | } 69 | return c.ServeGraphQLStream(request) 70 | } 71 | 72 | func ToWsURL(url string) (string, error) { 73 | parsed, err := url2.Parse(url) 74 | if err != nil { 75 | return "", err 76 | } 77 | switch parsed.Scheme { 78 | case "http": 79 | parsed.Scheme = "ws" 80 | case "https": 81 | parsed.Scheme = "wss" 82 | } 83 | wsUrl := parsed.String() 84 | return wsUrl, nil 85 | } 86 | 87 | type wsConnection struct { 88 | client *Client 89 | url string 90 | serviceCommands chan interface{} 91 | websocket *websocket.Conn 92 | 93 | // these fields should only be mutated by the service* methods. 94 | nextStreamId int64 95 | streams map[int64]*wsOperation 96 | idleFlag bool 97 | err error 98 | } 99 | 100 | func (c *wsConnection) Close() { 101 | c.serviceCommands <- "close" 102 | close(c.serviceCommands) 103 | } 104 | 105 | func (c *wsConnection) ServeGraphQLStream(request *graphql.Request) graphql.ResponseStream { 106 | stream := &wsOperation{ 107 | request: request, 108 | responseChannel: make(chan *graphql.Response, 1), 109 | } 110 | c.serviceCommands <- stream 111 | return stream.responseChannel 112 | } 113 | 114 | func service(c *wsConnection) { 115 | ticker := time.NewTicker(60 * time.Second) 116 | defer func() { 117 | c.websocket.Close() 118 | ticker.Stop() 119 | }() 120 | 121 | go func() { 122 | 123 | // This is the read side goroutine 124 | defer func() { 125 | close(c.serviceCommands) 126 | c.client.mu.Lock() 127 | if c.client.connections[c.url] == c { 128 | delete(c.client.connections, c.url) 129 | } 130 | c.client.mu.Unlock() 131 | }() 132 | 133 | for { 134 | o := OperationMessage{} 135 | err := c.websocket.ReadJSON(&o) 136 | if err != nil { 137 | c.serviceCommands <- err 138 | return 139 | } 140 | c.serviceCommands <- o 141 | } 142 | }() 143 | 144 | for { 145 | select { 146 | case command := <-c.serviceCommands: 147 | switch command := command.(type) { 148 | case *wsOperation: 149 | c.idleFlag = false 150 | serviceOpenOperation(c, command) 151 | case closeOperation: 152 | c.idleFlag = false 153 | serviceCloseOperation(c, command) 154 | case OperationMessage: 155 | c.idleFlag = false 156 | serviceRead(c, command) 157 | case error: 158 | serviceError(c, command) 159 | return 160 | case string: 161 | if command == "close" { 162 | serviceClose(c) 163 | return 164 | } 165 | } 166 | 167 | //case "shutdown": 168 | // log.Println("interrupt") 169 | // 170 | // // Cleanly close the connection by sending a close message and then 171 | // // waiting (with timeout) for the server to close the connection. 172 | // err := c.websocket.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 173 | // if err != nil { 174 | // log.Println("write close:", err) 175 | // return 176 | // } 177 | // select { 178 | // case <-c.commands: 179 | // case <-time.After(time.Second): 180 | // } 181 | // return 182 | 183 | case <-ticker.C: 184 | if c.idleFlag && len(c.streams) == 0 { 185 | 186 | } 187 | c.idleFlag = true 188 | } 189 | } 190 | 191 | } 192 | 193 | func serviceClose(c *wsConnection) { 194 | err := c.websocket.WriteJSON(OperationMessage{ 195 | Type: "connection_terminate", 196 | }) 197 | if err != nil { 198 | serviceError(c, err) 199 | return 200 | } 201 | err = c.websocket.Close() 202 | if err != nil { 203 | serviceError(c, err) 204 | return 205 | } 206 | } 207 | 208 | type wsOperation struct { 209 | id int64 210 | request *graphql.Request 211 | responseChannel chan *graphql.Response 212 | } 213 | 214 | func serviceOpenOperation(c *wsConnection, stream *wsOperation) { 215 | payload, err := json.Marshal(stream.request) 216 | if err != nil { 217 | stream.responseChannel <- graphql.NewResponse().AddError(err) 218 | close(stream.responseChannel) 219 | return 220 | } 221 | stream.request = nil 222 | streamId := c.nextStreamId 223 | c.nextStreamId += 1 224 | err = c.websocket.WriteJSON(OperationMessage{ 225 | Id: streamId, 226 | Type: "start", 227 | Payload: payload, 228 | }) 229 | if err != nil { 230 | stream.responseChannel <- graphql.NewResponse().AddError(err) 231 | close(stream.responseChannel) 232 | return 233 | } 234 | stream.id = streamId 235 | c.streams[streamId] = stream 236 | } 237 | 238 | type closeOperation struct { 239 | stream *wsOperation 240 | } 241 | 242 | func serviceCloseOperation(c *wsConnection, command closeOperation) { 243 | id := command.stream.id 244 | if c.streams[id] == nil { 245 | return // it was already closed... 246 | } 247 | 248 | // let the server know we want it to close the stream. 249 | // it wil respond with a complete. 250 | c.websocket.WriteJSON(OperationMessage{ 251 | Id: id, 252 | Type: "stop", 253 | }) 254 | } 255 | 256 | func serviceError(c *wsConnection, err error) { 257 | for _, s := range c.streams { 258 | r := &graphql.Response{} 259 | r.AddError(err) 260 | s.responseChannel <- r 261 | close(s.responseChannel) 262 | } 263 | c.streams = map[int64]*wsOperation{} 264 | c.err = err 265 | } 266 | 267 | func serviceRead(c *wsConnection, command OperationMessage) { 268 | 269 | switch command.Type { 270 | case "data": 271 | id := command.Id.(float64) 272 | stream := serviceOperation(c, int64(id)) 273 | if stream == nil { 274 | return 275 | } 276 | response := &graphql.Response{} 277 | err := json.Unmarshal(command.Payload, &response) 278 | if err != nil { 279 | response.AddError(err) 280 | } 281 | stream.responseChannel <- response 282 | 283 | case "complete": 284 | id := command.Id.(float64) 285 | stream := serviceOperation(c, int64(id)) 286 | if stream == nil { 287 | return 288 | } 289 | delete(c.streams, stream.id) 290 | close(stream.responseChannel) 291 | 292 | case "ka": 293 | // keep alive. 294 | case "connection_ack": 295 | // server accepted the connection 296 | case "connection_error": 297 | serviceError(c, fmt.Errorf("graphql connection error: %s", string(command.Payload))) 298 | } 299 | } 300 | 301 | func serviceOperation(c *wsConnection, id int64) *wsOperation { 302 | stream := c.streams[id] 303 | if stream == nil { 304 | serviceError(c, fmt.Errorf("invalid operation id received: %v", id)) 305 | return nil 306 | } 307 | return stream 308 | } 309 | -------------------------------------------------------------------------------- /schema/query.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "text/scanner" 7 | 8 | "github.com/chirino/graphql/internal/lexer" 9 | "github.com/chirino/graphql/qerrors" 10 | ) 11 | 12 | type QueryDocument 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 { 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 { 33 | return f 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | type SelectionList []Selection 40 | 41 | type Operation struct { 42 | Type OperationType 43 | Name string 44 | NameLoc Location 45 | Vars InputValueList 46 | Selections SelectionList 47 | Directives DirectiveList 48 | Loc qerrors.Location 49 | } 50 | 51 | type Fragment struct { 52 | On TypeName 53 | Selections SelectionList 54 | } 55 | 56 | type FragmentDecl struct { 57 | Fragment 58 | Name string 59 | NameLoc Location 60 | Directives DirectiveList 61 | Loc qerrors.Location 62 | } 63 | 64 | type Selection interface { 65 | Formatter 66 | GetSelections(doc *QueryDocument) SelectionList 67 | SetSelections(doc *QueryDocument, v SelectionList) 68 | Location() qerrors.Location 69 | } 70 | 71 | type FieldSelection struct { 72 | Alias string 73 | AliasLoc Location 74 | Name string 75 | NameLoc Location 76 | Arguments ArgumentList 77 | Directives DirectiveList 78 | Selections SelectionList 79 | SelectionSetLoc qerrors.Location 80 | Schema *FieldSchema 81 | Extension interface{} 82 | } 83 | 84 | type FieldSchema struct { 85 | Field *Field 86 | Parent NamedType 87 | } 88 | 89 | type InlineFragment struct { 90 | Fragment 91 | Directives DirectiveList 92 | Loc qerrors.Location 93 | } 94 | 95 | type FragmentSpread struct { 96 | Name string 97 | NameLoc Location 98 | Directives DirectiveList 99 | Loc qerrors.Location 100 | } 101 | 102 | func (t *Operation) SetSelections(doc *QueryDocument, v SelectionList) { t.Selections = v } 103 | func (t *FieldSelection) SetSelections(doc *QueryDocument, v SelectionList) { t.Selections = v } 104 | func (t *InlineFragment) SetSelections(doc *QueryDocument, v SelectionList) { t.Selections = v } 105 | func (t *FragmentSpread) SetSelections(doc *QueryDocument, v SelectionList) { 106 | frag := doc.Fragments.Get(t.Name) 107 | if frag == nil { 108 | return 109 | } 110 | frag.Selections = v 111 | } 112 | 113 | func (t Operation) GetSelections(doc *QueryDocument) SelectionList { return t.Selections } 114 | func (t FieldSelection) GetSelections(doc *QueryDocument) SelectionList { return t.Selections } 115 | func (t InlineFragment) GetSelections(doc *QueryDocument) SelectionList { return t.Selections } 116 | func (t FragmentSpread) GetSelections(doc *QueryDocument) SelectionList { 117 | frag := doc.Fragments.Get(t.Name) 118 | if frag == nil { 119 | return nil 120 | } 121 | return frag.Selections 122 | } 123 | 124 | func (t Operation) Location() qerrors.Location { return t.Loc } 125 | func (t FieldSelection) Location() qerrors.Location { return t.NameLoc } 126 | func (t InlineFragment) Location() qerrors.Location { return t.Loc } 127 | func (t FragmentSpread) Location() qerrors.Location { return t.Loc } 128 | 129 | func (doc *QueryDocument) ParseWithDescriptions(queryString string) error { 130 | l := lexer.Get(queryString) 131 | err := l.CatchSyntaxError(func() { parseDocument(l, doc) }) 132 | lexer.Put(l) 133 | return err 134 | } 135 | 136 | func (doc *QueryDocument) Parse(queryString string) error { 137 | l := lexer.Get(queryString) 138 | l.SkipDescriptions = true 139 | err := l.CatchSyntaxError(func() { parseDocument(l, doc) }) 140 | lexer.Put(l) 141 | return err 142 | } 143 | 144 | func parseDocument(l *lexer.Lexer, d *QueryDocument) { 145 | l.Consume() 146 | for l.Peek() != scanner.EOF { 147 | if l.Peek() == '{' { 148 | op := &Operation{Type: Query, Loc: l.Location()} 149 | op.Selections = parseSelectionSet(l) 150 | d.Operations = append(d.Operations, op) 151 | continue 152 | } 153 | 154 | loc := l.Location() 155 | switch x := l.ConsumeIdentIntern(); x { 156 | case "query": 157 | op := parseOperation(l, Query) 158 | op.Loc = loc 159 | d.Operations = append(d.Operations, op) 160 | 161 | case "mutation": 162 | d.Operations = append(d.Operations, parseOperation(l, Mutation)) 163 | 164 | case "subscription": 165 | d.Operations = append(d.Operations, parseOperation(l, Subscription)) 166 | 167 | case "fragment": 168 | frag := parseFragment(l) 169 | frag.Loc = loc 170 | d.Fragments = append(d.Fragments, frag) 171 | 172 | default: 173 | l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "fragment"`, x)) 174 | } 175 | } 176 | } 177 | 178 | func parseOperation(l *lexer.Lexer, opType OperationType) *Operation { 179 | op := &Operation{Type: opType} 180 | op.Loc = l.Location() 181 | if l.Peek() == scanner.Ident { 182 | op.Name, op.NameLoc = l.ConsumeIdentInternWithLoc() 183 | } 184 | op.Directives = ParseDirectives(l) 185 | if l.Peek() == '(' { 186 | l.ConsumeToken('(') 187 | for l.Peek() != ')' { 188 | loc := l.Location() 189 | l.ConsumeToken('$') 190 | iv := ParseInputValue(l) 191 | iv.Name = "$" + iv.Name 192 | iv.Loc = loc 193 | op.Vars = append(op.Vars, iv) 194 | } 195 | l.ConsumeToken(')') 196 | } 197 | op.Selections = parseSelectionSet(l) 198 | return op 199 | } 200 | 201 | func parseFragment(l *lexer.Lexer) *FragmentDecl { 202 | f := &FragmentDecl{} 203 | f.Name, f.NameLoc = l.ConsumeIdentInternWithLoc() 204 | l.ConsumeKeyword("on") 205 | f.On = parseTypeName(l) 206 | f.Directives = ParseDirectives(l) 207 | f.Selections = parseSelectionSet(l) 208 | return f 209 | } 210 | 211 | func parseSelectionSet(l *lexer.Lexer) SelectionList { 212 | var sels []Selection 213 | l.ConsumeToken('{') 214 | for l.Peek() != '}' { 215 | sels = append(sels, parseSelection(l)) 216 | } 217 | l.ConsumeToken('}') 218 | return sels 219 | } 220 | 221 | func parseSelection(l *lexer.Lexer) Selection { 222 | if l.Peek() == '.' { 223 | return parseSpread(l) 224 | } 225 | return parseField(l) 226 | } 227 | 228 | var fieldPool = sync.Pool{ 229 | New: func() interface{} { return new(FieldSelection) }, 230 | } 231 | 232 | func returnFieldsToPool(sl SelectionList) { 233 | for _, s := range sl { 234 | switch s := s.(type) { 235 | case *FieldSelection: 236 | returnFieldsToPool(s.Selections) 237 | *s = FieldSelection{} 238 | fieldPool.Put(s) 239 | case *InlineFragment: 240 | returnFieldsToPool(s.Selections) 241 | } 242 | } 243 | } 244 | 245 | func (d *QueryDocument) Close() { 246 | for _, o := range d.Operations { 247 | returnFieldsToPool(o.Selections) 248 | } 249 | d.Operations = OperationList{} 250 | for _, f := range d.Fragments { 251 | returnFieldsToPool(f.Selections) 252 | } 253 | d.Fragments = FragmentList{} 254 | } 255 | 256 | func parseField(l *lexer.Lexer) *FieldSelection { 257 | f := fieldPool.Get().(*FieldSelection) 258 | f.Alias, f.AliasLoc = l.ConsumeIdentInternWithLoc() 259 | f.Name = f.Alias 260 | if l.Peek() == ':' { 261 | l.ConsumeToken(':') 262 | f.Name, f.NameLoc = l.ConsumeIdentInternWithLoc() 263 | } 264 | if l.Peek() == '(' { 265 | f.Arguments = ParseArguments(l) 266 | } 267 | f.Directives = ParseDirectives(l) 268 | if l.Peek() == '{' { 269 | f.SelectionSetLoc = l.Location() 270 | f.Selections = parseSelectionSet(l) 271 | } 272 | return f 273 | } 274 | 275 | func parseSpread(l *lexer.Lexer) Selection { 276 | loc := l.Location() 277 | l.ConsumeToken('.') 278 | l.ConsumeToken('.') 279 | l.ConsumeToken('.') 280 | 281 | f := &InlineFragment{Loc: loc} 282 | if l.Peek() == scanner.Ident { 283 | ident, identLoc := l.ConsumeIdentInternWithLoc() 284 | if ident != "on" { 285 | fs := &FragmentSpread{ 286 | Name: ident, 287 | NameLoc: identLoc, 288 | Loc: loc, 289 | } 290 | fs.Directives = ParseDirectives(l) 291 | return fs 292 | } 293 | f.On = parseTypeName(l) 294 | } 295 | f.Directives = ParseDirectives(l) 296 | f.Selections = parseSelectionSet(l) 297 | return f 298 | } 299 | 300 | func (d *QueryDocument) GetOperation(operationName string) (*Operation, error) { 301 | if len(d.Operations) == 0 { 302 | return nil, fmt.Errorf("no operations in query document") 303 | } 304 | 305 | if operationName == "" { 306 | if len(d.Operations) > 1 { 307 | return nil, fmt.Errorf("more than one operation in query document and no operation name given") 308 | } 309 | for _, op := range d.Operations { 310 | return op, nil // return the one and only operation 311 | } 312 | } 313 | 314 | op := d.Operations.Get(operationName) 315 | if op == nil { 316 | return nil, fmt.Errorf("no operation with name %q", operationName) 317 | } 318 | return op, nil 319 | } 320 | -------------------------------------------------------------------------------- /schema/schema_format.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/chirino/graphql/internal/lexer" 7 | "github.com/chirino/graphql/text" 8 | "io" 9 | "reflect" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | func (t *List) WriteTo(out io.StringWriter) { 16 | out.WriteString("[") 17 | t.OfType.WriteTo(out) 18 | out.WriteString("]") 19 | } 20 | func (t *NonNull) WriteTo(out io.StringWriter) { 21 | out.WriteString("!") 22 | t.OfType.WriteTo(out) 23 | } 24 | func (t *TypeName) WriteTo(out io.StringWriter) { 25 | out.WriteString(t.Name) 26 | } 27 | 28 | func (s *Schema) WriteTo(out io.StringWriter) { 29 | 30 | for _, entry := range mapToSortedArray(s.DeclaredDirectives) { 31 | value := entry.Value.(*DirectiveDecl) 32 | if isBuiltIn(value) { 33 | continue 34 | } 35 | value.WriteTo(out) 36 | } 37 | 38 | for _, entry := range mapToSortedArray(s.Types) { 39 | value := entry.Value.(NamedType) 40 | if isBuiltIn(value) { 41 | continue 42 | } 43 | value.WriteTo(out) 44 | } 45 | 46 | out.WriteString("schema {\n") 47 | for _, entry := range mapToSortedArray(s.EntryPoints) { 48 | key := entry.Key.(OperationType) 49 | value := entry.Value.(NamedType) 50 | out.WriteString(fmt.Sprintf(" %s: %s\n", key, value.TypeName())) 51 | } 52 | out.WriteString("}\n") 53 | } 54 | 55 | func (t *Scalar) WriteTo(out io.StringWriter) { 56 | writeDescription(out, t.Desc) 57 | out.WriteString("scalar ") 58 | out.WriteString(t.Name) 59 | out.WriteString("\n") 60 | } 61 | 62 | func (t *DirectiveDecl) WriteTo(out io.StringWriter) { 63 | writeDescription(out, t.Desc) 64 | out.WriteString("directive @") 65 | out.WriteString(t.Name) 66 | t.Args.WriteTo(out) 67 | out.WriteString(" on ") 68 | for i, loc := range t.Locs { 69 | if i != 0 { 70 | out.WriteString(" | ") 71 | } 72 | out.WriteString(loc) 73 | } 74 | out.WriteString("\n") 75 | } 76 | 77 | func (t *Object) WriteTo(out io.StringWriter) { 78 | writeDescription(out, t.Desc) 79 | out.WriteString("type ") 80 | out.WriteString(t.Name) 81 | if len(t.Interfaces) > 0 { 82 | out.WriteString(" implements") 83 | for i, intf := range t.Interfaces { 84 | if i != 0 { 85 | out.WriteString(" &") 86 | } 87 | out.WriteString(" ") 88 | out.WriteString(intf.Name) 89 | } 90 | out.WriteString(" ") 91 | t.Directives.WriteTo(out) 92 | } 93 | if len(t.Directives) > 0 { 94 | out.WriteString(" ") 95 | t.Directives.WriteTo(out) 96 | } 97 | out.WriteString(" {\n") 98 | for _, f := range t.Fields { 99 | i := &indent{} 100 | f.WriteTo(i) 101 | i.WriteString("\n") 102 | i.Done(out) 103 | } 104 | out.WriteString("}\n") 105 | } 106 | 107 | type indent struct { 108 | bytes.Buffer 109 | } 110 | 111 | func (i indent) Done(out io.StringWriter) { 112 | out.WriteString(text.Indent(i.String(), " ")) 113 | } 114 | 115 | func (t *Interface) WriteTo(out io.StringWriter) { 116 | writeDescription(out, t.Desc) 117 | out.WriteString("interface ") 118 | out.WriteString(t.Name) 119 | out.WriteString(" {\n") 120 | for _, f := range t.Fields { 121 | i := &indent{} 122 | f.WriteTo(i) 123 | i.WriteString("\n") 124 | i.Done(out) 125 | } 126 | out.WriteString("}\n") 127 | } 128 | func (t *Union) WriteTo(out io.StringWriter) { 129 | writeDescription(out, t.Desc) 130 | out.WriteString("union ") 131 | out.WriteString(t.Name) 132 | out.WriteString(" = ") 133 | for i, f := range t.PossibleTypes { 134 | if i != 0 { 135 | out.WriteString(" | ") 136 | } 137 | out.WriteString(f.Name) 138 | } 139 | out.WriteString("\n") 140 | } 141 | 142 | func (t *Enum) WriteTo(out io.StringWriter) { 143 | writeDescription(out, t.Desc) 144 | out.WriteString("enum ") 145 | out.WriteString(t.Name) 146 | out.WriteString(" {\n") 147 | for _, f := range t.Values { 148 | i := &indent{} 149 | f.WriteTo(i) 150 | i.WriteString("\n") 151 | i.Done(out) 152 | } 153 | out.WriteString("}\n") 154 | } 155 | 156 | func (t *InputObject) WriteTo(out io.StringWriter) { 157 | writeDescription(out, t.Desc) 158 | out.WriteString("input ") 159 | out.WriteString(t.Name) 160 | out.WriteString(" {\n") 161 | for _, f := range t.Fields { 162 | i := &indent{} 163 | f.WriteTo(i) 164 | i.WriteString("\n") 165 | i.Done(out) 166 | } 167 | out.WriteString("}\n") 168 | } 169 | 170 | func (t *EnumValue) WriteTo(out io.StringWriter) { 171 | writeDescription(out, t.Desc) 172 | out.WriteString(t.Name) 173 | t.Directives.WriteTo(out) 174 | } 175 | 176 | func (t *Field) WriteTo(out io.StringWriter) { 177 | writeDescription(out, t.Desc) 178 | out.WriteString(t.Name) 179 | t.Args.WriteTo(out) 180 | out.WriteString(":") 181 | out.WriteString(t.Type.String()) 182 | t.Directives.WriteTo(out) 183 | } 184 | 185 | func (t DirectiveList) WriteTo(out io.StringWriter) { 186 | if len(t) > 0 { 187 | for i, d := range t { 188 | if i != 0 { 189 | out.WriteString(", ") 190 | } 191 | d.WriteTo(out) 192 | } 193 | } 194 | } 195 | 196 | func (t *Directive) WriteTo(out io.StringWriter) { 197 | out.WriteString("@") 198 | out.WriteString(t.Name) 199 | t.Args.WriteTo(out) 200 | } 201 | 202 | func (t ArgumentList) WriteTo(out io.StringWriter) { 203 | if len(t) > 0 { 204 | out.WriteString("(") 205 | for i, v := range t { 206 | if i != 0 { 207 | out.WriteString(", ") 208 | } 209 | v.WriteTo(out) 210 | } 211 | out.WriteString(")") 212 | } 213 | } 214 | 215 | func (t *Argument) WriteTo(out io.StringWriter) { 216 | out.WriteString(t.Name) 217 | out.WriteString(":") 218 | t.Value.WriteTo(out) 219 | } 220 | 221 | func (t InputValueList) WriteTo(out io.StringWriter) { 222 | size := len(t) 223 | if size > 0 { 224 | var indentBlock *indent 225 | 226 | out.WriteString("(") 227 | 228 | b := bytes.Buffer{} 229 | for _, v := range t { 230 | b.Reset() 231 | v.WriteTo(&b) 232 | arg := b.String() 233 | if strings.Contains(arg, "\n") { 234 | out.WriteString("\n") 235 | indentBlock = &indent{} 236 | break 237 | } 238 | } 239 | 240 | for i, v := range t { 241 | 242 | b.Reset() 243 | v.WriteTo(&b) 244 | arg := b.String() 245 | if indentBlock!=nil { 246 | indentBlock.WriteString(arg) 247 | if i != size-1 { 248 | indentBlock.WriteString(",") 249 | } 250 | indentBlock.WriteString("\n") 251 | } else { 252 | if i != 0 { 253 | out.WriteString(", ") 254 | } 255 | out.WriteString(arg) 256 | } 257 | 258 | } 259 | if indentBlock !=nil { 260 | indentBlock.Done(out) 261 | } 262 | out.WriteString(")") 263 | } 264 | } 265 | 266 | func (t *InputValue) WriteTo(out io.StringWriter) { 267 | writeDescription(out, t.Desc) 268 | out.WriteString(t.Name) 269 | out.WriteString(":") 270 | out.WriteString(t.Type.String()) 271 | if t.Default != nil { 272 | out.WriteString("=") 273 | t.Default.WriteTo(out) 274 | } 275 | } 276 | 277 | func (lit *BasicLit) WriteTo(out io.StringWriter) { 278 | out.WriteString(lit.Text) 279 | } 280 | func (lit *ListLit) WriteTo(out io.StringWriter) { 281 | out.WriteString("[") 282 | for i, v := range lit.Entries { 283 | if i != 0 { 284 | out.WriteString(", ") 285 | } 286 | v.WriteTo(out) 287 | } 288 | out.WriteString("]") 289 | } 290 | func (lit *NullLit) WriteTo(out io.StringWriter) { 291 | out.WriteString("null") 292 | } 293 | func (t *ObjectLit) WriteTo(out io.StringWriter) { 294 | out.WriteString("{") 295 | for i, v := range t.Fields { 296 | if i != 0 { 297 | out.WriteString(", ") 298 | } 299 | out.WriteString(v.Name) 300 | out.WriteString(": ") 301 | v.Value.WriteTo(out) 302 | } 303 | out.WriteString("}") 304 | } 305 | func (lit *Variable) WriteTo(out io.StringWriter) { 306 | out.WriteString("$") 307 | out.WriteString(lit.Name) 308 | } 309 | 310 | func writeDescription(out io.StringWriter, desc Description) { 311 | if desc.ShowType == lexer.NoDescription || desc.Text == "" { 312 | return 313 | } 314 | showType := desc.ShowType 315 | text := desc.Text 316 | if showType == lexer.PossibleDescription && strings.Contains(desc.Text, "\n") { 317 | text = "\n" + text + "\n" 318 | showType = lexer.ShowBlockDescription 319 | } 320 | if showType == lexer.ShowBlockDescription { 321 | out.WriteString(`"""`) 322 | out.WriteString(text) 323 | out.WriteString(`"""` + "\n") 324 | } else { 325 | out.WriteString(strconv.Quote(text)) 326 | out.WriteString("\n") 327 | } 328 | } 329 | 330 | type entry struct { 331 | Key interface{} 332 | Value interface{} 333 | } 334 | 335 | func mapToSortedArray(m interface{}) []entry { 336 | var result []entry 337 | iter := reflect.ValueOf(m).MapRange() 338 | for iter.Next() { 339 | result = append(result, entry{ 340 | Key: iter.Key().Interface(), 341 | Value: iter.Value().Interface(), 342 | }) 343 | } 344 | sort.Slice(result, func(i, j int) bool { 345 | key1 := fmt.Sprintf("%s", result[i].Key) 346 | key2 := fmt.Sprintf("%s", result[j].Key) 347 | return key1 < key2 348 | }) 349 | return result 350 | } 351 | func isBuiltIn(m interface{}) bool { 352 | for _, t := range Meta.Types { 353 | if m == t { 354 | return true 355 | } 356 | } 357 | for _, d := range Meta.DeclaredDirectives { 358 | if m == d { 359 | return true 360 | } 361 | } 362 | return false 363 | } 364 | -------------------------------------------------------------------------------- /resolvers/field_resolver_reflect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Most of this file was reused from the ecoding/json package, 6 | // since we want to use the same json encoding. 7 | package resolvers 8 | 9 | import ( 10 | "reflect" 11 | "sort" 12 | "strings" 13 | "unicode" 14 | ) 15 | 16 | var childFieldTypeCache Cache 17 | 18 | func getChildField(parent *reflect.Value, fieldName string) (*reflect.Value, bool) { 19 | parentType := parent.Type() 20 | // use a cache to make subsequent lookups cheap 21 | fields := childFieldTypeCache.GetOrElseUpdate(parentType, func() interface{} { 22 | fieldMap := map[string]field{} 23 | for _, field := range typeFields(parentType) { 24 | fieldMap[field.name] = field 25 | } 26 | return fieldMap 27 | }).(map[string]field) 28 | 29 | field, found := fields[fieldName] 30 | if !found { 31 | return nil, false 32 | } 33 | child := fieldByIndex(*parent, field.index) 34 | return &child, true 35 | } 36 | 37 | func typeFields(t reflect.Type) []field { 38 | // Anonymous fields to explore at the current level and the next. 39 | current := []field{} 40 | next := []field{{typ: t}} 41 | 42 | // Count of queued names for current level and the next. 43 | count := map[reflect.Type]int{} 44 | nextCount := map[reflect.Type]int{} 45 | 46 | // Types already visited at an earlier level. 47 | visited := map[reflect.Type]bool{} 48 | 49 | // Fields found. 50 | var fields []field 51 | 52 | for len(next) > 0 { 53 | current, next = next, current[:0] 54 | count, nextCount = nextCount, map[reflect.Type]int{} 55 | 56 | for _, f := range current { 57 | if visited[f.typ] { 58 | continue 59 | } 60 | visited[f.typ] = true 61 | 62 | // Scan f.typ for fields to include. 63 | for i := 0; i < f.typ.NumField(); i++ { 64 | sf := f.typ.Field(i) 65 | isUnexported := sf.PkgPath != "" 66 | if sf.Anonymous { 67 | t := sf.Type 68 | if t.Kind() == reflect.Ptr { 69 | t = t.Elem() 70 | } 71 | if isUnexported && t.Kind() != reflect.Struct { 72 | // Ignore embedded fields of unexported non-struct types. 73 | continue 74 | } 75 | // Do not ignore embedded fields of unexported struct types 76 | // since they may have exported fields. 77 | } else if isUnexported { 78 | // Ignore unexported non-embedded fields. 79 | continue 80 | } 81 | tag := sf.Tag.Get("json") 82 | if tag == "-" { 83 | continue 84 | } 85 | name, opts := parseTag(tag) 86 | if !isValidTag(name) { 87 | name = "" 88 | } 89 | index := make([]int, len(f.index)+1) 90 | copy(index, f.index) 91 | index[len(f.index)] = i 92 | 93 | ft := sf.Type 94 | if ft.Name() == "" && ft.Kind() == reflect.Ptr { 95 | // Follow pointer. 96 | ft = ft.Elem() 97 | } 98 | 99 | // Only strings, floats, integers, and booleans can be quoted. 100 | quoted := false 101 | if opts.Contains("string") { 102 | switch ft.Kind() { 103 | case reflect.Bool, 104 | reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 105 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, 106 | reflect.Float32, reflect.Float64, 107 | reflect.String: 108 | quoted = true 109 | } 110 | } 111 | 112 | // Record found field and index sequence. 113 | if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { 114 | tagged := name != "" 115 | if name == "" { 116 | name = sf.Name 117 | } 118 | fields = append(fields, field{ 119 | name: name, 120 | tag: tagged, 121 | index: index, 122 | typ: ft, 123 | quoted: quoted, 124 | }) 125 | if count[f.typ] > 1 { 126 | // If there were multiple instances, add a second, 127 | // so that the annihilation code will see a duplicate. 128 | // It only cares about the distinction between 1 or 2, 129 | // so don't bother generating any more copies. 130 | fields = append(fields, fields[len(fields)-1]) 131 | } 132 | continue 133 | } 134 | 135 | // Record new anonymous struct to explore in next round. 136 | nextCount[ft]++ 137 | if nextCount[ft] == 1 { 138 | next = append(next, field{name: ft.Name(), index: index, typ: ft}) 139 | } 140 | } 141 | } 142 | } 143 | 144 | sort.Slice(fields, func(i, j int) bool { 145 | x := fields 146 | // sort field by name, breaking ties with depth, then 147 | // breaking ties with "name came from json tag", then 148 | // breaking ties with index sequence. 149 | if x[i].name != x[j].name { 150 | return x[i].name < x[j].name 151 | } 152 | if len(x[i].index) != len(x[j].index) { 153 | return len(x[i].index) < len(x[j].index) 154 | } 155 | if x[i].tag != x[j].tag { 156 | return x[i].tag 157 | } 158 | return byIndex(x).Less(i, j) 159 | }) 160 | 161 | // Delete all fields that are hidden by the Go rules for embedded fields, 162 | // except that fields with JSON tags are promoted. 163 | 164 | // The fields are sorted in primary order of name, secondary order 165 | // of field index length. Loop over names; for each name, delete 166 | // hidden fields by choosing the one dominant field that survives. 167 | out := fields[:0] 168 | for advance, i := 0, 0; i < len(fields); i += advance { 169 | // One iteration per name. 170 | // Find the sequence of fields with the name of this first field. 171 | fi := fields[i] 172 | name := fi.name 173 | for advance = 1; i+advance < len(fields); advance++ { 174 | fj := fields[i+advance] 175 | if fj.name != name { 176 | break 177 | } 178 | } 179 | if advance == 1 { // Only one field with this name 180 | out = append(out, fi) 181 | continue 182 | } 183 | dominant, ok := dominantField(fields[i : i+advance]) 184 | if ok { 185 | out = append(out, dominant) 186 | } 187 | } 188 | 189 | fields = out 190 | sort.Sort(byIndex(fields)) 191 | return fields 192 | } 193 | 194 | type tagOptions string 195 | 196 | // parseTag splits a struct field's json tag into its name and 197 | // comma-separated options. 198 | func parseTag(tag string) (string, tagOptions) { 199 | if idx := strings.Index(tag, ","); idx != -1 { 200 | return tag[:idx], tagOptions(tag[idx+1:]) 201 | } 202 | return tag, tagOptions("") 203 | } 204 | 205 | // Contains reports whether a comma-separated list of options 206 | // contains a particular substr flag. substr must be surrounded by a 207 | // string boundary or commas. 208 | func (o tagOptions) Contains(optionName string) bool { 209 | if len(o) == 0 { 210 | return false 211 | } 212 | s := string(o) 213 | for s != "" { 214 | var next string 215 | i := strings.Index(s, ",") 216 | if i >= 0 { 217 | s, next = s[:i], s[i+1:] 218 | } 219 | if s == optionName { 220 | return true 221 | } 222 | s = next 223 | } 224 | return false 225 | } 226 | 227 | type field struct { 228 | name string 229 | 230 | tag bool 231 | index []int 232 | typ reflect.Type 233 | quoted bool 234 | } 235 | 236 | // dominantField looks through the fields, all of which are known to 237 | // have the same name, to find the single field that dominates the 238 | // others using Go's embedding rules, modified by the presence of 239 | // JSON tags. If there are multiple top-level fields, the boolean 240 | // will be false: This condition is an error in Go and we skip all 241 | // the fields. 242 | func dominantField(fields []field) (field, bool) { 243 | // The fields are sorted in increasing index-length order. The winner 244 | // must therefore be one with the shortest index length. Drop all 245 | // longer entries, which is easy: just truncate the slice. 246 | length := len(fields[0].index) 247 | tagged := -1 // Index of first tagged field. 248 | for i, f := range fields { 249 | if len(f.index) > length { 250 | fields = fields[:i] 251 | break 252 | } 253 | if f.tag { 254 | if tagged >= 0 { 255 | // Multiple tagged fields at the same level: conflict. 256 | // Return no field. 257 | return field{}, false 258 | } 259 | tagged = i 260 | } 261 | } 262 | if tagged >= 0 { 263 | return fields[tagged], true 264 | } 265 | // All remaining fields have the same length. If there's more than one, 266 | // we have a conflict (two fields named "X" at the same level) and we 267 | // return no field. 268 | if len(fields) > 1 { 269 | return field{}, false 270 | } 271 | return fields[0], true 272 | } 273 | 274 | type byIndex []field 275 | 276 | func (x byIndex) Len() int { return len(x) } 277 | 278 | func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 279 | 280 | func (x byIndex) Less(i, j int) bool { 281 | for k, xik := range x[i].index { 282 | if k >= len(x[j].index) { 283 | return false 284 | } 285 | if xik != x[j].index[k] { 286 | return xik < x[j].index[k] 287 | } 288 | } 289 | return len(x[i].index) < len(x[j].index) 290 | } 291 | 292 | func isValidTag(s string) bool { 293 | if s == "" { 294 | return false 295 | } 296 | for _, c := range s { 297 | switch { 298 | case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): 299 | // Backslash and quote chars are reserved, but 300 | // otherwise any punctuation chars are allowed 301 | // in a tag name. 302 | default: 303 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) { 304 | return false 305 | } 306 | } 307 | } 308 | return true 309 | } 310 | 311 | func fieldByIndex(v reflect.Value, index []int) reflect.Value { 312 | for _, i := range index { 313 | if v.Kind() == reflect.Ptr { 314 | if v.IsNil() { 315 | return reflect.Value{} 316 | } 317 | v = v.Elem() 318 | } 319 | v = v.Field(i) 320 | } 321 | return v 322 | } 323 | --------------------------------------------------------------------------------