Skip to content

Commit

Permalink
validation: UniqueInputFieldNames
Browse files Browse the repository at this point in the history
  • Loading branch information
neelance committed May 23, 2017
1 parent 94cb291 commit 47c5cde
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 53 deletions.
33 changes: 16 additions & 17 deletions internal/common/literals.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package common

import (
"sort"
"strconv"
"strings"
"text/scanner"
Expand Down Expand Up @@ -86,28 +85,27 @@ func (lit *ListLit) Location() errors.Location {
}

type ObjectLit struct {
Fields map[string]Literal
Fields []*ObjectLitField
Loc errors.Location
}

type ObjectLitField struct {
Name Ident
Value Literal
}

func (lit *ObjectLit) Value(vars map[string]interface{}) interface{} {
fields := make(map[string]interface{}, len(lit.Fields))
for k, v := range lit.Fields {
fields[k] = v.Value(vars)
for _, f := range lit.Fields {
fields[f.Name.Name] = f.Value.Value(vars)
}
return fields
}

func (lit *ObjectLit) String() string {
names := make([]string, 0, len(lit.Fields))
for name := range lit.Fields {
names = append(names, name)
}
sort.Strings(names)

entries := make([]string, 0, len(names))
for _, name := range names {
entries = append(entries, name+": "+lit.Fields[name].String())
entries := make([]string, 0, len(lit.Fields))
for _, f := range lit.Fields {
entries = append(entries, f.Name.Name+": "+f.Value.String())
}
return "{" + strings.Join(entries, ", ") + "}"
}
Expand Down Expand Up @@ -179,14 +177,15 @@ func ParseLiteral(l *Lexer, constOnly bool) Literal {

case '{':
l.ConsumeToken('{')
obj := make(map[string]Literal)
var fields []*ObjectLitField
for l.Peek() != '}' {
name := l.ConsumeIdent()
name := l.ConsumeIdentWithLoc()
l.ConsumeToken(':')
obj[name] = ParseLiteral(l, constOnly)
value := ParseLiteral(l, constOnly)
fields = append(fields, &ObjectLitField{name, value})
}
l.ConsumeToken('}')
return &ObjectLit{obj, loc}
return &ObjectLit{fields, loc}

default:
l.SyntaxError("invalid value")
Expand Down
5 changes: 1 addition & 4 deletions internal/tests/testdata/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,13 @@ require('./src/validation/__tests__/ScalarLeafs-test');
require('./src/validation/__tests__/UniqueArgumentNames-test');
require('./src/validation/__tests__/UniqueDirectivesPerLocation-test');
require('./src/validation/__tests__/UniqueFragmentNames-test');
// require('./src/validation/__tests__/UniqueInputFieldNames-test');
require('./src/validation/__tests__/UniqueInputFieldNames-test');
require('./src/validation/__tests__/UniqueOperationNames-test');
require('./src/validation/__tests__/UniqueVariableNames-test');
require('./src/validation/__tests__/VariablesAreInputTypes-test');
// require('./src/validation/__tests__/VariablesInAllowedPosition-test');

let output = JSON.stringify(tests, null, 2)
output = output.replace(/There can only be one/g, 'There can be only one');
output = output.replace('{stringListField: [\\"one\\", 2], requiredField: true}', '{requiredField: true, stringListField: [\\"one\\", 2]}');
output = output.replace('{requiredField: null, intField: null}', '{intField: null, requiredField: null}');
output = output.replace(' Did you mean to use an inline fragment on \\"Dog\\" or \\"Cat\\"?', '');
output = output.replace(' Did you mean to use an inline fragment on \\"Being\\", \\"Pet\\", \\"Canine\\", \\"Dog\\", or \\"Cat\\"?', '');
output = output.replace(' Did you mean \\"Pet\\"?', '');
Expand Down
81 changes: 79 additions & 2 deletions internal/tests/testdata/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@
"query": "\n {\n complicatedArgs {\n complexArgField(complexArg: {\n stringListField: [\"one\", 2],\n requiredField: true,\n })\n }\n }\n ",
"errors": [
{
"message": "Argument \"complexArg\" has invalid value {requiredField: true, stringListField: [\"one\", 2]}.\nIn field \"stringListField\": In element #1: Expected type \"String\", found 2.",
"message": "Argument \"complexArg\" has invalid value {stringListField: [\"one\", 2], requiredField: true}.\nIn field \"stringListField\": In element #1: Expected type \"String\", found 2.",
"locations": [
{
"line": 4,
Expand Down Expand Up @@ -825,7 +825,7 @@
]
},
{
"message": "Variable \"$c\" of type \"ComplexInput\" has invalid default value {intField: null, requiredField: null}.\nIn field \"requiredField\": Expected \"Boolean!\", found null.",
"message": "Variable \"$c\" of type \"ComplexInput\" has invalid default value {requiredField: null, intField: null}.\nIn field \"requiredField\": Expected \"Boolean!\", found null.",
"locations": [
{
"line": 5,
Expand Down Expand Up @@ -2697,6 +2697,83 @@
}
]
},
{
"name": "Validate: Unique input field names/input object with fields",
"rule": "UniqueInputFieldNames",
"query": "\n {\n field(arg: { f: true })\n }\n ",
"errors": []
},
{
"name": "Validate: Unique input field names/same input object within two args",
"rule": "UniqueInputFieldNames",
"query": "\n {\n field(arg1: { f: true }, arg2: { f: true })\n }\n ",
"errors": []
},
{
"name": "Validate: Unique input field names/multiple input object fields",
"rule": "UniqueInputFieldNames",
"query": "\n {\n field(arg: { f1: \"value\", f2: \"value\", f3: \"value\" })\n }\n ",
"errors": []
},
{
"name": "Validate: Unique input field names/allows for nested input objects with similar fields",
"rule": "UniqueInputFieldNames",
"query": "\n {\n field(arg: {\n deep: {\n deep: {\n id: 1\n }\n id: 1\n }\n id: 1\n })\n }\n ",
"errors": []
},
{
"name": "Validate: Unique input field names/duplicate input object fields",
"rule": "UniqueInputFieldNames",
"query": "\n {\n field(arg: { f1: \"value\", f1: \"value\" })\n }\n ",
"errors": [
{
"message": "There can be only one input field named \"f1\".",
"locations": [
{
"line": 3,
"column": 22
},
{
"line": 3,
"column": 35
}
]
}
]
},
{
"name": "Validate: Unique input field names/many duplicate input object fields",
"rule": "UniqueInputFieldNames",
"query": "\n {\n field(arg: { f1: \"value\", f1: \"value\", f1: \"value\" })\n }\n ",
"errors": [
{
"message": "There can be only one input field named \"f1\".",
"locations": [
{
"line": 3,
"column": 22
},
{
"line": 3,
"column": 35
}
]
},
{
"message": "There can be only one input field named \"f1\".",
"locations": [
{
"line": 3,
"column": 22
},
{
"line": 3,
"column": 48
}
]
}
]
},
{
"name": "Validate: Unique operation names/no operations",
"rule": "UniqueOperationNames",
Expand Down
88 changes: 58 additions & 30 deletions internal/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,17 @@ func Validate(s *schema.Schema, doc *query.Document) []*errors.QueryError {
c.addErr(v.TypeLoc, "VariablesAreInputTypes", "Variable %q cannot be non-input type %q.", "$"+v.Name.Name, t)
}

if t != nil && v.Default != nil {
if nn, ok := t.(*common.NonNull); ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q is required and will not use the default value. Perhaps you meant to use type %q.", "$"+v.Name.Name, t, nn.OfType)
}
if v.Default != nil {
c.validateLiteral(v.Default)

if t != nil {
if nn, ok := t.(*common.NonNull); ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q is required and will not use the default value. Perhaps you meant to use type %q.", "$"+v.Name.Name, t, nn.OfType)
}

if ok, reason := validateValue(v.Default, t); !ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q has invalid default value %s.\n%s", "$"+v.Name.Name, t, v.Default, reason)
if ok, reason := validateValueType(v.Default, t); !ok {
c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q has invalid default value %s.\n%s", "$"+v.Name.Name, t, v.Default, reason)
}
}
}
}
Expand Down Expand Up @@ -157,13 +161,9 @@ func (c *context) shallowValidateSelection(sel query.Selection, t common.Type) {
}
}

names := make(nameSet)
for _, arg := range sel.Arguments {
c.validateName(names, arg.Name, "UniqueArgumentNames", "argument")
}

c.validateArgumentLiterals(sel.Arguments)
if f != nil {
c.validateArguments(sel.Arguments, f.Args, sel.Alias.Loc,
c.validateArgumentTypes(sel.Arguments, f.Args, sel.Alias.Loc,
func() string { return fmt.Sprintf("field %q of type %q", fieldName, t) },
func() string { return fmt.Sprintf("Field %q", fieldName) },
)
Expand Down Expand Up @@ -331,10 +331,7 @@ func (c *context) validateDirectives(loc string, directives common.DirectiveList
return fmt.Sprintf("The directive %q can only be used once at this location.", dirName)
})

argNames := make(nameSet)
for _, arg := range d.Args {
c.validateName(argNames, arg.Name, "UniqueArgumentNames", "argument")
}
c.validateArgumentLiterals(d.Args)

dd, ok := c.schema.Directives[dirName]
if !ok {
Expand All @@ -353,7 +350,7 @@ func (c *context) validateDirectives(loc string, directives common.DirectiveList
c.addErr(d.Name.Loc, "KnownDirectives", "Directive %q may not be used on %s.", dirName, loc)
}

c.validateArguments(d.Args, dd.Args, d.Name.Loc,
c.validateArgumentTypes(d.Args, dd.Args, d.Name.Loc,
func() string { return fmt.Sprintf("directive %q", "@"+dirName) },
func() string { return fmt.Sprintf("Directive %q", "@"+dirName) },
)
Expand All @@ -378,15 +375,15 @@ func (c *context) validateNameCustomMsg(set nameSet, name common.Ident, rule str
return
}

func (c *context) validateArguments(args common.ArgumentList, argDecls common.InputValueList, loc errors.Location, owner1, owner2 func() string) {
func (c *context) validateArgumentTypes(args common.ArgumentList, argDecls common.InputValueList, loc errors.Location, owner1, owner2 func() string) {
for _, selArg := range args {
arg := argDecls.Get(selArg.Name.Name)
if arg == nil {
c.addErr(selArg.Name.Loc, "KnownArgumentNames", "Unknown argument %q on %s.", selArg.Name.Name, owner1())
continue
}
value := selArg.Value
if ok, reason := validateValue(value, arg.Type); !ok {
if ok, reason := validateValueType(value, arg.Type); !ok {
c.addErr(value.Location(), "ArgumentsOfCorrectType", "Argument %q has invalid value %s.\n%s", arg.Name.Name, value, reason)
}
}
Expand All @@ -399,7 +396,30 @@ func (c *context) validateArguments(args common.ArgumentList, argDecls common.In
}
}

func validateValue(v common.Literal, t common.Type) (bool, string) {
func (c *context) validateArgumentLiterals(args common.ArgumentList) {
argNames := make(nameSet)
for _, arg := range args {
c.validateName(argNames, arg.Name, "UniqueArgumentNames", "argument")
c.validateLiteral(arg.Value)
}
}

func (c *context) validateLiteral(l common.Literal) {
switch l := l.(type) {
case *common.ObjectLit:
fieldNames := make(nameSet)
for _, f := range l.Fields {
c.validateName(fieldNames, f.Name, "UniqueInputFieldNames", "input field")
c.validateLiteral(f.Value)
}
case *common.ListLit:
for _, entry := range l.Entries {
c.validateLiteral(entry)
}
}
}

func validateValueType(v common.Literal, t common.Type) (bool, string) {
if nn, ok := t.(*common.NonNull); ok {
if isNull(v) {
return false, fmt.Sprintf("Expected %q, found null.", t)
Expand All @@ -426,10 +446,10 @@ func validateValue(v common.Literal, t common.Type) (bool, string) {
case *common.List:
list, ok := v.(*common.ListLit)
if !ok {
return validateValue(v, t.OfType) // single value instead of list
return validateValueType(v, t.OfType) // single value instead of list
}
for i, entry := range list.Entries {
if ok, reason := validateValue(entry, t.OfType); !ok {
if ok, reason := validateValueType(entry, t.OfType); !ok {
return false, fmt.Sprintf("In element #%d: %s", i, reason)
}
}
Expand All @@ -440,19 +460,27 @@ func validateValue(v common.Literal, t common.Type) (bool, string) {
if !ok {
return false, fmt.Sprintf("Expected %q, found not an object.", t)
}
for name, entry := range v.Fields {
f := t.Values.Get(name)
if f == nil {
for _, f := range v.Fields {
name := f.Name.Name
iv := t.Values.Get(name)
if iv == nil {
return false, fmt.Sprintf("In field %q: Unknown field.", name)
}
if ok, reason := validateValue(entry, f.Type); !ok {
if ok, reason := validateValueType(f.Value, iv.Type); !ok {
return false, fmt.Sprintf("In field %q: %s", name, reason)
}
}
for _, f := range t.Values {
if _, ok := v.Fields[f.Name.Name]; !ok {
if _, ok := f.Type.(*common.NonNull); ok && f.Default == nil {
return false, fmt.Sprintf("In field %q: Expected %q, found null.", f.Name.Name, f.Type)
for _, iv := range t.Values {
found := false
for _, f := range v.Fields {
if f.Name.Name == iv.Name.Name {
found = true
break
}
}
if !found {
if _, ok := iv.Type.(*common.NonNull); ok && iv.Default == nil {
return false, fmt.Sprintf("In field %q: Expected %q, found null.", iv.Name.Name, iv.Type)
}
}
}
Expand Down

0 comments on commit 47c5cde

Please sign in to comment.