Skip to content

Commit 7be4d3e

Browse files
sogkochris-ramon
authored andcommitted
Include possible field, argument, type names when validation fails (graphql-go#355)
* Add suggestionList to return strings based on how simular they are to the input * Suggests valid fields in `FieldsOnCorrectType` * Suggest argument names * Suggested valid type names * Fix flow and unit test * addressed comments in PR: move file, update comment, filter out more options, remove redundant warning * fix typos * fix lint Commit: 5bc1b2541d1b5767de4016e10ae77021f81310fc [5bc1b25] Parents: 4b08c36e86 Author: Yuzhi <[email protected]> Date: 27 April 2016 at 2:48:45 AM SGT Committer: Lee Byron <[email protected]>
1 parent 002c85d commit 7be4d3e

6 files changed

+229
-33
lines changed

rules.go

+177-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/graphql-go/graphql/language/kinds"
88
"github.com/graphql-go/graphql/language/printer"
99
"github.com/graphql-go/graphql/language/visitor"
10+
"math"
1011
"sort"
1112
"strings"
1213
)
@@ -162,8 +163,14 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI
162163
VisitorOpts: visitorOpts,
163164
}
164165
}
165-
166-
func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes []string) string {
166+
func quoteStrings(slice []string) []string {
167+
quoted := []string{}
168+
for _, s := range slice {
169+
quoted = append(quoted, fmt.Sprintf(`"%v"`, s))
170+
}
171+
return quoted
172+
}
173+
func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes []string, suggestedFields []string) string {
167174

168175
quoteStrings := func(slice []string) []string {
169176
quoted := []string{}
@@ -175,15 +182,27 @@ func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes []
175182

176183
// construct helpful (but long) message
177184
message := fmt.Sprintf(`Cannot query field "%v" on type "%v".`, fieldName, ttypeName)
178-
suggestions := strings.Join(quoteStrings(suggestedTypes), ", ")
179185
const MaxLength = 5
180186
if len(suggestedTypes) > 0 {
187+
suggestions := ""
181188
if len(suggestedTypes) > MaxLength {
182189
suggestions = strings.Join(quoteStrings(suggestedTypes[0:MaxLength]), ", ") +
183190
fmt.Sprintf(`, and %v other types`, len(suggestedTypes)-MaxLength)
191+
} else {
192+
suggestions = strings.Join(quoteStrings(suggestedTypes), ", ")
193+
}
194+
message = fmt.Sprintf(`%v However, this field exists on %v. `+
195+
`Perhaps you meant to use an inline fragment?`, message, suggestions)
196+
}
197+
if len(suggestedFields) > 0 {
198+
suggestions := ""
199+
if len(suggestedFields) > MaxLength {
200+
suggestions = strings.Join(quoteStrings(suggestedFields[0:MaxLength]), ", ") +
201+
fmt.Sprintf(`, or %v other field`, len(suggestedFields)-MaxLength)
202+
} else {
203+
suggestions = strings.Join(quoteStrings(suggestedFields), ", ")
184204
}
185-
message = message + fmt.Sprintf(` However, this field exists on %v.`, suggestions)
186-
message = message + ` Perhaps you meant to use an inline fragment?`
205+
message = fmt.Sprintf(`%v Did you mean to query %v?`, message, suggestions)
187206
}
188207

189208
return message
@@ -232,11 +251,29 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance
232251
}
233252
}
234253

235-
message := UndefinedFieldMessage(nodeName, ttype.Name(), suggestedTypes)
254+
suggestedFieldNames := []string{}
255+
suggestedFields := []string{}
256+
switch ttype := ttype.(type) {
257+
case *Object:
258+
for name := range ttype.Fields() {
259+
suggestedFieldNames = append(suggestedFieldNames, name)
260+
}
261+
suggestedFields = suggestionList(nodeName, suggestedFieldNames)
262+
case *Interface:
263+
for name := range ttype.Fields() {
264+
suggestedFieldNames = append(suggestedFieldNames, name)
265+
}
266+
suggestedFields = suggestionList(nodeName, suggestedFieldNames)
267+
case *InputObject:
268+
for name := range ttype.Fields() {
269+
suggestedFieldNames = append(suggestedFieldNames, name)
270+
}
271+
suggestedFields = suggestionList(nodeName, suggestedFieldNames)
272+
}
236273

237274
reportError(
238275
context,
239-
message,
276+
UndefinedFieldMessage(nodeName, ttype.Name(), suggestedTypes, suggestedFields),
240277
[]ast.Node{node},
241278
)
242279
}
@@ -380,6 +417,28 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn
380417
}
381418
}
382419

420+
func unknownArgMessage(argName string, fieldName string, parentTypeName string, suggestedArgs []string) string {
421+
message := fmt.Sprintf(`Unknown argument "%v" on field "%v" of type "%v".`, argName, fieldName, parentTypeName)
422+
423+
if len(suggestedArgs) > 0 {
424+
suggestions := strings.Join(quoteStrings(suggestedArgs), ", ")
425+
message = fmt.Sprintf(`%v Perhaps you meant %v?`, message, suggestions)
426+
}
427+
428+
return message
429+
}
430+
431+
func unknownDirectiveArgMessage(argName string, directiveName string, suggestedArgs []string) string {
432+
message := fmt.Sprintf(`Unknown argument "%v" on directive "@%v".`, argName, directiveName)
433+
434+
if len(suggestedArgs) > 0 {
435+
suggestions := strings.Join(quoteStrings(suggestedArgs), ", ")
436+
message = fmt.Sprintf(`%v Perhaps you meant %v?`, message, suggestions)
437+
}
438+
439+
return message
440+
}
441+
383442
// KnownArgumentNamesRule Known argument names
384443
//
385444
// A GraphQL field is only valid if all supplied arguments are defined by
@@ -399,6 +458,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
399458
if argumentOf == nil {
400459
return action, result
401460
}
461+
var fieldArgDef *Argument
402462
if argumentOf.GetKind() == kinds.Field {
403463
fieldDef := context.FieldDef()
404464
if fieldDef == nil {
@@ -408,8 +468,9 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
408468
if node.Name != nil {
409469
nodeName = node.Name.Value
410470
}
411-
var fieldArgDef *Argument
471+
argNames := []string{}
412472
for _, arg := range fieldDef.Args {
473+
argNames = append(argNames, arg.Name())
413474
if arg.Name() == nodeName {
414475
fieldArgDef = arg
415476
}
@@ -422,7 +483,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
422483
}
423484
reportError(
424485
context,
425-
fmt.Sprintf(`Unknown argument "%v" on field "%v" of type "%v".`, nodeName, fieldDef.Name, parentTypeName),
486+
unknownArgMessage(nodeName, fieldDef.Name, parentTypeName, suggestionList(nodeName, argNames)),
426487
[]ast.Node{node},
427488
)
428489
}
@@ -435,16 +496,18 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
435496
if node.Name != nil {
436497
nodeName = node.Name.Value
437498
}
499+
argNames := []string{}
438500
var directiveArgDef *Argument
439501
for _, arg := range directive.Args {
502+
argNames = append(argNames, arg.Name())
440503
if arg.Name() == nodeName {
441504
directiveArgDef = arg
442505
}
443506
}
444507
if directiveArgDef == nil {
445508
reportError(
446509
context,
447-
fmt.Sprintf(`Unknown argument "%v" on directive "@%v".`, nodeName, directive.Name),
510+
unknownDirectiveArgMessage(nodeName, directive.Name, suggestionList(nodeName, argNames)),
448511
[]ast.Node{node},
449512
)
450513
}
@@ -606,6 +669,23 @@ func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance
606669
}
607670
}
608671

672+
func unknownTypeMessage(typeName string, suggestedTypes []string) string {
673+
message := fmt.Sprintf(`Unknown type "%v".`, typeName)
674+
675+
const MaxLength = 5
676+
if len(suggestedTypes) > 0 {
677+
suggestions := ""
678+
if len(suggestedTypes) < MaxLength {
679+
suggestions = strings.Join(quoteStrings(suggestedTypes), ", ")
680+
} else {
681+
suggestions = strings.Join(quoteStrings(suggestedTypes[0:MaxLength]), ", ")
682+
}
683+
message = fmt.Sprintf(`%v Perhaps you meant one of the following: %v?`, message, suggestions)
684+
}
685+
686+
return message
687+
}
688+
609689
// KnownTypeNamesRule Known type names
610690
//
611691
// A GraphQL document is only valid if referenced types (specifically
@@ -643,9 +723,13 @@ func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance {
643723
}
644724
ttype := context.Schema().Type(typeNameValue)
645725
if ttype == nil {
726+
suggestedTypes := []string{}
727+
for key := range context.Schema().TypeMap() {
728+
suggestedTypes = append(suggestedTypes, key)
729+
}
646730
reportError(
647731
context,
648-
fmt.Sprintf(`Unknown type "%v".`, typeNameValue),
732+
unknownTypeMessage(typeNameValue, suggestionList(typeNameValue, suggestedTypes)),
649733
[]ast.Node{node},
650734
)
651735
}
@@ -2210,3 +2294,85 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
22102294

22112295
return true, nil
22122296
}
2297+
2298+
// Internal struct to sort results from suggestionList()
2299+
type suggestionListResult struct {
2300+
Options []string
2301+
Distances []float64
2302+
}
2303+
2304+
func (s suggestionListResult) Len() int {
2305+
return len(s.Options)
2306+
}
2307+
func (s suggestionListResult) Swap(i, j int) {
2308+
s.Options[i], s.Options[j] = s.Options[j], s.Options[i]
2309+
}
2310+
func (s suggestionListResult) Less(i, j int) bool {
2311+
return s.Distances[i] < s.Distances[j]
2312+
}
2313+
2314+
// suggestionList Given an invalid input string and a list of valid options, returns a filtered
2315+
// list of valid options sorted based on their similarity with the input.
2316+
func suggestionList(input string, options []string) []string {
2317+
dists := []float64{}
2318+
filteredOpts := []string{}
2319+
inputThreshold := float64(len(input) / 2)
2320+
2321+
for _, opt := range options {
2322+
dist := lexicalDistance(input, opt)
2323+
threshold := math.Max(inputThreshold, float64(len(opt)/2))
2324+
threshold = math.Max(threshold, 1)
2325+
if dist <= threshold {
2326+
filteredOpts = append(filteredOpts, opt)
2327+
dists = append(dists, dist)
2328+
}
2329+
}
2330+
//sort results
2331+
suggested := suggestionListResult{filteredOpts, dists}
2332+
sort.Sort(suggested)
2333+
return suggested.Options
2334+
}
2335+
2336+
// lexicalDistance Computes the lexical distance between strings A and B.
2337+
// The "distance" between two strings is given by counting the minimum number
2338+
// of edits needed to transform string A into string B. An edit can be an
2339+
// insertion, deletion, or substitution of a single character, or a swap of two
2340+
// adjacent characters.
2341+
// This distance can be useful for detecting typos in input or sorting
2342+
func lexicalDistance(a, b string) float64 {
2343+
d := [][]float64{}
2344+
aLen := len(a)
2345+
bLen := len(b)
2346+
for i := 0; i <= aLen; i++ {
2347+
d = append(d, []float64{float64(i)})
2348+
}
2349+
for k := 1; k <= bLen; k++ {
2350+
d[0] = append(d[0], float64(k))
2351+
}
2352+
2353+
for i := 1; i <= aLen; i++ {
2354+
for k := 1; k <= bLen; k++ {
2355+
cost := 1.0
2356+
if a[i-1] == b[k-1] {
2357+
cost = 0.0
2358+
}
2359+
minCostFloat := math.Min(
2360+
d[i-1][k]+1.0,
2361+
d[i][k-1]+1.0,
2362+
)
2363+
minCostFloat = math.Min(
2364+
minCostFloat,
2365+
d[i-1][k-1]+cost,
2366+
)
2367+
d[i] = append(d[i], minCostFloat)
2368+
2369+
if i > 1 && k < 1 &&
2370+
a[i-1] == b[k-2] &&
2371+
a[i-2] == b[k-1] {
2372+
d[i][k] = math.Min(d[i][k], d[i-2][k-2]+cost)
2373+
}
2374+
}
2375+
}
2376+
2377+
return d[aLen][bLen]
2378+
}

rules_fields_on_correct_type_test.go

+19-13
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) {
7373
meowVolume
7474
}
7575
`, []gqlerrors.FormattedError{
76-
testutil.RuleError(`Cannot query field "meowVolume" on type "Dog".`, 3, 9),
76+
testutil.RuleError(`Cannot query field "meowVolume" on type "Dog". Did you mean to query "barkVolume"?`, 3, 9),
7777
})
7878
}
7979
func TestValidate_FieldsOnCorrectType_IgnoreDeeplyUnknownField(t *testing.T) {
@@ -106,7 +106,7 @@ func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnInlineFragment(t *testing
106106
}
107107
}
108108
`, []gqlerrors.FormattedError{
109-
testutil.RuleError(`Cannot query field "meowVolume" on type "Dog".`, 4, 11),
109+
testutil.RuleError(`Cannot query field "meowVolume" on type "Dog". Did you mean to query "barkVolume"?`, 4, 11),
110110
})
111111
}
112112
func TestValidate_FieldsOnCorrectType_AliasedFieldTargetNotDefined(t *testing.T) {
@@ -115,7 +115,7 @@ func TestValidate_FieldsOnCorrectType_AliasedFieldTargetNotDefined(t *testing.T)
115115
volume : mooVolume
116116
}
117117
`, []gqlerrors.FormattedError{
118-
testutil.RuleError(`Cannot query field "mooVolume" on type "Dog".`, 3, 9),
118+
testutil.RuleError(`Cannot query field "mooVolume" on type "Dog". Did you mean to query "barkVolume"?`, 3, 9),
119119
})
120120
}
121121
func TestValidate_FieldsOnCorrectType_AliasedLyingFieldTargetNotDefined(t *testing.T) {
@@ -124,7 +124,7 @@ func TestValidate_FieldsOnCorrectType_AliasedLyingFieldTargetNotDefined(t *testi
124124
barkVolume : kawVolume
125125
}
126126
`, []gqlerrors.FormattedError{
127-
testutil.RuleError(`Cannot query field "kawVolume" on type "Dog".`, 3, 9),
127+
testutil.RuleError(`Cannot query field "kawVolume" on type "Dog". Did you mean to query "barkVolume"?`, 3, 9),
128128
})
129129
}
130130
func TestValidate_FieldsOnCorrectType_NotDefinedOnInterface(t *testing.T) {
@@ -142,7 +142,7 @@ func TestValidate_FieldsOnCorrectType_DefinedOnImplementorsButNotOnInterface(t *
142142
nickname
143143
}
144144
`, []gqlerrors.FormattedError{
145-
testutil.RuleError(`Cannot query field "nickname" on type "Pet". However, this field exists on "Cat", "Dog". Perhaps you meant to use an inline fragment?`, 3, 9),
145+
testutil.RuleError(`Cannot query field "nickname" on type "Pet". However, this field exists on "Cat", "Dog". Perhaps you meant to use an inline fragment? Did you mean to query "name"?`, 3, 9),
146146
})
147147
}
148148
func TestValidate_FieldsOnCorrectType_MetaFieldSelectionOnUnion(t *testing.T) {
@@ -184,27 +184,33 @@ func TestValidate_FieldsOnCorrectType_ValidFieldInInlineFragment(t *testing.T) {
184184
}
185185

186186
func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithNoSuggestions(t *testing.T) {
187-
message := graphql.UndefinedFieldMessage("T", "f", []string{})
188-
expected := `Cannot query field "T" on type "f".`
187+
message := graphql.UndefinedFieldMessage("f", "T", []string{}, []string{})
188+
expected := `Cannot query field "f" on type "T".`
189189
if message != expected {
190190
t.Fatalf("Unexpected message, expected: %v, got %v", expected, message)
191191
}
192192
}
193193

194194
func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithNoSmallNumbersOfSuggestions(t *testing.T) {
195-
message := graphql.UndefinedFieldMessage("T", "f", []string{"A", "B"})
196-
expected := `Cannot query field "T" on type "f". ` +
195+
message := graphql.UndefinedFieldMessage("f", "T", []string{"A", "B"}, []string{"z", "y"})
196+
expected := `Cannot query field "f" on type "T". ` +
197197
`However, this field exists on "A", "B". ` +
198-
`Perhaps you meant to use an inline fragment?`
198+
`Perhaps you meant to use an inline fragment? ` +
199+
`Did you mean to query "z", "y"?`
199200
if message != expected {
200201
t.Fatalf("Unexpected message, expected: %v, got %v", expected, message)
201202
}
202203
}
203204
func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithLotsOfSuggestions(t *testing.T) {
204-
message := graphql.UndefinedFieldMessage("T", "f", []string{"A", "B", "C", "D", "E", "F"})
205-
expected := `Cannot query field "T" on type "f". ` +
205+
message := graphql.UndefinedFieldMessage(
206+
"f", "T",
207+
[]string{"A", "B", "C", "D", "E", "F"},
208+
[]string{"z", "y", "x", "w", "v", "u"},
209+
)
210+
expected := `Cannot query field "f" on type "T". ` +
206211
`However, this field exists on "A", "B", "C", "D", "E", and 1 other types. ` +
207-
`Perhaps you meant to use an inline fragment?`
212+
`Perhaps you meant to use an inline fragment? ` +
213+
`Did you mean to query "z", "y", "x", "w", "v", or 1 other field?`
208214
if message != expected {
209215
t.Fatalf("Unexpected message, expected: %v, got %v", expected, message)
210216
}

rules_known_type_names_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestValidate_KnownTypeNames_UnknownTypeNamesAreInValid(t *testing.T) {
3434
`, []gqlerrors.FormattedError{
3535
testutil.RuleError(`Unknown type "JumbledUpLetters".`, 2, 23),
3636
testutil.RuleError(`Unknown type "Badger".`, 5, 25),
37-
testutil.RuleError(`Unknown type "Peettt".`, 8, 29),
37+
testutil.RuleError(`Unknown type "Peettt". Perhaps you meant one of the following: "Pet"?`, 8, 29),
3838
})
3939
}
4040

0 commit comments

Comments
 (0)