Skip to content

Commit

Permalink
Added support of custom directives
Browse files Browse the repository at this point in the history
  • Loading branch information
eko committed Apr 12, 2021
1 parent 21b77bd commit 4a97316
Show file tree
Hide file tree
Showing 25 changed files with 166 additions and 45 deletions.
5 changes: 3 additions & 2 deletions example/caching/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/example/caching"
"github.com/graph-gophers/graphql-go/example/caching/cache"
"github.com/graph-gophers/graphql-go/pkg/common"
)

var schema *graphql.Schema
Expand Down Expand Up @@ -40,12 +41,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var hint *cache.Hint
if cacheable(r) {
ctx, hints, done := cache.Hintable(r.Context())
response = h.Schema.Exec(ctx, p.Query, p.OperationName, p.Variables)
response = h.Schema.Exec(ctx, p.Query, p.OperationName, p.Variables, map[string]common.DirectiveVisitor{})
done()
v := <-hints
hint = &v
} else {
response = h.Schema.Exec(r.Context(), p.Query, p.OperationName, p.Variables)
response = h.Schema.Exec(r.Context(), p.Query, p.OperationName, p.Variables, map[string]common.DirectiveVisitor{})
}
responseJSON, err := json.Marshal(response)
if err != nil {
Expand Down
20 changes: 11 additions & 9 deletions gqltesting/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ import (

graphql "github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/pkg/common"
)

// Test is a GraphQL test case to be used with RunTest(s).
type Test struct {
Context context.Context
Schema *graphql.Schema
Query string
OperationName string
Variables map[string]interface{}
ExpectedResult string
ExpectedErrors []*errors.QueryError
RawResponse bool
Context context.Context
Schema *graphql.Schema
Query string
OperationName string
Variables map[string]interface{}
ExpectedResult string
ExpectedErrors []*errors.QueryError
RawResponse bool
DirectiveVisitors map[string]common.DirectiveVisitor
}

// RunTests runs the given GraphQL test cases as subtests.
Expand All @@ -45,7 +47,7 @@ func RunTest(t *testing.T, test *Test) {
if test.Context == nil {
test.Context = context.Background()
}
result := test.Schema.Exec(test.Context, test.Query, test.OperationName, test.Variables)
result := test.Schema.Exec(test.Context, test.Query, test.OperationName, test.Variables, test.DirectiveVisitors)

checkErrors(t, test.ExpectedErrors, result.Errors)

Expand Down
15 changes: 8 additions & 7 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"time"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/exec"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/internal/exec/selected"
Expand All @@ -17,6 +16,7 @@ import (
"github.com/graph-gophers/graphql-go/internal/validation"
"github.com/graph-gophers/graphql-go/introspection"
"github.com/graph-gophers/graphql-go/log"
"github.com/graph-gophers/graphql-go/pkg/common"
"github.com/graph-gophers/graphql-go/trace"
)

Expand Down Expand Up @@ -181,14 +181,14 @@ func (s *Schema) ValidateWithVariables(queryString string, variables map[string]
// Exec executes the given query with the schema's resolver. It panics if the schema was created
// without a resolver. If the context get cancelled, no further resolvers will be called and a
// the context error will be returned as soon as possible (not immediately).
func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response {
func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, visitors map[string]common.DirectiveVisitor) *Response {
if s.res.Resolver == (reflect.Value{}) {
panic("schema created without resolver, can not exec")
}
return s.exec(ctx, queryString, operationName, variables, s.res)
return s.exec(ctx, queryString, operationName, variables, visitors, s.res)
}

func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response {
func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, visitors map[string]common.DirectiveVisitor, res *resolvable.Schema) *Response {
doc, qErr := query.Parse(queryString)
if qErr != nil {
return &Response{Errors: []*errors.QueryError{qErr}}
Expand Down Expand Up @@ -239,9 +239,10 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str
Schema: s.schema,
DisableIntrospection: s.disableIntrospection,
},
Limiter: make(chan struct{}, s.maxParallelism),
Tracer: s.tracer,
Logger: s.logger,
Limiter: make(chan struct{}, s.maxParallelism),
Tracer: s.tracer,
Logger: s.logger,
Visitors: visitors,
}
varTypes := make(map[string]*introspection.Type)
for _, v := range op.Vars {
Expand Down
78 changes: 77 additions & 1 deletion graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
gqlerrors "github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/example/starwars"
"github.com/graph-gophers/graphql-go/gqltesting"
"github.com/graph-gophers/graphql-go/pkg/common"
)

type helloWorldResolver1 struct{}
Expand Down Expand Up @@ -45,6 +46,20 @@ func (r *helloSnakeResolver2) SayHello(ctx context.Context, args struct{ FullNam
return "Hello " + args.FullName + "!", nil
}

type customDirectiveVisitor struct{}

func (v customDirectiveVisitor) Before(directive *common.Directive, input interface{}) error {
return nil
}

func (v customDirectiveVisitor) After(directive *common.Directive, output interface{}) (interface{}, error) {
if value, ok := directive.Args.Get("customAttribute"); ok {
return fmt.Sprintf("Directive '%s' (with arg '%s') modified result: %s", directive.Name.Name, value.String(), output.(string)), nil
} else {
return fmt.Sprintf("Directive '%s' modified result: %s", directive.Name.Name, output.(string)), nil
}
}

type theNumberResolver struct {
number int32
}
Expand Down Expand Up @@ -213,6 +228,67 @@ func TestHelloWorld(t *testing.T) {
})
}

func TestCustomDirective(t *testing.T) {
t.Parallel()

gqltesting.RunTests(t, []*gqltesting.Test{
{
Schema: graphql.MustParseSchema(`
directive @customDirective on FIELD_DEFINITION
schema {
query: Query
}
type Query {
hello_html: String! @customDirective
}
`, &helloSnakeResolver1{}),
Query: `
{
hello_html
}
`,
ExpectedResult: `
{
"hello_html": "Directive 'customDirective' modified result: Hello snake!"
}
`,
DirectiveVisitors: map[string]common.DirectiveVisitor{
"customDirective": customDirectiveVisitor{},
},
},
{
Schema: graphql.MustParseSchema(`
directive @customDirective(
customAttribute: String!
) on FIELD_DEFINITION
schema {
query: Query
}
type Query {
say_hello(full_name: String!): String! @customDirective(customAttribute: hi)
}
`, &helloSnakeResolver1{}),
Query: `
{
say_hello(full_name: "Johnny")
}
`,
ExpectedResult: `
{
"say_hello": "Directive 'customDirective' (with arg 'hi') modified result: Hello Johnny!"
}
`,
DirectiveVisitors: map[string]common.DirectiveVisitor{
"customDirective": customDirectiveVisitor{},
},
},
})
}

func TestHelloSnake(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -3728,7 +3804,7 @@ func TestSchema_Exec_without_resolver(t *testing.T) {
t.Fail()
}
}()
_ = s.Exec(context.Background(), tt.Args.Query, "", map[string]interface{}{})
_ = s.Exec(context.Background(), tt.Args.Query, "", map[string]interface{}{}, map[string]common.DirectiveVisitor{})
})
}
}
Expand Down
37 changes: 36 additions & 1 deletion internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import (
"time"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"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"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/log"
"github.com/graph-gophers/graphql-go/pkg/common"
"github.com/graph-gophers/graphql-go/trace"
)

Expand All @@ -25,6 +25,7 @@ type Request struct {
Tracer trace.Tracer
Logger log.Logger
SubscribeResolverTimeout time.Duration
Visitors map[string]common.DirectiveVisitor
}

func (r *Request) handlePanic(ctx context.Context) {
Expand Down Expand Up @@ -207,8 +208,42 @@ func execFieldSelection(ctx context.Context, r *Request, s *resolvable.Schema, f
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}

// Before hook directive visitor
if len(f.field.Directives) > 0 {
for _, directive := range f.field.Directives {
if visitor, ok := r.Visitors[directive.Name.Name]; ok {
var values = make([]interface{}, 0)
for _, inValue := range in {
values = append(values, inValue.Interface())
}

if err := visitor.Before(directive, values); err != nil {
return nil
}
}
}
}

// Call method
callOut := res.Method(f.field.MethodIndex).Call(in)
result = callOut[0]

// After hook directive visitor (when no error is returned from resolver)
if !f.field.HasError && len(f.field.Directives) > 0 {
for _, directive := range f.field.Directives {
if visitor, ok := r.Visitors[directive.Name.Name]; ok {
returned, err := visitor.After(directive, result.Interface())
if err != nil {
f.field.HasError = true
callOut[1] = reflect.ValueOf(err)
} else {
result = reflect.ValueOf(returned)
}
}
}
}

if f.field.HasError && !callOut[1].IsNil() {
resolverErr := callOut[1].Interface().(error)
err := errors.Errorf("%s", resolverErr)
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/packer/packer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"strings"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/pkg/common"
)

type packer interface {
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/resolvable/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"fmt"
"reflect"

"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/introspection"
"github.com/graph-gophers/graphql-go/pkg/common"
)

// Meta defines the details of the metadata schema for introspection.
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/resolvable/resolvable.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"reflect"
"strings"

"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/exec/packer"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/pkg/common"
)

type Schema struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/selected/selected.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import (
"sync"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/exec/packer"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"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/pkg/common"
)

type Request struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
"time"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"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"
"github.com/graph-gophers/graphql-go/pkg/common"
)

type Response struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"text/scanner"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/pkg/common"
)

type Document struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"text/scanner"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/pkg/common"
)

// Schema represents a GraphQL service's collective type system capabilities.
Expand Down
2 changes: 1 addition & 1 deletion internal/schema/schema_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"testing"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/pkg/common"
)

func TestParseInterfaceDef(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
"text/scanner"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/query"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/pkg/common"
)

type varSet map[*common.InputValue]struct{}
Expand Down
3 changes: 2 additions & 1 deletion introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/introspection"
"github.com/graph-gophers/graphql-go/pkg/common"
)

// Inspect allows inspection of the given schema.
Expand All @@ -15,7 +16,7 @@ func (s *Schema) Inspect() *introspection.Schema {

// ToJSON encodes the schema in a JSON format used by tools like Relay.
func (s *Schema) ToJSON() ([]byte, error) {
result := s.exec(context.Background(), introspectionQuery, "", nil, &resolvable.Schema{
result := s.exec(context.Background(), introspectionQuery, "", nil, map[string]common.DirectiveVisitor{}, &resolvable.Schema{
Meta: s.res.Meta,
Query: &resolvable.Object{},
Schema: *s.schema,
Expand Down
Loading

0 comments on commit 4a97316

Please sign in to comment.