7
7
"github.com/graphql-go/graphql/language/kinds"
8
8
"github.com/graphql-go/graphql/language/printer"
9
9
"github.com/graphql-go/graphql/language/visitor"
10
+ "math"
10
11
"sort"
11
12
"strings"
12
13
)
@@ -162,8 +163,14 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI
162
163
VisitorOpts : visitorOpts ,
163
164
}
164
165
}
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 {
167
174
168
175
quoteStrings := func (slice []string ) []string {
169
176
quoted := []string {}
@@ -175,15 +182,27 @@ func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes []
175
182
176
183
// construct helpful (but long) message
177
184
message := fmt .Sprintf (`Cannot query field "%v" on type "%v".` , fieldName , ttypeName )
178
- suggestions := strings .Join (quoteStrings (suggestedTypes ), ", " )
179
185
const MaxLength = 5
180
186
if len (suggestedTypes ) > 0 {
187
+ suggestions := ""
181
188
if len (suggestedTypes ) > MaxLength {
182
189
suggestions = strings .Join (quoteStrings (suggestedTypes [0 :MaxLength ]), ", " ) +
183
190
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 ), ", " )
184
204
}
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 )
187
206
}
188
207
189
208
return message
@@ -232,11 +251,29 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance
232
251
}
233
252
}
234
253
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
+ }
236
273
237
274
reportError (
238
275
context ,
239
- message ,
276
+ UndefinedFieldMessage ( nodeName , ttype . Name (), suggestedTypes , suggestedFields ) ,
240
277
[]ast.Node {node },
241
278
)
242
279
}
@@ -380,6 +417,28 @@ func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleIn
380
417
}
381
418
}
382
419
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
+
383
442
// KnownArgumentNamesRule Known argument names
384
443
//
385
444
// A GraphQL field is only valid if all supplied arguments are defined by
@@ -399,6 +458,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
399
458
if argumentOf == nil {
400
459
return action , result
401
460
}
461
+ var fieldArgDef * Argument
402
462
if argumentOf .GetKind () == kinds .Field {
403
463
fieldDef := context .FieldDef ()
404
464
if fieldDef == nil {
@@ -408,8 +468,9 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
408
468
if node .Name != nil {
409
469
nodeName = node .Name .Value
410
470
}
411
- var fieldArgDef * Argument
471
+ argNames := [] string {}
412
472
for _ , arg := range fieldDef .Args {
473
+ argNames = append (argNames , arg .Name ())
413
474
if arg .Name () == nodeName {
414
475
fieldArgDef = arg
415
476
}
@@ -422,7 +483,7 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
422
483
}
423
484
reportError (
424
485
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 ) ),
426
487
[]ast.Node {node },
427
488
)
428
489
}
@@ -435,16 +496,18 @@ func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance
435
496
if node .Name != nil {
436
497
nodeName = node .Name .Value
437
498
}
499
+ argNames := []string {}
438
500
var directiveArgDef * Argument
439
501
for _ , arg := range directive .Args {
502
+ argNames = append (argNames , arg .Name ())
440
503
if arg .Name () == nodeName {
441
504
directiveArgDef = arg
442
505
}
443
506
}
444
507
if directiveArgDef == nil {
445
508
reportError (
446
509
context ,
447
- fmt . Sprintf ( `Unknown argument "%v" on directive "@%v".` , nodeName , directive . Name ),
510
+ unknownDirectiveArgMessage ( nodeName , directive . Name , suggestionList ( nodeName , argNames ) ),
448
511
[]ast.Node {node },
449
512
)
450
513
}
@@ -606,6 +669,23 @@ func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance
606
669
}
607
670
}
608
671
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
+
609
689
// KnownTypeNamesRule Known type names
610
690
//
611
691
// A GraphQL document is only valid if referenced types (specifically
@@ -643,9 +723,13 @@ func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance {
643
723
}
644
724
ttype := context .Schema ().Type (typeNameValue )
645
725
if ttype == nil {
726
+ suggestedTypes := []string {}
727
+ for key := range context .Schema ().TypeMap () {
728
+ suggestedTypes = append (suggestedTypes , key )
729
+ }
646
730
reportError (
647
731
context ,
648
- fmt . Sprintf ( `Unknown type "%v".` , typeNameValue ),
732
+ unknownTypeMessage ( typeNameValue , suggestionList ( typeNameValue , suggestedTypes ) ),
649
733
[]ast.Node {node },
650
734
)
651
735
}
@@ -2210,3 +2294,85 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
2210
2294
2211
2295
return true , nil
2212
2296
}
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
+ }
0 commit comments