From 9b679109bd98e0865803ca409d237be3fb51339e Mon Sep 17 00:00:00 2001 From: Brandon Buck Date: Sun, 14 Feb 2016 23:36:29 -0600 Subject: [PATCH] Add Pointer support to GraphQL --- definition.go | 53 +++++++++++++++++++++++++++++++++++++++ definition_test.go | 31 +++++++++++++++++++++++ executor.go | 18 ++++++++++++-- executor_schema_test.go | 55 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/definition.go b/definition.go index c7aaa3be..461b0986 100644 --- a/definition.go +++ b/definition.go @@ -164,6 +164,7 @@ var _ Named = (*Interface)(nil) var _ Named = (*Union)(nil) var _ Named = (*Enum)(nil) var _ Named = (*InputObject)(nil) +var _ Named = (*Pointer)(nil) func GetNamed(ttype Type) Named { unmodifiedType := ttype @@ -1304,6 +1305,58 @@ func (gl *NonNull) Error() error { return gl.err } +// Pointer represents just that, a pointer to a value of another type. Pointers +// are specific to langauges that support them, such as Go. +type Pointer struct { + OfType Type `json:"ofType"` + + err error +} + +// NewPointer takes the given type and constructs a GraphQL pointer for that +// type. +func NewPointer(t Type) *Pointer { + p := &Pointer{} + + err := invariant(t != nil, fmt.Sprintf("Can only create Pointer of a Type but got: %v.", t)) + if err != nil { + p.err = err + + return p + } + + p.OfType = t + + return p +} + +// Name returns a GraphQL 'type name' that should represent what piece of data +// is defined. +func (p *Pointer) Name() string { + return fmt.Sprintf("*%v", p.OfType) +} + +// Description is empty at the moment. +func (p *Pointer) Description() string { + return "" +} + +// String returns the Name of the pointer, also defined to fit *Pointer to +// the fmt.Stringer interface. +func (p *Pointer) String() string { + if p.OfType != nil { + return p.Name() + } + + return "" +} + +// Error returns an error value that may be attached to the Pointer value from +// previous encounters. +func (p *Pointer) Error() error { + return p.err +} + var NAME_REGEXP, _ = regexp.Compile("^[_a-zA-Z][_a-zA-Z0-9]*$") func assertValidName(name string) error { diff --git a/definition_test.go b/definition_test.go index 6664feab..ee3fd9f5 100644 --- a/definition_test.go +++ b/definition_test.go @@ -64,6 +64,21 @@ var blogArticle = graphql.NewObject(graphql.ObjectConfig{ "body": &graphql.Field{ Type: graphql.String, }, + "comments": &graphql.Field{ + Type: graphql.NewPointer(graphql.NewList(blogComment)), + }, + }, +}) + +var blogComment = graphql.NewObject(graphql.ObjectConfig{ + Name: "Comment", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.NewPointer(graphql.String), + }, + "body": &graphql.Field{ + Type: graphql.NewPointer(graphql.String), + }, }, }) @@ -203,6 +218,19 @@ func TestTypeSystem_DefinitionExample_DefinesAQueryOnlySchema(t *testing.T) { if feedField.Name != "feed" { t.Fatalf("feedField.Name expected to equal `feed`, got: %v", feedField.Name) } + + commentField := articleFieldTypeObject.Fields()["comments"] + commentFieldPtr, ok := commentField.Type.(*graphql.Pointer) + if !ok { + t.Fatalf("expected commentFieldPtr to be a Pointer, got: %v", commentField) + } + commentFieldPtrList, ok := commentFieldPtr.OfType.(*graphql.List) + if !ok { + t.Fatalf("expected commentFieldPtrList to be a List, got: %v", commentFieldPtrList) + } + if commentFieldPtrList.OfType != blogComment { + t.Fatalf("commentFieldPtrList.OfType expected to equal blogComment, got: %v", commentFieldPtrList.OfType) + } } func TestTypeSystem_DefinitionExample_DefinesAMutationScheme(t *testing.T) { blogSchema, err := graphql.NewSchema(graphql.SchemaConfig{ @@ -377,6 +405,9 @@ func TestTypeSystem_DefinitionExample_StringifiesSimpleTypes(t *testing.T) { Test{graphql.NewNonNull(graphql.NewList(graphql.Int)), "[Int]!"}, Test{graphql.NewList(graphql.NewNonNull(graphql.Int)), "[Int!]"}, Test{graphql.NewList(graphql.NewList(graphql.Int)), "[[Int]]"}, + Test{graphql.NewPointer(graphql.Int), "*Int"}, + Test{graphql.NewPointer(graphql.NewList(graphql.Int)), "*[Int]"}, + Test{graphql.NewNonNull(graphql.NewPointer(graphql.Int)), "*Int!"}, } for _, test := range tests { ttypeStr := fmt.Sprintf("%v", test.ttype) diff --git a/executor.go b/executor.go index fb4e14c1..d95d4a8a 100644 --- a/executor.go +++ b/executor.go @@ -397,10 +397,10 @@ func doesFragmentConditionMatch(eCtx *ExecutionContext, fragment ast.Node, ttype if conditionalType == ttype { return true } - if conditionalType.Name() == ttype.Name() { + if conditionalType.Name() == ttype.Name() { return true } - + if conditionalType, ok := conditionalType.(Abstract); ok { return conditionalType.IsPossibleType(ttype) } @@ -588,6 +588,20 @@ func completeValue(eCtx *ExecutionContext, returnType Type, fieldASTs []*ast.Fie return nil } + // If field type is Pointer, pull value from pointer and pass it along + if returnType, ok := returnType.(*Pointer); ok { + resultVal := reflect.ValueOf(result) + err := invariant( + resultVal.IsValid() && resultVal.Type().Kind() == reflect.Ptr, + "User Error: expected pointer, but did not find one.", + ) + if err != nil { + panic(gqlerrors.FormatError(err)) + } + + return completeValue(eCtx, returnType.OfType, fieldASTs, info, resultVal.Elem().Interface()) + } + // If field type is List, complete each item in the list with the inner type if returnType, ok := returnType.(*List); ok { diff --git a/executor_schema_test.go b/executor_schema_test.go index b39c4c3a..a4d49440 100644 --- a/executor_schema_test.go +++ b/executor_schema_test.go @@ -28,6 +28,10 @@ type testAuthor struct { Pic testPicFn `json:"pic"` RecentArticle *testArticle `json:"recentArticle"` } +type testComment struct { + Id *int `json:"id"` + Body *string `json:"body"` +} type testArticle struct { Id string `json:"id"` IsPublished string `json:"isPublished"` @@ -36,6 +40,7 @@ type testArticle struct { Body string `json:"body"` Hidden string `json:"hidden"` Keywords []interface{} `json:"keywords"` + Comments []testComment `json:"comments"` } func getPic(id int, width, height string) *testPic { @@ -48,6 +53,14 @@ func getPic(id int, width, height string) *testPic { var johnSmith *testAuthor +func intToPtr(i int) *int { + return &i +} + +func stringToPtr(s string) *string { + return &s +} + func article(id interface{}) *testArticle { return &testArticle{ Id: fmt.Sprintf("%v", id), @@ -59,6 +72,10 @@ func article(id interface{}) *testArticle { Keywords: []interface{}{ "foo", "bar", 1, true, nil, }, + Comments: []testComment{ + {intToPtr(10), stringToPtr("This is a comment body")}, + {intToPtr(20), stringToPtr("This is another comment body")}, + }, } } @@ -118,6 +135,17 @@ func TestExecutesUsingAComplexSchema(t *testing.T) { "recentArticle": &graphql.Field{}, }, }) + blogComment := graphql.NewObject(graphql.ObjectConfig{ + Name: "Comment", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.NewPointer(graphql.Int), + }, + "body": &graphql.Field{ + Type: graphql.NewPointer(graphql.String), + }, + }, + }) blogArticle := graphql.NewObject(graphql.ObjectConfig{ Name: "Article", Fields: graphql.Fields{ @@ -139,6 +167,9 @@ func TestExecutesUsingAComplexSchema(t *testing.T) { "keywords": &graphql.Field{ Type: graphql.NewList(graphql.String), }, + "comments": &graphql.Field{ + Type: graphql.NewList(blogComment), + }, }, }) @@ -219,6 +250,10 @@ func TestExecutesUsingAComplexSchema(t *testing.T) { body, hidden, notdefined + comments { + id, + body + } } ` @@ -247,6 +282,26 @@ func TestExecutesUsingAComplexSchema(t *testing.T) { "true", nil, }, + "comments": []interface{}{ + map[string]interface{}{ + "id": 10, + "body": "This is a comment body", + }, + map[string]interface{}{ + "id": 20, + "body": "This is another comment body", + }, + }, + }, + }, + "comments": []interface{}{ + map[string]interface{}{ + "id": 10, + "body": "This is a comment body", + }, + map[string]interface{}{ + "id": 20, + "body": "This is another comment body", }, }, "id": "1",