diff --git a/.circleci/config.yml b/.circleci/config.yml index 608cb8939..d121181be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ test_with_go_modules: &test_with_go_modules - run: go vet ./... test_without_go_modules: &test_without_go_modules - working_directory: /go/src/github.com/graphql-go/graphql + working_directory: /go/src/github.com/teamjobot/graphql steps: - checkout - run: go get -v -t -d ./... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9d4e59d3..b9d846253 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ This document is based on the [Node.js contribution guidelines](https://github.c ## Chat room -[![Join the chat at https://gitter.im/graphql-go/graphql](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/graphql-go/graphql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join the chat at https://gitter.im/teamjobot/graphql](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/teamjobot/graphql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Feel free to participate in the chat room for informal discussions and queries. @@ -55,8 +55,8 @@ The basics are as follows: 2. `go get` the upstream repo and set it up as the `upstream` remote and your own repo as the `origin` remote: ```bash -$ go get github.com/graphql-go/graphql -$ cd $GOPATH/src/github.com/graphql-go/graphql +$ go get github.com/teamjobot/graphql +$ cd $GOPATH/src/github.com/teamjobot/graphql $ git remote rename origin upstream $ git remote add origin git@github.com/YOUR_GITHUB_NAME/graphql ``` diff --git a/README.md b/README.md index 4bb203ae1..2d7482fb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# graphql [![CircleCI](https://circleci.com/gh/graphql-go/graphql/tree/master.svg?style=svg)](https://circleci.com/gh/graphql-go/graphql/tree/master) [![GoDoc](https://godoc.org/graphql.co/graphql?status.svg)](https://godoc.org/github.com/graphql-go/graphql) [![Coverage Status](https://coveralls.io/repos/github/graphql-go/graphql/badge.svg?branch=master)](https://coveralls.io/github/graphql-go/graphql?branch=master) [![Join the chat at https://gitter.im/graphql-go/graphql](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/graphql-go/graphql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# graphql [![CircleCI](https://circleci.com/gh/teamjobot/graphql/tree/master.svg?style=svg)](https://circleci.com/gh/teamjobot/graphql/tree/master) [![Go Reference](https://pkg.go.dev/badge/github.com/teamjobot/graphql.svg)](https://pkg.go.dev/github.com/teamjobot/graphql) [![Coverage Status](https://coveralls.io/repos/github/teamjobot/graphql/badge.svg?branch=master)](https://coveralls.io/github/teamjobot/graphql?branch=master) [![Join the chat at https://gitter.im/teamjobot/graphql](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/teamjobot/graphql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) An implementation of GraphQL in Go. Follows the official reference implementation [`graphql-js`](https://github.com/graphql/graphql-js). @@ -6,13 +6,13 @@ Supports: queries, mutations & subscriptions. ### Documentation -godoc: https://godoc.org/github.com/graphql-go/graphql +godoc: https://pkg.go.dev/github.com/teamjobot/graphql ### Getting Started To install the library, run: ```bash -go get github.com/graphql-go/graphql +go get github.com/teamjobot/graphql ``` The following is a simple example which defines a schema with a single `hello` string-type field and a `Resolve` method which returns the string `world`. A GraphQL query is performed against this schema with the resulting output printed in JSON format. @@ -25,7 +25,7 @@ import ( "fmt" "log" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) func main() { @@ -60,13 +60,13 @@ func main() { fmt.Printf("%s \n", rJSON) // {"data":{"hello":"world"}} } ``` -For more complex examples, refer to the [examples/](https://github.com/graphql-go/graphql/tree/master/examples/) directory and [graphql_test.go](https://github.com/graphql-go/graphql/blob/master/graphql_test.go). +For more complex examples, refer to the [examples/](https://github.com/teamjobot/graphql/tree/master/examples/) directory and [graphql_test.go](https://github.com/teamjobot/graphql/blob/master/graphql_test.go). ### Third Party Libraries | Name | Author | Description | |:-------------:|:-------------:|:------------:| -| [graphql-go-handler](https://github.com/graphql-go/graphql-go-handler) | [Hafiz Ismail](https://github.com/sogko) | Middleware to handle GraphQL queries through HTTP requests. | -| [graphql-relay-go](https://github.com/graphql-go/graphql-relay-go) | [Hafiz Ismail](https://github.com/sogko) | Lib to construct a graphql-go server supporting react-relay. | +| [teamjobot-handler](https://github.com/teamjobot/teamjobot-handler) | [Hafiz Ismail](https://github.com/sogko) | Middleware to handle GraphQL queries through HTTP requests. | +| [graphql-relay-go](https://github.com/teamjobot/graphql-relay-go) | [Hafiz Ismail](https://github.com/sogko) | Lib to construct a teamjobot server supporting react-relay. | | [golang-relay-starter-kit](https://github.com/sogko/golang-relay-starter-kit) | [Hafiz Ismail](https://github.com/sogko) | Barebones starting point for a Relay application with Golang GraphQL server. | | [dataloader](https://github.com/nicksrandall/dataloader) | [Nick Randall](https://github.com/nicksrandall) | [DataLoader](https://github.com/facebook/dataloader) implementation in Go. | diff --git a/abstract_test.go b/abstract_test.go index 0a2da642f..2bae5bbd3 100644 --- a/abstract_test.go +++ b/abstract_test.go @@ -4,10 +4,10 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) type testDog struct { diff --git a/benchutil/list_schema.go b/benchutil/list_schema.go index 196a7f5c3..69581931b 100644 --- a/benchutil/list_schema.go +++ b/benchutil/list_schema.go @@ -3,7 +3,7 @@ package benchutil import ( "fmt" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) type color struct { diff --git a/benchutil/wide_schema.go b/benchutil/wide_schema.go index 1177fce66..056e823d2 100644 --- a/benchutil/wide_schema.go +++ b/benchutil/wide_schema.go @@ -3,7 +3,7 @@ package benchutil import ( "fmt" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) func WideSchemaWithXFieldsAndYItems(x int, y int) graphql.Schema { diff --git a/bind.go b/bind.go new file mode 100644 index 000000000..d8c8dfcd7 --- /dev/null +++ b/bind.go @@ -0,0 +1,251 @@ +package graphql + +import ( + "context" + "encoding/json" + "fmt" + "reflect" +) + +var ctxType = reflect.TypeOf((*context.Context)(nil)).Elem() +var errType = reflect.TypeOf((*error)(nil)).Elem() + +/* + Bind will create a Field around a function formatted a certain way, or any value. + + The input parameters can be, in any order, + - context.Context, or *context.Context (optional) + - An input struct, or pointer (optional) + + The output parameters can be, in any order, + - A primitive, an output struct, or pointer (required for use in schema) + - error (optional) + + Input or output types provided will be automatically bound using BindType. +*/ +func Bind(bindTo interface{}, additionalFields ...Fields) *Field { + combinedAdditionalFields := MergeFields(additionalFields...) + val := reflect.ValueOf(bindTo) + tipe := reflect.TypeOf(bindTo) + if tipe.Kind() == reflect.Func { + in := tipe.NumIn() + out := tipe.NumOut() + + var ctxIn *int + var inputIn *int + + var errOut *int + var outputOut *int + + queryArgs := FieldConfigArgument{} + + if in > 2 { + panic(fmt.Sprintf("Mismatch on number of inputs. Expected 0, 1, or 2. got %d.", tipe.NumIn())) + } + + if out > 2 { + panic(fmt.Sprintf("Mismatch on number of outputs. Expected 0, 1, or 2, got %d.", tipe.NumOut())) + } + + // inTypes := make([]reflect.Type, in) + // outTypes := make([]reflect.Type, out) + + for i := 0; i < in; i++ { + t := tipe.In(i) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + switch t { + case ctxType: + if ctxIn != nil { + panic(fmt.Sprintf("Unexpected multiple *context.Context inputs.")) + } + ctxIn = intP(i) + default: + if inputIn != nil { + panic(fmt.Sprintf("Unexpected multiple inputs.")) + } + inputType := tipe.In(i) + if inputType.Kind() == reflect.Ptr { + inputType = inputType.Elem() + } + inputFields := BindFields(reflect.New(inputType).Interface()) + for key, inputField := range inputFields { + queryArgs[key] = &ArgumentConfig{ + Type: inputField.Type, + } + } + + inputIn = intP(i) + } + } + + for i := 0; i < out; i++ { + t := tipe.Out(i) + switch t.String() { + case errType.String(): + if errOut != nil { + panic(fmt.Sprintf("Unexpected multiple error outputs")) + } + errOut = intP(i) + default: + if outputOut != nil { + panic(fmt.Sprintf("Unexpected multiple outputs")) + } + outputOut = intP(i) + } + } + + resolve := func(p ResolveParams) (output interface{}, err error) { + inputs := make([]reflect.Value, in) + if ctxIn != nil { + isPtr := tipe.In(*ctxIn).Kind() == reflect.Ptr + if isPtr { + if p.Context == nil { + inputs[*ctxIn] = reflect.New(ctxType) + } else { + inputs[*ctxIn] = reflect.ValueOf(&p.Context) + } + } else { + if p.Context == nil { + inputs[*ctxIn] = reflect.New(ctxType).Elem() + } else { + inputs[*ctxIn] = reflect.ValueOf(p.Context).Convert(ctxType).Elem() + } + } + } + if inputIn != nil { + var inputType, inputBaseType, sourceType, sourceBaseType reflect.Type + sourceVal := reflect.ValueOf(p.Source) + sourceExists := !sourceVal.IsZero() + if sourceExists { + sourceType = sourceVal.Type() + if sourceType.Kind() == reflect.Ptr { + sourceBaseType = sourceType.Elem() + } else { + sourceBaseType = sourceType + } + } + inputType = tipe.In(*inputIn) + isPtr := tipe.In(*inputIn).Kind() == reflect.Ptr + if isPtr { + inputBaseType = inputType.Elem() + } else { + inputBaseType = inputType + } + var input interface{} + if sourceExists && sourceBaseType.AssignableTo(inputBaseType) { + input = sourceVal.Interface() + } else { + input = reflect.New(inputBaseType).Interface() + j, err := json.Marshal(p.Args) + if err == nil { + err = json.Unmarshal(j, &input) + } + if err != nil { + return nil, err + } + } + + inputs[*inputIn], err = convertValue(reflect.ValueOf(input), inputType) + if err != nil { + return nil, err + } + } + results := val.Call(inputs) + if errOut != nil { + val := results[*errOut].Interface() + if val != nil { + err = val.(error) + } + if err != nil { + return output, err + } + } + if outputOut != nil { + var val reflect.Value + val, err = convertValue(results[*outputOut], tipe.Out(*outputOut)) + if err != nil { + return nil, err + } + if !val.IsZero() { + output = val.Interface() + } + } + return output, err + } + + var outputType Output + if outputOut != nil { + outputType = BindType(tipe.Out(*outputOut)) + extendType(outputType, combinedAdditionalFields) + } + + field := &Field{ + Type: outputType, + Resolve: resolve, + Args: queryArgs, + } + + return field + } else if tipe.Kind() == reflect.Struct { + fieldType := BindType(reflect.TypeOf(bindTo)) + extendType(fieldType, combinedAdditionalFields) + field := &Field{ + Type: fieldType, + Resolve: func(p ResolveParams) (data interface{}, err error) { + return bindTo, nil + }, + } + return field + } else { + if len(additionalFields) > 0 { + panic("Cannot add field resolvers to a scalar type.") + } + return &Field{ + Type: getGraphType(tipe), + Resolve: func(p ResolveParams) (data interface{}, err error) { + return bindTo, nil + }, + } + } +} + +func extendType(t Type, fields Fields) { + switch t.(type) { + case *Object: + object := t.(*Object) + for fieldName, fieldConfig := range fields { + object.AddFieldConfig(fieldName, fieldConfig) + } + return + case *List: + list := t.(*List) + extendType(list.OfType, fields) + return + } +} + +func convertValue(value reflect.Value, targetType reflect.Type) (ret reflect.Value, err error) { + if !value.IsValid() || value.IsZero() { + return reflect.Zero(targetType), nil + } + if value.Type().Kind() == reflect.Ptr { + if targetType.Kind() == reflect.Ptr { + return value, nil + } else { + return value.Elem(), nil + } + } else { + if targetType.Kind() == reflect.Ptr { + // Will throw an informative error + return value.Convert(targetType), nil + } else { + return value, nil + } + } +} + +func intP(i int) *int { + return &i +} diff --git a/bind_test.go b/bind_test.go new file mode 100644 index 000000000..bd244eeea --- /dev/null +++ b/bind_test.go @@ -0,0 +1,241 @@ +package graphql_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + "testing" + "time" + + "github.com/teamjobot/graphql" +) + +type HelloOutput struct { + Message string `json:"message"` +} + +func Hello(ctx *context.Context) (output *HelloOutput, err error) { + output = &HelloOutput{ + Message: "Hello World", + } + return output, nil +} + +func Hellos() []HelloOutput { + return []HelloOutput{ + { + Message: "Hello One", + }, + { + Message: "Hello Two", + }, + } +} + +func Upper(ctx *context.Context, source HelloOutput) string { + return strings.ToUpper(source.Message) +} + +type GreetingInput struct { + Name string `json:"name"` +} + +type GreetingOutput struct { + Message string `json:"message"` + Timestamp time.Time `json:"timestamp"` +} + +func GreetingPtr(ctx *context.Context, input *GreetingInput) (output *GreetingOutput, err error) { + return &GreetingOutput{ + Message: fmt.Sprintf("Hello %s.", input.Name), + Timestamp: time.Now(), + }, nil +} + +func Greeting(ctx context.Context, input GreetingInput) (output GreetingOutput, err error) { + return GreetingOutput{ + Message: fmt.Sprintf("Hello %s.", input.Name), + Timestamp: time.Now(), + }, nil +} + +type FriendRecur struct { + Name string `json:"name"` + Friends []FriendRecur `json:"friends"` +} + +func friends(ctx *context.Context) (output *FriendRecur) { + recursiveFriendRecur := FriendRecur{ + Name: "Recursion", + } + recursiveFriendRecur.Friends = make([]FriendRecur, 2) + recursiveFriendRecur.Friends[0] = recursiveFriendRecur + recursiveFriendRecur.Friends[1] = recursiveFriendRecur + + return &FriendRecur{ + Name: "Alan", + Friends: []FriendRecur{ + recursiveFriendRecur, + { + Name: "Samantha", + Friends: []FriendRecur{ + { + Name: "Olivia", + }, + { + Name: "Eric", + }, + }, + }, + { + Name: "Brian", + Friends: []FriendRecur{ + { + Name: "Windy", + }, + { + Name: "Kevin", + }, + }, + }, + { + Name: "Kevin", + Friends: []FriendRecur{ + { + Name: "Sergei", + }, + { + Name: "Michael", + }, + }, + }, + }, + } +} + +func TestBindHappyPath(t *testing.T) { + // Schema + fields := graphql.Fields{ + "hello": graphql.Bind(Hello), + "hellos": graphql.Bind(Hellos, graphql.Fields{ + "upper": graphql.Bind(Upper), + }), + "greeting": graphql.Bind(Greeting), + "greetingPtr": graphql.Bind(GreetingPtr), + "friends": graphql.Bind(friends), + "string": graphql.Bind("Hello World"), + "number": graphql.Bind(12345), + "float": graphql.Bind(123.45), + "anonymous": graphql.Bind(struct { + SomeField string `json:"someField"` + }{ + SomeField: "Some Value", + }), + "simpleFunc": graphql.Bind(func() string { + return "Hello World" + }), + } + rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields} + schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} + schema, err := graphql.NewSchema(schemaConfig) + if err != nil { + log.Fatalf("failed to create new schema, error: %v", err) + } + + // Query + query := ` + { + hello { + message + upper + } + hellos { + message + upper + } + greeting(name:"Alan") { + message + timestamp + } + greetingPtr(name:"Alan") { + message + timestamp + } + friends { + name + friends { + name + friends { + name + friends { + name + friends { + name + } + } + } + } + } + string + number + float + anonymous { + someField + } + simpleFunc + } + ` + params := graphql.Params{Schema: schema, RequestString: query} + r := graphql.Do(params) + if len(r.Errors) > 0 { + t.Errorf("failed to execute graphql operation, errors: %+v", r.Errors) + } + json, err := json.MarshalIndent(r.Data, "", " ") + fmt.Println(string(json)) +} + +func TestBindPanicImproperInput(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected Bind to panic due to improper function signature") + } + }() + graphql.Bind(func(a, b, c string) {}) +} + +func TestBindPanicImproperOutput(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected Bind to panic due to improper function signature") + } + }() + graphql.Bind(func() (string, string) { return "Hello", "World" }) +} + +func TestBindWithRuntimeError(t *testing.T) { + rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: graphql.Fields{ + "throwError": graphql.Bind(func() (string, error) { + return "", errors.New("Some Error") + }), + }} + schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} + schema, err := graphql.NewSchema(schemaConfig) + if err != nil { + log.Fatalf("failed to create new schema, error: %v", err) + } + + // Query + query := ` + { + throwError + } + ` + params := graphql.Params{Schema: schema, RequestString: query} + r := graphql.Do(params) + if len(r.Errors) == 0 { + t.Error("Expected error") + } +} diff --git a/definition.go b/definition.go index 4b1329914..6f9a06f91 100644 --- a/definition.go +++ b/definition.go @@ -6,7 +6,7 @@ import ( "reflect" "regexp" - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql/language/ast" ) // Type interface for all of the possible kinds of GraphQL types @@ -193,13 +193,12 @@ func GetNamed(ttype Type) Named { // // Example: // -// var OddType = new Scalar({ -// name: 'Odd', -// serialize(value) { -// return value % 2 === 1 ? value : null; -// } -// }); -// +// var OddType = new Scalar({ +// name: 'Odd', +// serialize(value) { +// return value % 2 === 1 ? value : null; +// } +// }); type Scalar struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -306,19 +305,19 @@ func (st *Scalar) Error() error { // have a name, but most importantly describe their fields. // Example: // -// var AddressType = new Object({ -// name: 'Address', -// fields: { -// street: { type: String }, -// number: { type: Int }, -// formatted: { -// type: String, -// resolve(obj) { -// return obj.number + ' ' + obj.street -// } -// } -// } -// }); +// var AddressType = new Object({ +// name: 'Address', +// fields: { +// street: { type: String }, +// number: { type: Int }, +// formatted: { +// type: String, +// resolve(obj) { +// return obj.number + ' ' + obj.street +// } +// } +// } +// }); // // When two types need to refer to each other, or a type needs to refer to // itself in a field, you can use a function expression (aka a closure or a @@ -326,13 +325,13 @@ func (st *Scalar) Error() error { // // Example: // -// var PersonType = new Object({ -// name: 'Person', -// fields: () => ({ -// name: { type: String }, -// bestFriend: { type: PersonType }, -// }) -// }); +// var PersonType = new Object({ +// name: 'Person', +// fields: () => ({ +// name: { type: String }, +// bestFriend: { type: PersonType }, +// }) +// }); // // / type Object struct { @@ -419,7 +418,7 @@ func (gt *Object) Name() string { return gt.PrivateName } func (gt *Object) Description() string { - return "" + return gt.PrivateDescription } func (gt *Object) String() string { return gt.PrivateName @@ -534,6 +533,7 @@ func defineFieldMap(ttype Named, fieldMap Fields) (FieldDefinitionMap, error) { Description: field.Description, Type: field.Type, Resolve: field.Resolve, + Subscribe: field.Subscribe, DeprecationReason: field.DeprecationReason, } @@ -606,6 +606,7 @@ type Field struct { Type Output `json:"type"` Args FieldConfigArgument `json:"args"` Resolve FieldResolveFn `json:"-"` + Subscribe FieldResolveFn `json:"-"` DeprecationReason string `json:"deprecationReason"` Description string `json:"description"` } @@ -625,6 +626,7 @@ type FieldDefinition struct { Type Output `json:"type"` Args []*Argument `json:"args"` Resolve FieldResolveFn `json:"-"` + Subscribe FieldResolveFn `json:"-"` DeprecationReason string `json:"deprecationReason"` } @@ -665,14 +667,12 @@ func (st *Argument) Error() error { // // Example: // -// var EntityType = new Interface({ -// name: 'Entity', -// fields: { -// name: { type: String } -// } -// }); -// -// +// var EntityType = new Interface({ +// name: 'Entity', +// fields: { +// name: { type: String } +// } +// }); type Interface struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -776,32 +776,36 @@ func (it *Interface) Error() error { // // Example: // -// var PetType = new Union({ -// name: 'Pet', -// types: [ DogType, CatType ], -// resolveType(value) { -// if (value instanceof Dog) { -// return DogType; -// } -// if (value instanceof Cat) { -// return CatType; -// } -// } -// }); +// var PetType = new Union({ +// name: 'Pet', +// types: [ DogType, CatType ], +// resolveType(value) { +// if (value instanceof Dog) { +// return DogType; +// } +// if (value instanceof Cat) { +// return CatType; +// } +// } +// }); type Union struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` ResolveType ResolveTypeFn - typeConfig UnionConfig - types []*Object - possibleTypes map[string]bool + typeConfig UnionConfig + initalizedTypes bool + types []*Object + possibleTypes map[string]bool err error } + +type UnionTypesThunk func() []*Object + type UnionConfig struct { - Name string `json:"name"` - Types []*Object `json:"types"` + Name string `json:"name"` + Types interface{} `json:"types"` ResolveType ResolveTypeFn Description string `json:"description"` } @@ -819,48 +823,80 @@ func NewUnion(config UnionConfig) *Union { objectType.PrivateDescription = config.Description objectType.ResolveType = config.ResolveType - if objectType.err = invariantf( - len(config.Types) > 0, - `Must provide Array of types for Union %v.`, config.Name, - ); objectType.err != nil { - return objectType + objectType.typeConfig = config + + return objectType +} + +func (ut *Union) Types() []*Object { + if ut.initalizedTypes { + return ut.types + } + + var unionTypes []*Object + switch utype := ut.typeConfig.Types.(type) { + case UnionTypesThunk: + unionTypes = utype() + case []*Object: + unionTypes = utype + case nil: + default: + ut.err = fmt.Errorf("Unknown Union.Types type: %T", ut.typeConfig.Types) + ut.initalizedTypes = true + return nil } - for _, ttype := range config.Types { - if objectType.err = invariantf( + + ut.types, ut.err = defineUnionTypes(ut, unionTypes) + ut.initalizedTypes = true + return ut.types +} + +func defineUnionTypes(objectType *Union, unionTypes []*Object) ([]*Object, error) { + definedUnionTypes := []*Object{} + + if err := invariantf( + len(unionTypes) > 0, + `Must provide Array of types for Union %v.`, objectType.Name(), + ); err != nil { + return definedUnionTypes, err + } + + for _, ttype := range unionTypes { + if err := invariantf( ttype != nil, `%v may only contain Object types, it cannot contain: %v.`, objectType, ttype, - ); objectType.err != nil { - return objectType + ); err != nil { + return definedUnionTypes, err } if objectType.ResolveType == nil { - if objectType.err = invariantf( + if err := invariantf( ttype.IsTypeOf != nil, `Union Type %v does not provide a "resolveType" function `+ `and possible Type %v does not provide a "isTypeOf" `+ `function. There is no way to resolve this possible type `+ `during execution.`, objectType, ttype, - ); objectType.err != nil { - return objectType + ); err != nil { + return definedUnionTypes, err } } + definedUnionTypes = append(definedUnionTypes, ttype) } - objectType.types = config.Types - objectType.typeConfig = config - return objectType -} -func (ut *Union) Types() []*Object { - return ut.types + return definedUnionTypes, nil } + func (ut *Union) String() string { return ut.PrivateName } + func (ut *Union) Name() string { return ut.PrivateName } + func (ut *Union) Description() string { return ut.PrivateDescription } + func (ut *Union) Error() error { return ut.err } @@ -1046,18 +1082,18 @@ func (gt *Enum) getNameLookup() map[string]*EnumValueDefinition { // An input object defines a structured collection of fields which may be // supplied to a field argument. // -// Using `NonNull` will ensure that a value must be provided by the query +// # Using `NonNull` will ensure that a value must be provided by the query // // Example: // -// var GeoPoint = new InputObject({ -// name: 'GeoPoint', -// fields: { -// lat: { type: new NonNull(Float) }, -// lon: { type: new NonNull(Float) }, -// alt: { type: Float, defaultValue: 0 }, -// } -// }); +// var GeoPoint = new InputObject({ +// name: 'GeoPoint', +// fields: { +// lat: { type: new NonNull(Float) }, +// lon: { type: new NonNull(Float) }, +// alt: { type: Float, defaultValue: 0 }, +// } +// }); type InputObject struct { PrivateName string `json:"name"` PrivateDescription string `json:"description"` @@ -1143,7 +1179,7 @@ func (gt *InputObject) defineFieldMap() InputObjectFieldMap { if gt.err = invariantf( fieldConfig.Type != nil, `%v.%v field type must be Input Type but got: %v.`, gt, fieldName, fieldConfig.Type, - ); err != nil { + ); gt.err != nil { return resultFieldMap } field := &InputObjectField{} @@ -1196,14 +1232,13 @@ func (gt *InputObject) Error() error { // // Example: // -// var PersonType = new Object({ -// name: 'Person', -// fields: () => ({ -// parents: { type: new List(Person) }, -// children: { type: new List(Person) }, -// }) -// }) -// +// var PersonType = new Object({ +// name: 'Person', +// fields: () => ({ +// parents: { type: new List(Person) }, +// children: { type: new List(Person) }, +// }) +// }) type List struct { OfType Type `json:"ofType"` @@ -1222,14 +1257,14 @@ func NewList(ofType Type) *List { return gl } func (gl *List) Name() string { - return fmt.Sprintf("%v", gl.OfType) + return fmt.Sprintf("[%v]", gl.OfType) } func (gl *List) Description() string { return "" } func (gl *List) String() string { if gl.OfType != nil { - return fmt.Sprintf("[%v]", gl.OfType) + return gl.Name() } return "" } @@ -1247,12 +1282,12 @@ func (gl *List) Error() error { // // Example: // -// var RowType = new Object({ -// name: 'Row', -// fields: () => ({ -// id: { type: new NonNull(String) }, -// }) -// }) +// var RowType = new Object({ +// name: 'Row', +// fields: () => ({ +// id: { type: new NonNull(String) }, +// }) +// }) // // Note: the enforcement of non-nullability occurs within the executor. type NonNull struct { diff --git a/definition_test.go b/definition_test.go index 12824f219..5317266d2 100644 --- a/definition_test.go +++ b/definition_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" ) var blogImage = graphql.NewObject(graphql.ObjectConfig{ @@ -519,6 +519,7 @@ func TestTypeSystem_DefinitionExample_ProhibitsNilTypeInUnions(t *testing.T) { Name: "BadUnion", Types: []*graphql.Object{nil}, }) + ttype.Types() expected := `BadUnion may only contain Object types, it cannot contain: .` if ttype.Error().Error() != expected { t.Fatalf(`expected %v , got: %v`, expected, ttype.Error()) @@ -666,3 +667,64 @@ func TestTypeSystem_DefinitionExample_CanAddInputObjectField(t *testing.T) { t.Fatal("Unexpected result, inputObject should have a field named 'newValue'") } } + +func TestTypeSystem_DefinitionExample_IncludesUnionTypesThunk(t *testing.T) { + someObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "SomeObject", + Fields: graphql.Fields{ + "f": &graphql.Field{ + Type: graphql.Int, + }, + }, + }) + + someOtherObject := graphql.NewObject(graphql.ObjectConfig{ + Name: "SomeOtherObject", + Fields: graphql.Fields{ + "g": &graphql.Field{ + Type: graphql.Int, + }, + }, + }) + + someUnion := graphql.NewUnion(graphql.UnionConfig{ + Name: "SomeUnion", + Types: (graphql.UnionTypesThunk)(func() []*graphql.Object { + return []*graphql.Object{someObject, someOtherObject} + }), + ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object { + return nil + }, + }) + + unionTypes := someUnion.Types() + + if someUnion.Error() != nil { + t.Fatalf("unexpected error, got: %v", someUnion.Error().Error()) + } + if len(unionTypes) != 2 { + t.Fatalf("Unexpected result, someUnion should have two unionTypes, has %d", len(unionTypes)) + } +} + +func TestTypeSystem_DefinitionExample_HandlesInvalidUnionTypes(t *testing.T) { + someUnion := graphql.NewUnion(graphql.UnionConfig{ + Name: "SomeUnion", + Types: (graphql.InterfacesThunk)(func() []*graphql.Interface { + return []*graphql.Interface{} + }), + ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object { + return nil + }, + }) + + unionTypes := someUnion.Types() + expected := "Unknown Union.Types type: graphql.InterfacesThunk" + + if someUnion.Error().Error() != expected { + t.Fatalf("Unexpected error, got: %v, want: %v", someUnion.Error().Error(), expected) + } + if unionTypes != nil { + t.Fatalf("Unexpected result, got: %v, want: nil", unionTypes) + } +} diff --git a/directives_test.go b/directives_test.go index 30b3028a5..598779a43 100644 --- a/directives_test.go +++ b/directives_test.go @@ -4,9 +4,9 @@ import ( "errors" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) var directivesTestSchema, _ = graphql.NewSchema(graphql.SchemaConfig{ diff --git a/enum_type_test.go b/enum_type_test.go index 33ad67a3c..ed92b147d 100644 --- a/enum_type_test.go +++ b/enum_type_test.go @@ -4,10 +4,10 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) var enumTypeTestColorType = graphql.NewEnum(graphql.EnumConfig{ diff --git a/examples/bind-complex/main.go b/examples/bind-complex/main.go new file mode 100644 index 000000000..bbe7f6334 --- /dev/null +++ b/examples/bind-complex/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + + "github.com/teamjobot/graphql" +) + +var people = []Person{ + { + Name: "Alan", + Friends: []Person{ + { + Name: "Nadeem", + Friends: []Person{ + { + Name: "Heidi", + }, + }, + }, + }, + }, +} + +type Person struct { + Name string `json:"name"` + Friends []Person `json:"friends"` +} + +type GetPersonInput struct { + Name string `json:"name"` +} + +type GetPersonOutput struct { + Person +} + +func GetPerson(ctx context.Context, input GetPersonInput) (*GetPersonOutput, error) { + for _, person := range people { + if person.Name == input.Name { + return &GetPersonOutput{ + Person: person, + }, nil + } + } + return nil, errors.New("Could not find person.") +} + +func main() { + rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: graphql.Fields{ + "person": graphql.Bind(GetPerson), + }} + + schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} + schema, err := graphql.NewSchema(schemaConfig) + if err != nil { + log.Fatalf("failed to create new schema, error: %v", err) + } + + // Query + query := ` + { + person(name: "Alan") { + name + friends { + name + friends { + name + } + } + } + } + ` + params := graphql.Params{Schema: schema, RequestString: query} + r := graphql.Do(params) + if len(r.Errors) > 0 { + log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors) + } + rJSON, _ := json.Marshal(r) + fmt.Printf("%s \n", rJSON) +} diff --git a/examples/bind-simple/main.go b/examples/bind-simple/main.go new file mode 100644 index 000000000..373d7025e --- /dev/null +++ b/examples/bind-simple/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/teamjobot/graphql" +) + +type GreetingInput struct { + Name string `json:"name"` +} + +func Greeting(input GreetingInput) string { + return fmt.Sprintf("Hello %s", input.Name) +} + +func main() { + rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: graphql.Fields{ + "greeting": graphql.Bind(Greeting), + }} + + schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} + schema, err := graphql.NewSchema(schemaConfig) + if err != nil { + log.Fatalf("failed to create new schema, error: %v", err) + } + + // Query + query := ` + { + greeting(name: "Alan") + } + ` + params := graphql.Params{Schema: schema, RequestString: query} + r := graphql.Do(params) + if len(r.Errors) > 0 { + log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors) + } + rJSON, _ := json.Marshal(r) + fmt.Printf("%s \n", rJSON) +} diff --git a/examples/concurrent-resolvers/main.go b/examples/concurrent-resolvers/main.go index 08a1c9ef7..7369105d4 100644 --- a/examples/concurrent-resolvers/main.go +++ b/examples/concurrent-resolvers/main.go @@ -5,7 +5,7 @@ import ( "fmt" "log" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) type Foo struct { diff --git a/examples/context/main.go b/examples/context/main.go index 149a92919..60b8f07a2 100644 --- a/examples/context/main.go +++ b/examples/context/main.go @@ -7,7 +7,7 @@ import ( "log" "net/http" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) var Schema graphql.Schema diff --git a/examples/crud/main.go b/examples/crud/main.go index dac20bcda..909d70443 100644 --- a/examples/crud/main.go +++ b/examples/crud/main.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) // Product contains information about one product diff --git a/examples/custom-scalar-type/main.go b/examples/custom-scalar-type/main.go index e7203a062..33898f3a2 100644 --- a/examples/custom-scalar-type/main.go +++ b/examples/custom-scalar-type/main.go @@ -5,8 +5,8 @@ import ( "fmt" "log" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/language/ast" ) type CustomID struct { diff --git a/examples/hello-world/main.go b/examples/hello-world/main.go index d014e9426..a092322e2 100644 --- a/examples/hello-world/main.go +++ b/examples/hello-world/main.go @@ -5,7 +5,7 @@ import ( "fmt" "log" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) func main() { diff --git a/examples/http-post/main.go b/examples/http-post/main.go new file mode 100644 index 000000000..4b9a5e2cf --- /dev/null +++ b/examples/http-post/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/examples/todo/schema" +) + +type postData struct { + Query string `json:"query"` + Operation string `json:"operationName"` + Variables map[string]interface{} `json:"variables"` +} + +func main() { + http.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) { + var p postData + if err := json.NewDecoder(req.Body).Decode(&p); err != nil { + w.WriteHeader(400) + return + } + result := graphql.Do(graphql.Params{ + Context: req.Context(), + Schema: schema.TodoSchema, + RequestString: p.Query, + VariableValues: p.Variables, + OperationName: p.Operation, + }) + if err := json.NewEncoder(w).Encode(result); err != nil { + fmt.Printf("could not write result to response: %s", err) + } + }) + + fmt.Println("Now server is running on port 8080") + + fmt.Println("") + + fmt.Println(`Get single todo: +curl \ +-X POST \ +-H "Content-Type: application/json" \ +--data '{ "query": "{ todo(id:\"b\") { id text done } }" }' \ +http://localhost:8080/graphql`) + + fmt.Println("") + + fmt.Println(`Create new todo: +curl \ +-X POST \ +-H "Content-Type: application/json" \ +--data '{ "query": "mutation { createTodo(text:\"My New todo\") { id text done } }" }' \ +http://localhost:8080/graphql`) + + fmt.Println("") + + fmt.Println(`Update todo: +curl \ +-X POST \ +-H "Content-Type: application/json" \ +--data '{ "query": "mutation { updateTodo(id:\"a\", done: true) { id text done } }" }' \ +http://localhost:8080/graphql`) + + fmt.Println("") + + fmt.Println(`Load todo list: +curl \ +-X POST \ +-H "Content-Type: application/json" \ +--data '{ "query": "{ todoList { id text done } }" }' \ +http://localhost:8080/graphql`) + + http.ListenAndServe(":8080", nil) +} diff --git a/examples/http/main.go b/examples/http/main.go index a1f2836e8..e37e43bb6 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -6,7 +6,7 @@ import ( "io/ioutil" "net/http" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) type user struct { diff --git a/examples/httpdynamic/main.go b/examples/httpdynamic/main.go index ff8abb893..85efc1f33 100644 --- a/examples/httpdynamic/main.go +++ b/examples/httpdynamic/main.go @@ -10,7 +10,7 @@ import ( "strconv" "syscall" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) /*****************************************************************************/ diff --git a/examples/modify-context/main.go b/examples/modify-context/main.go index 0432b3bad..98fe4e433 100644 --- a/examples/modify-context/main.go +++ b/examples/modify-context/main.go @@ -6,7 +6,7 @@ import ( "fmt" "log" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) type User struct { diff --git a/examples/sql-nullstring/main.go b/examples/sql-nullstring/main.go index cfcf63903..0d699fa37 100644 --- a/examples/sql-nullstring/main.go +++ b/examples/sql-nullstring/main.go @@ -4,9 +4,10 @@ import ( "database/sql" "encoding/json" "fmt" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/language/ast" "log" + + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/language/ast" ) // NullString to be used in place of sql.NullString @@ -192,6 +193,9 @@ query { log.Fatal(r1) } b1, err := json.MarshalIndent(r1, "", " ") + if err != nil { + log.Fatal(err) + } b2, err := json.MarshalIndent(r2, "", " ") if err != nil { log.Fatal(err) diff --git a/examples/star-wars/main.go b/examples/star-wars/main.go index ea178dea9..e1e197907 100644 --- a/examples/star-wars/main.go +++ b/examples/star-wars/main.go @@ -5,8 +5,8 @@ import ( "fmt" "net/http" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" ) func main() { diff --git a/examples/todo/main.go b/examples/todo/main.go index b36b08d01..b51f0f345 100644 --- a/examples/todo/main.go +++ b/examples/todo/main.go @@ -7,193 +7,19 @@ import ( "net/http" "time" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/examples/todo/schema" ) -type Todo struct { - ID string `json:"id"` - Text string `json:"text"` - Done bool `json:"done"` -} - -var TodoList []Todo -var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func RandStringRunes(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] - } - return string(b) -} - func init() { - todo1 := Todo{ID: "a", Text: "A todo not to forget", Done: false} - todo2 := Todo{ID: "b", Text: "This is the most important", Done: false} - todo3 := Todo{ID: "c", Text: "Please do this or else", Done: false} - TodoList = append(TodoList, todo1, todo2, todo3) + todo1 := schema.Todo{ID: "a", Text: "A todo not to forget", Done: false} + todo2 := schema.Todo{ID: "b", Text: "This is the most important", Done: false} + todo3 := schema.Todo{ID: "c", Text: "Please do this or else", Done: false} + schema.TodoList = append(schema.TodoList, todo1, todo2, todo3) rand.Seed(time.Now().UnixNano()) } -// define custom GraphQL ObjectType `todoType` for our Golang struct `Todo` -// Note that -// - the fields in our todoType maps with the json tags for the fields in our struct -// - the field type matches the field type in our struct -var todoType = graphql.NewObject(graphql.ObjectConfig{ - Name: "Todo", - Fields: graphql.Fields{ - "id": &graphql.Field{ - Type: graphql.String, - }, - "text": &graphql.Field{ - Type: graphql.String, - }, - "done": &graphql.Field{ - Type: graphql.Boolean, - }, - }, -}) - -// root mutation -var rootMutation = graphql.NewObject(graphql.ObjectConfig{ - Name: "RootMutation", - Fields: graphql.Fields{ - /* - curl -g 'http://localhost:8080/graphql?query=mutation+_{createTodo(text:"My+new+todo"){id,text,done}}' - */ - "createTodo": &graphql.Field{ - Type: todoType, // the return type for this field - Description: "Create new todo", - Args: graphql.FieldConfigArgument{ - "text": &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.String), - }, - }, - Resolve: func(params graphql.ResolveParams) (interface{}, error) { - - // marshall and cast the argument value - text, _ := params.Args["text"].(string) - - // figure out new id - newID := RandStringRunes(8) - - // perform mutation operation here - // for e.g. create a Todo and save to DB. - newTodo := Todo{ - ID: newID, - Text: text, - Done: false, - } - - TodoList = append(TodoList, newTodo) - - // return the new Todo object that we supposedly save to DB - // Note here that - // - we are returning a `Todo` struct instance here - // - we previously specified the return Type to be `todoType` - // - `Todo` struct maps to `todoType`, as defined in `todoType` ObjectConfig` - return newTodo, nil - }, - }, - /* - curl -g 'http://localhost:8080/graphql?query=mutation+_{updateTodo(id:"a",done:true){id,text,done}}' - */ - "updateTodo": &graphql.Field{ - Type: todoType, // the return type for this field - Description: "Update existing todo, mark it done or not done", - Args: graphql.FieldConfigArgument{ - "done": &graphql.ArgumentConfig{ - Type: graphql.Boolean, - }, - "id": &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.String), - }, - }, - Resolve: func(params graphql.ResolveParams) (interface{}, error) { - // marshall and cast the argument value - done, _ := params.Args["done"].(bool) - id, _ := params.Args["id"].(string) - affectedTodo := Todo{} - - // Search list for todo with id and change the done variable - for i := 0; i < len(TodoList); i++ { - if TodoList[i].ID == id { - TodoList[i].Done = done - // Assign updated todo so we can return it - affectedTodo = TodoList[i] - break - } - } - // Return affected todo - return affectedTodo, nil - }, - }, - }, -}) - -// root query -// we just define a trivial example here, since root query is required. -// Test with curl -// curl -g 'http://localhost:8080/graphql?query={lastTodo{id,text,done}}' -var rootQuery = graphql.NewObject(graphql.ObjectConfig{ - Name: "RootQuery", - Fields: graphql.Fields{ - - /* - curl -g 'http://localhost:8080/graphql?query={todo(id:"b"){id,text,done}}' - */ - "todo": &graphql.Field{ - Type: todoType, - Description: "Get single todo", - Args: graphql.FieldConfigArgument{ - "id": &graphql.ArgumentConfig{ - Type: graphql.String, - }, - }, - Resolve: func(params graphql.ResolveParams) (interface{}, error) { - - idQuery, isOK := params.Args["id"].(string) - if isOK { - // Search for el with id - for _, todo := range TodoList { - if todo.ID == idQuery { - return todo, nil - } - } - } - - return Todo{}, nil - }, - }, - - "lastTodo": &graphql.Field{ - Type: todoType, - Description: "Last todo added", - Resolve: func(params graphql.ResolveParams) (interface{}, error) { - return TodoList[len(TodoList)-1], nil - }, - }, - - /* - curl -g 'http://localhost:8080/graphql?query={todoList{id,text,done}}' - */ - "todoList": &graphql.Field{ - Type: graphql.NewList(todoType), - Description: "List of todos", - Resolve: func(p graphql.ResolveParams) (interface{}, error) { - return TodoList, nil - }, - }, - }, -}) - -// define schema, with our rootQuery and rootMutation -var schema, _ = graphql.NewSchema(graphql.SchemaConfig{ - Query: rootQuery, - Mutation: rootMutation, -}) - func executeQuery(query string, schema graphql.Schema) *graphql.Result { result := graphql.Do(graphql.Params{ Schema: schema, @@ -207,7 +33,7 @@ func executeQuery(query string, schema graphql.Schema) *graphql.Result { func main() { http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) { - result := executeQuery(r.URL.Query().Get("query"), schema) + result := executeQuery(r.URL.Query().Get("query"), schema.TodoSchema) json.NewEncoder(w).Encode(result) }) // Serve static files diff --git a/examples/todo/schema/schema.go b/examples/todo/schema/schema.go new file mode 100644 index 000000000..034379036 --- /dev/null +++ b/examples/todo/schema/schema.go @@ -0,0 +1,183 @@ +package schema + +import ( + "math/rand" + + "github.com/teamjobot/graphql" +) + +var TodoList []Todo + +type Todo struct { + ID string `json:"id"` + Text string `json:"text"` + Done bool `json:"done"` +} + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func RandStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} + +// define custom GraphQL ObjectType `todoType` for our Golang struct `Todo` +// Note that +// - the fields in our todoType maps with the json tags for the fields in our struct +// - the field type matches the field type in our struct +var todoType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Todo", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.String, + }, + "text": &graphql.Field{ + Type: graphql.String, + }, + "done": &graphql.Field{ + Type: graphql.Boolean, + }, + }, +}) + +// root mutation +var rootMutation = graphql.NewObject(graphql.ObjectConfig{ + Name: "RootMutation", + Fields: graphql.Fields{ + /* + curl -g 'http://localhost:8080/graphql?query=mutation+_{createTodo(text:"My+new+todo"){id,text,done}}' + */ + "createTodo": &graphql.Field{ + Type: todoType, // the return type for this field + Description: "Create new todo", + Args: graphql.FieldConfigArgument{ + "text": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + + // marshall and cast the argument value + text, _ := params.Args["text"].(string) + + // figure out new id + newID := RandStringRunes(8) + + // perform mutation operation here + // for e.g. create a Todo and save to DB. + newTodo := Todo{ + ID: newID, + Text: text, + Done: false, + } + + TodoList = append(TodoList, newTodo) + + // return the new Todo object that we supposedly save to DB + // Note here that + // - we are returning a `Todo` struct instance here + // - we previously specified the return Type to be `todoType` + // - `Todo` struct maps to `todoType`, as defined in `todoType` ObjectConfig` + return newTodo, nil + }, + }, + /* + curl -g 'http://localhost:8080/graphql?query=mutation+_{updateTodo(id:"a",done:true){id,text,done}}' + */ + "updateTodo": &graphql.Field{ + Type: todoType, // the return type for this field + Description: "Update existing todo, mark it done or not done", + Args: graphql.FieldConfigArgument{ + "done": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + "id": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + // marshall and cast the argument value + done, _ := params.Args["done"].(bool) + id, _ := params.Args["id"].(string) + affectedTodo := Todo{} + + // Search list for todo with id and change the done variable + for i := 0; i < len(TodoList); i++ { + if TodoList[i].ID == id { + TodoList[i].Done = done + // Assign updated todo so we can return it + affectedTodo = TodoList[i] + break + } + } + // Return affected todo + return affectedTodo, nil + }, + }, + }, +}) + +// root query +// we just define a trivial example here, since root query is required. +// Test with curl +// curl -g 'http://localhost:8080/graphql?query={lastTodo{id,text,done}}' +var rootQuery = graphql.NewObject(graphql.ObjectConfig{ + Name: "RootQuery", + Fields: graphql.Fields{ + + /* + curl -g 'http://localhost:8080/graphql?query={todo(id:"b"){id,text,done}}' + */ + "todo": &graphql.Field{ + Type: todoType, + Description: "Get single todo", + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + + idQuery, isOK := params.Args["id"].(string) + if isOK { + // Search for el with id + for _, todo := range TodoList { + if todo.ID == idQuery { + return todo, nil + } + } + } + + return Todo{}, nil + }, + }, + + "lastTodo": &graphql.Field{ + Type: todoType, + Description: "Last todo added", + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + return TodoList[len(TodoList)-1], nil + }, + }, + + /* + curl -g 'http://localhost:8080/graphql?query={todoList{id,text,done}}' + */ + "todoList": &graphql.Field{ + Type: graphql.NewList(todoType), + Description: "List of todos", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return TodoList, nil + }, + }, + }, +}) + +// define schema, with our rootQuery and rootMutation +var TodoSchema, _ = graphql.NewSchema(graphql.SchemaConfig{ + Query: rootQuery, + Mutation: rootMutation, +}) diff --git a/executor.go b/executor.go index 3c8441d22..43dafbabf 100644 --- a/executor.go +++ b/executor.go @@ -8,8 +8,8 @@ import ( "sort" "strings" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" ) type ExecuteParams struct { @@ -800,10 +800,8 @@ func completeAbstractValue(eCtx *executionContext, returnType Abstract, fieldAST runtimeType = defaultResolveTypeFn(resolveTypeParams, returnType) } - err := invariant(runtimeType != nil, - fmt.Sprintf(`Abstract type %v must resolve to an Object type at runtime `+ - `for field %v.%v with value "%v", received "%v".`, - returnType, info.ParentType, info.FieldName, result, runtimeType), + err := invariantf(runtimeType != nil, `Abstract type %v must resolve to an Object type at runtime `+ + `for field %v.%v with value "%v", received "%v".`, returnType, info.ParentType, info.FieldName, result, runtimeType, ) if err != nil { panic(err) @@ -945,7 +943,7 @@ func DefaultResolveFn(p ResolveParams) (interface{}, error) { } // try to resolve p.Source as a struct - if sourceVal.IsValid() && sourceVal.Type().Kind() == reflect.Ptr { + if sourceVal.IsValid() && !sourceVal.IsZero() && sourceVal.Type().Kind() == reflect.Ptr { sourceVal = sourceVal.Elem() } if !sourceVal.IsValid() { diff --git a/executor_resolve_test.go b/executor_resolve_test.go index 7430cd866..03534d3d6 100644 --- a/executor_resolve_test.go +++ b/executor_resolve_test.go @@ -2,10 +2,11 @@ package graphql_test import ( "encoding/json" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/testutil" "reflect" "testing" + + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" ) func testSchema(t *testing.T, testField *graphql.Field) graphql.Schema { diff --git a/executor_schema_test.go b/executor_schema_test.go index b39c4c3a1..fc26c7863 100644 --- a/executor_schema_test.go +++ b/executor_schema_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" ) // TODO: have a separate package for other tests for eg `parser` diff --git a/executor_test.go b/executor_test.go index 856aadf3e..61da87353 100644 --- a/executor_test.go +++ b/executor_test.go @@ -9,10 +9,10 @@ import ( "testing" "time" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) func TestExecutesArbitraryCode(t *testing.T) { diff --git a/extensions.go b/extensions.go index 1c448fbfe..b151c10b7 100644 --- a/extensions.go +++ b/extensions.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/graphql-go/graphql/gqlerrors" + "github.com/teamjobot/graphql/gqlerrors" ) type ( diff --git a/extensions_test.go b/extensions_test.go index ea23f7527..5bcc23f35 100644 --- a/extensions_test.go +++ b/extensions_test.go @@ -7,9 +7,9 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func tinit(t *testing.T) graphql.Schema { diff --git a/go.mod b/go.mod index 399b200db..1540fde4e 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ -module github.com/graphql-go/graphql +module github.com/teamjobot/graphql + +go 1.13 diff --git a/gqlerrors/error.go b/gqlerrors/error.go index 569e752c2..326b5ba10 100644 --- a/gqlerrors/error.go +++ b/gqlerrors/error.go @@ -4,9 +4,9 @@ import ( "fmt" "reflect" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/language/source" ) type Error struct { diff --git a/gqlerrors/formatted.go b/gqlerrors/formatted.go index fb422b630..ea99d354f 100644 --- a/gqlerrors/formatted.go +++ b/gqlerrors/formatted.go @@ -3,7 +3,7 @@ package gqlerrors import ( "errors" - "github.com/graphql-go/graphql/language/location" + "github.com/teamjobot/graphql/language/location" ) type ExtendedError interface { diff --git a/gqlerrors/located.go b/gqlerrors/located.go index b02fcd8a9..8746dea14 100644 --- a/gqlerrors/located.go +++ b/gqlerrors/located.go @@ -2,7 +2,8 @@ package gqlerrors import ( "errors" - "github.com/graphql-go/graphql/language/ast" + + "github.com/teamjobot/graphql/language/ast" ) // NewLocatedError creates a graphql.Error with location info diff --git a/gqlerrors/syntax.go b/gqlerrors/syntax.go index abad6ade0..110b0fc18 100644 --- a/gqlerrors/syntax.go +++ b/gqlerrors/syntax.go @@ -5,9 +5,9 @@ import ( "regexp" "strings" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/language/source" ) func NewSyntaxError(s *source.Source, position int, description string) *Error { diff --git a/graphql.go b/graphql.go index 2b1f6a298..ec801318b 100644 --- a/graphql.go +++ b/graphql.go @@ -3,9 +3,9 @@ package graphql import ( "context" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/parser" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/parser" + "github.com/teamjobot/graphql/language/source" ) type Params struct { diff --git a/graphql_bench_test.go b/graphql_bench_test.go index 5b135192b..e4a1a2df1 100644 --- a/graphql_bench_test.go +++ b/graphql_bench_test.go @@ -3,8 +3,8 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/benchutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/benchutil" ) type B struct { diff --git a/graphql_test.go b/graphql_test.go index 8b06a7b1d..3033eea25 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" ) type T struct { diff --git a/introspection.go b/introspection.go index 51feb42d4..95d4930aa 100644 --- a/introspection.go +++ b/introspection.go @@ -5,8 +5,8 @@ import ( "reflect" "sort" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/printer" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/printer" ) const ( diff --git a/introspection_test.go b/introspection_test.go index c0e62bf14..27d6bca85 100644 --- a/introspection_test.go +++ b/introspection_test.go @@ -3,10 +3,10 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) func g(t *testing.T, p graphql.Params) *graphql.Result { diff --git a/language/ast/arguments.go b/language/ast/arguments.go index 2ebd0fa7c..a9ec2943b 100644 --- a/language/ast/arguments.go +++ b/language/ast/arguments.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) // Argument implements Node diff --git a/language/ast/definitions.go b/language/ast/definitions.go index e16cf18dc..3a960f49f 100644 --- a/language/ast/definitions.go +++ b/language/ast/definitions.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) type Definition interface { diff --git a/language/ast/directives.go b/language/ast/directives.go index 0c8a8c0ef..756212a94 100644 --- a/language/ast/directives.go +++ b/language/ast/directives.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) // Directive implements Node diff --git a/language/ast/document.go b/language/ast/document.go index dcb67034c..fcb6e933b 100644 --- a/language/ast/document.go +++ b/language/ast/document.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) // Document implements Node diff --git a/language/ast/location.go b/language/ast/location.go index 266dc8477..0cf6a1211 100644 --- a/language/ast/location.go +++ b/language/ast/location.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/language/source" ) type Location struct { diff --git a/language/ast/name.go b/language/ast/name.go index ce0e9ebd2..ae74e2de8 100644 --- a/language/ast/name.go +++ b/language/ast/name.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) // Name implements Node diff --git a/language/ast/selections.go b/language/ast/selections.go index 55df71a32..dbadf77cb 100644 --- a/language/ast/selections.go +++ b/language/ast/selections.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) type Selection interface { diff --git a/language/ast/type_definitions.go b/language/ast/type_definitions.go index aefa70ed6..9c5ece653 100644 --- a/language/ast/type_definitions.go +++ b/language/ast/type_definitions.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) // DescribableNode are nodes that have descriptions associated with them. diff --git a/language/ast/types.go b/language/ast/types.go index 0308a6091..6335462d3 100644 --- a/language/ast/types.go +++ b/language/ast/types.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) type Type interface { diff --git a/language/ast/values.go b/language/ast/values.go index 6c3c88640..d3b978555 100644 --- a/language/ast/values.go +++ b/language/ast/values.go @@ -1,7 +1,7 @@ package ast import ( - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/kinds" ) type Value interface { diff --git a/language/lexer/lexer.go b/language/lexer/lexer.go index 1988c5fdc..cda8ecd5c 100644 --- a/language/lexer/lexer.go +++ b/language/lexer/lexer.go @@ -7,8 +7,8 @@ import ( "strings" "unicode/utf8" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/source" ) type TokenKind int diff --git a/language/lexer/lexer_test.go b/language/lexer/lexer_test.go index c476d8fa4..8ec8e8341 100644 --- a/language/lexer/lexer_test.go +++ b/language/lexer/lexer_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/language/source" ) type Test struct { diff --git a/language/location/location.go b/language/location/location.go index 04bbde6e3..525705675 100644 --- a/language/location/location.go +++ b/language/location/location.go @@ -3,7 +3,7 @@ package location import ( "regexp" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/language/source" ) type SourceLocation struct { diff --git a/language/parser/parser.go b/language/parser/parser.go index 4ee1577c4..86f1d67b6 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -3,10 +3,10 @@ package parser import ( "fmt" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/lexer" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/lexer" + "github.com/teamjobot/graphql/language/source" ) type parseFn func(parser *Parser) (interface{}, error) @@ -19,12 +19,6 @@ var tokenDefinitionFn map[string]parseDefinitionFn func init() { tokenDefinitionFn = make(map[string]parseDefinitionFn) { - // for sign - tokenDefinitionFn[lexer.BRACE_L.String()] = parseOperationDefinition - tokenDefinitionFn[lexer.STRING.String()] = parseTypeSystemDefinition - tokenDefinitionFn[lexer.BLOCK_STRING.String()] = parseTypeSystemDefinition - tokenDefinitionFn[lexer.NAME.String()] = parseTypeSystemDefinition - // for NAME tokenDefinitionFn[lexer.FRAGMENT] = parseFragmentDefinition tokenDefinitionFn[lexer.QUERY] = parseOperationDefinition tokenDefinitionFn[lexer.MUTATION] = parseOperationDefinition @@ -79,8 +73,8 @@ func Parse(p ParseParams) (*ast.Document, error) { return doc, nil } -// TODO: test and expose parseValue as a public -func parseValue(p ParseParams) (ast.Value, error) { +// ParseValue parses params and returns ast value +func ParseValue(p ParseParams) (ast.Value, error) { var value ast.Value var sourceObj *source.Source switch src := p.Source.(type) { @@ -145,8 +139,10 @@ func parseDocument(parser *Parser) (*ast.Document, error) { break } switch kind := parser.Token.Kind; kind { - case lexer.BRACE_L, lexer.NAME, lexer.STRING, lexer.BLOCK_STRING: - item = tokenDefinitionFn[kind.String()] + case lexer.BRACE_L: + item = parseOperationDefinition + case lexer.NAME, lexer.STRING, lexer.BLOCK_STRING: + item = parseTypeSystemDefinition default: return nil, unexpected(parser, lexer.Token{}) } diff --git a/language/parser/parser_test.go b/language/parser/parser_test.go index 3cc4253a8..f553e2cf7 100644 --- a/language/parser/parser_test.go +++ b/language/parser/parser_test.go @@ -7,11 +7,11 @@ import ( "strings" "testing" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/language/printer" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/language/printer" + "github.com/teamjobot/graphql/language/source" ) func TestBadToken(t *testing.T) { @@ -736,6 +736,15 @@ func TestParseCreatesAst(t *testing.T) { } +func TestDoesNotAcceptStringAsDefinition(t *testing.T) { + test := errorMessageTest{ + `String`, + `Syntax Error GraphQL (1:1) Unexpected Name "String"`, + false, + } + testErrorMessage(t, test) +} + type errorMessageTest struct { source interface{} expectedMessage string diff --git a/language/parser/schema_parser_test.go b/language/parser/schema_parser_test.go index 2a122cae9..7adfd8df5 100644 --- a/language/parser/schema_parser_test.go +++ b/language/parser/schema_parser_test.go @@ -4,10 +4,10 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/language/source" ) func parse(t *testing.T, query string) *ast.Document { diff --git a/language/printer/printer.go b/language/printer/printer.go index eba872bbc..ac88563db 100644 --- a/language/printer/printer.go +++ b/language/printer/printer.go @@ -2,12 +2,13 @@ package printer import ( "fmt" + "strconv" "strings" "reflect" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/visitor" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/visitor" ) func getMapValue(m map[string]interface{}, key string) interface{} { @@ -372,7 +373,7 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ "StringValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { case *ast.StringValue: - return visitor.ActionUpdate, `"` + fmt.Sprintf("%v", node.Value) + `"` + return visitor.ActionUpdate, strconv.Quote(node.Value) case map[string]interface{}: return visitor.ActionUpdate, `"` + getMapValueString(node, "Value") + `"` } diff --git a/language/printer/printer_test.go b/language/printer/printer_test.go index 1c48426d3..ded09255f 100644 --- a/language/printer/printer_test.go +++ b/language/printer/printer_test.go @@ -5,10 +5,10 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/parser" - "github.com/graphql-go/graphql/language/printer" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/parser" + "github.com/teamjobot/graphql/language/printer" + "github.com/teamjobot/graphql/testutil" ) func parse(t *testing.T, query string) *ast.Document { @@ -186,3 +186,17 @@ fragment frag on Follower { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, results)) } } + +func TestPrinter_CorrectlyPrintsStringArgumentsWithProperQuoting(t *testing.T) { + queryAst := `query { foo(jsonStr: "{\"foo\": \"bar\"}") }` + expected := `{ + foo(jsonStr: "{\"foo\": \"bar\"}") +} +` + astDoc := parse(t, queryAst) + results := printer.Print(astDoc) + + if !reflect.DeepEqual(expected, results) { + t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, results)) + } +} diff --git a/language/printer/schema_printer_test.go b/language/printer/schema_printer_test.go index d080f551a..14910d8c2 100644 --- a/language/printer/schema_printer_test.go +++ b/language/printer/schema_printer_test.go @@ -5,9 +5,9 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/printer" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/printer" + "github.com/teamjobot/graphql/testutil" ) func TestSchemaPrinter_PrintsMinimalAST(t *testing.T) { diff --git a/language/typeInfo/type_info.go b/language/typeInfo/type_info.go index e012ee027..e570e3623 100644 --- a/language/typeInfo/type_info.go +++ b/language/typeInfo/type_info.go @@ -1,7 +1,7 @@ package typeInfo import ( - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql/language/ast" ) // TypeInfoI defines the interface for TypeInfo Implementation diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index efd720dd3..06222650f 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -4,8 +4,8 @@ import ( "encoding/json" "reflect" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/typeInfo" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/typeInfo" ) const ( diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index 33e6fee75..c5eb35088 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -7,13 +7,13 @@ import ( "fmt" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/kinds" - "github.com/graphql-go/graphql/language/parser" - "github.com/graphql-go/graphql/language/printer" - "github.com/graphql-go/graphql/language/visitor" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/kinds" + "github.com/teamjobot/graphql/language/parser" + "github.com/teamjobot/graphql/language/printer" + "github.com/teamjobot/graphql/language/visitor" + "github.com/teamjobot/graphql/testutil" ) func parse(t *testing.T, query string) *ast.Document { diff --git a/lists_test.go b/lists_test.go index 9c098ea39..f9a27397b 100644 --- a/lists_test.go +++ b/lists_test.go @@ -4,10 +4,10 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) func checkList(t *testing.T, testType graphql.Type, testData interface{}, expected *graphql.Result) { diff --git a/located.go b/located.go index 66c61e49c..80f7e3a13 100644 --- a/located.go +++ b/located.go @@ -3,8 +3,8 @@ package graphql import ( "errors" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" ) func NewLocatedError(err interface{}, nodes []ast.Node) *gqlerrors.Error { diff --git a/mutations_test.go b/mutations_test.go index a97dda523..66b5c58f5 100644 --- a/mutations_test.go +++ b/mutations_test.go @@ -4,10 +4,10 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) // testNumberHolder maps to numberHolderType diff --git a/nonnull_test.go b/nonnull_test.go index b5a6683a8..da30ef62e 100644 --- a/nonnull_test.go +++ b/nonnull_test.go @@ -4,10 +4,10 @@ import ( "sort" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) var syncError = "sync" diff --git a/race_test.go b/race_test.go index 87eb770fd..93f1f2d97 100644 --- a/race_test.go +++ b/race_test.go @@ -23,7 +23,7 @@ func TestRace(t *testing.T) { "runtime" "sync" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) func main() { diff --git a/rules.go b/rules.go index ae0c75b9d..9b6df279d 100644 --- a/rules.go +++ b/rules.go @@ -7,11 +7,11 @@ import ( "sort" "strings" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/kinds" - "github.com/graphql-go/graphql/language/printer" - "github.com/graphql-go/graphql/language/visitor" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/kinds" + "github.com/teamjobot/graphql/language/printer" + "github.com/teamjobot/graphql/language/visitor" ) // SpecifiedRules set includes all validation rules defined by the GraphQL spec. diff --git a/rules_arguments_of_correct_type_test.go b/rules_arguments_of_correct_type_test.go index ecd4bea4f..bc678b5a4 100644 --- a/rules_arguments_of_correct_type_test.go +++ b/rules_arguments_of_correct_type_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodIntValue(t *testing.T) { diff --git a/rules_default_values_of_correct_type_test.go b/rules_default_values_of_correct_type_test.go index 8457b3889..2dfb46e93 100644 --- a/rules_default_values_of_correct_type_test.go +++ b/rules_default_values_of_correct_type_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_VariableDefaultValuesOfCorrectType_VariablesWithNoDefaultValues(t *testing.T) { diff --git a/rules_fields_on_correct_type_test.go b/rules_fields_on_correct_type_test.go index 8cde9f377..e17e78c5b 100644 --- a/rules_fields_on_correct_type_test.go +++ b/rules_fields_on_correct_type_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_FieldsOnCorrectType_ObjectFieldSelection(t *testing.T) { diff --git a/rules_fragments_on_composite_types_test.go b/rules_fragments_on_composite_types_test.go index efe072abd..3e35daa20 100644 --- a/rules_fragments_on_composite_types_test.go +++ b/rules_fragments_on_composite_types_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_FragmentsOnCompositeTypes_ObjectIsValidFragmentType(t *testing.T) { diff --git a/rules_known_argument_names_test.go b/rules_known_argument_names_test.go index 332cfd887..3b57b29cc 100644 --- a/rules_known_argument_names_test.go +++ b/rules_known_argument_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_KnownArgumentNames_SingleArgIsKnown(t *testing.T) { diff --git a/rules_known_directives_rule_test.go b/rules_known_directives_rule_test.go index f3d8231c3..8b44ce845 100644 --- a/rules_known_directives_rule_test.go +++ b/rules_known_directives_rule_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_KnownDirectives_WithNoDirectives(t *testing.T) { diff --git a/rules_known_fragment_names_test.go b/rules_known_fragment_names_test.go index eb522b260..adb716263 100644 --- a/rules_known_fragment_names_test.go +++ b/rules_known_fragment_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_KnownFragmentNames_KnownFragmentNamesAreValid(t *testing.T) { diff --git a/rules_known_type_names_test.go b/rules_known_type_names_test.go index 611a80378..28ff736fb 100644 --- a/rules_known_type_names_test.go +++ b/rules_known_type_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_KnownTypeNames_KnownTypeNamesAreValid(t *testing.T) { diff --git a/rules_lone_anonymous_operation_rule_test.go b/rules_lone_anonymous_operation_rule_test.go index 8fb6894f6..fce4417e4 100644 --- a/rules_lone_anonymous_operation_rule_test.go +++ b/rules_lone_anonymous_operation_rule_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_AnonymousOperationMustBeAlone_NoOperations(t *testing.T) { diff --git a/rules_no_fragment_cycles_test.go b/rules_no_fragment_cycles_test.go index f194e3055..2740b9a05 100644 --- a/rules_no_fragment_cycles_test.go +++ b/rules_no_fragment_cycles_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_NoCircularFragmentSpreads_SingleReferenceIsValid(t *testing.T) { diff --git a/rules_no_undefined_variables_test.go b/rules_no_undefined_variables_test.go index 0b2537159..364c7d907 100644 --- a/rules_no_undefined_variables_test.go +++ b/rules_no_undefined_variables_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_NoUndefinedVariables_AllVariablesDefined(t *testing.T) { diff --git a/rules_no_unused_fragments_test.go b/rules_no_unused_fragments_test.go index 47f70ad31..d4d6906eb 100644 --- a/rules_no_unused_fragments_test.go +++ b/rules_no_unused_fragments_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_NoUnusedFragments_AllFragmentNamesAreUsed(t *testing.T) { diff --git a/rules_no_unused_variables_test.go b/rules_no_unused_variables_test.go index 7c331f4a4..092479ee0 100644 --- a/rules_no_unused_variables_test.go +++ b/rules_no_unused_variables_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_NoUnusedVariables_UsesAllVariables(t *testing.T) { diff --git a/rules_overlapping_fields_can_be_merged.go b/rules_overlapping_fields_can_be_merged.go index ccb769d0f..621b89337 100644 --- a/rules_overlapping_fields_can_be_merged.go +++ b/rules_overlapping_fields_can_be_merged.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/kinds" - "github.com/graphql-go/graphql/language/printer" - "github.com/graphql-go/graphql/language/visitor" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/kinds" + "github.com/teamjobot/graphql/language/printer" + "github.com/teamjobot/graphql/language/visitor" ) func fieldsConflictMessage(responseName string, reason conflictReason) string { diff --git a/rules_overlapping_fields_can_be_merged_test.go b/rules_overlapping_fields_can_be_merged_test.go index bf36bae82..58d6e4ed6 100644 --- a/rules_overlapping_fields_can_be_merged_test.go +++ b/rules_overlapping_fields_can_be_merged_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_OverlappingFieldsCanBeMerged_UniqueFields(t *testing.T) { diff --git a/rules_possible_fragment_spreads_test.go b/rules_possible_fragment_spreads_test.go index 9c0dff545..155dcab4b 100644 --- a/rules_possible_fragment_spreads_test.go +++ b/rules_possible_fragment_spreads_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_PossibleFragmentSpreads_OfTheSameObject(t *testing.T) { diff --git a/rules_provided_non_null_arguments_test.go b/rules_provided_non_null_arguments_test.go index fed6c0088..5ae122850 100644 --- a/rules_provided_non_null_arguments_test.go +++ b/rules_provided_non_null_arguments_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_ProvidedNonNullArguments_IgnoresUnknownArguments(t *testing.T) { diff --git a/rules_scalar_leafs_test.go b/rules_scalar_leafs_test.go index 097299524..15ff679a2 100644 --- a/rules_scalar_leafs_test.go +++ b/rules_scalar_leafs_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_ScalarLeafs_ValidScalarSelection(t *testing.T) { diff --git a/rules_unique_argument_names_test.go b/rules_unique_argument_names_test.go index b0e3ec517..86b5c30cc 100644 --- a/rules_unique_argument_names_test.go +++ b/rules_unique_argument_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_UniqueArgumentNames_NoArgumentsOnField(t *testing.T) { diff --git a/rules_unique_fragment_names_test.go b/rules_unique_fragment_names_test.go index 5cacd5e9e..9e0245a25 100644 --- a/rules_unique_fragment_names_test.go +++ b/rules_unique_fragment_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_UniqueFragmentNames_NoFragments(t *testing.T) { diff --git a/rules_unique_input_field_names_test.go b/rules_unique_input_field_names_test.go index a2e2e251f..15117756a 100644 --- a/rules_unique_input_field_names_test.go +++ b/rules_unique_input_field_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_UniqueInputFieldNames_InputObjectWithFields(t *testing.T) { diff --git a/rules_unique_operation_names_test.go b/rules_unique_operation_names_test.go index 265c3f423..ebad347f6 100644 --- a/rules_unique_operation_names_test.go +++ b/rules_unique_operation_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_UniqueOperationNames_NoOperations(t *testing.T) { diff --git a/rules_unique_variable_names_test.go b/rules_unique_variable_names_test.go index 63bf77785..2adad405f 100644 --- a/rules_unique_variable_names_test.go +++ b/rules_unique_variable_names_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_UniqueVariableNames_UniqueVariableNames(t *testing.T) { diff --git a/rules_variables_are_input_types_test.go b/rules_variables_are_input_types_test.go index fb1d16757..8b7c10a24 100644 --- a/rules_variables_are_input_types_test.go +++ b/rules_variables_are_input_types_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_VariablesAreInputTypes_(t *testing.T) { diff --git a/rules_variables_in_allowed_position_test.go b/rules_variables_in_allowed_position_test.go index 78dd77ea0..d12fb12c1 100644 --- a/rules_variables_in_allowed_position_test.go +++ b/rules_variables_in_allowed_position_test.go @@ -3,9 +3,9 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/testutil" ) func TestValidate_VariablesInAllowedPosition_BooleanToBoolean(t *testing.T) { diff --git a/scalars.go b/scalars.go index 45479b545..6e995f1e5 100644 --- a/scalars.go +++ b/scalars.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql/language/ast" ) // As per the GraphQL Spec, Integers are only treated as valid when a valid diff --git a/scalars_parse_test.go b/scalars_parse_test.go index 4388e4a9b..4c6636ffd 100644 --- a/scalars_parse_test.go +++ b/scalars_parse_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/language/ast" ) func TestTypeSystem_Scalar_ParseValueOutputDateTime(t *testing.T) { diff --git a/scalars_serialization_test.go b/scalars_serialization_test.go index e6a85f515..a10bc1595 100644 --- a/scalars_serialization_test.go +++ b/scalars_serialization_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/graphql-go/graphql" + "github.com/teamjobot/graphql" ) type intSerializationTest struct { diff --git a/schema.go b/schema.go index 53971645a..35519ac42 100644 --- a/schema.go +++ b/schema.go @@ -1,9 +1,5 @@ package graphql -import ( - "fmt" -) - type SchemaConfig struct { Query *Object Mutation *Object @@ -403,12 +399,12 @@ func assertObjectImplementsInterface(schema *Schema, object *Object, iface *Inte // Assert interface field type is satisfied by object field type, by being // a valid subtype. (covariant) - err = invariant( + err = invariantf( isTypeSubTypeOf(schema, objectField.Type, ifaceField.Type), - fmt.Sprintf(`%v.%v expects type "%v" but `+ + `%v.%v expects type "%v" but `+ `%v.%v provides type "%v".`, - iface, fieldName, ifaceField.Type, - object, fieldName, objectField.Type), + iface, fieldName, ifaceField.Type, + object, fieldName, objectField.Type, ) if err != nil { return err @@ -425,12 +421,12 @@ func assertObjectImplementsInterface(schema *Schema, object *Object, iface *Inte } } // Assert interface field arg exists on object field. - err = invariant( + err = invariantf( objectArg != nil, - fmt.Sprintf(`%v.%v expects argument "%v" but `+ + `%v.%v expects argument "%v" but `+ `%v.%v does not provide it.`, - iface, fieldName, argName, - object, fieldName), + iface, fieldName, argName, + object, fieldName, ) if err != nil { return err @@ -438,14 +434,13 @@ func assertObjectImplementsInterface(schema *Schema, object *Object, iface *Inte // Assert interface field arg type matches object field arg type. // (invariant) - err = invariant( + err = invariantf( isEqualType(ifaceArg.Type, objectArg.Type), - fmt.Sprintf( - `%v.%v(%v:) expects type "%v" `+ - `but %v.%v(%v:) provides `+ - `type "%v".`, - iface, fieldName, argName, ifaceArg.Type, - object, fieldName, argName, objectArg.Type), + `%v.%v(%v:) expects type "%v" `+ + `but %v.%v(%v:) provides `+ + `type "%v".`, + iface, fieldName, argName, ifaceArg.Type, + object, fieldName, argName, objectArg.Type, ) if err != nil { return err @@ -464,12 +459,12 @@ func assertObjectImplementsInterface(schema *Schema, object *Object, iface *Inte if ifaceArg == nil { _, ok := objectArg.Type.(*NonNull) - err = invariant( + err = invariantf( !ok, - fmt.Sprintf(`%v.%v(%v:) is of required type `+ + `%v.%v(%v:) is of required type `+ `"%v" but is not also provided by the interface %v.%v.`, - object, fieldName, argName, - objectArg.Type, iface, fieldName), + object, fieldName, argName, + objectArg.Type, iface, fieldName, ) if err != nil { return err diff --git a/subscription.go b/subscription.go new file mode 100644 index 000000000..3b41c486b --- /dev/null +++ b/subscription.go @@ -0,0 +1,228 @@ +package graphql + +import ( + "context" + "fmt" + + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/parser" + "github.com/teamjobot/graphql/language/source" +) + +// SubscribeParams parameters for subscribing +type SubscribeParams struct { + Schema Schema + RequestString string + RootValue interface{} + // ContextValue context.Context + VariableValues map[string]interface{} + OperationName string + FieldResolver FieldResolveFn + FieldSubscriber FieldResolveFn +} + +// Subscribe performs a subscribe operation on the given query and schema +// To finish a subscription you can simply close the channel from inside the `Subscribe` function +// currently does not support extensions hooks +func Subscribe(p Params) chan *Result { + + source := source.NewSource(&source.Source{ + Body: []byte(p.RequestString), + Name: "GraphQL request", + }) + + // TODO run extensions hooks + + // parse the source + AST, err := parser.Parse(parser.ParseParams{Source: source}) + if err != nil { + + // merge the errors from extensions and the original error from parser + return sendOneResultAndClose(&Result{ + Errors: gqlerrors.FormatErrors(err), + }) + } + + // validate document + validationResult := ValidateDocument(&p.Schema, AST, nil) + + if !validationResult.IsValid { + // run validation finish functions for extensions + return sendOneResultAndClose(&Result{ + Errors: validationResult.Errors, + }) + + } + return ExecuteSubscription(ExecuteParams{ + Schema: p.Schema, + Root: p.RootObject, + AST: AST, + OperationName: p.OperationName, + Args: p.VariableValues, + Context: p.Context, + }) +} + +func sendOneResultAndClose(res *Result) chan *Result { + resultChannel := make(chan *Result, 1) + resultChannel <- res + close(resultChannel) + return resultChannel +} + +// ExecuteSubscription is similar to graphql.Execute but returns a channel instead of a Result +// currently does not support extensions +func ExecuteSubscription(p ExecuteParams) chan *Result { + + if p.Context == nil { + p.Context = context.Background() + } + + var mapSourceToResponse = func(payload interface{}) *Result { + return Execute(ExecuteParams{ + Schema: p.Schema, + Root: payload, + AST: p.AST, + OperationName: p.OperationName, + Args: p.Args, + Context: p.Context, + }) + } + var resultChannel = make(chan *Result) + go func() { + defer close(resultChannel) + defer func() { + if err := recover(); err != nil { + e, ok := err.(error) + if !ok { + return + } + resultChannel <- &Result{ + Errors: gqlerrors.FormatErrors(e), + } + } + return + }() + + exeContext, err := buildExecutionContext(buildExecutionCtxParams{ + Schema: p.Schema, + Root: p.Root, + AST: p.AST, + OperationName: p.OperationName, + Args: p.Args, + Context: p.Context, + }) + + if err != nil { + resultChannel <- &Result{ + Errors: gqlerrors.FormatErrors(err), + } + + return + } + + operationType, err := getOperationRootType(p.Schema, exeContext.Operation) + if err != nil { + resultChannel <- &Result{ + Errors: gqlerrors.FormatErrors(err), + } + + return + } + + fields := collectFields(collectFieldsParams{ + ExeContext: exeContext, + RuntimeType: operationType, + SelectionSet: exeContext.Operation.GetSelectionSet(), + }) + + responseNames := []string{} + for name := range fields { + responseNames = append(responseNames, name) + } + responseName := responseNames[0] + fieldNodes := fields[responseName] + fieldNode := fieldNodes[0] + fieldName := fieldNode.Name.Value + fieldDef := getFieldDef(p.Schema, operationType, fieldName) + + if fieldDef == nil { + resultChannel <- &Result{ + Errors: gqlerrors.FormatErrors(fmt.Errorf("the subscription field %q is not defined", fieldName)), + } + + return + } + + resolveFn := fieldDef.Subscribe + + if resolveFn == nil { + resultChannel <- &Result{ + Errors: gqlerrors.FormatErrors(fmt.Errorf("the subscription function %q is not defined", fieldName)), + } + return + } + fieldPath := &ResponsePath{ + Key: responseName, + } + + args := getArgumentValues(fieldDef.Args, fieldNode.Arguments, exeContext.VariableValues) + info := ResolveInfo{ + FieldName: fieldName, + FieldASTs: fieldNodes, + Path: fieldPath, + ReturnType: fieldDef.Type, + ParentType: operationType, + Schema: p.Schema, + Fragments: exeContext.Fragments, + RootValue: exeContext.Root, + Operation: exeContext.Operation, + VariableValues: exeContext.VariableValues, + } + + fieldResult, err := resolveFn(ResolveParams{ + Source: p.Root, + Args: args, + Info: info, + Context: p.Context, + }) + if err != nil { + resultChannel <- &Result{ + Errors: gqlerrors.FormatErrors(err), + } + + return + } + + if fieldResult == nil { + resultChannel <- &Result{ + Errors: gqlerrors.FormatErrors(fmt.Errorf("no field result")), + } + + return + } + + switch fieldResult.(type) { + case chan interface{}: + sub := fieldResult.(chan interface{}) + for { + select { + case <-p.Context.Done(): + return + + case res, more := <-sub: + if !more { + return + } + resultChannel <- mapSourceToResponse(res) + } + } + default: + resultChannel <- mapSourceToResponse(fieldResult) + return + } + }() + + // return a result channel + return resultChannel +} diff --git a/subscription_test.go b/subscription_test.go new file mode 100644 index 000000000..21e7cc244 --- /dev/null +++ b/subscription_test.go @@ -0,0 +1,287 @@ +package graphql_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" +) + +func TestSchemaSubscribe(t *testing.T) { + + testutil.RunSubscribes(t, []*testutil.TestSubscription{ + { + Name: "subscribe without resolver", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "sub_without_resolver": &graphql.Field{ + Type: graphql.String, + Subscribe: makeSubscribeToMapFunction([]map[string]interface{}{ + { + "sub_without_resolver": "a", + }, + { + "sub_without_resolver": "b", + }, + { + "sub_without_resolver": "c", + }, + }), + }, + }, + }), + Query: ` + subscription { + sub_without_resolver + } + `, + ExpectedResults: []testutil.TestResponse{ + {Data: `{ "sub_without_resolver": "a" }`}, + {Data: `{ "sub_without_resolver": "b" }`}, + {Data: `{ "sub_without_resolver": "c" }`}, + }, + }, + { + Name: "subscribe with resolver", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "sub_with_resolver": &graphql.Field{ + Type: graphql.String, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return p.Source, nil + }, + Subscribe: makeSubscribeToStringFunction([]string{"a", "b", "c"}), + }, + }, + }), + Query: ` + subscription { + sub_with_resolver + } + `, + ExpectedResults: []testutil.TestResponse{ + {Data: `{ "sub_with_resolver": "a" }`}, + {Data: `{ "sub_with_resolver": "b" }`}, + {Data: `{ "sub_with_resolver": "c" }`}, + }, + }, + { + Name: "receive query validation error", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "sub_without_resolver": &graphql.Field{ + Type: graphql.String, + Subscribe: makeSubscribeToStringFunction([]string{"a", "b", "c"}), + }, + }, + }), + Query: ` + subscription { + sub_without_resolver + xxx + } + `, + ExpectedResults: []testutil.TestResponse{ + {Errors: []string{"Cannot query field \"xxx\" on type \"Subscription\"."}}, + }, + }, + { + Name: "panic inside subscribe is recovered", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "should_error": &graphql.Field{ + Type: graphql.String, + Subscribe: func(p graphql.ResolveParams) (interface{}, error) { + panic(errors.New("got a panic error")) + }, + }, + }, + }), + Query: ` + subscription { + should_error + } + `, + ExpectedResults: []testutil.TestResponse{ + {Errors: []string{"got a panic error"}}, + }, + }, + { + Name: "subscribe with resolver changes output", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "sub_with_resolver": &graphql.Field{ + Type: graphql.String, + Subscribe: makeSubscribeToStringFunction([]string{"a", "b", "c", "d"}), + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return fmt.Sprintf("result=%v", p.Source), nil + }, + }, + }, + }), + Query: ` + subscription { + sub_with_resolver + } + `, + ExpectedResults: []testutil.TestResponse{ + {Data: `{ "sub_with_resolver": "result=a" }`}, + {Data: `{ "sub_with_resolver": "result=b" }`}, + {Data: `{ "sub_with_resolver": "result=c" }`}, + {Data: `{ "sub_with_resolver": "result=d" }`}, + }, + }, + { + Name: "subscribe to a nested object", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "sub_with_object": &graphql.Field{ + Type: graphql.NewObject(graphql.ObjectConfig{ + Name: "Obj", + Fields: graphql.Fields{ + "field": &graphql.Field{ + Type: graphql.String, + }, + }, + }), + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return p.Source, nil + }, + Subscribe: makeSubscribeToMapFunction([]map[string]interface{}{ + { + "field": "hello", + }, + { + "field": "bye", + }, + { + "field": nil, + }, + }), + }, + }, + }), + Query: ` + subscription { + sub_with_object { + field + } + } + `, + ExpectedResults: []testutil.TestResponse{ + {Data: `{ "sub_with_object": { "field": "hello" } }`}, + {Data: `{ "sub_with_object": { "field": "bye" } }`}, + {Data: `{ "sub_with_object": { "field": null } }`}, + }, + }, + + { + Name: "subscription_resolver_can_error", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "should_error": &graphql.Field{ + Type: graphql.String, + Subscribe: func(p graphql.ResolveParams) (interface{}, error) { + return nil, errors.New("got a subscribe error") + }, + }, + }, + }), + Query: ` + subscription { + should_error + } + `, + ExpectedResults: []testutil.TestResponse{ + { + Errors: []string{"got a subscribe error"}, + }, + }, + }, + { + Name: "schema_without_subscribe_errors", + Schema: makeSubscriptionSchema(t, graphql.ObjectConfig{ + Name: "Subscription", + Fields: graphql.Fields{ + "should_error": &graphql.Field{ + Type: graphql.String, + }, + }, + }), + Query: ` + subscription { + should_error + } + `, + ExpectedResults: []testutil.TestResponse{ + { + Errors: []string{"the subscription function \"should_error\" is not defined"}, + }, + }, + }, + }) +} + +func makeSubscribeToStringFunction(elements []string) func(p graphql.ResolveParams) (interface{}, error) { + return func(p graphql.ResolveParams) (interface{}, error) { + c := make(chan interface{}) + go func() { + for _, r := range elements { + select { + case <-p.Context.Done(): + close(c) + return + case c <- r: + } + } + close(c) + }() + return c, nil + } +} + +func makeSubscribeToMapFunction(elements []map[string]interface{}) func(p graphql.ResolveParams) (interface{}, error) { + return func(p graphql.ResolveParams) (interface{}, error) { + c := make(chan interface{}) + go func() { + for _, r := range elements { + select { + case <-p.Context.Done(): + close(c) + return + case c <- r: + } + } + close(c) + }() + return c, nil + } +} + +func makeSubscriptionSchema(t *testing.T, c graphql.ObjectConfig) graphql.Schema { + schema, err := graphql.NewSchema(graphql.SchemaConfig{ + Query: dummyQuery, + Subscription: graphql.NewObject(c), + }) + if err != nil { + t.Errorf("failed to create schema: %v", err) + } + return schema +} + +var dummyQuery = graphql.NewObject(graphql.ObjectConfig{ + Name: "Query", + Fields: graphql.Fields{ + + "hello": &graphql.Field{Type: graphql.String}, + }, +}) diff --git a/testutil/rules_test_harness.go b/testutil/rules_test_harness.go index 384f447e7..3466a8549 100644 --- a/testutil/rules_test_harness.go +++ b/testutil/rules_test_harness.go @@ -3,11 +3,11 @@ package testutil import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/language/parser" - "github.com/graphql-go/graphql/language/source" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/language/parser" + "github.com/teamjobot/graphql/language/source" ) var TestSchema *graphql.Schema diff --git a/testutil/subscription.go b/testutil/subscription.go new file mode 100644 index 000000000..c02ce4323 --- /dev/null +++ b/testutil/subscription.go @@ -0,0 +1,149 @@ +package testutil + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "strconv" + "testing" + + "github.com/teamjobot/graphql" +) + +// TestResponse models the expected response +type TestResponse struct { + Data string + Errors []string +} + +// TestSubscription is a GraphQL test case to be used with RunSubscribe. +type TestSubscription struct { + Name string + Schema graphql.Schema + Query string + OperationName string + Variables map[string]interface{} + ExpectedResults []TestResponse +} + +// RunSubscribes runs the given GraphQL subscription test cases as subtests. +func RunSubscribes(t *testing.T, tests []*TestSubscription) { + for i, test := range tests { + if test.Name == "" { + test.Name = strconv.Itoa(i + 1) + } + + t.Run(test.Name, func(t *testing.T) { + RunSubscribe(t, test) + }) + } +} + +// RunSubscribe runs a single GraphQL subscription test case. +func RunSubscribe(t *testing.T, test *TestSubscription) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + c := graphql.Subscribe(graphql.Params{ + Context: ctx, + OperationName: test.OperationName, + RequestString: test.Query, + VariableValues: test.Variables, + Schema: test.Schema, + }) + // if err != nil { + // if err.Error() != test.ExpectedErr.Error() { + // t.Fatalf("unexpected error: got %+v, want %+v", err, test.ExpectedErr) + // } + + // return + // } + + var results []*graphql.Result + for res := range c { + t.Log(pretty(res)) + results = append(results, res) + } + + for i, expected := range test.ExpectedResults { + if len(results)-1 < i { + t.Error(errors.New("not enough results, expected results are more than actual results")) + return + } + res := results[i] + + var errs []string + for _, err := range res.Errors { + errs = append(errs, err.Message) + } + checkErrorStrings(t, expected.Errors, errs) + if expected.Data == "" { + continue + } + + got, err := json.MarshalIndent(res.Data, "", " ") + if err != nil { + t.Fatalf("got: invalid JSON: %s; raw: %s", err, got) + } + + if err != nil { + t.Fatal(err) + } + want, err := formatJSON(expected.Data) + if err != nil { + t.Fatalf("got: invalid JSON: %s; raw: %s", err, res.Data) + } + + if !bytes.Equal(got, want) { + t.Logf("got: %s", got) + t.Logf("want: %s", want) + t.Fail() + } + } +} + +func checkErrorStrings(t *testing.T, expected, actual []string) { + expectedCount, actualCount := len(expected), len(actual) + + if expectedCount != actualCount { + t.Fatalf("unexpected number of errors: want `%d`, got `%d`", expectedCount, actualCount) + } + + if expectedCount > 0 { + for i, want := range expected { + got := actual[i] + + if got != want { + t.Fatalf("unexpected error: got `%+v`, want `%+v`", got, want) + } + } + + // Return because we're done checking. + return + } + + for _, err := range actual { + t.Errorf("unexpected error: '%s'", err) + } +} + +func formatJSON(data string) ([]byte, error) { + var v interface{} + if err := json.Unmarshal([]byte(data), &v); err != nil { + return nil, err + } + formatted, err := json.MarshalIndent(v, "", " ") + if err != nil { + return nil, err + } + return formatted, nil +} + +func pretty(x interface{}) string { + got, err := json.MarshalIndent(x, "", " ") + if err != nil { + panic(err) + } + return string(got) +} diff --git a/testutil/testutil.go b/testutil/testutil.go index 0d905542d..f3564565e 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -7,10 +7,10 @@ import ( "strconv" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/parser" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/parser" ) var ( diff --git a/testutil/testutil_test.go b/testutil/testutil_test.go index ca61eec73..915ff75ac 100644 --- a/testutil/testutil_test.go +++ b/testutil/testutil_test.go @@ -3,7 +3,7 @@ package testutil_test import ( "testing" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql/testutil" ) func TestSubsetSlice_Simple(t *testing.T) { diff --git a/type_info.go b/type_info.go index 20c8886a4..a44d16802 100644 --- a/type_info.go +++ b/type_info.go @@ -1,8 +1,8 @@ package graphql import ( - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/kinds" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/kinds" ) // TODO: can move TypeInfo to a utils package if there ever is one diff --git a/types.go b/types.go index 5b991d8c4..d4e481a56 100644 --- a/types.go +++ b/types.go @@ -1,7 +1,7 @@ package graphql import ( - "github.com/graphql-go/graphql/gqlerrors" + "github.com/teamjobot/graphql/gqlerrors" ) // type Schema interface{} diff --git a/union_interface_test.go b/union_interface_test.go index 8f850d609..5a778187d 100644 --- a/union_interface_test.go +++ b/union_interface_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" ) type testNamedType interface { diff --git a/util.go b/util.go index ea20f47c7..a48ebca2d 100644 --- a/util.go +++ b/util.go @@ -1,19 +1,95 @@ package graphql import ( + "encoding" "fmt" "reflect" "strings" ) const TAG = "json" +const TYPETAG = "graphql" + +var boundTypes = map[string]*Object{} +var anonTypes = 0 + +func MergeFields(fieldses ...Fields) (ret Fields) { + ret = Fields{} + for _, fields := range fieldses { + for key, field := range fields { + if _, ok := ret[key]; ok { + panic(fmt.Sprintf("Dupliate field: %s", key)) + } + ret[key] = field + } + } + return ret +} + +func BindType(tipe reflect.Type) Type { + if tipe.Kind() == reflect.Ptr { + tipe = tipe.Elem() + } + + kind := tipe.Kind() + switch kind { + case reflect.String: + return String + case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64: + return Int + case reflect.Float32, reflect.Float64: + return Float + case reflect.Bool: + return Boolean + case reflect.Slice: + return getGraphList(tipe) + } + + typeName := safeName(tipe) + object, ok := boundTypes[typeName] + if !ok { + // Allows for recursion + object = &Object{} + boundTypes[typeName] = object + *object = *NewObject(ObjectConfig{ + Name: typeName, + Fields: BindFields(reflect.New(tipe).Interface()), + }) + } + + return object +} + +func safeName(tipe reflect.Type) string { + name := fmt.Sprint(tipe) + if strings.HasPrefix(name, "struct ") { + anonTypes++ + name = fmt.Sprintf("Anon%d", anonTypes) + } else { + name = strings.Replace(fmt.Sprint(tipe), ".", "_", -1) + } + return name +} + +func getType(typeTag string) Output { + switch strings.ToLower(typeTag) { + case "int": + return Int + case "float": + return Float + case "string": + return String + case "boolean": + return Boolean + case "id": + return ID + case "datetime": + return DateTime + default: + panic(fmt.Sprintf("Unsupported graphql type: %s", typeTag)) + } +} -// can't take recursive slice type -// e.g -// type Person struct{ -// Friends []Person -// } -// it will throw panic stack-overflow func BindFields(obj interface{}) Fields { t := reflect.TypeOf(obj) v := reflect.ValueOf(obj) @@ -32,27 +108,34 @@ func BindFields(obj interface{}) Fields { continue } + typeTag := field.Tag.Get(TYPETAG) + fieldType := field.Type if fieldType.Kind() == reflect.Ptr { fieldType = fieldType.Elem() } - var graphType Output - if fieldType.Kind() == reflect.Struct { - structFields := BindFields(v.Field(i).Interface()) + if typeTag != "" { + graphType = getType(typeTag) + } else if fieldType.Kind() == reflect.Struct { + itf := v.Field(i).Interface() + if _, ok := itf.(encoding.TextMarshaler); ok { + fieldType = reflect.TypeOf("") + goto nonStruct + } + + structFields := BindFields(itf) if tag == "" { fields = appendFields(fields, structFields) continue } else { - graphType = NewObject(ObjectConfig{ - Name: tag, - Fields: structFields, - }) + graphType = BindType(fieldType) } } + nonStruct: if tag == "" { continue } @@ -102,11 +185,7 @@ func getGraphList(tipe reflect.Type) *List { } // finally bind object t := reflect.New(tipe.Elem()) - name := strings.Replace(fmt.Sprint(tipe.Elem()), ".", "_", -1) - obj := NewObject(ObjectConfig{ - Name: name, - Fields: BindFields(t.Elem().Interface()), - }) + obj := BindType(t.Elem().Type()) return NewList(obj) } @@ -122,15 +201,29 @@ func extractValue(originTag string, obj interface{}) interface{} { for j := 0; j < val.NumField(); j++ { field := val.Type().Field(j) + found := originTag == extractTag(field.Tag) if field.Type.Kind() == reflect.Struct { - res := extractValue(originTag, val.Field(j).Interface()) - if res != nil { - return res + fieldVal := val.Field(j) + if !fieldVal.IsZero() { + itf := fieldVal.Interface() + + if str, ok := itf.(encoding.TextMarshaler); ok && found { + byt, _ := str.MarshalText() + return string(byt) + } + + res := extractValue(originTag, itf) + if res != nil { + return res + } } } - if originTag == extractTag(field.Tag) { - return reflect.Indirect(val.Field(j)).Interface() + if found { + fieldVal := val.Field(j) + if !fieldVal.IsZero() { + return reflect.Indirect(fieldVal).Interface() + } } } return nil diff --git a/util_test.go b/util_test.go index 6fb6e9003..88f9f57a4 100644 --- a/util_test.go +++ b/util_test.go @@ -5,9 +5,10 @@ import ( "log" "reflect" "testing" + "time" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/testutil" ) type Person struct { @@ -19,9 +20,10 @@ type Person struct { } type Human struct { - Alive bool `json:"alive,omitempty"` - Age int `json:"age"` - Weight float64 `json:"weight"` + Alive bool `json:"alive,omitempty"` + Age int `json:"age"` + Weight float64 `json:"weight"` + DoB time.Time `json:"dob"` } type Friend struct { @@ -40,6 +42,7 @@ var personSource = Person{ Age: 24, Weight: 70.1, Alive: true, + DoB: time.Date(2019, 01, 01, 01, 01, 01, 0, time.UTC), }, Name: "John Doe", Home: Address{ @@ -82,6 +85,7 @@ func TestBindFields(t *testing.T) { { person{ name, + dob, home{street,city}, friends{name,address}, age, diff --git a/validation_test.go b/validation_test.go index 6c8fc5214..a04640885 100644 --- a/validation_test.go +++ b/validation_test.go @@ -3,8 +3,8 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/language/ast" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/language/ast" ) var someScalarType = graphql.NewScalar(graphql.ScalarConfig{ diff --git a/validator.go b/validator.go index 33379b858..5c8900401 100644 --- a/validator.go +++ b/validator.go @@ -1,10 +1,10 @@ package graphql import ( - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/kinds" - "github.com/graphql-go/graphql/language/visitor" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/kinds" + "github.com/teamjobot/graphql/language/visitor" ) type ValidationResult struct { diff --git a/validator_test.go b/validator_test.go index 6eaf00052..66271d069 100644 --- a/validator_test.go +++ b/validator_test.go @@ -3,13 +3,13 @@ package graphql_test import ( "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/language/parser" - "github.com/graphql-go/graphql/language/source" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/language/parser" + "github.com/teamjobot/graphql/language/source" + "github.com/teamjobot/graphql/testutil" ) func expectValid(t *testing.T, schema *graphql.Schema, queryString string) { diff --git a/values.go b/values.go index 06c08af6e..2e71ed97c 100644 --- a/values.go +++ b/values.go @@ -8,10 +8,10 @@ import ( "sort" "strings" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/kinds" - "github.com/graphql-go/graphql/language/printer" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/kinds" + "github.com/teamjobot/graphql/language/printer" ) // Prepares an object map of variableValues of the correct type based on the diff --git a/variables_test.go b/variables_test.go index 9dc430df1..91e1bff12 100644 --- a/variables_test.go +++ b/variables_test.go @@ -5,11 +5,11 @@ import ( "reflect" "testing" - "github.com/graphql-go/graphql" - "github.com/graphql-go/graphql/gqlerrors" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/location" - "github.com/graphql-go/graphql/testutil" + "github.com/teamjobot/graphql" + "github.com/teamjobot/graphql/gqlerrors" + "github.com/teamjobot/graphql/language/ast" + "github.com/teamjobot/graphql/language/location" + "github.com/teamjobot/graphql/testutil" ) var testComplexScalar *graphql.Scalar = graphql.NewScalar(graphql.ScalarConfig{