├── .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 |
--------------------------------------------------------------------------------