Skip to content

Commit

Permalink
[validator] Add suggested types to incorrect field message
Browse files Browse the repository at this point in the history
Commit:
7861b226b364979feaba4deef70dc472c54c8d3d [7861b22]
Parents:
337925e19c
Author:
dschafer <dschafer@fb.com>
Date:
2 February 2016 at 4:33:10 AM SGT
Commit Date:
3 February 2016 at 2:49:18 AM SGT
  • Loading branch information
sogko committed Apr 11, 2016
1 parent 13d7acd commit f04b607
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 21 deletions.
1 change: 1 addition & 0 deletions definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func IsCompositeType(ttype interface{}) bool {

// These types may describe the parent context of a selection set.
type Abstract interface {
Name() string
ObjectType(value interface{}, info ResolveInfo) *Object
PossibleTypes() []*Object
IsPossibleType(ttype *Object) bool
Expand Down
136 changes: 134 additions & 2 deletions rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,32 @@ func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleI
}
}

func UndefinedFieldMessage(fieldName string, ttypeName string, suggestedTypes []string) string {

quoteStrings := func(slice []string) []string {
quoted := []string{}
for _, s := range slice {
quoted = append(quoted, fmt.Sprintf(`"%v"`, s))
}
return quoted
}

// construct helpful (but long) message
message := fmt.Sprintf(`Cannot query field "%v" on type "%v".`, fieldName, ttypeName)
suggestions := strings.Join(quoteStrings(suggestedTypes), ", ")
const MAX_LENGTH = 5
if len(suggestedTypes) > 0 {
if len(suggestedTypes) > MAX_LENGTH {
suggestions = strings.Join(quoteStrings(suggestedTypes[0:MAX_LENGTH]), ", ") +
fmt.Sprintf(`, and %v other types`, len(suggestedTypes)-MAX_LENGTH)
}
message = message + fmt.Sprintf(` However, this field exists on %v.`, suggestions)
message = message + ` Perhaps you meant to use an inline fragment?`
}

return message
}

/**
* FieldsOnCorrectTypeRule
* Fields on correct type
Expand All @@ -182,14 +208,37 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance
if ttype != nil {
fieldDef := context.FieldDef()
if fieldDef == nil {
// This isn't valid. Let's find suggestions, if any.
suggestedTypes := []string{}

nodeName := ""
if node.Name != nil {
nodeName = node.Name.Value
}

if ttype, ok := ttype.(Abstract); ok {
siblingInterfaces := getSiblingInterfacesIncludingField(ttype, nodeName)
implementations := getImplementationsIncludingField(ttype, nodeName)
suggestedMaps := map[string]bool{}
for _, s := range siblingInterfaces {
if _, ok := suggestedMaps[s]; !ok {
suggestedMaps[s] = true
suggestedTypes = append(suggestedTypes, s)
}
}
for _, s := range implementations {
if _, ok := suggestedMaps[s]; !ok {
suggestedMaps[s] = true
suggestedTypes = append(suggestedTypes, s)
}
}
}

message := UndefinedFieldMessage(nodeName, ttype.Name(), suggestedTypes)

return reportError(
context,
fmt.Sprintf(`Cannot query field "%v" on "%v".`,
nodeName, ttype.Name()),
message,
[]ast.Node{node},
)
}
Expand All @@ -205,6 +254,89 @@ func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance
}
}

/**
* Return implementations of `type` that include `fieldName` as a valid field.
*/
func getImplementationsIncludingField(ttype Abstract, fieldName string) []string {

result := []string{}
for _, t := range ttype.PossibleTypes() {
fields := t.Fields()
if _, ok := fields[fieldName]; ok {
result = append(result, fmt.Sprintf(`%v`, t.Name()))
}
}

sort.Strings(result)
return result
}

/**
* Go through all of the implementations of type, and find other interaces
* that they implement. If those interfaces include `field` as a valid field,
* return them, sorted by how often the implementations include the other
* interface.
*/
func getSiblingInterfacesIncludingField(ttype Abstract, fieldName string) []string {
implementingObjects := ttype.PossibleTypes()

result := []string{}
suggestedInterfaceSlice := []*suggestedInterface{}

// stores a map of interface name => index in suggestedInterfaceSlice
suggestedInterfaceMap := map[string]int{}

for _, t := range implementingObjects {
for _, i := range t.Interfaces() {
if i == nil {
continue
}
fields := i.Fields()
if _, ok := fields[fieldName]; !ok {
continue
}
index, ok := suggestedInterfaceMap[i.Name()]
if !ok {
suggestedInterfaceSlice = append(suggestedInterfaceSlice, &suggestedInterface{
name: i.Name(),
count: 0,
})
index = len(suggestedInterfaceSlice) - 1
}
if index < len(suggestedInterfaceSlice) {
s := suggestedInterfaceSlice[index]
if s.name == i.Name() {
s.count = s.count + 1
}
}
}
}
sort.Sort(suggestedInterfaceSortedSlice(suggestedInterfaceSlice))

for _, s := range suggestedInterfaceSlice {
result = append(result, fmt.Sprintf(`%v`, s.name))
}
return result

}

type suggestedInterface struct {
name string
count int
}

type suggestedInterfaceSortedSlice []*suggestedInterface

func (s suggestedInterfaceSortedSlice) Len() int {
return len(s)
}
func (s suggestedInterfaceSortedSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s suggestedInterfaceSortedSlice) Less(i, j int) bool {
return s[i].count < s[j].count
}

/**
* FragmentsOnCompositeTypesRule
* Fragments on composite type
Expand Down
55 changes: 41 additions & 14 deletions rules_fields_on_correct_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestValidate_FieldsOnCorrectType_IgnoresFieldsOnUnknownType(t *testing.T) {
}
`)
}
func TestValidate_FieldsOnCorrectType_ReportErrosWhenTheTypeIsKnownAgain(t *testing.T) {
func TestValidate_FieldsOnCorrectType_ReportErrorsWhenTheTypeIsKnownAgain(t *testing.T) {
testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, `
fragment typeKnownAgain on Pet {
unknown_pet_field {
Expand All @@ -63,8 +63,8 @@ func TestValidate_FieldsOnCorrectType_ReportErrosWhenTheTypeIsKnownAgain(t *test
}
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "unknown_pet_field" on "Pet".`, 3, 9),
testutil.RuleError(`Cannot query field "unknown_cat_field" on "Cat".`, 5, 13),
testutil.RuleError(`Cannot query field "unknown_pet_field" on type "Pet".`, 3, 9),
testutil.RuleError(`Cannot query field "unknown_cat_field" on type "Cat".`, 5, 13),
})
}
func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) {
Expand All @@ -73,7 +73,7 @@ func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) {
meowVolume
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "meowVolume" on "Dog".`, 3, 9),
testutil.RuleError(`Cannot query field "meowVolume" on type "Dog".`, 3, 9),
})
}
func TestValidate_FieldsOnCorrectType_IgnoreDeeplyUnknownField(t *testing.T) {
Expand All @@ -84,7 +84,7 @@ func TestValidate_FieldsOnCorrectType_IgnoreDeeplyUnknownField(t *testing.T) {
}
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "unknown_field" on "Dog".`, 3, 9),
testutil.RuleError(`Cannot query field "unknown_field" on type "Dog".`, 3, 9),
})
}
func TestValidate_FieldsOnCorrectType_SubFieldNotDefined(t *testing.T) {
Expand All @@ -95,7 +95,7 @@ func TestValidate_FieldsOnCorrectType_SubFieldNotDefined(t *testing.T) {
}
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "unknown_field" on "Pet".`, 4, 11),
testutil.RuleError(`Cannot query field "unknown_field" on type "Pet".`, 4, 11),
})
}
func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnInlineFragment(t *testing.T) {
Expand All @@ -106,7 +106,7 @@ func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnInlineFragment(t *testing
}
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "meowVolume" on "Dog".`, 4, 11),
testutil.RuleError(`Cannot query field "meowVolume" on type "Dog".`, 4, 11),
})
}
func TestValidate_FieldsOnCorrectType_AliasedFieldTargetNotDefined(t *testing.T) {
Expand All @@ -115,7 +115,7 @@ func TestValidate_FieldsOnCorrectType_AliasedFieldTargetNotDefined(t *testing.T)
volume : mooVolume
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "mooVolume" on "Dog".`, 3, 9),
testutil.RuleError(`Cannot query field "mooVolume" on type "Dog".`, 3, 9),
})
}
func TestValidate_FieldsOnCorrectType_AliasedLyingFieldTargetNotDefined(t *testing.T) {
Expand All @@ -124,7 +124,7 @@ func TestValidate_FieldsOnCorrectType_AliasedLyingFieldTargetNotDefined(t *testi
barkVolume : kawVolume
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "kawVolume" on "Dog".`, 3, 9),
testutil.RuleError(`Cannot query field "kawVolume" on type "Dog".`, 3, 9),
})
}
func TestValidate_FieldsOnCorrectType_NotDefinedOnInterface(t *testing.T) {
Expand All @@ -133,7 +133,7 @@ func TestValidate_FieldsOnCorrectType_NotDefinedOnInterface(t *testing.T) {
tailLength
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "tailLength" on "Pet".`, 3, 9),
testutil.RuleError(`Cannot query field "tailLength" on type "Pet".`, 3, 9),
})
}
func TestValidate_FieldsOnCorrectType_DefinedOnImplementorsButNotOnInterface(t *testing.T) {
Expand All @@ -142,7 +142,7 @@ func TestValidate_FieldsOnCorrectType_DefinedOnImplementorsButNotOnInterface(t *
nickname
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "nickname" on "Pet".`, 3, 9),
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),
})
}
func TestValidate_FieldsOnCorrectType_MetaFieldSelectionOnUnion(t *testing.T) {
Expand All @@ -158,16 +158,16 @@ func TestValidate_FieldsOnCorrectType_DirectFieldSelectionOnUnion(t *testing.T)
directField
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "directField" on "CatOrDog".`, 3, 9),
testutil.RuleError(`Cannot query field "directField" on type "CatOrDog".`, 3, 9),
})
}
func TestValidate_FieldsOnCorrectType_DirectImplementorsQueriedOnUnion(t *testing.T) {
func TestValidate_FieldsOnCorrectType_DefinedImplementorsQueriedOnUnion(t *testing.T) {
testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, `
fragment definedOnImplementorsQueriedOnUnion on CatOrDog {
name
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Cannot query field "name" on "CatOrDog".`, 3, 9),
testutil.RuleError(`Cannot query field "name" on type "CatOrDog". However, this field exists on "Being", "Pet", "Canine", "Cat", "Dog". Perhaps you meant to use an inline fragment?`, 3, 9),
})
}
func TestValidate_FieldsOnCorrectType_ValidFieldInInlineFragment(t *testing.T) {
Expand All @@ -182,3 +182,30 @@ func TestValidate_FieldsOnCorrectType_ValidFieldInInlineFragment(t *testing.T) {
}
`)
}

func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithNoSuggestions(t *testing.T) {
message := graphql.UndefinedFieldMessage("T", "f", []string{})
expected := `Cannot query field "T" on type "f".`
if message != expected {
t.Fatalf("Unexpected message, expected: %v, got %v", expected, message)
}
}

func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithNoSmallNumbersOfSuggestions(t *testing.T) {
message := graphql.UndefinedFieldMessage("T", "f", []string{"A", "B"})
expected := `Cannot query field "T" on type "f". ` +
`However, this field exists on "A", "B". ` +
`Perhaps you meant to use an inline fragment?`
if message != expected {
t.Fatalf("Unexpected message, expected: %v, got %v", expected, message)
}
}
func TestValidate_FieldsOnCorrectTypeErrorMessage_WorksWithLotsOfSuggestions(t *testing.T) {
message := graphql.UndefinedFieldMessage("T", "f", []string{"A", "B", "C", "D", "E", "F"})
expected := `Cannot query field "T" on type "f". ` +
`However, this field exists on "A", "B", "C", "D", "E", and 1 other types. ` +
`Perhaps you meant to use an inline fragment?`
if message != expected {
t.Fatalf("Unexpected message, expected: %v, got %v", expected, message)
}
}
14 changes: 14 additions & 0 deletions testutil/rules_test_harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ func init() {
},
},
})
var canineInterface = graphql.NewInterface(graphql.InterfaceConfig{
Name: "Canine",
Fields: graphql.Fields{
"name": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"surname": &graphql.ArgumentConfig{
Type: graphql.Boolean,
},
},
},
},
})
var dogCommandEnum = graphql.NewEnum(graphql.EnumConfig{
Name: "DogCommand",
Values: graphql.EnumValueConfigMap{
Expand Down Expand Up @@ -110,6 +123,7 @@ func init() {
Interfaces: []*graphql.Interface{
beingInterface,
petInterface,
canineInterface,
},
})
var furColorEnum = graphql.NewEnum(graphql.EnumConfig{
Expand Down
8 changes: 3 additions & 5 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/graphql-go/graphql/language/parser"
"github.com/graphql-go/graphql/language/source"
"github.com/graphql-go/graphql/testutil"
"github.com/kr/pretty"
"reflect"
)

Expand Down Expand Up @@ -75,25 +74,24 @@ func TestValidator_SupportsFullValidation_ValidatesUsingACustomTypeInfo(t *testi

expectedErrors := []gqlerrors.FormattedError{
{
Message: "Cannot query field \"catOrDog\" on \"QueryRoot\".",
Message: "Cannot query field \"catOrDog\" on type \"QueryRoot\".",
Locations: []location.SourceLocation{
{Line: 3, Column: 9},
},
},
{
Message: "Cannot query field \"furColor\" on \"Cat\".",
Message: "Cannot query field \"furColor\" on type \"Cat\".",
Locations: []location.SourceLocation{
{Line: 5, Column: 13},
},
},
{
Message: "Cannot query field \"isHousetrained\" on \"Dog\".",
Message: "Cannot query field \"isHousetrained\" on type \"Dog\".",
Locations: []location.SourceLocation{
{Line: 8, Column: 13},
},
},
}
pretty.Println(errors)
if !reflect.DeepEqual(expectedErrors, errors) {
t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedErrors, errors))
}
Expand Down

0 comments on commit f04b607

Please sign in to comment.