Skip to content

Commit

Permalink
Merge branch 'master' into feat-duplicate-check
Browse files Browse the repository at this point in the history
  • Loading branch information
vektah authored and Adam Scarr committed Nov 25, 2018
2 parents b182c53 + c336811 commit 9c02695
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 9 deletions.
1 change: 1 addition & 0 deletions ast/definition.go
Expand Up @@ -30,6 +30,7 @@ type Definition struct {
EnumValues EnumValueList // enum

Position *Position `dump:"-"`
BuiltIn bool `dump:"-"`
}

func (d *Definition) IsLeafType() bool {
Expand Down
5 changes: 3 additions & 2 deletions ast/source.go
@@ -1,8 +1,9 @@
package ast

type Source struct {
Name string
Input string
Name string
Input string
BuiltIn bool
}

type Position struct {
Expand Down
14 changes: 13 additions & 1 deletion parser/schema.go
Expand Up @@ -10,7 +10,19 @@ func ParseSchema(source *Source) (*SchemaDocument, *gqlerror.Error) {
p := parser{
lexer: lexer.New(source),
}
return p.parseSchemaDocument(), p.err
ast, err := p.parseSchemaDocument(), p.err
if err != nil {
return nil, err
}

for _, def := range ast.Definitions {
def.BuiltIn = source.BuiltIn
}
for _, def := range ast.Extensions {
def.BuiltIn = source.BuiltIn
}

return ast, nil
}

func ParseSchemas(inputs ...*Source) (*SchemaDocument, *gqlerror.Error) {
Expand Down
6 changes: 5 additions & 1 deletion validator/prelude.go
Expand Up @@ -2,4 +2,8 @@ package validator

import "github.com/vektah/gqlparser/ast"

var Prelude = &ast.Source{Name: "prelude.graphql", Input: "# This file defines all the implicitly declared types that are required by the graphql spec. It is implicitly included by calls to LoadSchema\n\n# The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.\nscalar Int\n\n# The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).\nscalar Float\n\n# The `String`scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.\nscalar String\n\n# The `Boolean` scalar type represents ` + \"`\" + `true` + \"`\" + ` or ` + \"`\" + `false` + \"`\" + `.\nscalar Boolean\n\n# The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as \"4\") or integer (such as 4) input value will be accepted as an ID.\nscalar ID\n\n# The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.\ndirective @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n# The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.\ndirective @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n# The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema, such as deprecated fields on a type or deprecated enum values.\ndirective @deprecated(reason: String = \"No longer supported\") on FIELD_DEFINITION | ENUM_VALUE\n\ntype __Schema {\n types: [__Type!]!\n queryType: __Type!\n mutationType: __Type\n subscriptionType: __Type\n directives: [__Directive!]!\n}\n\ntype __Type {\n kind: __TypeKind!\n name: String\n description: String\n\n # OBJECT and INTERFACE only\n fields(includeDeprecated: Boolean = false): [__Field!]\n\n # OBJECT only\n interfaces: [__Type!]\n\n # INTERFACE and UNION only\n possibleTypes: [__Type!]\n\n # ENUM only\n enumValues(includeDeprecated: Boolean = false): [__EnumValue!]\n\n # INPUT_OBJECT only\n inputFields: [__InputValue!]\n\n # NON_NULL and LIST only\n ofType: __Type\n}\n\ntype __Field {\n name: String!\n description: String\n args: [__InputValue!]!\n type: __Type!\n isDeprecated: Boolean!\n deprecationReason: String\n}\n\ntype __InputValue {\n name: String!\n description: String\n type: __Type!\n defaultValue: String\n}\n\ntype __EnumValue {\n name: String!\n description: String\n isDeprecated: Boolean!\n deprecationReason: String\n}\n\nenum __TypeKind {\n SCALAR\n OBJECT\n INTERFACE\n UNION\n ENUM\n INPUT_OBJECT\n LIST\n NON_NULL\n}\n\ntype __Directive {\n name: String!\n description: String\n locations: [__DirectiveLocation!]!\n args: [__InputValue!]!\n}\n\nenum __DirectiveLocation {\n QUERY\n MUTATION\n SUBSCRIPTION\n FIELD\n FRAGMENT_DEFINITION\n FRAGMENT_SPREAD\n INLINE_FRAGMENT\n SCHEMA\n SCALAR\n OBJECT\n FIELD_DEFINITION\n ARGUMENT_DEFINITION\n INTERFACE\n UNION\n ENUM\n ENUM_VALUE\n INPUT_OBJECT\n INPUT_FIELD_DEFINITION\n}\n"}
var Prelude = &ast.Source{
Name: "prelude.graphql",
Input: "# This file defines all the implicitly declared types that are required by the graphql spec. It is implicitly included by calls to LoadSchema\n\n# The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.\nscalar Int\n\n# The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).\nscalar Float\n\n# The `String`scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.\nscalar String\n\n# The `Boolean` scalar type represents ` + \"`\" + `true` + \"`\" + ` or ` + \"`\" + `false` + \"`\" + `.\nscalar Boolean\n\n# The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as \"4\") or integer (such as 4) input value will be accepted as an ID.\nscalar ID\n\n# The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.\ndirective @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n# The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.\ndirective @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n# The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema, such as deprecated fields on a type or deprecated enum values.\ndirective @deprecated(reason: String = \"No longer supported\") on FIELD_DEFINITION | ENUM_VALUE\n\ntype __Schema {\n types: [__Type!]!\n queryType: __Type!\n mutationType: __Type\n subscriptionType: __Type\n directives: [__Directive!]!\n}\n\ntype __Type {\n kind: __TypeKind!\n name: String\n description: String\n\n # OBJECT and INTERFACE only\n fields(includeDeprecated: Boolean = false): [__Field!]\n\n # OBJECT only\n interfaces: [__Type!]\n\n # INTERFACE and UNION only\n possibleTypes: [__Type!]\n\n # ENUM only\n enumValues(includeDeprecated: Boolean = false): [__EnumValue!]\n\n # INPUT_OBJECT only\n inputFields: [__InputValue!]\n\n # NON_NULL and LIST only\n ofType: __Type\n}\n\ntype __Field {\n name: String!\n description: String\n args: [__InputValue!]!\n type: __Type!\n isDeprecated: Boolean!\n deprecationReason: String\n}\n\ntype __InputValue {\n name: String!\n description: String\n type: __Type!\n defaultValue: String\n}\n\ntype __EnumValue {\n name: String!\n description: String\n isDeprecated: Boolean!\n deprecationReason: String\n}\n\nenum __TypeKind {\n SCALAR\n OBJECT\n INTERFACE\n UNION\n ENUM\n INPUT_OBJECT\n LIST\n NON_NULL\n}\n\ntype __Directive {\n name: String!\n description: String\n locations: [__DirectiveLocation!]!\n args: [__InputValue!]!\n}\n\nenum __DirectiveLocation {\n QUERY\n MUTATION\n SUBSCRIPTION\n FIELD\n FRAGMENT_DEFINITION\n FRAGMENT_SPREAD\n INLINE_FRAGMENT\n SCHEMA\n SCALAR\n OBJECT\n FIELD_DEFINITION\n ARGUMENT_DEFINITION\n INTERFACE\n UNION\n ENUM\n ENUM_VALUE\n INPUT_OBJECT\n INPUT_FIELD_DEFINITION\n}\n",
BuiltIn: true,
}
33 changes: 33 additions & 0 deletions validator/schema.go
Expand Up @@ -4,6 +4,7 @@ package validator

import (
"strconv"
"strings"

. "github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/gqlerror"
Expand Down Expand Up @@ -158,11 +159,20 @@ func ValidateSchemaDocument(ast *SchemaDocument) (*Schema, *gqlerror.Error) {
}

func validateDirective(schema *Schema, def *DirectiveDefinition) *gqlerror.Error {
if err := validateName(def.Position, def.Name); err != nil {
// now, GraphQL spec doesn't have reserved directive name
return err
}

return validateArgs(schema, def.Arguments, def)
}

func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
for _, field := range def.Fields {
if err := validateName(field.Position, field.Name); err != nil {
// now, GraphQL spec doesn't have reserved field name
return err
}
if err := validateTypeRef(schema, field.Type); err != nil {
return err
}
Expand Down Expand Up @@ -207,6 +217,14 @@ func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
}
}

if !def.BuiltIn {
// GraphQL spec has reserved type names a lot!
err := validateName(def.Position, def.Name)
if err != nil {
return err
}
}

return validateDirectives(schema, def.Directives, nil)
}

Expand All @@ -219,6 +237,10 @@ func validateTypeRef(schema *Schema, typ *Type) *gqlerror.Error {

func validateArgs(schema *Schema, args ArgumentDefinitionList, currentDirective *DirectiveDefinition) *gqlerror.Error {
for _, arg := range args {
if err := validateName(arg.Position, arg.Name); err != nil {
// now, GraphQL spec doesn't have reserved argument name
return err
}
if err := validateTypeRef(schema, arg.Type); err != nil {
return err
}
Expand All @@ -231,6 +253,10 @@ func validateArgs(schema *Schema, args ArgumentDefinitionList, currentDirective

func validateDirectives(schema *Schema, dirs DirectiveList, currentDirective *DirectiveDefinition) *gqlerror.Error {
for _, dir := range dirs {
if err := validateName(dir.Position, dir.Name); err != nil {
// now, GraphQL spec doesn't have reserved directive name
return err
}
if currentDirective != nil && dir.Name == currentDirective.Name {
return gqlerror.ErrorPosf(dir.Position, "Directive %s cannot refer to itself.", currentDirective.Name)
}
Expand All @@ -241,3 +267,10 @@ func validateDirectives(schema *Schema, dirs DirectiveList, currentDirective *Di
}
return nil
}

func validateName(pos *Position, name string) *gqlerror.Error {
if strings.HasPrefix(name, "__") {
return gqlerror.ErrorPosf(pos, `Name "%s" must not begin with "__", which is reserved by GraphQL introspection.`, name)
}
return nil
}
56 changes: 56 additions & 0 deletions validator/schema_test.yml
Expand Up @@ -63,6 +63,31 @@ object types:
error:
message: 'OBJECT must define one or more fields.'
locations: [{line: 6, column: 6}]
- name: check reserved names on type name
input: |
type __FooBar {
id: ID
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 6}]
- name: check reserved names on type field
input: |
type FooBar {
__id: ID
}
error:
message: 'Name "__id" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 2, column: 3}]

- name: check reserved names on type field argument
input: |
type FooBar {
foo(__bar: ID): ID
}
error:
message: 'Name "__bar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 2, column: 7}]

interfaces:
- name: must exist
Expand Down Expand Up @@ -114,6 +139,14 @@ interfaces:
error:
message: 'INTERFACE must define one or more fields.'
locations: [{line: 6, column: 11}]
- name: check reserved names on type name
input: |
interface __FooBar {
id: ID
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 11}]

inputs:
- name: must define one or more input fields
Expand All @@ -135,6 +168,14 @@ inputs:
error:
message: 'INPUT_OBJECT must define one or more input fields.'
locations: [{line: 6, column: 7}]
- name: check reserved names on type name
input: |
input __FooBar {
id: ID
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 7}]

enums:
- name: must define one or more unique enum values
Expand All @@ -156,6 +197,15 @@ enums:
error:
message: 'ENUM must define one or more unique enum values.'
locations: [{line: 6, column: 6}]
- name: check reserved names on type name
input: |
enum __FooBar {
A
B
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 6}]

type extensions:
- name: cannot extend non existant types
Expand Down Expand Up @@ -201,6 +251,12 @@ directives:
error:
message: "Directive A cannot refer to itself."
locations: [{line: 1, column: 25}]
- name: check reserved names on type name
input: |
directive @__A on FIELD_DEFINITION
error:
message: 'Name "__A" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 12}]

entry points:
- name: multiple schema entry points
Expand Down
14 changes: 9 additions & 5 deletions validator/vars.go
Expand Up @@ -73,12 +73,19 @@ type varValidator struct {
}

func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) *gqlerror.Error {
currentPath := v.path
resetPath := func() {
v.path = currentPath
}
defer resetPath()

if typ.Elem != nil {
if val.Kind() != reflect.Slice {
return gqlerror.ErrorPathf(v.path, "must be an array")
}

for i := 0; i < val.Len(); i++ {
resetPath()
v.path = append(v.path, i)
field := val.Index(i)

Expand All @@ -92,8 +99,6 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) *gqlerr
if err := v.validateVarType(typ.Elem, field); err != nil {
return err
}

v.path = v.path[0 : len(v.path)-1]
}

return nil
Expand Down Expand Up @@ -150,15 +155,16 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) *gqlerr
for _, name := range val.MapKeys() {
val.MapIndex(name)
fieldDef := def.Fields.ForName(name.String())
resetPath()
v.path = append(v.path, name.String())

if fieldDef == nil {
return gqlerror.ErrorPathf(v.path, "unknown field")
}
v.path = v.path[0 : len(v.path)-1]
}

for _, fieldDef := range def.Fields {
resetPath()
v.path = append(v.path, fieldDef.Name)

field := val.MapIndex(reflect.ValueOf(fieldDef.Name))
Expand All @@ -184,8 +190,6 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) *gqlerr
if err != nil {
return err
}

v.path = v.path[0 : len(v.path)-1]
}
default:
panic(fmt.Errorf("unsupported type %s", def.Kind))
Expand Down
12 changes: 12 additions & 0 deletions validator/vars_test.go
Expand Up @@ -163,6 +163,18 @@ func TestValidateVars(t *testing.T) {
})
require.EqualError(t, gerr, "input: variable.var[0].name must be defined")
})
t.Run("invalid variable paths", func(t *testing.T) {
q := gqlparser.MustLoadQuery(schema, `query foo($var1: InputType!, $var2: InputType!) { a:structArg(i: $var1) b:structArg(i: $var2) }`)
_, gerr := validator.VariableValues(schema, q.Operations.ForName(""), map[string]interface{}{
"var1": map[string]interface{}{
"name": "foobar",
},
"var2": map[string]interface{}{
"nullName": "foobar",
},
})
require.EqualError(t, gerr, "input: variable.var2.name must be defined")
})
})

t.Run("Scalars", func(t *testing.T) {
Expand Down

0 comments on commit 9c02695

Please sign in to comment.