From 498fe3961c3058c45f5ad90b1212a48adfbba266 Mon Sep 17 00:00:00 2001 From: Richard Musiol Date: Sun, 12 Mar 2017 21:37:38 +0100 Subject: [PATCH] support for @deprecated directive on fields (fixes #64) --- graphql_test.go | 78 ++++++++++++++++++++++++++++++++++ internal/common/directive.go | 2 +- internal/schema/meta.go | 8 ++++ internal/schema/schema.go | 26 ++++++++++-- introspection/introspection.go | 19 ++++++--- 5 files changed, 123 insertions(+), 10 deletions(-) diff --git a/graphql_test.go b/graphql_test.go index 9fcfd214..8c49e42a 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -462,6 +462,66 @@ func TestIncludeDirective(t *testing.T) { }) } +type testDeprecatedDirectiveResolver struct{} + +func (r *testDeprecatedDirectiveResolver) A() int32 { + return 0 +} + +func (r *testDeprecatedDirectiveResolver) B() int32 { + return 0 +} + +func (r *testDeprecatedDirectiveResolver) C() int32 { + return 0 +} + +func TestDeprecatedDirective(t *testing.T) { + graphql.RunTests(t, []*graphql.Test{ + { + Schema: graphql.MustParseSchema(` + schema { + query: Query + } + + type Query { + a: Int! + b: Int! @deprecated + c: Int! @deprecated(reason: "We don't like it") + } + `, &testDeprecatedDirectiveResolver{}), + Query: ` + { + __type(name: "Query") { + fields { + name + } + allFields: fields(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + } + `, + ExpectedResult: ` + { + "__type": { + "fields": [ + { "name": "a" } + ], + "allFields": [ + { "name": "a", "isDeprecated": false, "deprecationReason": null }, + { "name": "b", "isDeprecated": true, "deprecationReason": "No longer supported" }, + { "name": "c", "isDeprecated": true, "deprecationReason": "We don't like it" } + ] + } + } + `, + }, + }) +} + func TestInlineFragments(t *testing.T) { graphql.RunTests(t, []*graphql.Test{ { @@ -1134,6 +1194,24 @@ func TestIntrospection(t *testing.T) { { "__schema": { "directives": [ + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion\nfor how to access supported similar data. Formatted in\n[Markdown](https://daringfireball.net/projects/markdown/).", + "type": { + "kind": "SCALAR", + "ofType": null + } + } + ] + }, { "name": "include", "description": "Directs the executor to include this field or fragment only when the ` + "`" + `if` + "`" + ` argument is true.", diff --git a/internal/common/directive.go b/internal/common/directive.go index 3ec600ad..a488a285 100644 --- a/internal/common/directive.go +++ b/internal/common/directive.go @@ -11,7 +11,7 @@ func ParseDirectives(l *lexer.Lexer) map[string]DirectiveArgs { for l.Peek() == '@' { l.ConsumeToken('@') name := l.ConsumeIdent() - var args DirectiveArgs + args := make(DirectiveArgs) if l.Peek() == '(' { args = ParseArguments(l) } diff --git a/internal/schema/meta.go b/internal/schema/meta.go index f3c791a2..b48bf7ac 100644 --- a/internal/schema/meta.go +++ b/internal/schema/meta.go @@ -38,6 +38,14 @@ var metaSrc = ` if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + # Marks an element of a GraphQL schema as no longer supported. + directive @deprecated( + # Explains why this element was deprecated, usually also including a suggestion + # for how to access supported similar data. Formatted in + # [Markdown](https://daringfireball.net/projects/markdown/). + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + # A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. # # In some cases, you need to provide options to alter GraphQL's execution behavior diff --git a/internal/schema/schema.go b/internal/schema/schema.go index b36d22c5..45dcc56f 100644 --- a/internal/schema/schema.go +++ b/internal/schema/schema.go @@ -115,10 +115,11 @@ func (t *Enum) Description() string { return t.Desc } func (t *InputObject) Description() string { return t.Desc } type Field struct { - Name string - Args common.InputMap - Type common.Type - Desc string + Name string + Args common.InputMap + Type common.Type + Directives map[string]common.DirectiveArgs + Desc string } func New() *Schema { @@ -238,6 +239,22 @@ func resolveField(s *Schema, f *Field) error { return err } f.Type = t + for name, args := range f.Directives { + d, ok := s.Directives[name] + if !ok { + return errors.Errorf("directive %q not found", name) + } + for argName := range args { + if _, ok := d.Args[argName]; !ok { + return errors.Errorf("invalid argument %q for directive %q", argName, name) + } + } + for argName, arg := range d.Args { + if _, ok := args[argName]; !ok { + args[argName] = arg.Default + } + } + } return resolveInputObject(s, &f.Args) } @@ -410,6 +427,7 @@ func parseFields(l *lexer.Lexer) (map[string]*Field, []string) { } l.ConsumeToken(':') f.Type = common.ParseType(l) + f.Directives = common.ParseDirectives(l) fields[f.Name] = f fieldOrder = append(fieldOrder, f.Name) } diff --git a/introspection/introspection.go b/introspection/introspection.go index 061ab3b5..09ddcadb 100644 --- a/introspection/introspection.go +++ b/introspection/introspection.go @@ -115,9 +115,12 @@ func (r *Type) Fields(args *struct{ IncludeDeprecated bool }) *[]*Field { return nil } - l := make([]*Field, len(fieldOrder)) - for i, name := range fieldOrder { - l[i] = &Field{fields[name]} + var l []*Field + for _, name := range fieldOrder { + f := fields[name] + if _, ok := f.Directives["deprecated"]; !ok || args.IncludeDeprecated { + l = append(l, &Field{f}) + } } return &l } @@ -218,11 +221,17 @@ func (r *Field) Type() *Type { } func (r *Field) IsDeprecated() bool { - return false + _, ok := r.field.Directives["deprecated"] + return ok } func (r *Field) DeprecationReason() *string { - return nil + args, ok := r.field.Directives["deprecated"] + if !ok { + return nil + } + reason := args["reason"].(string) + return &reason } type InputValue struct {