Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added possibility to explore the actual field selection tree #422

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package graphql

import (
"context"

gcontext "github.com/graph-gophers/graphql-go/internal/context"
"github.com/graph-gophers/graphql-go/selected"
)

type Context struct {
Field selected.Field
}

// GraphQLContext is used to retrieved the graphql from the context. If no graphql
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong comment

// is present in the context, the `fallbackGraphql` received in parameter
// is returned instead.
func GraphQLContext(ctx context.Context) *Context {
field, found := gcontext.GraphQL(ctx)
if !found {
return nil
}

return &Context{
Field: field.ToSelection().(selected.Field),
}
}
14 changes: 12 additions & 2 deletions example/starwars/starwars.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
package starwars

import (
"context"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes here are just for demonstration purposes if someone wants to play with the feature a bit. To try it out, run the starwars example server and perform this curl call:

curl 'http://localhost:8080/query' -d '{"query":"query {search(text:\"C-3PO\") {... on Droid {name} ... on Starship {name}}}","variables":null}'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not intend to ship that if the feature is merged, it will be removed.

"encoding/base64"
"fmt"
"strconv"
"strings"

graphql "github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/selected"
)

var Schema = `
Expand Down Expand Up @@ -93,6 +95,8 @@ var Schema = `
appearsIn: [Episode!]!
# This droid's primary function
primaryFunction: String


}
# A connection object for a character's friends
type FriendsConnection {
Expand Down Expand Up @@ -301,7 +305,10 @@ func (r *Resolver) Reviews(args struct{ Episode string }) []*reviewResolver {
return l
}

func (r *Resolver) Search(args struct{ Text string }) []*searchResultResolver {
func (r *Resolver) Search(ctx context.Context, args struct{ Text string }) []*searchResultResolver {
graphqlContext := graphql.GraphQLContext(ctx)
selected.Dump(graphqlContext.Field)

var l []*searchResultResolver
for _, h := range humans {
if strings.Contains(h.Name, args.Text) {
Expand Down Expand Up @@ -338,7 +345,10 @@ func (r *Resolver) Human(args struct{ ID graphql.ID }) *humanResolver {
return nil
}

func (r *Resolver) Droid(args struct{ ID graphql.ID }) *droidResolver {
func (r *Resolver) Droid(ctx context.Context, args struct{ ID graphql.ID }) *droidResolver {
graphqlContext := graphql.GraphQLContext(ctx)
selected.Dump(graphqlContext.Field)

if d := droidData[args.ID]; d != nil {
return &droidResolver{d}
}
Expand Down
32 changes: 32 additions & 0 deletions internal/context/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package context

import (
"context"

"github.com/graph-gophers/graphql-go/internal/exec/selected"
)

type graphqlKeyType int

const graphqlFieldKey graphqlKeyType = iota

// WithGraphQLContext is used to create a new context with a graphql added to it
// so it can be later retrieved using `Graphql`.
func WithGraphQLContext(ctx context.Context, field *selected.SchemaField) context.Context {
return context.WithValue(ctx, graphqlFieldKey, field)
}

// GraphQL is used to retrieved the graphql from the context. If no graphql
// is present in the context, the `fallbackGraphql` received in parameter
// is returned instead.
func GraphQL(ctx context.Context) (field *selected.SchemaField, found bool) {
if ctx == nil {
return
}

if v, ok := ctx.Value(graphqlFieldKey).(*selected.SchemaField); ok {
return v, true
}

return
}
3 changes: 2 additions & 1 deletion internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
gcontext "github.com/graph-gophers/graphql-go/internal/context"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/internal/exec/selected"
"github.com/graph-gophers/graphql-go/internal/query"
Expand Down Expand Up @@ -197,7 +198,7 @@ func execFieldSelection(ctx context.Context, r *Request, s *resolvable.Schema, f
if f.field.UseMethodResolver() {
var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(traceCtx))
in = append(in, reflect.ValueOf(gcontext.WithGraphQLContext(traceCtx, f.field)))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
Expand Down
84 changes: 84 additions & 0 deletions internal/exec/selected/selected.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/graph-gophers/graphql-go/internal/query"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/introspection"
"github.com/graph-gophers/graphql-go/selected"
)

type Request struct {
Expand Down Expand Up @@ -44,6 +45,19 @@ func ApplyOperation(r *Request, s *resolvable.Schema, op *query.Operation) []Sel

type Selection interface {
isSelection()
ToSelection() selected.Selection
}

func toSelections(sels []Selection) (out []selected.Selection) {
if len(sels) == 0 {
return
}

out = make([]selected.Selection, len(sels))
for i, sel := range sels {
out[i] = sel.ToSelection()
}
return
}

type SchemaField struct {
Expand All @@ -56,16 +70,86 @@ type SchemaField struct {
FixedResult reflect.Value
}

func (f *SchemaField) Kind() selected.Kind {
return selected.FieldKind
}

func (f *SchemaField) Identifier() string {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't use Name() method here yet since the backing internal/exec/selected/*SchemaField already has a field Name so it conflicts.

At one point, if we all agree on the feature and the interface, I suggest the internal field changes to the interface looks better.

This is the same reasoning for Aliased below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't you use its own Name field then?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the new public interface would exposes it, hence a field with the same name cannot exist.

return f.Name
}

func (f *SchemaField) Aliased() string {
return f.Alias
}

func (f *SchemaField) Children() (out []selected.Selection) {
return toSelections(f.Sels)
}

func (f *SchemaField) ToSelection() selected.Selection {
return selected.Selection(f)
}

type TypeAssertion struct {
resolvable.TypeAssertion
Sels []Selection
}

func (f *TypeAssertion) Kind() selected.Kind {
return selected.TypeAssertionKind
}

func (f *TypeAssertion) Type() string {
var toType func(resolvable.Resolvable) string
toType = func(r resolvable.Resolvable) string {
if f.TypeExec == nil {
return ""
}

switch v := f.TypeExec.(type) {
case *resolvable.Scalar:
return "scalar"
case *resolvable.List:
return toType(v.Elem)
case *resolvable.Object:
return v.Name
default:
return "<unknown>"
}
}

return toType(f.TypeExec)
}

func (f *TypeAssertion) Children() (out []selected.Selection) {
return toSelections(f.Sels)
}

func (f *TypeAssertion) ToSelection() selected.Selection {
return selected.Selection(f)
}

type TypenameField struct {
resolvable.Object
Alias string
}

func (f *TypenameField) Kind() selected.Kind {
return selected.TypenameFieldKind
}

func (f *TypenameField) Aliased() string {
return f.Alias
}

func (f *TypenameField) Type() string {
return f.Name
}

func (f *TypenameField) ToSelection() selected.Selection {
return selected.Selection(f)
}

func (*SchemaField) isSelection() {}
func (*TypeAssertion) isSelection() {}
func (*TypenameField) isSelection() {}
Expand Down
3 changes: 2 additions & 1 deletion internal/exec/subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
gcontext "github.com/graph-gophers/graphql-go/internal/context"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/internal/exec/selected"
"github.com/graph-gophers/graphql-go/internal/query"
Expand Down Expand Up @@ -40,7 +41,7 @@ func (r *Request) Subscribe(ctx context.Context, s *resolvable.Schema, op *query

var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(ctx))
in = append(in, reflect.ValueOf(gcontext.WithGraphQLContext(ctx, f.field)))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
Expand Down
78 changes: 78 additions & 0 deletions selected/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package selected

import (
"fmt"
)

type Kind int

func (k Kind) String() string {
switch k {
case FieldKind:
return "field"
case TypeAssertionKind:
return "type_assertion"
case TypenameFieldKind:
return "typename_field"
default:
panic(fmt.Errorf("invalid kind %d received", k))
}
}

const (
FieldKind Kind = iota
TypeAssertionKind
TypenameFieldKind
)

type Selection interface {
Kind() Kind
}

type Field interface {
Selection
Identifier() string
Aliased() string
Children() []Selection
}

type TypeAssertion interface {
Selection
Type() string
Children() []Selection
}

type TypenameField interface {
Selection
Type() string
Aliased() string
}

func Dump(selection Selection) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also present mainly for debugging/exploration purposes, another method that could be removed.

if selection == nil {
fmt.Println("Selection <nil>")
return
}

var print func(string, Selection)
print = func(indent string, sel Selection) {
switch v := sel.(type) {
case Field:
fmt.Printf(indent+"Field %s (%s)\n", v.Identifier(), v.Aliased())
for _, subSel := range v.Children() {
print(indent+" ", subSel)
}
case TypeAssertion:
fmt.Printf(indent+"TypeAssertion %s\n", v.Type())
for _, subSel := range v.Children() {
print(indent+" ", subSel)
}
case TypenameField:
fmt.Printf(indent+"TypenameField %s (%s)\n", v.Type(), v.Aliased())
default:
panic(fmt.Errorf("invalid selection %T received", v))
}
}

print("", selection)
}