diff --git a/README.md b/README.md index ec0c8d36..dcb5c9a9 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ import ( func main() { // Schema - fields := graphql.FieldConfigMap{ - "hello": &graphql.FieldConfig{ + fields := graphql.Fields{ + "hello": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.GQLFRParams) interface{} { return "world" diff --git a/abstract_test.go b/abstract_test.go index d5d15ed2..a0640cf8 100644 --- a/abstract_test.go +++ b/abstract_test.go @@ -11,17 +11,17 @@ import ( ) type testDog struct { - Name string - Woofs bool + Name string `json:"name"` + Woofs bool `json:"woofs"` } type testCat struct { - Name string - Meows bool + Name string `json:"name"` + Meows bool `json:"meows"` } type testHuman struct { - Name string + Name string `json:"name"` } func TestIsTypeOfUsedToResolveRuntimeTypeForInterface(t *testing.T) { @@ -168,21 +168,9 @@ func TestIsTypeOfUsedToResolveRuntimeTypeForUnion(t *testing.T) { Fields: graphql.Fields{ "name": &graphql.Field{ Type: graphql.String, - Resolve: func(p graphql.ResolveParams) interface{} { - if dog, ok := p.Source.(*testDog); ok { - return dog.Name - } - return nil - }, }, "woofs": &graphql.Field{ Type: graphql.Boolean, - Resolve: func(p graphql.ResolveParams) interface{} { - if dog, ok := p.Source.(*testDog); ok { - return dog.Woofs - } - return nil - }, }, }, }) @@ -195,21 +183,9 @@ func TestIsTypeOfUsedToResolveRuntimeTypeForUnion(t *testing.T) { Fields: graphql.Fields{ "name": &graphql.Field{ Type: graphql.String, - Resolve: func(p graphql.ResolveParams) interface{} { - if cat, ok := p.Source.(*testCat); ok { - return cat.Name - } - return nil - }, }, "meows": &graphql.Field{ Type: graphql.Boolean, - Resolve: func(p graphql.ResolveParams) interface{} { - if cat, ok := p.Source.(*testCat); ok { - return cat.Meows - } - return nil - }, }, }, }) @@ -219,15 +195,6 @@ func TestIsTypeOfUsedToResolveRuntimeTypeForUnion(t *testing.T) { Types: []*graphql.Object{ dogType, catType, }, - ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { - if _, ok := value.(*testCat); ok { - return catType - } - if _, ok := value.(*testDog); ok { - return dogType - } - return nil - }, }) schema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: graphql.NewObject(graphql.ObjectConfig{ @@ -251,11 +218,12 @@ func TestIsTypeOfUsedToResolveRuntimeTypeForUnion(t *testing.T) { query := `{ pets { - name ... on Dog { + name woofs } ... on Cat { + name meows } } @@ -281,7 +249,6 @@ func TestIsTypeOfUsedToResolveRuntimeTypeForUnion(t *testing.T) { Schema: schema, RequestString: query, }) - if len(result.Errors) != 0 { t.Fatalf("wrong result, unexpected errors: %v", result.Errors) } @@ -464,66 +431,28 @@ func TestResolveTypeOnUnionYieldsUsefulError(t *testing.T) { Fields: graphql.Fields{ "name": &graphql.Field{ Type: graphql.String, - Resolve: func(p graphql.ResolveParams) interface{} { - if human, ok := p.Source.(*testHuman); ok { - return human.Name - } - return nil - }, }, }, }) dogType := graphql.NewObject(graphql.ObjectConfig{ Name: "Dog", - IsTypeOf: func(value interface{}, info graphql.ResolveInfo) bool { - _, ok := value.(*testDog) - return ok - }, Fields: graphql.Fields{ "name": &graphql.Field{ Type: graphql.String, - Resolve: func(p graphql.ResolveParams) interface{} { - if dog, ok := p.Source.(*testDog); ok { - return dog.Name - } - return nil - }, }, "woofs": &graphql.Field{ Type: graphql.Boolean, - Resolve: func(p graphql.ResolveParams) interface{} { - if dog, ok := p.Source.(*testDog); ok { - return dog.Woofs - } - return nil - }, }, }, }) catType := graphql.NewObject(graphql.ObjectConfig{ Name: "Cat", - IsTypeOf: func(value interface{}, info graphql.ResolveInfo) bool { - _, ok := value.(*testCat) - return ok - }, Fields: graphql.Fields{ "name": &graphql.Field{ Type: graphql.String, - Resolve: func(p graphql.ResolveParams) interface{} { - if cat, ok := p.Source.(*testCat); ok { - return cat.Name - } - return nil - }, }, "meows": &graphql.Field{ Type: graphql.Boolean, - Resolve: func(p graphql.ResolveParams) interface{} { - if cat, ok := p.Source.(*testCat); ok { - return cat.Meows - } - return nil - }, }, }, }) @@ -568,11 +497,12 @@ func TestResolveTypeOnUnionYieldsUsefulError(t *testing.T) { query := `{ pets { - name ... on Dog { + name woofs } ... on Cat { + name meows } } diff --git a/definition.go b/definition.go index 2fba7539..657a5edc 100644 --- a/definition.go +++ b/definition.go @@ -42,34 +42,45 @@ var _ Input = (*List)(nil) var _ Input = (*NonNull)(nil) func IsInputType(ttype Type) bool { - Named := GetNamed(ttype) - if _, ok := Named.(*Scalar); ok { + named := GetNamed(ttype) + if _, ok := named.(*Scalar); ok { return true } - if _, ok := Named.(*Enum); ok { + if _, ok := named.(*Enum); ok { return true } - if _, ok := Named.(*InputObject); ok { + if _, ok := named.(*InputObject); ok { return true } return false } func IsOutputType(ttype Type) bool { - Named := GetNamed(ttype) - if _, ok := Named.(*Scalar); ok { + name := GetNamed(ttype) + if _, ok := name.(*Scalar); ok { return true } - if _, ok := Named.(*Object); ok { + if _, ok := name.(*Object); ok { return true } - if _, ok := Named.(*Interface); ok { + if _, ok := name.(*Interface); ok { return true } - if _, ok := Named.(*Union); ok { + if _, ok := name.(*Union); ok { return true } - if _, ok := Named.(*Enum); ok { + if _, ok := name.(*Enum); ok { + return true + } + return false +} + +func IsLeafType(ttype Type) bool { + named := GetNamed(ttype) + if _, ok := named.(*Scalar); ok { + return true + } + if _, ok := named.(*Enum); ok { return true } return false @@ -100,6 +111,19 @@ var _ Composite = (*Object)(nil) var _ Composite = (*Interface)(nil) var _ Composite = (*Union)(nil) +func IsCompositeType(ttype interface{}) bool { + if _, ok := ttype.(*Object); ok { + return true + } + if _, ok := ttype.(*Interface); ok { + return true + } + if _, ok := ttype.(*Union); ok { + return true + } + return false +} + // These types may describe the parent context of a selection set. type Abstract interface { ObjectType(value interface{}, info ResolveInfo) *Object @@ -110,6 +134,24 @@ type Abstract interface { var _ Abstract = (*Interface)(nil) var _ Abstract = (*Union)(nil) +type Nullable interface { +} + +var _ Nullable = (*Scalar)(nil) +var _ Nullable = (*Object)(nil) +var _ Nullable = (*Interface)(nil) +var _ Nullable = (*Union)(nil) +var _ Nullable = (*Enum)(nil) +var _ Nullable = (*InputObject)(nil) +var _ Nullable = (*List)(nil) + +func GetNullable(ttype Type) Nullable { + if ttype, ok := ttype.(*NonNull); ok { + return ttype.OfType + } + return ttype +} + // These named types do not include modifiers like List or NonNull. type Named interface { String() string diff --git a/examples/http/main.go b/examples/http/main.go index 34ebade0..3af79bd7 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -19,7 +19,7 @@ var data map[string]user /* Create User object type with fields "id" and "name" by using GraphQLObjectTypeConfig: - Name: name of object type - - Fields: a map of fields by using GraphQLFieldConfigMap + - Fields: a map of fields by using GraphQLFields Setup type of field use GraphQLFieldConfig */ var userType = graphql.NewObject( @@ -39,7 +39,7 @@ var userType = graphql.NewObject( /* Create Query object type with fields "user" has type [userType] by using GraphQLObjectTypeConfig: - Name: name of object type - - Fields: a map of fields by using GraphQLFieldConfigMap + - Fields: a map of fields by using GraphQLFields Setup type of field use GraphQLFieldConfig to define: - Type: type of field - Args: arguments to query with current field diff --git a/graphql.go b/graphql.go index 1d84eb1d..0621b6b5 100644 --- a/graphql.go +++ b/graphql.go @@ -25,7 +25,7 @@ func Do(p Params) *Result { Errors: gqlerrors.FormatErrors(err), } } - validationResult := ValidateDocument(p.Schema, AST) + validationResult := ValidateDocument(&p.Schema, AST, nil) if !validationResult.IsValid { return &Result{ diff --git a/language/kinds/kinds.go b/language/kinds/kinds.go index 1aafca81..d1161763 100644 --- a/language/kinds/kinds.go +++ b/language/kinds/kinds.go @@ -9,8 +9,8 @@ const ( Directive = "Directive" VariableDefinition = "VariableDefinition" Variable = "Variable" - Named = "Named" - List = "List" + Named = "Named" // previously NamedType + List = "List" // previously ListType NonNull = "NonNull" InlineFragment = "InlineFragment" FragmentSpread = "FragmentSpread" diff --git a/language/printer/printer.go b/language/printer/printer.go index c4a35f52..8d41b672 100644 --- a/language/printer/printer.go +++ b/language/printer/printer.go @@ -6,6 +6,7 @@ import ( "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/visitor" + "reflect" ) func getMapValue(m map[string]interface{}, key string) interface{} { @@ -57,13 +58,20 @@ func toSliceString(slice interface{}) []string { return []string{} } res := []string{} - for _, s := range slice.([]interface{}) { - switch s := s.(type) { - case string: - res = append(res, s) + switch reflect.TypeOf(slice).Kind() { + case reflect.Slice: + s := reflect.ValueOf(slice) + for i := 0; i < s.Len(); i++ { + elem := s.Index(i) + elemInterface := elem.Interface() + if elem, ok := elemInterface.(string); ok { + res = append(res, elem) + } } + return res + default: + return res } - return res } func join(str []string, sep string) string { @@ -106,6 +114,8 @@ func indent(maybeString interface{}) string { var printDocASTReducer = map[string]visitor.VisitFunc{ "Name": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.Name: + return visitor.ActionUpdate, node.Value case map[string]interface{}: return visitor.ActionUpdate, getMapValue(node, "Value") } @@ -113,6 +123,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "Variable": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.Variable: + return visitor.ActionUpdate, fmt.Sprintf("$%v", node.Name) case map[string]interface{}: return visitor.ActionUpdate, "$" + getMapValueString(node, "Name") } @@ -120,6 +132,9 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "Document": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.Document: + definitions := toSliceString(node.Definitions) + return visitor.ActionUpdate, join(definitions, "\n\n") + "\n" case map[string]interface{}: definitions := toSliceString(getMapValue(node, "Definitions")) return visitor.ActionUpdate, join(definitions, "\n\n") + "\n" @@ -128,6 +143,25 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "OperationDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.OperationDefinition: + op := node.Operation + name := fmt.Sprintf("%v", node.Name) + + defs := wrap("(", join(toSliceString(node.VariableDefinitions), ", "), ")") + directives := join(toSliceString(node.Directives), " ") + selectionSet := fmt.Sprintf("%v", node.SelectionSet) + str := "" + if name == "" { + str = selectionSet + } else { + str = join([]string{ + op, + join([]string{name, defs}, ""), + directives, + selectionSet, + }, " ") + } + return visitor.ActionUpdate, str case map[string]interface{}: op := getMapValueString(node, "Operation") name := getMapValueString(node, "Name") @@ -152,6 +186,12 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "VariableDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.VariableDefinition: + variable := fmt.Sprintf("%v", node.Variable) + ttype := fmt.Sprintf("%v", node.Type) + defaultValue := fmt.Sprintf("%v", node.DefaultValue) + + return visitor.ActionUpdate, variable + ": " + ttype + wrap(" = ", defaultValue, "") case map[string]interface{}: variable := getMapValueString(node, "Variable") @@ -165,6 +205,9 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "SelectionSet": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.SelectionSet: + str := block(node.Selections) + return visitor.ActionUpdate, str case map[string]interface{}: selections := getMapValue(node, "Selections") str := block(selections) @@ -175,6 +218,10 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "Field": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.Argument: + name := fmt.Sprintf("%v", node.Name) + value := fmt.Sprintf("%v", node.Value) + return visitor.ActionUpdate, name + ": " + value case map[string]interface{}: alias := getMapValueString(node, "Alias") @@ -197,6 +244,10 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "Argument": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.FragmentSpread: + name := fmt.Sprintf("%v", node.Name) + directives := toSliceString(node.Directives) + return visitor.ActionUpdate, "..." + name + wrap(" ", join(directives, " "), "") case map[string]interface{}: name := getMapValueString(node, "Name") value := getMapValueString(node, "Value") @@ -206,6 +257,11 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "FragmentSpread": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.InlineFragment: + typeCondition := fmt.Sprintf("%v", node.TypeCondition) + directives := toSliceString(node.Directives) + selectionSet := fmt.Sprintf("%v", node.SelectionSet) + return visitor.ActionUpdate, "... on " + typeCondition + " " + wrap("", join(directives, " "), " ") + selectionSet case map[string]interface{}: name := getMapValueString(node, "Name") directives := toSliceString(getMapValue(node, "Directives")) @@ -225,6 +281,12 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "FragmentDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.FragmentDefinition: + name := fmt.Sprintf("%v", node.Name) + typeCondition := fmt.Sprintf("%v", node.TypeCondition) + directives := toSliceString(node.Directives) + selectionSet := fmt.Sprintf("%v", node.SelectionSet) + return visitor.ActionUpdate, "fragment " + name + " on " + typeCondition + " " + wrap("", join(directives, " "), " ") + selectionSet case map[string]interface{}: name := getMapValueString(node, "Name") typeCondition := getMapValueString(node, "TypeCondition") @@ -237,6 +299,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ "IntValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.IntValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) case map[string]interface{}: return visitor.ActionUpdate, getMapValueString(node, "Value") } @@ -244,6 +308,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "FloatValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.FloatValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) case map[string]interface{}: return visitor.ActionUpdate, getMapValueString(node, "Value") } @@ -251,6 +317,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "StringValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.StringValue: + return visitor.ActionUpdate, `"` + fmt.Sprintf("%v", node.Value) + `"` case map[string]interface{}: return visitor.ActionUpdate, `"` + getMapValueString(node, "Value") + `"` } @@ -258,6 +326,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "BooleanValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.BooleanValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) case map[string]interface{}: return visitor.ActionUpdate, getMapValueString(node, "Value") } @@ -265,6 +335,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "EnumValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.EnumValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) case map[string]interface{}: return visitor.ActionUpdate, getMapValueString(node, "Value") } @@ -272,6 +344,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "ListValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.ListValue: + return visitor.ActionUpdate, "[" + join(toSliceString(node.Values), ", ") + "]" case map[string]interface{}: return visitor.ActionUpdate, "[" + join(toSliceString(getMapValue(node, "Values")), ", ") + "]" } @@ -279,6 +353,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "ObjectValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.ObjectValue: + return visitor.ActionUpdate, "{" + join(toSliceString(node.Fields), ", ") + "}" case map[string]interface{}: return visitor.ActionUpdate, "{" + join(toSliceString(getMapValue(node, "Fields")), ", ") + "}" } @@ -286,6 +362,10 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "ObjectField": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.ObjectField: + name := fmt.Sprintf("%v", node.Name) + value := fmt.Sprintf("%v", node.Value) + return visitor.ActionUpdate, name + ": " + value case map[string]interface{}: name := getMapValueString(node, "Name") value := getMapValueString(node, "Value") @@ -296,6 +376,10 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ "Directive": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.Directive: + name := fmt.Sprintf("%v", node.Name) + args := toSliceString(node.Arguments) + return visitor.ActionUpdate, "@" + name + wrap("(", join(args, ", "), ")") case map[string]interface{}: name := getMapValueString(node, "Name") args := toSliceString(getMapValue(node, "Arguments")) @@ -306,6 +390,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ "Named": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.Named: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Name) case map[string]interface{}: return visitor.ActionUpdate, getMapValueString(node, "Name") } @@ -313,6 +399,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "List": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.List: + return visitor.ActionUpdate, "[" + fmt.Sprintf("%v", node.Type) + "]" case map[string]interface{}: return visitor.ActionUpdate, "[" + getMapValueString(node, "Type") + "]" } @@ -320,6 +408,8 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "NonNull": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.NonNull: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Type) + "!" case map[string]interface{}: return visitor.ActionUpdate, getMapValueString(node, "Type") + "!" } @@ -328,6 +418,12 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ "ObjectDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.ObjectDefinition: + name := fmt.Sprintf("%v", node.Name) + interfaces := toSliceString(node.Interfaces) + fields := node.Fields + str := "type " + name + " " + wrap("implements ", join(interfaces, ", "), " ") + block(fields) + return visitor.ActionUpdate, str case map[string]interface{}: name := getMapValueString(node, "Name") interfaces := toSliceString(getMapValue(node, "Interfaces")) @@ -339,6 +435,12 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "FieldDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.FieldDefinition: + name := fmt.Sprintf("%v", node.Name) + ttype := fmt.Sprintf("%v", node.Type) + args := toSliceString(node.Arguments) + str := name + wrap("(", join(args, ", "), ")") + ": " + ttype + return visitor.ActionUpdate, str case map[string]interface{}: name := getMapValueString(node, "Name") ttype := getMapValueString(node, "Type") @@ -350,6 +452,12 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "InputValueDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.InputValueDefinition: + name := fmt.Sprintf("%v", node.Name) + ttype := fmt.Sprintf("%v", node.Type) + defaultValue := fmt.Sprintf("%v", node.DefaultValue) + str := name + ": " + ttype + wrap(" = ", defaultValue, "") + return visitor.ActionUpdate, str case map[string]interface{}: name := getMapValueString(node, "Name") ttype := getMapValueString(node, "Type") @@ -361,6 +469,11 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "InterfaceDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.InterfaceDefinition: + name := fmt.Sprintf("%v", node.Name) + fields := node.Fields + str := "interface " + name + " " + block(fields) + return visitor.ActionUpdate, str case map[string]interface{}: name := getMapValueString(node, "Name") fields := getMapValue(node, "Fields") @@ -371,6 +484,11 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "UnionDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.UnionDefinition: + name := fmt.Sprintf("%v", node.Name) + types := toSliceString(node.Types) + str := "union " + name + " = " + join(types, " | ") + return visitor.ActionUpdate, str case map[string]interface{}: name := getMapValueString(node, "Name") types := toSliceString(getMapValue(node, "Types")) @@ -381,6 +499,10 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "ScalarDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.ScalarDefinition: + name := fmt.Sprintf("%v", node.Name) + str := "scalar " + name + return visitor.ActionUpdate, str case map[string]interface{}: name := getMapValueString(node, "Name") str := "scalar " + name @@ -390,6 +512,11 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "EnumDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.EnumDefinition: + name := fmt.Sprintf("%v", node.Name) + values := node.Values + str := "enum " + name + " " + block(values) + return visitor.ActionUpdate, str case map[string]interface{}: name := getMapValueString(node, "Name") values := getMapValue(node, "Values") @@ -400,6 +527,9 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "EnumValueDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.EnumValueDefinition: + name := fmt.Sprintf("%v", node.Name) + return visitor.ActionUpdate, name case map[string]interface{}: name := getMapValueString(node, "Name") return visitor.ActionUpdate, name @@ -408,6 +538,10 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "InputObjectDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.InputObjectDefinition: + name := fmt.Sprintf("%v", node.Name) + fields := node.Fields + return visitor.ActionUpdate, "input " + name + " " + block(fields) case map[string]interface{}: name := getMapValueString(node, "Name") fields := getMapValue(node, "Fields") @@ -417,6 +551,10 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ }, "TypeExtensionDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { + case *ast.TypeExtensionDefinition: + definition := fmt.Sprintf("%v", node.Definition) + str := "extend " + definition + return visitor.ActionUpdate, str case map[string]interface{}: definition := getMapValueString(node, "Definition") str := "extend " + definition diff --git a/language/printer/printer_old.go b/language/printer/printer_old.go new file mode 100644 index 00000000..71d9157e --- /dev/null +++ b/language/printer/printer_old.go @@ -0,0 +1,359 @@ +package printer + +import ( + "fmt" + + "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/visitor" + // "log" +) + +var printDocASTReducer11 = map[string]visitor.VisitFunc{ + "Name": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Name: + return visitor.ActionUpdate, node.Value + } + return visitor.ActionNoChange, nil + + }, + "Variable": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Variable: + return visitor.ActionUpdate, fmt.Sprintf("$%v", node.Name) + } + return visitor.ActionNoChange, nil + }, + "Document": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Document: + definitions := toSliceString(node.Definitions) + return visitor.ActionUpdate, join(definitions, "\n\n") + "\n" + } + return visitor.ActionNoChange, nil + }, + "OperationDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.OperationDefinition: + op := node.Operation + name := fmt.Sprintf("%v", node.Name) + + defs := wrap("(", join(toSliceString(node.VariableDefinitions), ", "), ")") + directives := join(toSliceString(node.Directives), " ") + selectionSet := fmt.Sprintf("%v", node.SelectionSet) + str := "" + if name == "" { + str = selectionSet + } else { + str = join([]string{ + op, + join([]string{name, defs}, ""), + directives, + selectionSet, + }, " ") + } + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "VariableDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.VariableDefinition: + variable := fmt.Sprintf("%v", node.Variable) + ttype := fmt.Sprintf("%v", node.Type) + defaultValue := fmt.Sprintf("%v", node.DefaultValue) + + return visitor.ActionUpdate, variable + ": " + ttype + wrap(" = ", defaultValue, "") + + } + return visitor.ActionNoChange, nil + }, + "SelectionSet": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.SelectionSet: + str := block(node.Selections) + return visitor.ActionUpdate, str + + } + return visitor.ActionNoChange, nil + }, + "Field": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Field: + + alias := fmt.Sprintf("%v", node.Alias) + name := fmt.Sprintf("%v", node.Name) + args := toSliceString(node.Arguments) + directives := toSliceString(node.Directives) + selectionSet := fmt.Sprintf("%v", node.SelectionSet) + + str := join( + []string{ + wrap("", alias, ": ") + name + wrap("(", join(args, ", "), ")"), + join(directives, " "), + selectionSet, + }, + " ", + ) + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "Argument": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Argument: + name := fmt.Sprintf("%v", node.Name) + value := fmt.Sprintf("%v", node.Value) + return visitor.ActionUpdate, name + ": " + value + } + return visitor.ActionNoChange, nil + }, + "FragmentSpread": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.FragmentSpread: + name := fmt.Sprintf("%v", node.Name) + directives := toSliceString(node.Directives) + return visitor.ActionUpdate, "..." + name + wrap(" ", join(directives, " "), "") + } + return visitor.ActionNoChange, nil + }, + "InlineFragment": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.InlineFragment: + typeCondition := fmt.Sprintf("%v", node.TypeCondition) + directives := toSliceString(node.Directives) + selectionSet := fmt.Sprintf("%v", node.SelectionSet) + return visitor.ActionUpdate, "... on " + typeCondition + " " + wrap("", join(directives, " "), " ") + selectionSet + } + return visitor.ActionNoChange, nil + }, + "FragmentDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.FragmentDefinition: + name := fmt.Sprintf("%v", node.Name) + typeCondition := fmt.Sprintf("%v", node.TypeCondition) + directives := toSliceString(node.Directives) + selectionSet := fmt.Sprintf("%v", node.SelectionSet) + return visitor.ActionUpdate, "fragment " + name + " on " + typeCondition + " " + wrap("", join(directives, " "), " ") + selectionSet + } + return visitor.ActionNoChange, nil + }, + + "IntValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.IntValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) + } + return visitor.ActionNoChange, nil + }, + "FloatValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.FloatValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) + } + return visitor.ActionNoChange, nil + }, + "StringValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.StringValue: + return visitor.ActionUpdate, `"` + fmt.Sprintf("%v", node.Value) + `"` + } + return visitor.ActionNoChange, nil + }, + "BooleanValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.BooleanValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) + } + return visitor.ActionNoChange, nil + }, + "EnumValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.EnumValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) + } + return visitor.ActionNoChange, nil + }, + "ListValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.ListValue: + return visitor.ActionUpdate, "[" + join(toSliceString(node.Values), ", ") + "]" + } + return visitor.ActionNoChange, nil + }, + "ObjectValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.ObjectValue: + return visitor.ActionUpdate, "{" + join(toSliceString(node.Fields), ", ") + "}" + } + return visitor.ActionNoChange, nil + }, + "ObjectField": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.ObjectField: + name := fmt.Sprintf("%v", node.Name) + value := fmt.Sprintf("%v", node.Value) + return visitor.ActionUpdate, name + ": " + value + } + return visitor.ActionNoChange, nil + }, + + "Directive": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Directive: + name := fmt.Sprintf("%v", node.Name) + args := toSliceString(node.Arguments) + return visitor.ActionUpdate, "@" + name + wrap("(", join(args, ", "), ")") + } + return visitor.ActionNoChange, nil + }, + + "Named": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.Named: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Name) + } + return visitor.ActionNoChange, nil + }, + "List": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.List: + return visitor.ActionUpdate, "[" + fmt.Sprintf("%v", node.Type) + "]" + } + return visitor.ActionNoChange, nil + }, + "NonNull": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.NonNull: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Type) + "!" + } + return visitor.ActionNoChange, nil + }, + + "ObjectDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.ObjectDefinition: + name := fmt.Sprintf("%v", node.Name) + interfaces := toSliceString(node.Interfaces) + fields := node.Fields + str := "type " + name + " " + wrap("implements ", join(interfaces, ", "), " ") + block(fields) + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "FieldDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.FieldDefinition: + name := fmt.Sprintf("%v", node.Name) + ttype := fmt.Sprintf("%v", node.Type) + args := toSliceString(node.Arguments) + str := name + wrap("(", join(args, ", "), ")") + ": " + ttype + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "InputValueDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.InputValueDefinition: + name := fmt.Sprintf("%v", node.Name) + ttype := fmt.Sprintf("%v", node.Type) + defaultValue := fmt.Sprintf("%v", node.DefaultValue) + str := name + ": " + ttype + wrap(" = ", defaultValue, "") + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "InterfaceDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.InterfaceDefinition: + name := fmt.Sprintf("%v", node.Name) + fields := node.Fields + str := "interface " + name + " " + block(fields) + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "UnionDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.UnionDefinition: + name := fmt.Sprintf("%v", node.Name) + types := toSliceString(node.Types) + str := "union " + name + " = " + join(types, " | ") + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "ScalarDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.ScalarDefinition: + name := fmt.Sprintf("%v", node.Name) + str := "scalar " + name + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "EnumDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.EnumDefinition: + name := fmt.Sprintf("%v", node.Name) + values := node.Values + str := "enum " + name + " " + block(values) + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, + "EnumValueDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.EnumValueDefinition: + name := fmt.Sprintf("%v", node.Name) + return visitor.ActionUpdate, name + } + return visitor.ActionNoChange, nil + }, + "InputObjectDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.InputObjectDefinition: + name := fmt.Sprintf("%v", node.Name) + fields := node.Fields + return visitor.ActionUpdate, "input " + name + " " + block(fields) + } + return visitor.ActionNoChange, nil + }, + "TypeExtensionDefinition": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.TypeExtensionDefinition: + definition := fmt.Sprintf("%v", node.Definition) + str := "extend " + definition + return visitor.ActionUpdate, str + } + return visitor.ActionNoChange, nil + }, +} + +func Print11(astNode ast.Node) (printed interface{}) { + // defer func() interface{} { + // if r := recover(); r != nil { + // log.Println("Error: %v", r) + // return printed + // } + // return printed + // }() + printed = visitor.Visit(astNode, &visitor.VisitorOptions{ + LeaveKindMap: printDocASTReducer, + }, nil) + return printed +} + +// +//func PrintMap(astNodeMap map[string]interface{}) (printed interface{}) { +// defer func() interface{} { +// if r := recover(); r != nil { +// return fmt.Sprintf("%v", astNodeMap) +// } +// return printed +// }() +// printed = visitor.Visit(astNodeMap, &visitor.VisitorOptions{ +// LeaveKindMap: printDocASTReducer, +// }, nil) +// return printed +//} diff --git a/language/visitor/visitor.go b/language/visitor/visitor.go index 8110c9af..83edbd9b 100644 --- a/language/visitor/visitor.go +++ b/language/visitor/visitor.go @@ -3,6 +3,7 @@ package visitor import ( "encoding/json" "fmt" + "github.com/graphql-go/graphql/language/ast" "reflect" ) @@ -121,7 +122,7 @@ type stack struct { Index int Keys []interface{} Edits []*edit - InArray bool + inSlice bool Prev *stack } type edit struct { @@ -132,10 +133,11 @@ type edit struct { type VisitFuncParams struct { Node interface{} Key interface{} - Parent interface{} + Parent ast.Node Path []interface{} - Ancestors []interface{} + Ancestors []ast.Node } + type VisitFunc func(p VisitFuncParams) (string, interface{}) type NamedVisitFuncs struct { @@ -153,81 +155,126 @@ type VisitorOptions struct { LeaveKindMap map[string]VisitFunc // 4) Parallel visitors for entering and leaving nodes of a specific kind } -func Visit(root interface{}, visitorOpts *VisitorOptions, keyMap KeyMap) interface{} { +func Visit(root ast.Node, visitorOpts *VisitorOptions, keyMap KeyMap) interface{} { visitorKeys := keyMap if visitorKeys == nil { visitorKeys = QueryDocumentKeys } - var newRoot interface{} - // convert any interface{} into map[string]interface{} - b, err := json.Marshal(root) - if err != nil { - panic(fmt.Sprintf("Invalid root AST Node: %v", root)) - } - err = json.Unmarshal(b, &newRoot) - if err != nil || newRoot == nil { - panic(fmt.Sprintf("Invalid root AST Node (2): %v", root)) - } - + var result interface{} + var newRoot = root var sstack *stack var parent interface{} - inArray := isSlice(newRoot) + var parentSlice []interface{} + inSlice := false + prevInSlice := false keys := []interface{}{newRoot} index := -1 edits := []*edit{} path := []interface{}{} ancestors := []interface{}{} + ancestorsSlice := [][]interface{}{} Loop: for { index = index + 1 isLeaving := (len(keys) == index) - var key interface{} - var node interface{} + var key interface{} // string for structs or int for slices + var node interface{} // ast.Node or can be anything + var nodeSlice []interface{} isEdited := (isLeaving && len(edits) != 0) if isLeaving { - if len(ancestors) == 0 { - key = nil + if !inSlice { + if len(ancestors) == 0 { + key = nil + } else { + key, path = pop(path) + } } else { - key, path = pop(path) + if len(ancestorsSlice) == 0 { + key = nil + } else { + key, path = pop(path) + } } node = parent parent, ancestors = pop(ancestors) + nodeSlice = parentSlice + parentSlice, ancestorsSlice = popNodeSlice(ancestorsSlice) + if isEdited { + prevInSlice = inSlice editOffset := 0 for _, edit := range edits { arrayEditKey := 0 - if inArray { + if inSlice { keyInt := edit.Key.(int) edit.Key = keyInt - editOffset arrayEditKey = edit.Key.(int) } - if inArray && isNilNode(edit.Value) { - if n, ok := node.([]interface{}); ok { - node = splice(n, arrayEditKey) - } else { - panic(fmt.Sprintf("Invalid AST Node (1): %v", node)) - } + if inSlice && isNilNode(edit.Value) { + nodeSlice = spliceNode(nodeSlice, arrayEditKey) editOffset = editOffset + 1 } else { - if inArray { - if n, ok := node.([]interface{}); ok { - n[arrayEditKey] = edit.Value - node = n - } else { - panic(fmt.Sprintf("Invalid AST Node (2): %v", node)) - } + if inSlice { + nodeSlice[arrayEditKey] = edit.Value } else { - if n, ok := node.(map[string]interface{}); ok { - key := edit.Key.(string) - n[key] = edit.Value - node = n + key, _ := edit.Key.(string) + + var updatedNode interface{} + if !isSlice(edit.Value) { + if isStructNode(edit.Value) { + updatedNode = updateNodeField(node, key, edit.Value) + } else { + var todoNode map[string]interface{} + b, err := json.Marshal(node) + if err != nil { + panic(fmt.Sprintf("Invalid root AST Node: %v", root)) + } + err = json.Unmarshal(b, &todoNode) + if err != nil { + panic(fmt.Sprintf("Invalid root AST Node (2): %v", root)) + } + todoNode[key] = edit.Value + updatedNode = todoNode + } } else { - panic(fmt.Sprintf("Invalid AST Node (3): %v", node)) + isSliceOfNodes := true + + // check if edit.value slice is ast.nodes + switch reflect.TypeOf(edit.Value).Kind() { + case reflect.Slice: + s := reflect.ValueOf(edit.Value) + for i := 0; i < s.Len(); i++ { + elem := s.Index(i) + if !isStructNode(elem.Interface()) { + isSliceOfNodes = false + } + } + } + + // is a slice of real nodes + if isSliceOfNodes { + // the node we are writing to is an ast.Node + updatedNode = updateNodeField(node, key, edit.Value) + } else { + var todoNode map[string]interface{} + b, err := json.Marshal(node) + if err != nil { + panic(fmt.Sprintf("Invalid root AST Node: %v", root)) + } + err = json.Unmarshal(b, &todoNode) + if err != nil { + panic(fmt.Sprintf("Invalid root AST Node (2): %v", root)) + } + todoNode[key] = edit.Value + updatedNode = todoNode + } + } + node = updatedNode } } } @@ -235,59 +282,112 @@ Loop: index = sstack.Index keys = sstack.Keys edits = sstack.Edits - inArray = sstack.InArray + inSlice = sstack.inSlice sstack = sstack.Prev } else { // get key - if !isNilNode(parent) { - if inArray { - key = index + if !inSlice { + if !isNilNode(parent) { + key = getFieldValue(keys, index) } else { - key = getField(keys, index) + // initial conditions + key = nil } } else { - // initial conditions - key = nil + key = index } // get node - if !isNilNode(parent) { - node = getField(parent, key) + if !inSlice { + if !isNilNode(parent) { + fieldValue := getFieldValue(parent, key) + if isNode(fieldValue) { + node = fieldValue.(ast.Node) + } + if isSlice(fieldValue) { + nodeSlice = toSliceInterfaces(fieldValue) + } + } else { + // initial conditions + node = newRoot + } } else { - // initial conditions - node = newRoot + if len(parentSlice) != 0 { + fieldValue := getFieldValue(parentSlice, key) + if isNode(fieldValue) { + node = fieldValue.(ast.Node) + } + if isSlice(fieldValue) { + nodeSlice = toSliceInterfaces(fieldValue) + } + } else { + // initial conditions + nodeSlice = []interface{}{} + } } - if isNilNode(node) { + if isNilNode(node) && len(nodeSlice) == 0 { continue } - if !isNilNode(parent) { - path = append(path, key) + + if !inSlice { + if !isNilNode(parent) { + path = append(path, key) + } + } else { + if len(parentSlice) != 0 { + path = append(path, key) + } } } // get result from visitFn for a node if set var result interface{} resultIsUndefined := true - if !isSlice(node) && !isNilNode(node) { - if !isNode(node) { + if !isNilNode(node) { + if !isNode(node) { // is node-ish. panic(fmt.Sprintf("Invalid AST Node (4): %v", node)) } - n, ok := node.(map[string]interface{}) - if !ok { - panic(fmt.Sprintf("Invalid AST Node (5): %v", node)) + + // Try to pass in current node as ast.Node + // Note that since user can potentially return a non-ast.Node from visit functions. + // In that case, we try to unmarshal map[string]interface{} into ast.Node + var nodeIn interface{} + if _, ok := node.(map[string]interface{}); ok { + b, err := json.Marshal(node) + if err != nil { + panic(fmt.Sprintf("Invalid root AST Node: %v", root)) + } + err = json.Unmarshal(b, &nodeIn) + if err != nil { + panic(fmt.Sprintf("Invalid root AST Node (2a): %v", root)) + } + } else { + nodeIn = node } - kind, ok := n["Kind"].(string) - if !ok { - kind = "" + parentConcrete, _ := parent.(ast.Node) + ancestorsConcrete := []ast.Node{} + for _, ancestor := range ancestors { + if ancestorConcrete, ok := ancestor.(ast.Node); ok { + ancestorsConcrete = append(ancestorsConcrete, ancestorConcrete) + } } - visitFn := getVisitFn(visitorOpts, isLeaving, kind) + + kind := "" + if node, ok := node.(map[string]interface{}); ok { + kind, _ = node["Kind"].(string) + } + if node, ok := node.(ast.Node); ok { + kind = node.GetKind() + } + + visitFn := GetVisitFn(visitorOpts, isLeaving, kind) if visitFn != nil { p := VisitFuncParams{ - Node: node, + Node: nodeIn, Key: key, - Parent: parent, + Parent: parentConcrete, Path: path, - Ancestors: ancestors, + Ancestors: ancestorsConcrete, } action := ActionUpdate action, result = visitFn(p) @@ -295,7 +395,7 @@ Loop: break Loop } if action == ActionSkip { - if isLeaving { + if !isLeaving { _, path = pop(path) continue } @@ -320,19 +420,27 @@ Loop: } } + + // collect back edits on the way out if resultIsUndefined && isEdited { - edits = append(edits, &edit{ - Key: key, - Value: node, - }) + if !prevInSlice { + edits = append(edits, &edit{ + Key: key, + Value: node, + }) + } else { + edits = append(edits, &edit{ + Key: key, + Value: nodeSlice, + }) + } } - if !isLeaving { // add to stack prevStack := sstack sstack = &stack{ - InArray: inArray, + inSlice: inSlice, Index: index, Keys: keys, Edits: edits, @@ -340,42 +448,39 @@ Loop: } // replace keys - inArray = isSlice(node) + inSlice = false + if len(nodeSlice) > 0 { + inSlice = true + } keys = []interface{}{} - if !isNilNode(node) { - if inArray { - // get keys - if n, ok := node.([]interface{}); ok { - for _, m := range n { - keys = append(keys, m) - } - } else { - panic(fmt.Sprintf("Invalid AST Node (6): %v", node)) - } - } else { - if n, ok := node.(map[string]interface{}); ok { - kind, ok := n["Kind"].(string) - if !ok { - kind = "" - } + if inSlice { + // get keys + for _, m := range nodeSlice { + keys = append(keys, m) + } + } else { + if !isNilNode(node) { + if node, ok := node.(ast.Node); ok { + kind := node.GetKind() if n, ok := visitorKeys[kind]; ok { for _, m := range n { keys = append(keys, m) } } - } else { - panic(fmt.Sprintf("Invalid AST Node (7): %v", node)) } + } - } + } index = -1 edits = []*edit{} - if !isNilNode(parent) { - ancestors = append(ancestors, parent) - } + + ancestors = append(ancestors, parent) parent = node + ancestorsSlice = append(ancestorsSlice, parentSlice) + parentSlice = nodeSlice + } // loop guard @@ -384,9 +489,9 @@ Loop: } } if len(edits) != 0 { - newRoot = edits[0].Value + result = edits[0].Value } - return newRoot + return result } func pop(a []interface{}) (x interface{}, aa []interface{}) { @@ -396,17 +501,39 @@ func pop(a []interface{}) (x interface{}, aa []interface{}) { x, aa = a[len(a)-1], a[:len(a)-1] return x, aa } -func splice(a []interface{}, i int) []interface{} { - if i >= len(a) { - return a +func popNodeSlice(a [][]interface{}) (x []interface{}, aa [][]interface{}) { + if len(a) == 0 { + return x, aa } + x, aa = a[len(a)-1], a[:len(a)-1] + return x, aa +} +func spliceNode(a interface{}, i int) (result []interface{}) { if i < 0 { - return []interface{}{} + return result + } + typeOf := reflect.TypeOf(a) + if typeOf == nil { + return result + } + switch typeOf.Kind() { + case reflect.Slice: + s := reflect.ValueOf(a) + for i := 0; i < s.Len(); i++ { + elem := s.Index(i) + elemInterface := elem.Interface() + result = append(result, elemInterface) + } + if i >= s.Len() { + return result + } + return append(result[:i], result[i+1:]...) + default: + return result } - return append(a[:i], a[i+1:]...) } -func getField(obj interface{}, key interface{}) interface{} { +func getFieldValue(obj interface{}, key interface{}) interface{} { val := reflect.ValueOf(obj) if val.Type().Kind() == reflect.Ptr { val = val.Elem() @@ -447,8 +574,82 @@ func getField(obj interface{}, key interface{}) interface{} { return nil } -func isSlice(Value interface{}) bool { - val := reflect.ValueOf(Value) +func updateNodeField(value interface{}, fieldName string, fieldValue interface{}) (retVal interface{}) { + retVal = value + val := reflect.ValueOf(value) + + isPtr := false + if val.IsValid() && val.Type().Kind() == reflect.Ptr { + val = val.Elem() + isPtr = true + } + if !val.IsValid() { + return retVal + } + if val.Type().Kind() == reflect.Struct { + for i := 0; i < val.NumField(); i++ { + valueField := val.Field(i) + typeField := val.Type().Field(i) + + // try matching the field name + if typeField.Name == fieldName { + fieldValueVal := reflect.ValueOf(fieldValue) + if valueField.CanSet() { + + if fieldValueVal.IsValid() { + if valueField.Type().Kind() == fieldValueVal.Type().Kind() { + if fieldValueVal.Type().Kind() == reflect.Slice { + newSliceValue := reflect.MakeSlice(reflect.TypeOf(valueField.Interface()), fieldValueVal.Len(), fieldValueVal.Len()) + for i := 0; i < newSliceValue.Len(); i++ { + dst := newSliceValue.Index(i) + src := fieldValueVal.Index(i) + srcValue := reflect.ValueOf(src.Interface()) + if dst.CanSet() { + dst.Set(srcValue) + } + } + valueField.Set(newSliceValue) + + } else { + valueField.Set(fieldValueVal) + } + } + } else { + valueField.Set(reflect.New(valueField.Type()).Elem()) + } + if isPtr == true { + retVal = val.Addr().Interface() + return retVal + } else { + retVal = val.Interface() + return retVal + } + + } + } + } + } + return retVal +} +func toSliceInterfaces(slice interface{}) (result []interface{}) { + switch reflect.TypeOf(slice).Kind() { + case reflect.Slice: + s := reflect.ValueOf(slice) + for i := 0; i < s.Len(); i++ { + elem := s.Index(i) + elemInterface := elem.Interface() + if elem, ok := elemInterface.(ast.Node); ok { + result = append(result, elem) + } + } + return result + default: + return result + } +} + +func isSlice(value interface{}) bool { + val := reflect.ValueOf(value) if val.IsValid() && val.Type().Kind() == reflect.Slice { return true } @@ -456,19 +657,38 @@ func isSlice(Value interface{}) bool { } func isNode(node interface{}) bool { val := reflect.ValueOf(node) + if val.IsValid() && val.Type().Kind() == reflect.Ptr { + val = val.Elem() + } if !val.IsValid() { return false } - if val.Type().Kind() == reflect.Ptr { - val = val.Elem() - } if val.Type().Kind() == reflect.Map { keyVal := reflect.ValueOf("Kind") valField := val.MapIndex(keyVal) return valField.IsValid() } + if val.Type().Kind() == reflect.Struct { + valField := val.FieldByName("Kind") + return valField.IsValid() + } return false } +func isStructNode(node interface{}) bool { + val := reflect.ValueOf(node) + if val.IsValid() && val.Type().Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return false + } + if val.Type().Kind() == reflect.Struct { + valField := val.FieldByName("Kind") + return valField.IsValid() + } + return false +} + func isNilNode(node interface{}) bool { val := reflect.ValueOf(node) if !val.IsValid() { @@ -489,7 +709,7 @@ func isNilNode(node interface{}) bool { return val.Interface() == nil } -func getVisitFn(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitFunc { +func GetVisitFn(visitorOpts *VisitorOptions, isLeaving bool, kind string) VisitFunc { if visitorOpts == nil { return nil } diff --git a/language/visitor/visitor_test.go b/language/visitor/visitor_test.go index d2323c0f..412f96c0 100644 --- a/language/visitor/visitor_test.go +++ b/language/visitor/visitor_test.go @@ -1,10 +1,8 @@ package visitor_test import ( - "encoding/json" "io/ioutil" "reflect" - "strings" "testing" "github.com/graphql-go/graphql/language/ast" @@ -26,47 +24,6 @@ func parse(t *testing.T, query string) *ast.Document { return astDoc } -// Helper functions to get map value by key -// Allow keys to specify dot paths (e.g `Name.Value`) -func getMapValue(m map[string]interface{}, key string) interface{} { - tokens := strings.Split(key, ".") - valMap := m - for _, token := range tokens { - v, ok := valMap[token] - if !ok { - return nil - } - switch v := v.(type) { - case []interface{}: - return v - case map[string]interface{}: - valMap = v - continue - case string: - return v - } - } - return valMap -} -func getMapValueString(m map[string]interface{}, key string) string { - tokens := strings.Split(key, ".") - valMap := m - for _, token := range tokens { - v, ok := valMap[token] - if !ok { - return "" - } - switch v := v.(type) { - case map[string]interface{}: - valMap = v - continue - case string: - return v - } - } - return "" -} - func TestVisitor_AllowsForEditingOnEnter(t *testing.T) { query := `{ a, b, c { a, b, c } }` @@ -77,8 +34,8 @@ func TestVisitor_AllowsForEditingOnEnter(t *testing.T) { v := &visitor.VisitorOptions{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - if getMapValueString(node, "Kind") == "Field" && getMapValueString(node, "Name.Value") == "b" { + case *ast.Field: + if node.Name != nil && node.Name.Value == "b" { return visitor.ActionUpdate, nil } } @@ -87,7 +44,7 @@ func TestVisitor_AllowsForEditingOnEnter(t *testing.T) { } editedAst := visitor.Visit(astDoc, v, nil) - if !reflect.DeepEqual(ASTToJSON(t, expectedAST), editedAst) { + if !reflect.DeepEqual(expectedAST, editedAst) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedAST, editedAst)) } @@ -102,8 +59,8 @@ func TestVisitor_AllowsForEditingOnLeave(t *testing.T) { v := &visitor.VisitorOptions{ Leave: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - if getMapValueString(node, "Kind") == "Field" && getMapValueString(node, "Name.Value") == "b" { + case *ast.Field: + if node.Name != nil && node.Name.Value == "b" { return visitor.ActionUpdate, nil } } @@ -112,7 +69,7 @@ func TestVisitor_AllowsForEditingOnLeave(t *testing.T) { } editedAst := visitor.Visit(astDoc, v, nil) - if !reflect.DeepEqual(ASTToJSON(t, expectedAST), editedAst) { + if !reflect.DeepEqual(expectedAST, editedAst) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedAST, editedAst)) } } @@ -122,11 +79,11 @@ func TestVisitor_VisitsEditedNode(t *testing.T) { query := `{ a { x } }` astDoc := parse(t, query) - addedField := map[string]interface{}{ - "Kind": "Field", - "Name": map[string]interface{}{ - "Kind": "Name", - "Value": "__typename", + addedField := &ast.Field{ + Kind: "Field", + Name: &ast.Name{ + Kind: "Name", + Value: "__typename", }, } @@ -134,13 +91,15 @@ func TestVisitor_VisitsEditedNode(t *testing.T) { v := &visitor.VisitorOptions{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - if getMapValueString(node, "Kind") == "Field" && getMapValueString(node, "Name.Value") == "a" { - s := getMapValue(node, "SelectionSet.Selections").([]interface{}) + case *ast.Field: + if node.Name != nil && node.Name.Value == "a" { + s := node.SelectionSet.Selections s = append(s, addedField) - return visitor.ActionUpdate, map[string]interface{}{ - "Kind": "Field", - "SelectionSet": s, + ss := node.SelectionSet + ss.Selections = s + return visitor.ActionUpdate, &ast.Field{ + Kind: "Field", + SelectionSet: ss, } } if reflect.DeepEqual(node, addedField) { @@ -183,18 +142,28 @@ func TestVisitor_AllowsSkippingASubTree(t *testing.T) { v := &visitor.VisitorOptions{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - visited = append(visited, []interface{}{"enter", getMapValue(node, "Kind"), getMapValue(node, "Value")}) - if getMapValueString(node, "Kind") == "Field" && getMapValueString(node, "Name.Value") == "b" { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + case *ast.Field: + visited = append(visited, []interface{}{"enter", node.Kind, nil}) + if node.Name != nil && node.Name.Value == "b" { return visitor.ActionSkip, nil } + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) } return visitor.ActionNoChange, nil }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - visited = append(visited, []interface{}{"leave", getMapValue(node, "Kind"), getMapValue(node, "Value")}) + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) } return visitor.ActionNoChange, nil }, @@ -233,18 +202,26 @@ func TestVisitor_AllowsEarlyExitWhileVisiting(t *testing.T) { v := &visitor.VisitorOptions{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - visited = append(visited, []interface{}{"enter", getMapValue(node, "Kind"), getMapValue(node, "Value")}) - if getMapValue(node, "Kind") == "Name" && getMapValue(node, "Value") == "x" { + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) + if node.Value == "x" { return visitor.ActionBreak, nil } + case ast.Node: + visited = append(visited, []interface{}{"enter", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"enter", nil, nil}) } return visitor.ActionNoChange, nil }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - visited = append(visited, []interface{}{"leave", getMapValue(node, "Kind"), getMapValue(node, "Value")}) + case *ast.Name: + visited = append(visited, []interface{}{"leave", node.Kind, node.Value}) + case ast.Node: + visited = append(visited, []interface{}{"leave", node.GetKind(), nil}) + default: + visited = append(visited, []interface{}{"leave", nil, nil}) } return visitor.ActionNoChange, nil }, @@ -279,8 +256,8 @@ func TestVisitor_AllowsANamedFunctionsVisitorAPI(t *testing.T) { "Name": visitor.NamedVisitFuncs{ Kind: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - visited = append(visited, []interface{}{"enter", getMapValue(node, "Kind"), getMapValue(node, "Value")}) + case *ast.Name: + visited = append(visited, []interface{}{"enter", node.Kind, node.Value}) } return visitor.ActionNoChange, nil }, @@ -288,15 +265,15 @@ func TestVisitor_AllowsANamedFunctionsVisitorAPI(t *testing.T) { "SelectionSet": visitor.NamedVisitFuncs{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - visited = append(visited, []interface{}{"enter", getMapValue(node, "Kind"), getMapValue(node, "Value")}) + case *ast.SelectionSet: + visited = append(visited, []interface{}{"enter", node.Kind, nil}) } return visitor.ActionNoChange, nil }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - visited = append(visited, []interface{}{"leave", getMapValue(node, "Kind"), getMapValue(node, "Value")}) + case *ast.SelectionSet: + visited = append(visited, []interface{}{"leave", node.Kind, nil}) } return visitor.ActionNoChange, nil }, @@ -540,27 +517,23 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { v := &visitor.VisitorOptions{ Enter: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - - parent, ok := p.Parent.(map[string]interface{}) - if !ok { - parent = map[string]interface{}{} + case ast.Node: + if p.Parent != nil { + visited = append(visited, []interface{}{"enter", node.GetKind(), p.Key, p.Parent.GetKind()}) + } else { + visited = append(visited, []interface{}{"enter", node.GetKind(), p.Key, nil}) } - - visited = append(visited, []interface{}{"enter", getMapValue(node, "Kind"), p.Key, getMapValue(parent, "Kind")}) } return visitor.ActionNoChange, nil }, Leave: func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { - case map[string]interface{}: - - parent, ok := p.Parent.(map[string]interface{}) - if !ok { - parent = map[string]interface{}{} + case ast.Node: + if p.Parent != nil { + visited = append(visited, []interface{}{"leave", node.GetKind(), p.Key, p.Parent.GetKind()}) + } else { + visited = append(visited, []interface{}{"leave", node.GetKind(), p.Key, nil}) } - - visited = append(visited, []interface{}{"leave", getMapValue(node, "Kind"), p.Key, getMapValue(parent, "Kind")}) } return visitor.ActionNoChange, nil }, @@ -572,34 +545,3 @@ func TestVisitor_VisitsKitchenSink(t *testing.T) { t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expectedVisited, visited)) } } - -func TestVisitor_ProducesHelpfulErrorMessages(t *testing.T) { - defer func() { - if r := recover(); r != nil { - err := r.(string) - expectedErr := `Invalid AST Node (4): map[random:Data]` - if err != expectedErr { - t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(err, expectedErr)) - } - return - } - t.Fatalf("expected to panic") - }() - astDoc := map[string]interface{}{ - "random": "Data", - } - _ = visitor.Visit(astDoc, nil, nil) -} - -func ASTToJSON(t *testing.T, a ast.Node) interface{} { - b, err := json.Marshal(a) - if err != nil { - t.Fatalf("Failed to marshal Node %v", err) - } - var f interface{} - err = json.Unmarshal(b, &f) - if err != nil { - t.Fatalf("Failed to unmarshal Node %v", err) - } - return f -} diff --git a/rules.go b/rules.go new file mode 100644 index 00000000..80f10754 --- /dev/null +++ b/rules.go @@ -0,0 +1,1961 @@ +package graphql + +import ( + "fmt" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/kinds" + "github.com/graphql-go/graphql/language/printer" + "github.com/graphql-go/graphql/language/visitor" + "sort" + "strings" +) + +/** + * SpecifiedRules set includes all validation rules defined by the GraphQL spec. + */ +var SpecifiedRules = []ValidationRuleFn{ + ArgumentsOfCorrectTypeRule, + DefaultValuesOfCorrectTypeRule, + FieldsOnCorrectTypeRule, + FragmentsOnCompositeTypesRule, + KnownArgumentNamesRule, + KnownDirectivesRule, + KnownFragmentNamesRule, + KnownTypeNamesRule, + LoneAnonymousOperationRule, + NoFragmentCyclesRule, + NoUndefinedVariablesRule, + NoUnusedFragmentsRule, + NoUnusedVariablesRule, + OverlappingFieldsCanBeMergedRule, + PossibleFragmentSpreadsRule, + ProvidedNonNullArgumentsRule, + ScalarLeafsRule, + UniqueArgumentNamesRule, + UniqueFragmentNamesRule, + UniqueOperationNamesRule, + VariablesAreInputTypesRule, + VariablesInAllowedPositionRule, +} + +type ValidationRuleInstance struct { + VisitorOpts *visitor.VisitorOptions + VisitSpreadFragments bool +} +type ValidationRuleFn func(context *ValidationContext) *ValidationRuleInstance + +func newValidationRuleError(message string, nodes []ast.Node) (string, error) { + return visitor.ActionNoChange, gqlerrors.NewError( + message, + nodes, + "", + nil, + []int{}, + ) +} + +/** + * ArgumentsOfCorrectTypeRule + * Argument values of correct type + * + * A GraphQL document is only valid if all field argument literal values are + * of the type expected by their position. + */ +func ArgumentsOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Argument: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + if argAST, ok := p.Node.(*ast.Argument); ok { + value := argAST.Value + argDef := context.Argument() + if argDef != nil && !isValidLiteralValue(argDef.Type, value) { + argNameValue := "" + if argAST.Name != nil { + argNameValue = argAST.Name.Value + } + return newValidationRuleError( + fmt.Sprintf(`Argument "%v" expected type "%v" but got: %v.`, + argNameValue, argDef.Type, printer.Print(value)), + []ast.Node{value}, + ) + } + } + return action, result + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * DefaultValuesOfCorrectTypeRule + * Variable default values of correct type + * + * A GraphQL document is only valid if all variable default values are of the + * type expected by their definition. + */ +func DefaultValuesOfCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.VariableDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + if varDefAST, ok := p.Node.(*ast.VariableDefinition); ok { + name := "" + if varDefAST.Variable != nil && varDefAST.Variable.Name != nil { + name = varDefAST.Variable.Name.Value + } + defaultValue := varDefAST.DefaultValue + ttype := context.InputType() + + if ttype, ok := ttype.(*NonNull); ok && defaultValue != nil { + return newValidationRuleError( + fmt.Sprintf(`Variable "$%v" of type "%v" is required and will not use the default value. Perhaps you meant to use type "%v".`, + name, ttype, ttype.OfType), + []ast.Node{defaultValue}, + ) + } + if ttype != nil && defaultValue != nil && !isValidLiteralValue(ttype, defaultValue) { + return newValidationRuleError( + fmt.Sprintf(`Variable "$%v" of type "%v" has invalid default value: %v.`, + name, ttype, printer.Print(defaultValue)), + []ast.Node{defaultValue}, + ) + } + } + return action, result + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * FieldsOnCorrectTypeRule + * Fields on correct type + * + * A GraphQL document is only valid if all fields selected are defined by the + * parent type, or are an allowed meta field such as __typenamme + */ +func FieldsOnCorrectTypeRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Field: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + if node, ok := p.Node.(*ast.Field); ok { + ttype := context.ParentType() + + if ttype != nil { + fieldDef := context.FieldDef() + if fieldDef == nil { + nodeName := "" + if node.Name != nil { + nodeName = node.Name.Value + } + return newValidationRuleError( + fmt.Sprintf(`Cannot query field "%v" on "%v".`, + nodeName, ttype.Name()), + []ast.Node{node}, + ) + } + } + } + return action, result + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * FragmentsOnCompositeTypesRule + * Fragments on composite type + * + * Fragments use a type condition to determine if they apply, since fragments + * can only be spread into a composite type (object, interface, or union), the + * type condition must also be a composite type. + */ +func FragmentsOnCompositeTypesRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.InlineFragment: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.InlineFragment); ok { + ttype := context.Type() + if ttype != nil && !IsCompositeType(ttype) { + return newValidationRuleError( + fmt.Sprintf(`Fragment cannot condition on non composite type "%v".`, ttype), + []ast.Node{node.TypeCondition}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + kinds.FragmentDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.FragmentDefinition); ok { + ttype := context.Type() + if ttype != nil && !IsCompositeType(ttype) { + nodeName := "" + if node.Name != nil { + nodeName = node.Name.Value + } + return newValidationRuleError( + fmt.Sprintf(`Fragment "%v" cannot condition on non composite type "%v".`, nodeName, printer.Print(node.TypeCondition)), + []ast.Node{node.TypeCondition}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * KnownArgumentNamesRule + * Known argument names + * + * A GraphQL field is only valid if all supplied arguments are defined by + * that field. + */ +func KnownArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Argument: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + if node, ok := p.Node.(*ast.Argument); ok { + var argumentOf ast.Node + if len(p.Ancestors) > 0 { + argumentOf = p.Ancestors[len(p.Ancestors)-1] + } + if argumentOf == nil { + return action, result + } + if argumentOf.GetKind() == "Field" { + fieldDef := context.FieldDef() + if fieldDef == nil { + return action, result + } + nodeName := "" + if node.Name != nil { + nodeName = node.Name.Value + } + var fieldArgDef *Argument + for _, arg := range fieldDef.Args { + if arg.Name() == nodeName { + fieldArgDef = arg + } + } + if fieldArgDef == nil { + parentType := context.ParentType() + parentTypeName := "" + if parentType != nil { + parentTypeName = parentType.Name() + } + return newValidationRuleError( + fmt.Sprintf(`Unknown argument "%v" on field "%v" of type "%v".`, nodeName, fieldDef.Name, parentTypeName), + []ast.Node{node}, + ) + } + } else if argumentOf.GetKind() == "Directive" { + directive := context.Directive() + if directive == nil { + return action, result + } + nodeName := "" + if node.Name != nil { + nodeName = node.Name.Value + } + var directiveArgDef *Argument + for _, arg := range directive.Args { + if arg.Name() == nodeName { + directiveArgDef = arg + } + } + if directiveArgDef == nil { + return newValidationRuleError( + fmt.Sprintf(`Unknown argument "%v" on directive "@%v".`, nodeName, directive.Name), + []ast.Node{node}, + ) + } + } + + } + return action, result + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * Known directives + * + * A GraphQL document is only valid if all `@directives` are known by the + * schema and legally positioned. + */ +func KnownDirectivesRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Directive: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + if node, ok := p.Node.(*ast.Directive); ok { + + nodeName := "" + if node.Name != nil { + nodeName = node.Name.Value + } + + var directiveDef *Directive + for _, def := range context.Schema().Directives() { + if def.Name == nodeName { + directiveDef = def + } + } + if directiveDef == nil { + return newValidationRuleError( + fmt.Sprintf(`Unknown directive "%v".`, nodeName), + []ast.Node{node}, + ) + } + + var appliedTo ast.Node + if len(p.Ancestors) > 0 { + appliedTo = p.Ancestors[len(p.Ancestors)-1] + } + if appliedTo == nil { + return action, result + } + + if appliedTo.GetKind() == kinds.OperationDefinition && directiveDef.OnOperation == false { + return newValidationRuleError( + fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "operation"), + []ast.Node{node}, + ) + } + if appliedTo.GetKind() == kinds.Field && directiveDef.OnField == false { + return newValidationRuleError( + fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "field"), + []ast.Node{node}, + ) + } + if (appliedTo.GetKind() == kinds.FragmentSpread || + appliedTo.GetKind() == kinds.InlineFragment || + appliedTo.GetKind() == kinds.FragmentDefinition) && directiveDef.OnFragment == false { + return newValidationRuleError( + fmt.Sprintf(`Directive "%v" may not be used on "%v".`, nodeName, "fragment"), + []ast.Node{node}, + ) + } + + } + return action, result + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * KnownFragmentNamesRule + * Known fragment names + * + * A GraphQL document is only valid if all `...Fragment` fragment spreads refer + * to fragments defined in the same document. + */ +func KnownFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.FragmentSpread: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + if node, ok := p.Node.(*ast.FragmentSpread); ok { + + fragmentName := "" + if node.Name != nil { + fragmentName = node.Name.Value + } + + fragment := context.Fragment(fragmentName) + if fragment == nil { + return newValidationRuleError( + fmt.Sprintf(`Unknown fragment "%v".`, fragmentName), + []ast.Node{node.Name}, + ) + } + } + return action, result + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * KnownTypeNamesRule + * Known type names + * + * A GraphQL document is only valid if referenced types (specifically + * variable definitions and fragment conditions) are defined by the type schema. + */ +func KnownTypeNamesRule(context *ValidationContext) *ValidationRuleInstance { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Named: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.Named); ok { + typeNameValue := "" + typeName := node.Name + if typeName != nil { + typeNameValue = typeName.Value + } + ttype := context.Schema().Type(typeNameValue) + if ttype == nil { + return newValidationRuleError( + fmt.Sprintf(`Unknown type "%v".`, typeNameValue), + []ast.Node{node}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * LoneAnonymousOperationRule + * Lone anonymous operation + * + * A GraphQL document is only valid if when it contains an anonymous operation + * (the query short-hand) that it contains only that one operation definition. + */ +func LoneAnonymousOperationRule(context *ValidationContext) *ValidationRuleInstance { + var operationCount = 0 + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Document: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.Document); ok { + operationCount = 0 + for _, definition := range node.Definitions { + if definition.GetKind() == kinds.OperationDefinition { + operationCount = operationCount + 1 + } + } + } + return visitor.ActionNoChange, nil + }, + }, + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.OperationDefinition); ok { + if node.Name == nil && operationCount > 1 { + return newValidationRuleError( + `This anonymous operation must be the only defined operation.`, + []ast.Node{node}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +type nodeSet struct { + set map[ast.Node]bool +} + +func newNodeSet() *nodeSet { + return &nodeSet{ + set: map[ast.Node]bool{}, + } +} +func (set *nodeSet) Has(node ast.Node) bool { + _, ok := set.set[node] + return ok +} +func (set *nodeSet) Add(node ast.Node) bool { + if set.Has(node) { + return false + } + set.set[node] = true + return true +} + +/** + * NoFragmentCyclesRule + */ +func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance { + // Gather all the fragment spreads ASTs for each fragment definition. + // Importantly this does not include inline fragments. + definitions := context.Document().Definitions + spreadsInFragment := map[string][]*ast.FragmentSpread{} + for _, node := range definitions { + if node.GetKind() == kinds.FragmentDefinition { + if node, ok := node.(*ast.FragmentDefinition); ok && node != nil { + nodeName := "" + if node.Name != nil { + nodeName = node.Name.Value + } + spreadsInFragment[nodeName] = gatherSpreads(node) + } + } + } + // Tracks spreads known to lead to cycles to ensure that cycles are not + // redundantly reported. + knownToLeadToCycle := newNodeSet() + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.FragmentDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.FragmentDefinition); ok && node != nil { + errors := []error{} + spreadPath := []*ast.FragmentSpread{} + initialName := "" + if node.Name != nil { + initialName = node.Name.Value + } + var detectCycleRecursive func(fragmentName string) + detectCycleRecursive = func(fragmentName string) { + spreadNodes, _ := spreadsInFragment[fragmentName] + for _, spreadNode := range spreadNodes { + if knownToLeadToCycle.Has(spreadNode) { + continue + } + spreadNodeName := "" + if spreadNode.Name != nil { + spreadNodeName = spreadNode.Name.Value + } + if spreadNodeName == initialName { + cyclePath := []ast.Node{} + for _, path := range spreadPath { + cyclePath = append(cyclePath, path) + } + cyclePath = append(cyclePath, spreadNode) + for _, spread := range cyclePath { + knownToLeadToCycle.Add(spread) + } + via := "" + spreadNames := []string{} + for _, s := range spreadPath { + if s.Name != nil { + spreadNames = append(spreadNames, s.Name.Value) + } + } + if len(spreadNames) > 0 { + via = " via " + strings.Join(spreadNames, ", ") + } + _, err := newValidationRuleError( + fmt.Sprintf(`Cannot spread fragment "%v" within itself%v.`, initialName, via), + cyclePath, + ) + errors = append(errors, err) + continue + } + spreadPathHasCurrentNode := false + for _, spread := range spreadPath { + if spread == spreadNode { + spreadPathHasCurrentNode = true + } + } + if spreadPathHasCurrentNode { + continue + } + spreadPath = append(spreadPath, spreadNode) + detectCycleRecursive(spreadNodeName) + _, spreadPath = spreadPath[len(spreadPath)-1], spreadPath[:len(spreadPath)-1] + } + } + detectCycleRecursive(initialName) + if len(errors) > 0 { + return visitor.ActionNoChange, errors + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * NoUndefinedVariables + * No undefined variables + * + * A GraphQL operation is only valid if all variables encountered, both directly + * and via fragment spreads, are defined by that operation. + */ +func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstance { + var operation *ast.OperationDefinition + var visitedFragmentNames = map[string]bool{} + var definedVariableNames = map[string]bool{} + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil { + operation = node + visitedFragmentNames = map[string]bool{} + definedVariableNames = map[string]bool{} + } + return visitor.ActionNoChange, nil + }, + }, + kinds.VariableDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.VariableDefinition); ok && node != nil { + variableName := "" + if node.Variable != nil && node.Variable.Name != nil { + variableName = node.Variable.Name.Value + } + definedVariableNames[variableName] = true + } + return visitor.ActionNoChange, nil + }, + }, + kinds.Variable: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if variable, ok := p.Node.(*ast.Variable); ok && variable != nil { + variableName := "" + if variable.Name != nil { + variableName = variable.Name.Value + } + if val, _ := definedVariableNames[variableName]; !val { + withinFragment := false + for _, node := range p.Ancestors { + if node.GetKind() == kinds.FragmentDefinition { + withinFragment = true + break + } + } + if withinFragment == true && operation != nil && operation.Name != nil { + return newValidationRuleError( + fmt.Sprintf(`Variable "$%v" is not defined by operation "%v".`, variableName, operation.Name.Value), + []ast.Node{variable, operation}, + ) + } + return newValidationRuleError( + fmt.Sprintf(`Variable "$%v" is not defined.`, variableName), + []ast.Node{variable}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + kinds.FragmentSpread: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.FragmentSpread); ok && node != nil { + // Only visit fragments of a particular name once per operation + fragmentName := "" + if node.Name != nil { + fragmentName = node.Name.Value + } + if val, ok := visitedFragmentNames[fragmentName]; ok && val == true { + return visitor.ActionSkip, nil + } + visitedFragmentNames[fragmentName] = true + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitSpreadFragments: true, + VisitorOpts: visitorOpts, + } +} + +/** + * NoUnusedFragmentsRule + * No unused fragments + * + * A GraphQL document is only valid if all fragment definitions are spread + * within operations, or spread within other fragments spread within operations. + */ +func NoUnusedFragmentsRule(context *ValidationContext) *ValidationRuleInstance { + + var fragmentDefs = []*ast.FragmentDefinition{} + var spreadsWithinOperation = []map[string]bool{} + var fragAdjacencies = map[string]map[string]bool{} + var spreadNames = map[string]bool{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil { + spreadNames = map[string]bool{} + spreadsWithinOperation = append(spreadsWithinOperation, spreadNames) + } + return visitor.ActionNoChange, nil + }, + }, + kinds.FragmentDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if def, ok := p.Node.(*ast.FragmentDefinition); ok && def != nil { + defName := "" + if def.Name != nil { + defName = def.Name.Value + } + + fragmentDefs = append(fragmentDefs, def) + spreadNames = map[string]bool{} + fragAdjacencies[defName] = spreadNames + } + return visitor.ActionNoChange, nil + }, + }, + kinds.FragmentSpread: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if spread, ok := p.Node.(*ast.FragmentSpread); ok && spread != nil { + spreadName := "" + if spread.Name != nil { + spreadName = spread.Name.Value + } + spreadNames[spreadName] = true + } + return visitor.ActionNoChange, nil + }, + }, + kinds.Document: visitor.NamedVisitFuncs{ + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + + fragmentNameUsed := map[string]interface{}{} + + var reduceSpreadFragments func(spreads map[string]bool) + reduceSpreadFragments = func(spreads map[string]bool) { + for fragName, _ := range spreads { + if isFragNameUsed, _ := fragmentNameUsed[fragName]; isFragNameUsed != true { + fragmentNameUsed[fragName] = true + + if adjacencies, ok := fragAdjacencies[fragName]; ok { + reduceSpreadFragments(adjacencies) + } + } + } + } + for _, spreadWithinOperation := range spreadsWithinOperation { + reduceSpreadFragments(spreadWithinOperation) + } + errors := []error{} + for _, def := range fragmentDefs { + defName := "" + if def.Name != nil { + defName = def.Name.Value + } + + isFragNameUsed, ok := fragmentNameUsed[defName] + if !ok || isFragNameUsed != true { + _, err := newValidationRuleError( + fmt.Sprintf(`Fragment "%v" is never used.`, defName), + []ast.Node{def}, + ) + + errors = append(errors, err) + } + } + if len(errors) > 0 { + return visitor.ActionNoChange, errors + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * NoUnusedVariablesRule + * No unused variables + * + * A GraphQL operation is only valid if all variables defined by an operation + * are used, either directly or within a spread fragment. + */ +func NoUnusedVariablesRule(context *ValidationContext) *ValidationRuleInstance { + + var visitedFragmentNames = map[string]bool{} + var variableDefs = []*ast.VariableDefinition{} + var variableNameUsed = map[string]bool{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + visitedFragmentNames = map[string]bool{} + variableDefs = []*ast.VariableDefinition{} + variableNameUsed = map[string]bool{} + return visitor.ActionNoChange, nil + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + errors := []error{} + for _, def := range variableDefs { + variableName := "" + if def.Variable != nil && def.Variable.Name != nil { + variableName = def.Variable.Name.Value + } + if isVariableNameUsed, _ := variableNameUsed[variableName]; isVariableNameUsed != true { + _, err := newValidationRuleError( + fmt.Sprintf(`Variable "$%v" is never used.`, variableName), + []ast.Node{def}, + ) + errors = append(errors, err) + } + } + if len(errors) > 0 { + return visitor.ActionNoChange, errors + } + return visitor.ActionNoChange, nil + }, + }, + kinds.VariableDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if def, ok := p.Node.(*ast.VariableDefinition); ok && def != nil { + variableDefs = append(variableDefs, def) + } + // Do not visit deeper, or else the defined variable name will be visited. + return visitor.ActionSkip, nil + }, + }, + kinds.Variable: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if variable, ok := p.Node.(*ast.Variable); ok && variable != nil { + if variable.Name != nil { + variableNameUsed[variable.Name.Value] = true + } + } + return visitor.ActionNoChange, nil + }, + }, + kinds.FragmentSpread: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if spreadAST, ok := p.Node.(*ast.FragmentSpread); ok && spreadAST != nil { + // Only visit fragments of a particular name once per operation + spreadName := "" + if spreadAST.Name != nil { + spreadName = spreadAST.Name.Value + } + if hasVisitedFragmentNames, _ := visitedFragmentNames[spreadName]; hasVisitedFragmentNames == true { + return visitor.ActionSkip, nil + } + visitedFragmentNames[spreadName] = true + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + // Visit FragmentDefinition after visiting FragmentSpread + VisitSpreadFragments: true, + VisitorOpts: visitorOpts, + } +} + +type fieldDefPair struct { + Field *ast.Field + FieldDef *FieldDefinition +} + +func collectFieldASTsAndDefs(context *ValidationContext, parentType Named, selectionSet *ast.SelectionSet, visitedFragmentNames map[string]bool, astAndDefs map[string][]*fieldDefPair) map[string][]*fieldDefPair { + + if astAndDefs == nil { + astAndDefs = map[string][]*fieldDefPair{} + } + if visitedFragmentNames == nil { + visitedFragmentNames = map[string]bool{} + } + if selectionSet == nil { + return astAndDefs + } + for _, selection := range selectionSet.Selections { + switch selection := selection.(type) { + case *ast.Field: + fieldName := "" + if selection.Name != nil { + fieldName = selection.Name.Value + } + var fieldDef *FieldDefinition + if parentType, ok := parentType.(*Object); ok { + fieldDef, _ = parentType.Fields()[fieldName] + } + if parentType, ok := parentType.(*Interface); ok { + fieldDef, _ = parentType.Fields()[fieldName] + } + + responseName := fieldName + if selection.Alias != nil { + responseName = selection.Alias.Value + } + _, ok := astAndDefs[responseName] + if !ok { + astAndDefs[responseName] = []*fieldDefPair{} + } + astAndDefs[responseName] = append(astAndDefs[responseName], &fieldDefPair{ + Field: selection, + FieldDef: fieldDef, + }) + case *ast.InlineFragment: + parentType, _ := typeFromAST(*context.Schema(), selection.TypeCondition) + astAndDefs = collectFieldASTsAndDefs( + context, + parentType, + selection.SelectionSet, + visitedFragmentNames, + astAndDefs, + ) + case *ast.FragmentSpread: + fragName := "" + if selection.Name != nil { + fragName = selection.Name.Value + } + if _, ok := visitedFragmentNames[fragName]; ok { + continue + } + visitedFragmentNames[fragName] = true + fragment := context.Fragment(fragName) + if fragment == nil { + continue + } + parentType, _ := typeFromAST(*context.Schema(), fragment.TypeCondition) + astAndDefs = collectFieldASTsAndDefs( + context, + parentType, + fragment.SelectionSet, + visitedFragmentNames, + astAndDefs, + ) + } + } + return astAndDefs +} + +/** + * pairSet A way to keep track of pairs of things when the ordering of the pair does + * not matter. We do this by maintaining a sort of double adjacency sets. + */ +type pairSet struct { + data map[ast.Node]*nodeSet +} + +func newPairSet() *pairSet { + return &pairSet{ + data: map[ast.Node]*nodeSet{}, + } +} +func (pair *pairSet) Has(a ast.Node, b ast.Node) bool { + first, ok := pair.data[a] + if !ok || first == nil { + return false + } + res := first.Has(b) + return res +} +func (pair *pairSet) Add(a ast.Node, b ast.Node) bool { + pair.data = pairSetAdd(pair.data, a, b) + pair.data = pairSetAdd(pair.data, b, a) + return true +} + +func pairSetAdd(data map[ast.Node]*nodeSet, a, b ast.Node) map[ast.Node]*nodeSet { + set, ok := data[a] + if !ok || set == nil { + set = newNodeSet() + data[a] = set + } + set.Add(b) + return data +} + +type conflictReason struct { + Name string + Message interface{} // conflictReason || []conflictReason +} +type conflict struct { + Reason conflictReason + Fields []ast.Node +} + +func sameDirectives(directives1 []*ast.Directive, directives2 []*ast.Directive) bool { + if len(directives1) != len(directives1) { + return false + } + for _, directive1 := range directives1 { + directive1Name := "" + if directive1.Name != nil { + directive1Name = directive1.Name.Value + } + + var foundDirective2 *ast.Directive + for _, directive2 := range directives2 { + directive2Name := "" + if directive2.Name != nil { + directive2Name = directive2.Name.Value + } + if directive1Name == directive2Name { + foundDirective2 = directive2 + } + break + } + if foundDirective2 == nil { + return false + } + if sameArguments(directive1.Arguments, foundDirective2.Arguments) == false { + return false + } + } + + return true +} +func sameArguments(args1 []*ast.Argument, args2 []*ast.Argument) bool { + if len(args1) != len(args2) { + return false + } + + for _, arg1 := range args1 { + arg1Name := "" + if arg1.Name != nil { + arg1Name = arg1.Name.Value + } + + var foundArgs2 *ast.Argument + for _, arg2 := range args2 { + arg2Name := "" + if arg2.Name != nil { + arg2Name = arg2.Name.Value + } + if arg1Name == arg2Name { + foundArgs2 = arg2 + } + break + } + if foundArgs2 == nil { + return false + } + if sameValue(arg1.Value, foundArgs2.Value) == false { + return false + } + } + + return true +} +func sameValue(value1 ast.Value, value2 ast.Value) bool { + if value1 == nil && value2 == nil { + return true + } + val1 := printer.Print(value1) + val2 := printer.Print(value2) + + return val1 == val2 +} +func sameType(type1 Type, type2 Type) bool { + t := fmt.Sprintf("%v", type1) + t2 := fmt.Sprintf("%v", type2) + return t == t2 +} + +/** + * OverlappingFieldsCanBeMergedRule + * Overlapping fields can be merged + * + * A selection set is only valid if all fields (including spreading any + * fragments) either correspond to distinct response names or can be merged + * without ambiguity. + */ +func OverlappingFieldsCanBeMergedRule(context *ValidationContext) *ValidationRuleInstance { + + comparedSet := newPairSet() + var findConflicts func(fieldMap map[string][]*fieldDefPair) (conflicts []*conflict) + findConflict := func(responseName string, pair *fieldDefPair, pair2 *fieldDefPair) *conflict { + + ast1 := pair.Field + def1 := pair.FieldDef + + ast2 := pair2.Field + def2 := pair2.FieldDef + + if ast1 == ast2 || comparedSet.Has(ast1, ast2) { + return nil + } + comparedSet.Add(ast1, ast2) + + name1 := "" + if ast1.Name != nil { + name1 = ast1.Name.Value + } + name2 := "" + if ast2.Name != nil { + name2 = ast2.Name.Value + } + if name1 != name2 { + return &conflict{ + Reason: conflictReason{ + Name: responseName, + Message: fmt.Sprintf(`%v and %v are different fields`, name1, name2), + }, + Fields: []ast.Node{ast1, ast2}, + } + } + + var type1 Type + var type2 Type + if def1 != nil { + type1 = def1.Type + } + if def2 != nil { + type2 = def2.Type + } + + if type1 != nil && type2 != nil && !sameType(type1, type2) { + return &conflict{ + Reason: conflictReason{ + Name: responseName, + Message: fmt.Sprintf(`they return differing types %v and %v`, type1, type2), + }, + Fields: []ast.Node{ast1, ast2}, + } + } + if !sameArguments(ast1.Arguments, ast2.Arguments) { + return &conflict{ + Reason: conflictReason{ + Name: responseName, + Message: `they have differing arguments`, + }, + Fields: []ast.Node{ast1, ast2}, + } + } + if !sameDirectives(ast1.Directives, ast2.Directives) { + return &conflict{ + Reason: conflictReason{ + Name: responseName, + Message: `they have differing directives`, + }, + Fields: []ast.Node{ast1, ast2}, + } + } + + selectionSet1 := ast1.SelectionSet + selectionSet2 := ast2.SelectionSet + if selectionSet1 != nil && selectionSet2 != nil { + visitedFragmentNames := map[string]bool{} + subfieldMap := collectFieldASTsAndDefs( + context, + GetNamed(type1), + selectionSet1, + visitedFragmentNames, + nil, + ) + subfieldMap = collectFieldASTsAndDefs( + context, + GetNamed(type2), + selectionSet2, + visitedFragmentNames, + subfieldMap, + ) + conflicts := findConflicts(subfieldMap) + if len(conflicts) > 0 { + + conflictReasons := []conflictReason{} + conflictFields := []ast.Node{ast1, ast2} + for _, c := range conflicts { + conflictReasons = append(conflictReasons, c.Reason) + conflictFields = append(conflictFields, c.Fields...) + } + + return &conflict{ + Reason: conflictReason{ + Name: responseName, + Message: conflictReasons, + }, + Fields: conflictFields, + } + } + } + return nil + } + + findConflicts = func(fieldMap map[string][]*fieldDefPair) (conflicts []*conflict) { + + // ensure field traversal + orderedName := sort.StringSlice{} + for responseName, _ := range fieldMap { + orderedName = append(orderedName, responseName) + } + orderedName.Sort() + + for _, responseName := range orderedName { + fields, _ := fieldMap[responseName] + for _, fieldA := range fields { + for _, fieldB := range fields { + c := findConflict(responseName, fieldA, fieldB) + if c != nil { + conflicts = append(conflicts, c) + } + } + } + } + return conflicts + } + + var reasonMessage func(message interface{}) string + reasonMessage = func(message interface{}) string { + switch reason := message.(type) { + case string: + return reason + case conflictReason: + return reasonMessage(reason.Message) + case []conflictReason: + messages := []string{} + for _, r := range reason { + messages = append(messages, fmt.Sprintf( + `subfields "%v" conflict because %v`, + r.Name, + reasonMessage(r.Message), + )) + } + return strings.Join(messages, " and ") + } + return "" + } + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.SelectionSet: visitor.NamedVisitFuncs{ + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + if selectionSet, ok := p.Node.(*ast.SelectionSet); ok && selectionSet != nil { + parentType, _ := context.ParentType().(Named) + fieldMap := collectFieldASTsAndDefs( + context, + parentType, + selectionSet, + nil, + nil, + ) + conflicts := findConflicts(fieldMap) + if len(conflicts) > 0 { + errors := []error{} + for _, c := range conflicts { + responseName := c.Reason.Name + reason := c.Reason + _, err := newValidationRuleError( + fmt.Sprintf( + `Fields "%v" conflict because %v.`, + responseName, + reasonMessage(reason), + ), + c.Fields, + ) + errors = append(errors, err) + + } + return visitor.ActionNoChange, errors + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +func getFragmentType(context *ValidationContext, name string) Type { + frag := context.Fragment(name) + if frag == nil { + return nil + } + ttype, _ := typeFromAST(*context.Schema(), frag.TypeCondition) + return ttype +} + +func doTypesOverlap(t1 Type, t2 Type) bool { + if t1 == t2 { + return true + } + if _, ok := t1.(*Object); ok { + if _, ok := t2.(*Object); ok { + return false + } + if t2, ok := t2.(Abstract); ok { + for _, ttype := range t2.PossibleTypes() { + if ttype == t1 { + return true + } + } + return false + } + } + if t1, ok := t1.(Abstract); ok { + if _, ok := t2.(*Object); ok { + for _, ttype := range t1.PossibleTypes() { + if ttype == t2 { + return true + } + } + return false + } + t1TypeNames := map[string]bool{} + for _, ttype := range t1.PossibleTypes() { + t1TypeNames[ttype.Name()] = true + } + if t2, ok := t2.(Abstract); ok { + for _, ttype := range t2.PossibleTypes() { + if hasT1TypeName, _ := t1TypeNames[ttype.Name()]; hasT1TypeName { + return true + } + } + return false + } + } + return false +} + +/** + * PossibleFragmentSpreadsRule + * Possible fragment spread + * + * A fragment spread is only valid if the type condition could ever possibly + * be true: if there is a non-empty intersection of the possible parent types, + * and possible types which pass the type condition. + */ +func PossibleFragmentSpreadsRule(context *ValidationContext) *ValidationRuleInstance { + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.InlineFragment: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.InlineFragment); ok && node != nil { + fragType := context.Type() + parentType, _ := context.ParentType().(Type) + + if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { + return newValidationRuleError( + fmt.Sprintf(`Fragment cannot be spread here as objects of `+ + `type "%v" can never be of type "%v".`, parentType, fragType), + []ast.Node{node}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + kinds.FragmentSpread: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.FragmentSpread); ok && node != nil { + fragName := "" + if node.Name != nil { + fragName = node.Name.Value + } + fragType := getFragmentType(context, fragName) + parentType, _ := context.ParentType().(Type) + if fragType != nil && parentType != nil && !doTypesOverlap(fragType, parentType) { + return newValidationRuleError( + fmt.Sprintf(`Fragment "%v" cannot be spread here as objects of `+ + `type "%v" can never be of type "%v".`, fragName, parentType, fragType), + []ast.Node{node}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * ProvidedNonNullArgumentsRule + * Provided required arguments + * + * A field or directive is only valid if all required (non-null) field arguments + * have been provided. + */ +func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleInstance { + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Field: visitor.NamedVisitFuncs{ + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + // Validate on leave to allow for deeper errors to appear first. + if fieldAST, ok := p.Node.(*ast.Field); ok && fieldAST != nil { + fieldDef := context.FieldDef() + if fieldDef == nil { + return visitor.ActionSkip, nil + } + + errors := []error{} + argASTs := fieldAST.Arguments + + argASTMap := map[string]*ast.Argument{} + for _, arg := range argASTs { + name := "" + if arg.Name != nil { + name = arg.Name.Value + } + argASTMap[name] = arg + } + for _, argDef := range fieldDef.Args { + argAST, _ := argASTMap[argDef.Name()] + if argAST == nil { + if argDefType, ok := argDef.Type.(*NonNull); ok { + fieldName := "" + if fieldAST.Name != nil { + fieldName = fieldAST.Name.Value + } + _, err := newValidationRuleError( + fmt.Sprintf(`Field "%v" argument "%v" of type "%v" `+ + `is required but not provided.`, fieldName, argDef.Name(), argDefType), + []ast.Node{fieldAST}, + ) + errors = append(errors, err) + } + } + } + if len(errors) > 0 { + return visitor.ActionNoChange, errors + } + } + return visitor.ActionNoChange, nil + }, + }, + kinds.Directive: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + // Validate on leave to allow for deeper errors to appear first. + + if directiveAST, ok := p.Node.(*ast.Directive); ok && directiveAST != nil { + directiveDef := context.Directive() + if directiveDef == nil { + return visitor.ActionSkip, nil + } + errors := []error{} + argASTs := directiveAST.Arguments + + argASTMap := map[string]*ast.Argument{} + for _, arg := range argASTs { + name := "" + if arg.Name != nil { + name = arg.Name.Value + } + argASTMap[name] = arg + } + + for _, argDef := range directiveDef.Args { + argAST, _ := argASTMap[argDef.Name()] + if argAST == nil { + if argDefType, ok := argDef.Type.(*NonNull); ok { + directiveName := "" + if directiveAST.Name != nil { + directiveName = directiveAST.Name.Value + } + _, err := newValidationRuleError( + fmt.Sprintf(`Directive "@%v" argument "%v" of type `+ + `"%v" is required but not provided.`, directiveName, argDef.Name(), argDefType), + []ast.Node{directiveAST}, + ) + errors = append(errors, err) + } + } + } + if len(errors) > 0 { + return visitor.ActionNoChange, errors + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * ScalarLeafsRule + * Scalar leafs + * + * A GraphQL document is valid only if all leaf fields (fields without + * sub selections) are of scalar or enum types. + */ +func ScalarLeafsRule(context *ValidationContext) *ValidationRuleInstance { + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Field: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.Field); ok && node != nil { + nodeName := "" + if node.Name != nil { + nodeName = node.Name.Value + } + ttype := context.Type() + if ttype != nil { + if IsLeafType(ttype) { + if node.SelectionSet != nil { + return newValidationRuleError( + fmt.Sprintf(`Field "%v" of type "%v" must not have a sub selection.`, nodeName, ttype), + []ast.Node{node.SelectionSet}, + ) + } + } else if node.SelectionSet == nil { + return newValidationRuleError( + fmt.Sprintf(`Field "%v" of type "%v" must have a sub selection.`, nodeName, ttype), + []ast.Node{node}, + ) + } + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * UniqueArgumentNamesRule + * Unique argument names + * + * A GraphQL field or directive is only valid if all supplied arguments are + * uniquely named. + */ +func UniqueArgumentNamesRule(context *ValidationContext) *ValidationRuleInstance { + knownArgNames := map[string]*ast.Name{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.Field: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + knownArgNames = map[string]*ast.Name{} + return visitor.ActionNoChange, nil + }, + }, + kinds.Directive: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + knownArgNames = map[string]*ast.Name{} + return visitor.ActionNoChange, nil + }, + }, + kinds.Argument: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.Argument); ok { + argName := "" + if node.Name != nil { + argName = node.Name.Value + } + if nameAST, ok := knownArgNames[argName]; ok { + return newValidationRuleError( + fmt.Sprintf(`There can be only one argument named "%v".`, argName), + []ast.Node{nameAST, node.Name}, + ) + } + knownArgNames[argName] = node.Name + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * UniqueFragmentNamesRule + * Unique fragment names + * + * A GraphQL document is only valid if all defined fragments have unique names. + */ +func UniqueFragmentNamesRule(context *ValidationContext) *ValidationRuleInstance { + knownFragmentNames := map[string]*ast.Name{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.FragmentDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.FragmentDefinition); ok && node != nil { + fragmentName := "" + if node.Name != nil { + fragmentName = node.Name.Value + } + if nameAST, ok := knownFragmentNames[fragmentName]; ok { + return newValidationRuleError( + fmt.Sprintf(`There can only be one fragment named "%v".`, fragmentName), + []ast.Node{nameAST, node.Name}, + ) + } + knownFragmentNames[fragmentName] = node.Name + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * UniqueOperationNamesRule + * Unique operation names + * + * A GraphQL document is only valid if all defined operations have unique names. + */ +func UniqueOperationNamesRule(context *ValidationContext) *ValidationRuleInstance { + knownOperationNames := map[string]*ast.Name{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil { + operationName := "" + if node.Name != nil { + operationName = node.Name.Value + } + if nameAST, ok := knownOperationNames[operationName]; ok { + return newValidationRuleError( + fmt.Sprintf(`There can only be one operation named "%v".`, operationName), + []ast.Node{nameAST, node.Name}, + ) + } + knownOperationNames[operationName] = node.Name + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +/** + * VariablesAreInputTypesRule + * Variables are input types + * + * A GraphQL operation is only valid if all the variables it defines are of + * input types (scalar, enum, or input object). + */ +func VariablesAreInputTypesRule(context *ValidationContext) *ValidationRuleInstance { + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.VariableDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.VariableDefinition); ok && node != nil { + ttype, _ := typeFromAST(*context.Schema(), node.Type) + + // If the variable type is not an input type, return an error. + if ttype != nil && !IsInputType(ttype) { + variableName := "" + if node.Variable != nil && node.Variable.Name != nil { + variableName = node.Variable.Name.Value + } + return newValidationRuleError( + fmt.Sprintf(`Variable "$%v" cannot be non-input type "%v".`, + variableName, printer.Print(node.Type)), + []ast.Node{node.Type}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitorOpts: visitorOpts, + } +} + +// If a variable definition has a default value, it's effectively non-null. +func effectiveType(varType Type, varDef *ast.VariableDefinition) Type { + if varDef.DefaultValue == nil { + return varType + } + if _, ok := varType.(*NonNull); ok { + return varType + } + return NewNonNull(varType) +} + +// A var type is allowed if it is the same or more strict than the expected +// type. It can be more strict if the variable type is non-null when the +// expected type is nullable. If both are list types, the variable item type can +// be more strict than the expected item type. +func varTypeAllowedForType(varType Type, expectedType Type) bool { + if expectedType, ok := expectedType.(*NonNull); ok { + if varType, ok := varType.(*NonNull); ok { + return varTypeAllowedForType(varType.OfType, expectedType.OfType) + } + return false + } + if varType, ok := varType.(*NonNull); ok { + return varTypeAllowedForType(varType.OfType, expectedType) + } + if varType, ok := varType.(*List); ok { + if expectedType, ok := expectedType.(*List); ok { + return varTypeAllowedForType(varType.OfType, expectedType.OfType) + } + } + return varType == expectedType +} + +/** + * VariablesInAllowedPositionRule + * Variables passed to field arguments conform to type + */ +func VariablesInAllowedPositionRule(context *ValidationContext) *ValidationRuleInstance { + + varDefMap := map[string]*ast.VariableDefinition{} + visitedFragmentNames := map[string]bool{} + + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.OperationDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + varDefMap = map[string]*ast.VariableDefinition{} + visitedFragmentNames = map[string]bool{} + return visitor.ActionNoChange, nil + }, + }, + kinds.VariableDefinition: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if varDefAST, ok := p.Node.(*ast.VariableDefinition); ok { + defName := "" + if varDefAST.Variable != nil && varDefAST.Variable.Name != nil { + defName = varDefAST.Variable.Name.Value + } + varDefMap[defName] = varDefAST + } + return visitor.ActionNoChange, nil + }, + }, + kinds.FragmentSpread: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + // Only visit fragments of a particular name once per operation + if spreadAST, ok := p.Node.(*ast.FragmentSpread); ok { + spreadName := "" + if spreadAST.Name != nil { + spreadName = spreadAST.Name.Value + } + if hasVisited, _ := visitedFragmentNames[spreadName]; hasVisited { + return visitor.ActionSkip, nil + } + visitedFragmentNames[spreadName] = true + } + return visitor.ActionNoChange, nil + }, + }, + kinds.Variable: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if variableAST, ok := p.Node.(*ast.Variable); ok && variableAST != nil { + varName := "" + if variableAST.Name != nil { + varName = variableAST.Name.Value + } + varDef, _ := varDefMap[varName] + var varType Type + if varDef != nil { + varType, _ = typeFromAST(*context.Schema(), varDef.Type) + } + inputType := context.InputType() + if varType != nil && inputType != nil && !varTypeAllowedForType(effectiveType(varType, varDef), inputType) { + return newValidationRuleError( + fmt.Sprintf(`Variable "$%v" of type "%v" used in position `+ + `expecting type "%v".`, varName, varType, inputType), + []ast.Node{variableAST}, + ) + } + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + return &ValidationRuleInstance{ + VisitSpreadFragments: true, + VisitorOpts: visitorOpts, + } +} + +/** + * Utility for validators which determines if a value literal AST is valid given + * an input type. + * + * Note that this only validates literal values, variables are assumed to + * provide values of the correct type. + */ +func isValidLiteralValue(ttype Input, valueAST ast.Value) bool { + // A value must be provided if the type is non-null. + if ttype, ok := ttype.(*NonNull); ok { + if valueAST == nil { + return false + } + ofType, _ := ttype.OfType.(Input) + return isValidLiteralValue(ofType, valueAST) + } + + if valueAST == nil { + return true + } + + // This function only tests literals, and assumes variables will provide + // values of the correct type. + if valueAST.GetKind() == kinds.Variable { + return true + } + + // Lists accept a non-list value as a list of one. + if ttype, ok := ttype.(*List); ok { + itemType, _ := ttype.OfType.(Input) + if valueAST, ok := valueAST.(*ast.ListValue); ok { + for _, value := range valueAST.Values { + if isValidLiteralValue(itemType, value) == false { + return false + } + } + return true + } + return isValidLiteralValue(itemType, valueAST) + + } + + // Input objects check each defined field and look for undefined fields. + if ttype, ok := ttype.(*InputObject); ok { + valueAST, ok := valueAST.(*ast.ObjectValue) + if !ok { + return false + } + fields := ttype.Fields() + + // Ensure every provided field is defined. + // Ensure every defined field is valid. + fieldASTs := valueAST.Fields + fieldASTMap := map[string]*ast.ObjectField{} + for _, fieldAST := range fieldASTs { + fieldASTName := "" + if fieldAST.Name != nil { + fieldASTName = fieldAST.Name.Value + } + + fieldASTMap[fieldASTName] = fieldAST + + // check if field is defined + field, ok := fields[fieldASTName] + if !ok || field == nil { + return false + } + } + for fieldName, field := range fields { + fieldAST, _ := fieldASTMap[fieldName] + var fieldASTValue ast.Value + if fieldAST != nil { + fieldASTValue = fieldAST.Value + } + if !isValidLiteralValue(field.Type, fieldASTValue) { + return false + } + } + return true + } + + if ttype, ok := ttype.(*Scalar); ok { + return !isNullish(ttype.ParseLiteral(valueAST)) + } + if ttype, ok := ttype.(*Enum); ok { + return !isNullish(ttype.ParseLiteral(valueAST)) + } + + // Must be input type (not scalar or enum) + // Silently fail, instead of panic() + return false +} + +/** + * Given an operation or fragment AST node, gather all the + * named spreads defined within the scope of the fragment + * or operation + */ +func gatherSpreads(node ast.Node) (spreadNodes []*ast.FragmentSpread) { + visitorOpts := &visitor.VisitorOptions{ + KindFuncMap: map[string]visitor.NamedVisitFuncs{ + kinds.FragmentSpread: visitor.NamedVisitFuncs{ + Kind: func(p visitor.VisitFuncParams) (string, interface{}) { + if node, ok := p.Node.(*ast.FragmentSpread); ok && node != nil { + spreadNodes = append(spreadNodes, node) + } + return visitor.ActionNoChange, nil + }, + }, + }, + } + visitor.Visit(node, visitorOpts, nil) + return spreadNodes +} diff --git a/rules_arguments_of_correct_type_test.go b/rules_arguments_of_correct_type_test.go new file mode 100644 index 00000000..a89edbf3 --- /dev/null +++ b/rules_arguments_of_correct_type_test.go @@ -0,0 +1,799 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodIntValue(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + intArgField(intArg: 2) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodBooleanValue(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + booleanArgField(booleanArg: true) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodStringValue(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringArgField(stringArg: "foo") + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodFloatValue(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + floatArgField(floatArg: 1.1) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidValue_IntIntoFloat(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + floatArgField(floatArg: 1) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidValue_IntIntoID(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + idArgField(idArg: 1) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidValue_StringIntoID(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + idArgField(idArg: "someIdString") + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodEnumValue(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + doesKnowCommand(dogCommand: SIT) + } + } + `) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_IntIntoString(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringArgField(stringArg: 1) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "stringArg" expected type "String" but got: 1.`, + 4, 39, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_FloatIntoString(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringArgField(stringArg: 1.0) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "stringArg" expected type "String" but got: 1.0.`, + 4, 39, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_BooleanIntoString(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringArgField(stringArg: true) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "stringArg" expected type "String" but got: true.`, + 4, 39, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidStringValues_UnquotedStringIntoString(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringArgField(stringArg: BAR) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "stringArg" expected type "String" but got: BAR.`, + 4, 39, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_StringIntoInt(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + intArgField(intArg: "3") + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "intArg" expected type "Int" but got: "3".`, + 4, 33, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_BigIntIntoInt(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + intArgField(intArg: 829384293849283498239482938) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "intArg" expected type "Int" but got: 829384293849283498239482938.`, + 4, 33, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_UnquotedStringIntoInt(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + intArgField(intArg: FOO) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "intArg" expected type "Int" but got: FOO.`, + 4, 33, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_SimpleFloatIntoInt(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + intArgField(intArg: 3.0) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "intArg" expected type "Int" but got: 3.0.`, + 4, 33, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidIntValues_FloatIntoInt(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + intArgField(intArg: 3.333) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "intArg" expected type "Int" but got: 3.333.`, + 4, 33, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidFloatValues_StringIntoFloat(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + floatArgField(floatArg: "3.333") + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "floatArg" expected type "Float" but got: "3.333".`, + 4, 37, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidFloatValues_BooleanIntoFloat(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + floatArgField(floatArg: true) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "floatArg" expected type "Float" but got: true.`, + 4, 37, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidFloatValues_UnquotedIntoFloat(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + floatArgField(floatArg: FOO) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "floatArg" expected type "Float" but got: FOO.`, + 4, 37, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_IntIntoBoolean(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + booleanArgField(booleanArg: 2) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "booleanArg" expected type "Boolean" but got: 2.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_FloatIntoBoolean(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + booleanArgField(booleanArg: 1.0) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "booleanArg" expected type "Boolean" but got: 1.0.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_StringIntoBoolean(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + booleanArgField(booleanArg: "true") + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "booleanArg" expected type "Boolean" but got: "true".`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidBooleanValues_UnquotedStringIntoBoolean(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + booleanArgField(booleanArg: TRUE) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "booleanArg" expected type "Boolean" but got: TRUE.`, + 4, 41, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidIDValue_FloatIntoID(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + idArgField(idArg: 1.0) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "idArg" expected type "ID" but got: 1.0.`, + 4, 31, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidIDValue_BooleanIntoID(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + idArgField(idArg: true) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "idArg" expected type "ID" but got: true.`, + 4, 31, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidIDValue_UnquotedIntoID(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + idArgField(idArg: SOMETHING) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "idArg" expected type "ID" but got: SOMETHING.`, + 4, 31, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_IntIntoEnum(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + doesKnowCommand(dogCommand: 2) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "dogCommand" expected type "DogCommand" but got: 2.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_FloatIntoEnum(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + doesKnowCommand(dogCommand: 1.0) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "dogCommand" expected type "DogCommand" but got: 1.0.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_StringIntoEnum(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + doesKnowCommand(dogCommand: "SIT") + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "dogCommand" expected type "DogCommand" but got: "SIT".`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_BooleanIntoEnum(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + doesKnowCommand(dogCommand: true) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "dogCommand" expected type "DogCommand" but got: true.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_UnknownEnumValueIntoEnum(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + doesKnowCommand(dogCommand: JUGGLE) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "dogCommand" expected type "DogCommand" but got: JUGGLE.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidEnumValue_DifferentCaseEnumValueIntoEnum(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + doesKnowCommand(dogCommand: sit) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "dogCommand" expected type "DogCommand" but got: sit.`, + 4, 41, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_ValidListValue_GoodListValue(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringListArgField(stringListArg: ["one", "two"]) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidListValue_EmptyListValue(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringListArgField(stringListArg: []) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidListValue_SingleValueIntoList(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringListArgField(stringListArg: "one") + } + } + `) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidListValue_IncorrectItemType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringListArgField(stringListArg: ["one", 2]) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "stringListArg" expected type "[String]" but got: ["one", 2].`, + 4, 47, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidListValue_SingleValueOfIncorrentType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + stringListArgField(stringListArg: 1) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "stringListArg" expected type "[String]" but got: 1.`, + 4, 47, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_ArgOnOptionalArg(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + isHousetrained(atOtherHomes: true) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_NoArgOnOptionalArg(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog { + isHousetrained + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_MultipleArgs(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleReqs(req1: 1, req2: 2) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_MultipleArgsReverseOrder(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleReqs(req2: 2, req1: 1) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_NoArgsOnMultipleOptional(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleOpts + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_OneArgOnMultipleOptional(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleOpts(opt1: 1) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_SecondArgOnMultipleOptional(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleOpts(opt2: 1) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_MultipleRequiredsOnMixedList(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_MultipleRequiredsAndOptionalOnMixedList(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4, opt1: 5) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidNonNullableValue_AllRequiredsAndOptionalOnMixedList(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) + } + } + `) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidNonNullableValue_IncorrectValueType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleReqs(req2: "two", req1: "one") + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "req2" expected type "Int!" but got: "two".`, + 4, 32, + ), + testutil.RuleError( + `Argument "req1" expected type "Int!" but got: "one".`, + 4, 45, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidNonNullableValue_IncorrectValueAndMissingArgument(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + multipleReqs(req1: "one") + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "req1" expected type "Int!" but got: "one".`, + 4, 32, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_ValidInputObjectValue_OptionalArg_DespiteRequiredFieldInType(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidInputObjectValue_PartialObject_OnlyRequired(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { requiredField: true }) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidInputObjectValue_PartialObject_RequiredFieldCanBeFalsey(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { requiredField: false }) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidInputObjectValue_PartialObject_IncludingRequired(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { requiredField: false }) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidInputObjectValue_FullObject(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { + requiredField: true, + intField: 4, + stringField: "foo", + booleanField: false, + stringListField: ["one", "two"] + }) + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_ValidInputObjectValue_FullObject_WithFieldsInDifferentOrder(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { + stringListField: ["one", "two"], + booleanField: false, + requiredField: true, + stringField: "foo", + intField: 4, + }) + } + } + `) +} + +func TestValidate_ArgValuesOfCorrectType_InvalidInputObjectValue_PartialObject_MissingRequired(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { intField: 4 }) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "complexArg" expected type "ComplexInput" but got: {intField: 4}.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidInputObjectValue_PartialObject_InvalidFieldType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { + stringListField: ["one", 2], + requiredField: true, + }) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "complexArg" expected type "ComplexInput" but got: {stringListField: ["one", 2], requiredField: true}.`, + 4, 41, + ), + }) +} +func TestValidate_ArgValuesOfCorrectType_InvalidInputObjectValue_PartialObject_UnknownFieldArg(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + complicatedArgs { + complexArgField(complexArg: { + requiredField: true, + unknownField: "value" + }) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "complexArg" expected type "ComplexInput" but got: {requiredField: true, unknownField: "value"}.`, + 4, 41, + ), + }) +} + +func TestValidate_ArgValuesOfCorrectType_DirectiveArguments_WithDirectivesOfValidType(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog @include(if: true) { + name + } + human @skip(if: false) { + name + } + } + `) +} +func TestValidate_ArgValuesOfCorrectType_DirectiveArguments_WithDirectivesWithIncorrectTypes(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, ` + { + dog @include(if: "yes") { + name @skip(if: ENUM) + } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Argument "if" expected type "Boolean!" but got: "yes".`, + 3, 28, + ), + testutil.RuleError( + `Argument "if" expected type "Boolean!" but got: ENUM.`, + 4, 28, + ), + }) +} diff --git a/rules_default_values_of_correct_type_test.go b/rules_default_values_of_correct_type_test.go new file mode 100644 index 00000000..8ef76210 --- /dev/null +++ b/rules_default_values_of_correct_type_test.go @@ -0,0 +1,90 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_VariableDefaultValuesOfCorrectType_VariablesWithNoDefaultValues(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.DefaultValuesOfCorrectTypeRule, ` + query NullableValues($a: Int, $b: String, $c: ComplexInput) { + dog { name } + } + `) +} +func TestValidate_VariableDefaultValuesOfCorrectType_RequiredVariablesWithoutDefaultValues(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.DefaultValuesOfCorrectTypeRule, ` + query RequiredValues($a: Int!, $b: String!) { + dog { name } + } + `) +} +func TestValidate_VariableDefaultValuesOfCorrectType_VariablesWithValidDefaultValues(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.DefaultValuesOfCorrectTypeRule, ` + query WithDefaultValues( + $a: Int = 1, + $b: String = "ok", + $c: ComplexInput = { requiredField: true, intField: 3 } + ) { + dog { name } + } + `) +} +func TestValidate_VariableDefaultValuesOfCorrectType_NoRequiredVariablesWithDefaultValues(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.DefaultValuesOfCorrectTypeRule, ` + query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") { + dog { name } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError( + `Variable "$a" of type "Int!" is required and will not `+ + `use the default value. Perhaps you meant to use type "Int".`, + 2, 49, + ), + testutil.RuleError( + `Variable "$b" of type "String!" is required and will not `+ + `use the default value. Perhaps you meant to use type "String".`, + 2, 66, + ), + }) +} +func TestValidate_VariableDefaultValuesOfCorrectType_VariablesWithInvalidDefaultValues(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.DefaultValuesOfCorrectTypeRule, ` + query InvalidDefaultValues( + $a: Int = "one", + $b: String = 4, + $c: ComplexInput = "notverycomplex" + ) { + dog { name } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" of type "Int" has invalid default value: "one".`, 3, 19), + testutil.RuleError(`Variable "$b" of type "String" has invalid default value: 4.`, 4, 22), + testutil.RuleError(`Variable "$c" of type "ComplexInput" has invalid default value: "notverycomplex".`, 5, 28), + }) +} +func TestValidate_VariableDefaultValuesOfCorrectType_ComplexVariablesMissingRequiredField(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.DefaultValuesOfCorrectTypeRule, ` + query MissingRequiredField($a: ComplexInput = {intField: 3}) { + dog { name } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" of type "ComplexInput" has invalid default value: {intField: 3}.`, 2, 53), + }) +} +func TestValidate_VariableDefaultValuesOfCorrectType_ListVariablesWithInvalidItem(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.DefaultValuesOfCorrectTypeRule, ` + query InvalidItem($a: [String] = ["one", 2]) { + dog { name } + } + `, + []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" of type "[String]" has invalid default value: ["one", 2].`, 2, 40), + }) +} diff --git a/rules_fields_on_correct_type_test.go b/rules_fields_on_correct_type_test.go new file mode 100644 index 00000000..af1f571e --- /dev/null +++ b/rules_fields_on_correct_type_test.go @@ -0,0 +1,167 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_FieldsOnCorrectType_ObjectFieldSelection(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment objectFieldSelection on Dog { + __typename + name + } + `) +} +func TestValidate_FieldsOnCorrectType_AliasedObjectFieldSelection(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment aliasedObjectFieldSelection on Dog { + tn : __typename + otherName : name + } + `) +} +func TestValidate_FieldsOnCorrectType_InterfaceFieldSelection(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment interfaceFieldSelection on Pet { + __typename + name + } + `) +} +func TestValidate_FieldsOnCorrectType_AliasedInterfaceFieldSelection(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment interfaceFieldSelection on Pet { + otherName : name + } + `) +} +func TestValidate_FieldsOnCorrectType_LyingAliasSelection(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment lyingAliasSelection on Dog { + name : nickname + } + `) +} +func TestValidate_FieldsOnCorrectType_IgnoresFieldsOnUnknownType(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment unknownSelection on UnknownType { + unknownField + } + `) +} +func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment fieldNotDefined on Dog { + meowVolume + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "meowVolume" on "Dog".`, 3, 9), + }) +} +func TestValidate_FieldsOnCorrectType_FieldNotDefinedDeeplyOnlyReportsFirst(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment deepFieldNotDefined on Dog { + unknown_field { + deeper_unknown_field + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "unknown_field" on "Dog".`, 3, 9), + }) +} +func TestValidate_FieldsOnCorrectType_SubFieldNotDefined(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment subFieldNotDefined on Human { + pets { + unknown_field + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "unknown_field" on "Pet".`, 4, 11), + }) +} +func TestValidate_FieldsOnCorrectType_FieldNotDefinedOnInlineFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment fieldNotDefined on Pet { + ... on Dog { + meowVolume + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "meowVolume" on "Dog".`, 4, 11), + }) +} +func TestValidate_FieldsOnCorrectType_AliasedFieldTargetNotDefined(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment aliasedFieldTargetNotDefined on Dog { + volume : mooVolume + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "mooVolume" on "Dog".`, 3, 9), + }) +} +func TestValidate_FieldsOnCorrectType_AliasedLyingFieldTargetNotDefined(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment aliasedLyingFieldTargetNotDefined on Dog { + barkVolume : kawVolume + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "kawVolume" on "Dog".`, 3, 9), + }) +} +func TestValidate_FieldsOnCorrectType_NotDefinedOnInterface(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment notDefinedOnInterface on Pet { + tailLength + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "tailLength" on "Pet".`, 3, 9), + }) +} +func TestValidate_FieldsOnCorrectType_DefinedOnImplementorsButNotOnInterface(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment definedOnImplementorsButNotInterface on Pet { + nickname + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "nickname" on "Pet".`, 3, 9), + }) +} +func TestValidate_FieldsOnCorrectType_MetaFieldSelectionOnUnion(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment directFieldSelectionOnUnion on CatOrDog { + __typename + } + `) +} +func TestValidate_FieldsOnCorrectType_DirectFieldSelectionOnUnion(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment directFieldSelectionOnUnion on CatOrDog { + directField + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot query field "directField" on "CatOrDog".`, 3, 9), + }) +} +func TestValidate_FieldsOnCorrectType_DirectImplementorsQueriedOnUnion(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), + }) +} +func TestValidate_FieldsOnCorrectType_ValidFieldInInlineFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FieldsOnCorrectTypeRule, ` + fragment objectFieldSelection on Pet { + ... on Dog { + name + } + } + `) +} diff --git a/rules_fragments_on_composite_types_test.go b/rules_fragments_on_composite_types_test.go new file mode 100644 index 00000000..31fbf08b --- /dev/null +++ b/rules_fragments_on_composite_types_test.go @@ -0,0 +1,78 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_FragmentsOnCompositeTypes_ObjectIsValidFragmentType(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment validFragment on Dog { + barks + } + `) +} +func TestValidate_FragmentsOnCompositeTypes_InterfaceIsValidFragmentType(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment validFragment on Pet { + name + } + `) +} +func TestValidate_FragmentsOnCompositeTypes_ObjectIsValidInlineFragmentType(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment validFragment on Pet { + ... on Dog { + barks + } + } + `) +} +func TestValidate_FragmentsOnCompositeTypes_UnionIsValidFragmentType(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment validFragment on CatOrDog { + __typename + } + `) +} +func TestValidate_FragmentsOnCompositeTypes_ScalarIsInvalidFragmentType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment scalarFragment on Boolean { + bad + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "scalarFragment" cannot condition on non composite type "Boolean".`, 2, 34), + }) +} +func TestValidate_FragmentsOnCompositeTypes_EnumIsInvalidFragmentType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment scalarFragment on FurColor { + bad + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "scalarFragment" cannot condition on non composite type "FurColor".`, 2, 34), + }) +} +func TestValidate_FragmentsOnCompositeTypes_InputObjectIsInvalidFragmentType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment inputFragment on ComplexInput { + stringField + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "inputFragment" cannot condition on non composite type "ComplexInput".`, 2, 33), + }) +} +func TestValidate_FragmentsOnCompositeTypes_ScalarIsInvalidInlineFragmentType(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.FragmentsOnCompositeTypesRule, ` + fragment invalidFragment on Pet { + ... on String { + barks + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment cannot condition on non composite type "String".`, 3, 16), + }) +} diff --git a/rules_known_argument_names_test.go b/rules_known_argument_names_test.go new file mode 100644 index 00000000..7536161d --- /dev/null +++ b/rules_known_argument_names_test.go @@ -0,0 +1,115 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_KnownArgumentNames_SingleArgIsKnown(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownArgumentNamesRule, ` + fragment argOnRequiredArg on Dog { + doesKnowCommand(dogCommand: SIT) + } + `) +} +func TestValidate_KnownArgumentNames_MultipleArgsAreKnown(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownArgumentNamesRule, ` + fragment multipleArgs on ComplicatedArgs { + multipleReqs(req1: 1, req2: 2) + } + `) +} +func TestValidate_KnownArgumentNames_IgnoresArgsOfUnknownFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownArgumentNamesRule, ` + fragment argOnUnknownField on Dog { + unknownField(unknownArg: SIT) + } + `) +} +func TestValidate_KnownArgumentNames_MultipleArgsInReverseOrderAreKnown(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownArgumentNamesRule, ` + fragment multipleArgsReverseOrder on ComplicatedArgs { + multipleReqs(req2: 2, req1: 1) + } + `) +} +func TestValidate_KnownArgumentNames_NoArgsOnOptionalArg(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownArgumentNamesRule, ` + fragment noArgOnOptionalArg on Dog { + isHousetrained + } + `) +} +func TestValidate_KnownArgumentNames_ArgsAreKnownDeeply(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownArgumentNamesRule, ` + { + dog { + doesKnowCommand(dogCommand: SIT) + } + human { + pet { + ... on Dog { + doesKnowCommand(dogCommand: SIT) + } + } + } + } + `) +} +func TestValidate_KnownArgumentNames_DirectiveArgsAreKnown(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownArgumentNamesRule, ` + { + dog @skip(if: true) + } + `) +} +func TestValidate_KnownArgumentNames_UndirectiveArgsAreInvalid(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownArgumentNamesRule, ` + { + dog @skip(unless: true) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown argument "unless" on directive "@skip".`, 3, 19), + }) +} +func TestValidate_KnownArgumentNames_InvalidArgName(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownArgumentNamesRule, ` + fragment invalidArgName on Dog { + doesKnowCommand(unknown: true) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown argument "unknown" on field "doesKnowCommand" of type "Dog".`, 3, 25), + }) +} +func TestValidate_KnownArgumentNames_UnknownArgsAmongstKnownArgs(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownArgumentNamesRule, ` + fragment oneGoodArgOneInvalidArg on Dog { + doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown argument "whoknows" on field "doesKnowCommand" of type "Dog".`, 3, 25), + testutil.RuleError(`Unknown argument "unknown" on field "doesKnowCommand" of type "Dog".`, 3, 55), + }) +} +func TestValidate_KnownArgumentNames_UnknownArgsDeeply(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownArgumentNamesRule, ` + { + dog { + doesKnowCommand(unknown: true) + } + human { + pet { + ... on Dog { + doesKnowCommand(unknown: true) + } + } + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown argument "unknown" on field "doesKnowCommand" of type "Dog".`, 4, 27), + testutil.RuleError(`Unknown argument "unknown" on field "doesKnowCommand" of type "Dog".`, 9, 31), + }) +} diff --git a/rules_known_directives_rule_test.go b/rules_known_directives_rule_test.go new file mode 100644 index 00000000..0ece1888 --- /dev/null +++ b/rules_known_directives_rule_test.go @@ -0,0 +1,84 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_KnownDirectives_WithNoDirectives(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownDirectivesRule, ` + query Foo { + name + ...Frag + } + + fragment Frag on Dog { + name + } + `) +} +func TestValidate_KnownDirectives_WithKnownDirective(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownDirectivesRule, ` + { + dog @include(if: true) { + name + } + human @skip(if: false) { + name + } + } + `) +} +func TestValidate_KnownDirectives_WithUnknownDirective(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownDirectivesRule, ` + { + dog @unknown(directive: "value") { + name + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown directive "unknown".`, 3, 13), + }) +} +func TestValidate_KnownDirectives_WithManyUnknownDirectives(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownDirectivesRule, ` + { + dog @unknown(directive: "value") { + name + } + human @unknown(directive: "value") { + name + pets @unknown(directive: "value") { + name + } + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown directive "unknown".`, 3, 13), + testutil.RuleError(`Unknown directive "unknown".`, 6, 15), + testutil.RuleError(`Unknown directive "unknown".`, 8, 16), + }) +} +func TestValidate_KnownDirectives_WithWellPlacedDirectives(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownDirectivesRule, ` + query Foo { + name @include(if: true) + ...Frag @include(if: true) + skippedField @skip(if: true) + ...SkippedFrag @skip(if: true) + } + `) +} +func TestValidate_KnownDirectives_WithMisplacedDirectives(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownDirectivesRule, ` + query Foo @include(if: true) { + name + ...Frag + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Directive "include" may not be used on "operation".`, 2, 17), + }) +} diff --git a/rules_known_fragment_names_test.go b/rules_known_fragment_names_test.go new file mode 100644 index 00000000..b3d5d52e --- /dev/null +++ b/rules_known_fragment_names_test.go @@ -0,0 +1,52 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_KnownFragmentNames_KnownFragmentNamesAreValid(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownFragmentNamesRule, ` + { + human(id: 4) { + ...HumanFields1 + ... on Human { + ...HumanFields2 + } + } + } + fragment HumanFields1 on Human { + name + ...HumanFields3 + } + fragment HumanFields2 on Human { + name + } + fragment HumanFields3 on Human { + name + } + `) +} +func TestValidate_KnownFragmentNames_UnknownFragmentNamesAreInvalid(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownFragmentNamesRule, ` + { + human(id: 4) { + ...UnknownFragment1 + ... on Human { + ...UnknownFragment2 + } + } + } + fragment HumanFields on Human { + name + ...UnknownFragment3 + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown fragment "UnknownFragment1".`, 4, 14), + testutil.RuleError(`Unknown fragment "UnknownFragment2".`, 6, 16), + testutil.RuleError(`Unknown fragment "UnknownFragment3".`, 12, 12), + }) +} diff --git a/rules_known_type_names_test.go b/rules_known_type_names_test.go new file mode 100644 index 00000000..00c70263 --- /dev/null +++ b/rules_known_type_names_test.go @@ -0,0 +1,39 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_KnownTypeNames_KnownTypeNamesAreValid(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.KnownTypeNamesRule, ` + query Foo($var: String, $required: [String!]!) { + user(id: 4) { + pets { ... on Pet { name }, ...PetFields } + } + } + fragment PetFields on Pet { + name + } + `) +} +func TestValidate_KnownTypeNames_UnknownTypeNamesAreInValid(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.KnownTypeNamesRule, ` + query Foo($var: JumbledUpLetters) { + user(id: 4) { + name + pets { ... on Badger { name }, ...PetFields } + } + } + fragment PetFields on Peettt { + name + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Unknown type "JumbledUpLetters".`, 2, 23), + testutil.RuleError(`Unknown type "Badger".`, 5, 25), + testutil.RuleError(`Unknown type "Peettt".`, 8, 29), + }) +} diff --git a/rules_lone_anonymous_operation_rule_test.go b/rules_lone_anonymous_operation_rule_test.go new file mode 100644 index 00000000..cefaff64 --- /dev/null +++ b/rules_lone_anonymous_operation_rule_test.go @@ -0,0 +1,70 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_AnonymousOperationMustBeAlone_NoOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.LoneAnonymousOperationRule, ` + fragment fragA on Type { + field + } + `) +} +func TestValidate_AnonymousOperationMustBeAlone_OneAnonOperation(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.LoneAnonymousOperationRule, ` + { + field + } + `) +} +func TestValidate_AnonymousOperationMustBeAlone_MultipleNamedOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.LoneAnonymousOperationRule, ` + query Foo { + field + } + + query Bar { + field + } + `) +} +func TestValidate_AnonymousOperationMustBeAlone_AnonOperationWithFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.LoneAnonymousOperationRule, ` + { + ...Foo + } + fragment Foo on Type { + field + } + `) +} +func TestValidate_AnonymousOperationMustBeAlone_MultipleAnonOperations(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.LoneAnonymousOperationRule, ` + { + fieldA + } + { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`This anonymous operation must be the only defined operation.`, 2, 7), + testutil.RuleError(`This anonymous operation must be the only defined operation.`, 5, 7), + }) +} +func TestValidate_AnonymousOperationMustBeAlone_AnonOperationWithAnotherOperation(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.LoneAnonymousOperationRule, ` + { + fieldA + } + mutation Foo { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`This anonymous operation must be the only defined operation.`, 2, 7), + }) +} diff --git a/rules_no_fragment_cycles_test.go b/rules_no_fragment_cycles_test.go new file mode 100644 index 00000000..0eabdb77 --- /dev/null +++ b/rules_no_fragment_cycles_test.go @@ -0,0 +1,126 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_NoCircularFragmentSpreads_SingleReferenceIsValid(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragB } + fragment fragB on Dog { name } + `) +} +func TestValidate_NoCircularFragmentSpreads_SpreadingTwiceIsNotCircular(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragB, ...fragB } + fragment fragB on Dog { name } + `) +} +func TestValidate_NoCircularFragmentSpreads_SpreadingTwiceIndirectlyIsNotCircular(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragB, ...fragC } + fragment fragB on Dog { ...fragC } + fragment fragC on Dog { name } + `) +} +func TestValidate_NoCircularFragmentSpreads_DoubleSpreadWithinAbstractTypes(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoFragmentCyclesRule, ` + fragment nameFragment on Pet { + ... on Dog { name } + ... on Cat { name } + } + + fragment spreadsInAnon on Pet { + ... on Dog { ...nameFragment } + ... on Cat { ...nameFragment } + } + `) +} +func TestValidate_NoCircularFragmentSpreads_SpreadingRecursivelyWithinFieldFails(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Human { relatives { ...fragA } }, + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself.`, 2, 45), + }) +} + +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDirectly(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragA } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself.`, 2, 31), + }) +} +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDirectlyWithinInlineFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Pet { + ... on Dog { + ...fragA + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself.`, 4, 11), + }) +} + +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfIndirectly(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragB } + fragment fragB on Dog { ...fragA } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB.`, 2, 31, 3, 31), + }) +} +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfIndirectlyReportsOppositeOrder(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragB on Dog { ...fragA } + fragment fragA on Dog { ...fragB } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragB" within itself via fragA.`, 2, 31, 3, 31), + }) +} +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfIndirectlyWithinInlineFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Pet { + ... on Dog { + ...fragB + } + } + fragment fragB on Pet { + ... on Dog { + ...fragA + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB.`, 4, 11, 9, 11), + }) +} + +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeply(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragB } + fragment fragB on Dog { ...fragC } + fragment fragC on Dog { ...fragO } + fragment fragX on Dog { ...fragY } + fragment fragY on Dog { ...fragZ } + fragment fragZ on Dog { ...fragO } + fragment fragO on Dog { ...fragA, ...fragX } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB, fragC, fragO.`, 2, 31, 3, 31, 4, 31, 8, 31), + testutil.RuleError(`Cannot spread fragment "fragX" within itself via fragY, fragZ, fragO.`, 5, 31, 6, 31, 7, 31, 8, 41), + }) +} +func TestValidate_NoCircularFragmentSpreads_NoSpreadingItselfDeeplyTwoPaths(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoFragmentCyclesRule, ` + fragment fragA on Dog { ...fragB, ...fragC } + fragment fragB on Dog { ...fragA } + fragment fragC on Dog { ...fragA } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragB.`, 2, 31, 3, 31), + testutil.RuleError(`Cannot spread fragment "fragA" within itself via fragC.`, 2, 41, 4, 31), + }) +} diff --git a/rules_no_undefined_variables_test.go b/rules_no_undefined_variables_test.go new file mode 100644 index 00000000..64449842 --- /dev/null +++ b/rules_no_undefined_variables_test.go @@ -0,0 +1,265 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_NoUndefinedVariables_AllVariablesDefined(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + field(a: $a, b: $b, c: $c) + } + `) +} +func TestValidate_NoUndefinedVariables_AllVariablesDeeplyDefined(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + field(a: $a) { + field(b: $b) { + field(c: $c) + } + } + } + `) +} +func TestValidate_NoUndefinedVariables_AllVariablesDeeplyDefinedInInlineFragmentsDefined(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + ... on Type { + field(a: $a) { + field(b: $b) { + ... on Type { + field(c: $c) + } + } + } + } + } + `) +} +func TestValidate_NoUndefinedVariables_AllVariablesInFragmentsDeeplyDefined(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + fragment FragC on Type { + field(c: $c) + } + `) +} +func TestValidate_NoUndefinedVariables_VariablesWithinSingleFragmentDefinedInMultipleOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String) { + ...FragA + } + query Bar($a: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) + } + `) +} +func TestValidate_NoUndefinedVariables_VariableWithinFragmentsDefinedInOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String) { + ...FragA + } + query Bar($b: String) { + ...FragB + } + fragment FragA on Type { + field(a: $a) + } + fragment FragB on Type { + field(b: $b) + } + `) +} +func TestValidate_NoUndefinedVariables_VariableWithinRecursiveFragmentDefined(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) { + ...FragA + } + } + `) +} +func TestValidate_NoUndefinedVariables_VariableNotDefined(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + field(a: $a, b: $b, c: $c, d: $d) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$d" is not defined.`, 3, 39), + }) +} +func TestValidate_NoUndefinedVariables_VariableNotDefinedByUnnamedQuery(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + { + field(a: $a) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is not defined.`, 3, 18), + }) +} +func TestValidate_NoUndefinedVariables_MultipleVariablesNotDefined(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($b: String) { + field(a: $a, b: $b, c: $c) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is not defined.`, 3, 18), + testutil.RuleError(`Variable "$c" is not defined.`, 3, 32), + }) +} +func TestValidate_NoUndefinedVariables_VariableInFragmentNotDefinedByUnnamedQuery(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + { + ...FragA + } + fragment FragA on Type { + field(a: $a) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is not defined.`, 6, 18), + }) +} +func TestValidate_NoUndefinedVariables_VariableInFragmentNotDefinedByOperation(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String, $b: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + fragment FragC on Type { + field(c: $c) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$c" is not defined by operation "Foo".`, 16, 18, 2, 7), + }) +} +func TestValidate_NoUndefinedVariables_MultipleVariablesInFragmentsNotDefined(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($b: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + fragment FragC on Type { + field(c: $c) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is not defined by operation "Foo".`, 6, 18, 2, 7), + testutil.RuleError(`Variable "$c" is not defined by operation "Foo".`, 16, 18, 2, 7), + }) +} +func TestValidate_NoUndefinedVariables_SingleVariableInFragmentNotDefinedByMultipleOperations(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($a: String) { + ...FragAB + } + query Bar($a: String) { + ...FragAB + } + fragment FragAB on Type { + field(a: $a, b: $b) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$b" is not defined by operation "Foo".`, 9, 25, 2, 7), + testutil.RuleError(`Variable "$b" is not defined by operation "Bar".`, 9, 25, 5, 7), + }) +} +func TestValidate_NoUndefinedVariables_VariablesInFragmentNotDefinedByMultipleOperations(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($b: String) { + ...FragAB + } + query Bar($a: String) { + ...FragAB + } + fragment FragAB on Type { + field(a: $a, b: $b) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is not defined by operation "Foo".`, 9, 18, 2, 7), + testutil.RuleError(`Variable "$b" is not defined by operation "Bar".`, 9, 25, 5, 7), + }) +} +func TestValidate_NoUndefinedVariables_VariableInFragmentUsedByOtherOperation(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($b: String) { + ...FragA + } + query Bar($a: String) { + ...FragB + } + fragment FragA on Type { + field(a: $a) + } + fragment FragB on Type { + field(b: $b) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is not defined by operation "Foo".`, 9, 18, 2, 7), + testutil.RuleError(`Variable "$b" is not defined by operation "Bar".`, 12, 18, 5, 7), + }) +} +func TestValidate_NoUndefinedVariables_VaMultipleUndefinedVariablesProduceMultipleErrors(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUndefinedVariablesRule, ` + query Foo($b: String) { + ...FragAB + } + query Bar($a: String) { + ...FragAB + } + fragment FragAB on Type { + field1(a: $a, b: $b) + ...FragC + field3(a: $a, b: $b) + } + fragment FragC on Type { + field2(c: $c) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is not defined by operation "Foo".`, 9, 19, 2, 7), + testutil.RuleError(`Variable "$c" is not defined by operation "Foo".`, 14, 19, 2, 7), + testutil.RuleError(`Variable "$a" is not defined by operation "Foo".`, 11, 19, 2, 7), + testutil.RuleError(`Variable "$b" is not defined by operation "Bar".`, 9, 26, 5, 7), + testutil.RuleError(`Variable "$c" is not defined by operation "Bar".`, 14, 19, 5, 7), + testutil.RuleError(`Variable "$b" is not defined by operation "Bar".`, 11, 26, 5, 7), + }) +} diff --git a/rules_no_unused_fragments_test.go b/rules_no_unused_fragments_test.go new file mode 100644 index 00000000..47f70ad3 --- /dev/null +++ b/rules_no_unused_fragments_test.go @@ -0,0 +1,138 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_NoUnusedFragments_AllFragmentNamesAreUsed(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedFragmentsRule, ` + { + human(id: 4) { + ...HumanFields1 + ... on Human { + ...HumanFields2 + } + } + } + fragment HumanFields1 on Human { + name + ...HumanFields3 + } + fragment HumanFields2 on Human { + name + } + fragment HumanFields3 on Human { + name + } + `) +} +func TestValidate_NoUnusedFragments_AllFragmentNamesAreUsedByMultipleOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedFragmentsRule, ` + query Foo { + human(id: 4) { + ...HumanFields1 + } + } + query Bar { + human(id: 4) { + ...HumanFields2 + } + } + fragment HumanFields1 on Human { + name + ...HumanFields3 + } + fragment HumanFields2 on Human { + name + } + fragment HumanFields3 on Human { + name + } + `) +} +func TestValidate_NoUnusedFragments_ContainsUnknownFragments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedFragmentsRule, ` + query Foo { + human(id: 4) { + ...HumanFields1 + } + } + query Bar { + human(id: 4) { + ...HumanFields2 + } + } + fragment HumanFields1 on Human { + name + ...HumanFields3 + } + fragment HumanFields2 on Human { + name + } + fragment HumanFields3 on Human { + name + } + fragment Unused1 on Human { + name + } + fragment Unused2 on Human { + name + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "Unused1" is never used.`, 22, 7), + testutil.RuleError(`Fragment "Unused2" is never used.`, 25, 7), + }) +} +func TestValidate_NoUnusedFragments_ContainsUnknownFragmentsWithRefCycle(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedFragmentsRule, ` + query Foo { + human(id: 4) { + ...HumanFields1 + } + } + query Bar { + human(id: 4) { + ...HumanFields2 + } + } + fragment HumanFields1 on Human { + name + ...HumanFields3 + } + fragment HumanFields2 on Human { + name + } + fragment HumanFields3 on Human { + name + } + fragment Unused1 on Human { + name + ...Unused2 + } + fragment Unused2 on Human { + name + ...Unused1 + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "Unused1" is never used.`, 22, 7), + testutil.RuleError(`Fragment "Unused2" is never used.`, 26, 7), + }) +} +func TestValidate_NoUnusedFragments_ContainsUnknownAndUndefFragments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedFragmentsRule, ` + query Foo { + human(id: 4) { + ...bar + } + } + fragment foo on Human { + name + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "foo" is never used.`, 7, 7), + }) +} diff --git a/rules_no_unused_variables_test.go b/rules_no_unused_variables_test.go new file mode 100644 index 00000000..d3bcdae4 --- /dev/null +++ b/rules_no_unused_variables_test.go @@ -0,0 +1,189 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_NoUnusedVariables_UsesAllVariables(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + field(a: $a, b: $b, c: $c) + } + `) +} +func TestValidate_NoUnusedVariables_UsesAllVariablesDeeply(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + field(a: $a) { + field(b: $b) { + field(c: $c) + } + } + } + `) +} +func TestValidate_NoUnusedVariables_UsesAllVariablesDeeplyInInlineFragments(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + ... on Type { + field(a: $a) { + field(b: $b) { + ... on Type { + field(c: $c) + } + } + } + } + } + `) +} +func TestValidate_NoUnusedVariables_UsesAllVariablesInFragments(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + fragment FragC on Type { + field(c: $c) + } + `) +} +func TestValidate_NoUnusedVariables_VariableUsedByFragmentInMultipleOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String) { + ...FragA + } + query Bar($b: String) { + ...FragB + } + fragment FragA on Type { + field(a: $a) + } + fragment FragB on Type { + field(b: $b) + } + `) +} +func TestValidate_NoUnusedVariables_VariableUsedByRecursiveFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) { + ...FragA + } + } + `) +} +func TestValidate_NoUnusedVariables_VariableNotUsed(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + field(a: $a, b: $b) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + }) +} +func TestValidate_NoUnusedVariables_MultipleVariablesNotUsed(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + field(b: $b) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is never used.`, 2, 17), + testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + }) +} +func TestValidate_NoUnusedVariables_VariableNotUsedInFragments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + fragment FragC on Type { + field + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + }) +} +func TestValidate_NoUnusedVariables_MultipleVariablesNotUsed2(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($a: String, $b: String, $c: String) { + ...FragA + } + fragment FragA on Type { + field { + ...FragB + } + } + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + fragment FragC on Type { + field + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" is never used.`, 2, 17), + testutil.RuleError(`Variable "$c" is never used.`, 2, 41), + }) +} +func TestValidate_NoUnusedVariables_VariableNotUsedByUnreferencedFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($b: String) { + ...FragA + } + fragment FragA on Type { + field(a: $a) + } + fragment FragB on Type { + field(b: $b) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$b" is never used.`, 2, 17), + }) +} +func TestValidate_NoUnusedVariables_VariableNotUsedByFragmentUsedByOtherOperation(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.NoUnusedVariablesRule, ` + query Foo($b: String) { + ...FragA + } + query Bar($a: String) { + ...FragB + } + fragment FragA on Type { + field(a: $a) + } + fragment FragB on Type { + field(b: $b) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$b" is never used.`, 2, 17), + testutil.RuleError(`Variable "$a" is never used.`, 5, 17), + }) +} diff --git a/rules_overlapping_fields_can_be_merged_test.go b/rules_overlapping_fields_can_be_merged_test.go new file mode 100644 index 00000000..755c8bbe --- /dev/null +++ b/rules_overlapping_fields_can_be_merged_test.go @@ -0,0 +1,410 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_OverlappingFieldsCanBeMerged_UniqueFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment uniqueFields on Dog { + name + nickname + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_IdenticalFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment mergeIdenticalFields on Dog { + name + name + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_IdenticalFieldsWithIdenticalArgs(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment mergeIdenticalFieldsWithIdenticalArgs on Dog { + doesKnowCommand(dogCommand: SIT) + doesKnowCommand(dogCommand: SIT) + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_IdenticalFieldsWithIdenticalDirectives(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment mergeSameFieldsWithSameDirectives on Dog { + name @include(if: true) + name @include(if: true) + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_DifferentArgsWithDifferentAliases(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment differentArgsWithDifferentAliases on Dog { + knowsSit: doesKnowCommand(dogCommand: SIT) + knowsDown: doesKnowCommand(dogCommand: DOWN) + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_DifferentDirectivesWithDifferentAliases(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment differentDirectivesWithDifferentAliases on Dog { + nameIfTrue: name @include(if: true) + nameIfFalse: name @include(if: false) + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_SameAliasesWithDifferentFieldTargets(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment sameAliasesWithDifferentFieldTargets on Dog { + fido: name + fido: nickname + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "fido" conflict because name and nickname are different fields.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_AliasMakingDirectFieldAccess(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment aliasMaskingDirectFieldAccess on Dog { + name: nickname + name + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "name" conflict because nickname and name are different fields.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ConflictingArgs(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingArgs on Dog { + doesKnowCommand(dogCommand: SIT) + doesKnowCommand(dogCommand: HEEL) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing arguments.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ConflictingDirectives(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingDirectiveArgs on Dog { + name @include(if: true) + name @skip(if: false) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "name" conflict because they have differing directives.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ConflictingDirectiveArgs(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingDirectiveArgs on Dog { + name @include(if: true) + name @include(if: false) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "name" conflict because they have differing directives.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ConflictingArgsWithMatchingDirectives(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingArgsWithMatchingDirectiveArgs on Dog { + doesKnowCommand(dogCommand: SIT) @include(if: true) + doesKnowCommand(dogCommand: HEEL) @include(if: true) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing arguments.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ConflictingDirectivesWithMatchingArgs(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment conflictingDirectiveArgsWithMatchingArgs on Dog { + doesKnowCommand(dogCommand: SIT) @include(if: true) + doesKnowCommand(dogCommand: SIT) @skip(if: false) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "doesKnowCommand" conflict because they have differing directives.`, 3, 9, 4, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_EncountersConflictInFragments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + { + ...A + ...B + } + fragment A on Type { + x: a + } + fragment B on Type { + x: b + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "x" conflict because a and b are different fields.`, 7, 9, 10, 9), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ReportsEachConflictOnce(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + { + f1 { + ...A + ...B + } + f2 { + ...B + ...A + } + f3 { + ...A + ...B + x: c + } + } + fragment A on Type { + x: a + } + fragment B on Type { + x: b + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "x" conflict because a and b are different fields.`, 18, 9, 21, 9), + testutil.RuleError(`Fields "x" conflict because a and c are different fields.`, 18, 9, 14, 11), + testutil.RuleError(`Fields "x" conflict because b and c are different fields.`, 21, 9, 14, 11), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_DeepConflict(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + { + field { + x: a + }, + field { + x: b + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fields "field" conflict because subfields "x" conflict because a and b are different fields.`, + 3, 9, 6, 9, 4, 11, 7, 11), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_DeepConflictWithMultipleIssues(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + { + field { + x: a + y: c + }, + field { + x: b + y: d + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError( + `Fields "field" conflict because subfields "x" conflict because a and b are different fields and `+ + `subfields "y" conflict because c and d are different fields.`, + 3, 9, 7, 9, 4, 11, 8, 11, 5, 11, 9, 11), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_VeryDeepConflict(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + { + field { + deepField { + x: a + } + }, + field { + deepField { + x: b + } + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError( + `Fields "field" conflict because subfields "deepField" conflict because subfields "x" conflict because `+ + `a and b are different fields.`, + 3, 9, 8, 9, 4, 11, 9, 11, 5, 13, 10, 13), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ReportsDeepConflictToNearestCommonAncestor(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + { + field { + deepField { + x: a + } + deepField { + x: b + } + }, + field { + deepField { + y + } + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError( + `Fields "deepField" conflict because subfields "x" conflict because `+ + `a and b are different fields.`, + 4, 11, 7, 11, 5, 13, 8, 13), + }) +} + +var stringBoxObject = graphql.NewObject(graphql.ObjectConfig{ + Name: "StringBox", + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.String, + }, + }, +}) +var intBoxObject = graphql.NewObject(graphql.ObjectConfig{ + Name: "IntBox", + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.Int, + }, + }, +}) +var nonNullStringBox1Object = graphql.NewObject(graphql.ObjectConfig{ + Name: "NonNullStringBox1", + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + }, + }, +}) +var nonNullStringBox2Object = graphql.NewObject(graphql.ObjectConfig{ + Name: "NonNullStringBox2", + Fields: graphql.Fields{ + "scalar": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + }, + }, +}) +var boxUnionObject = graphql.NewUnion(graphql.UnionConfig{ + Name: "BoxUnion", + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + return stringBoxObject + }, + Types: []*graphql.Object{ + stringBoxObject, + intBoxObject, + nonNullStringBox1Object, + nonNullStringBox2Object, + }, +}) + +var connectionObject = graphql.NewObject(graphql.ObjectConfig{ + Name: "Connection", + Fields: graphql.Fields{ + "edges": &graphql.Field{ + Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{ + Name: "Edge", + Fields: graphql.Fields{ + "node": &graphql.Field{ + Type: graphql.NewObject(graphql.ObjectConfig{ + Name: "Node", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.ID, + }, + "name": &graphql.Field{ + Type: graphql.String, + }, + }, + }), + }, + }, + })), + }, + }, +}) +var schema, _ = graphql.NewSchema(graphql.SchemaConfig{ + Query: graphql.NewObject(graphql.ObjectConfig{ + Name: "QueryRoot", + Fields: graphql.Fields{ + "boxUnion": &graphql.Field{ + Type: boxUnionObject, + }, + "connection": &graphql.Field{ + Type: connectionObject, + }, + }, + }), +}) + +func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_ConflictingScalarReturnTypes(t *testing.T) { + testutil.ExpectFailsRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` + { + boxUnion { + ...on IntBox { + scalar + } + ...on StringBox { + scalar + } + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError( + `Fields "scalar" conflict because they return differing types Int and String.`, + 5, 15, 8, 15), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_SameWrappedScalarReturnTypes(t *testing.T) { + testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` + { + boxUnion { + ...on NonNullStringBox1 { + scalar + } + ...on NonNullStringBox2 { + scalar + } + } + } + `) +} +func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_ComparesDeepTypesIncludingList(t *testing.T) { + testutil.ExpectFailsRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` + { + connection { + ...edgeID + edges { + node { + id: name + } + } + } + } + + fragment edgeID on Connection { + edges { + node { + id + } + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError( + `Fields "edges" conflict because subfields "node" conflict because subfields "id" conflict because `+ + `id and name are different fields.`, + 14, 11, 5, 13, 15, 13, 6, 15, 16, 15, 7, 17), + }) +} +func TestValidate_OverlappingFieldsCanBeMerged_ReturnTypesMustBeUnambiguous_IgnoresUnknownTypes(t *testing.T) { + testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.OverlappingFieldsCanBeMergedRule, ` + { + boxUnion { + ...on UnknownType { + scalar + } + ...on NonNullStringBox2 { + scalar + } + } + } + `) +} diff --git a/rules_possible_fragment_spreads_test.go b/rules_possible_fragment_spreads_test.go new file mode 100644 index 00000000..9c0dff54 --- /dev/null +++ b/rules_possible_fragment_spreads_test.go @@ -0,0 +1,182 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_PossibleFragmentSpreads_OfTheSameObject(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment objectWithinObject on Dog { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + `) +} +func TestValidate_PossibleFragmentSpreads_OfTheSameObjectWithInlineFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment objectWithinObjectAnon on Dog { ... on Dog { barkVolume } } + `) +} +func TestValidate_PossibleFragmentSpreads_ObjectIntoAnImplementedInterface(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment objectWithinInterface on Pet { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + `) +} +func TestValidate_PossibleFragmentSpreads_ObjectIntoContainingUnion(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment objectWithinUnion on CatOrDog { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + `) +} + +func TestValidate_PossibleFragmentSpreads_UnionIntoContainedObject(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment unionWithinObject on Dog { ...catOrDogFragment } + fragment catOrDogFragment on CatOrDog { __typename } + `) +} +func TestValidate_PossibleFragmentSpreads_UnionIntoOverlappingInterface(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment unionWithinInterface on Pet { ...catOrDogFragment } + fragment catOrDogFragment on CatOrDog { __typename } + `) +} +func TestValidate_PossibleFragmentSpreads_UnionIntoOverlappingUnion(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment unionWithinUnion on DogOrHuman { ...catOrDogFragment } + fragment catOrDogFragment on CatOrDog { __typename } + `) +} + +func TestValidate_PossibleFragmentSpreads_InterfaceIntoImplementedObject(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment interfaceWithinObject on Dog { ...petFragment } + fragment petFragment on Pet { name } + `) +} +func TestValidate_PossibleFragmentSpreads_InterfaceIntoOverlappingInterface(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment interfaceWithinInterface on Pet { ...beingFragment } + fragment beingFragment on Being { name } + `) +} +func TestValidate_PossibleFragmentSpreads_InterfaceIntoOverlappingInterfaceInInlineFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment interfaceWithinInterface on Pet { ... on Being { name } } + `) +} +func TestValidate_PossibleFragmentSpreads_InterfaceIntoOverlappingUnion(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment interfaceWithinUnion on CatOrDog { ...petFragment } + fragment petFragment on Pet { name } + `) +} +func TestValidate_PossibleFragmentSpreads_DifferentObjectIntoObject(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidObjectWithinObject on Cat { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "dogFragment" cannot be spread here as objects of `+ + `type "Cat" can never be of type "Dog".`, 2, 51), + }) +} +func TestValidate_PossibleFragmentSpreads_DifferentObjectIntoObjectInInlineFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidObjectWithinObjectAnon on Cat { + ... on Dog { barkVolume } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment cannot be spread here as objects of `+ + `type "Cat" can never be of type "Dog".`, 3, 9), + }) +} + +func TestValidate_PossibleFragmentSpreads_ObjectIntoNotImplementingInterface(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidObjectWithinInterface on Pet { ...humanFragment } + fragment humanFragment on Human { pets { name } } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "humanFragment" cannot be spread here as objects of `+ + `type "Pet" can never be of type "Human".`, 2, 54), + }) +} +func TestValidate_PossibleFragmentSpreads_ObjectIntoNotContainingUnion(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidObjectWithinUnion on CatOrDog { ...humanFragment } + fragment humanFragment on Human { pets { name } } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "humanFragment" cannot be spread here as objects of `+ + `type "CatOrDog" can never be of type "Human".`, 2, 55), + }) +} + +func TestValidate_PossibleFragmentSpreads_UnionIntoNotContainedObject(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidUnionWithinObject on Human { ...catOrDogFragment } + fragment catOrDogFragment on CatOrDog { __typename } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "catOrDogFragment" cannot be spread here as objects of `+ + `type "Human" can never be of type "CatOrDog".`, 2, 52), + }) +} +func TestValidate_PossibleFragmentSpreads_UnionIntoNonOverlappingInterface(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidUnionWithinInterface on Pet { ...humanOrAlienFragment } + fragment humanOrAlienFragment on HumanOrAlien { __typename } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "humanOrAlienFragment" cannot be spread here as objects of `+ + `type "Pet" can never be of type "HumanOrAlien".`, 2, 53), + }) +} +func TestValidate_PossibleFragmentSpreads_UnionIntoNonOverlappingUnion(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidUnionWithinUnion on CatOrDog { ...humanOrAlienFragment } + fragment humanOrAlienFragment on HumanOrAlien { __typename } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "humanOrAlienFragment" cannot be spread here as objects of `+ + `type "CatOrDog" can never be of type "HumanOrAlien".`, 2, 54), + }) +} + +func TestValidate_PossibleFragmentSpreads_InterfaceIntoNonImplementingObject(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidInterfaceWithinObject on Cat { ...intelligentFragment } + fragment intelligentFragment on Intelligent { iq } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "intelligentFragment" cannot be spread here as objects of `+ + `type "Cat" can never be of type "Intelligent".`, 2, 54), + }) +} +func TestValidate_PossibleFragmentSpreads_InterfaceIntoNonOverlappingInterface(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidInterfaceWithinInterface on Pet { + ...intelligentFragment + } + fragment intelligentFragment on Intelligent { iq } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "intelligentFragment" cannot be spread here as objects of `+ + `type "Pet" can never be of type "Intelligent".`, 3, 9), + }) +} +func TestValidate_PossibleFragmentSpreads_InterfaceIntoNonOverlappingInterfaceInInlineFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidInterfaceWithinInterfaceAnon on Pet { + ...on Intelligent { iq } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment cannot be spread here as objects of `+ + `type "Pet" can never be of type "Intelligent".`, 3, 9), + }) +} +func TestValidate_PossibleFragmentSpreads_InterfaceIntoNonOverlappingUnion(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.PossibleFragmentSpreadsRule, ` + fragment invalidInterfaceWithinUnion on HumanOrAlien { ...petFragment } + fragment petFragment on Pet { name } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Fragment "petFragment" cannot be spread here as objects of `+ + `type "HumanOrAlien" can never be of type "Pet".`, 2, 62), + }) +} diff --git a/rules_provided_non_null_arguments_test.go b/rules_provided_non_null_arguments_test.go new file mode 100644 index 00000000..fed6c008 --- /dev/null +++ b/rules_provided_non_null_arguments_test.go @@ -0,0 +1,177 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_ProvidedNonNullArguments_IgnoresUnknownArguments(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + dog { + isHousetrained(unknownArgument: true) + } + } + `) +} + +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_ArgOnOptionalArg(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + dog { + isHousetrained(atOtherHomes: true) + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_NoArgOnOptionalArg(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + dog { + isHousetrained + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_MultipleArgs(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleReqs(req1: 1, req2: 2) + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_MultipleArgsReverseOrder(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleReqs(req2: 2, req1: 1) + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_NoArgsOnMultipleOptional(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleOpts + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_OneArgOnMultipleOptional(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleOpts(opt1: 1) + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_SecondArgOnMultipleOptional(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleOpts(opt2: 1) + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_MultipleReqsOnMixedList(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4) + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_MultipleReqsAndOneOptOnMixedList(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4, opt1: 5) + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_ValidNonNullableValue_AllReqsAndOptsOnMixedList(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) + } + } + `) +} + +func TestValidate_ProvidedNonNullArguments_InvalidNonNullableValue_MissingOneNonNullableArgument(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleReqs(req2: 2) + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "multipleReqs" argument "req1" of type "Int!" is required but not provided.`, 4, 13), + }) +} +func TestValidate_ProvidedNonNullArguments_InvalidNonNullableValue_MissingMultipleNonNullableArguments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleReqs + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "multipleReqs" argument "req1" of type "Int!" is required but not provided.`, 4, 13), + testutil.RuleError(`Field "multipleReqs" argument "req2" of type "Int!" is required but not provided.`, 4, 13), + }) +} +func TestValidate_ProvidedNonNullArguments_InvalidNonNullableValue_IncorrectValueAndMissingArgument(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + complicatedArgs { + multipleReqs(req1: "one") + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "multipleReqs" argument "req2" of type "Int!" is required but not provided.`, 4, 13), + }) +} + +func TestValidate_ProvidedNonNullArguments_DirectiveArguments_IgnoresUnknownDirectives(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + dog @unknown + } + `) +} +func TestValidate_ProvidedNonNullArguments_DirectiveArguments_WithDirectivesOfValidTypes(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + dog @include(if: true) { + name + } + human @skip(if: false) { + name + } + } + `) +} +func TestValidate_ProvidedNonNullArguments_DirectiveArguments_WithDirectiveWithMissingTypes(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ProvidedNonNullArgumentsRule, ` + { + dog @include { + name @skip + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Directive "@include" argument "if" of type "Boolean!" is required but not provided.`, 3, 15), + testutil.RuleError(`Directive "@skip" argument "if" of type "Boolean!" is required but not provided.`, 4, 18), + }) +} diff --git a/rules_scalar_leafs_test.go b/rules_scalar_leafs_test.go new file mode 100644 index 00000000..09729952 --- /dev/null +++ b/rules_scalar_leafs_test.go @@ -0,0 +1,88 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_ScalarLeafs_ValidScalarSelection(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ScalarLeafsRule, ` + fragment scalarSelection on Dog { + barks + } + `) +} +func TestValidate_ScalarLeafs_ObjectTypeMissingSelection(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ScalarLeafsRule, ` + query directQueryOnObjectWithoutSubFields { + human + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "human" of type "Human" must have a sub selection.`, 3, 9), + }) +} +func TestValidate_ScalarLeafs_InterfaceTypeMissingSelection(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ScalarLeafsRule, ` + { + human { pets } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "pets" of type "[Pet]" must have a sub selection.`, 3, 17), + }) +} +func TestValidate_ScalarLeafs_ValidScalarSelectionWithArgs(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.ScalarLeafsRule, ` + fragment scalarSelectionWithArgs on Dog { + doesKnowCommand(dogCommand: SIT) + } + `) +} + +func TestValidate_ScalarLeafs_ScalarSelectionNotAllowedOnBoolean(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ScalarLeafsRule, ` + fragment scalarSelectionsNotAllowedOnBoolean on Dog { + barks { sinceWhen } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "barks" of type "Boolean" must not have a sub selection.`, 3, 15), + }) +} +func TestValidate_ScalarLeafs_ScalarSelectionNotAllowedOnEnum(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ScalarLeafsRule, ` + fragment scalarSelectionsNotAllowedOnEnum on Cat { + furColor { inHexdec } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "furColor" of type "FurColor" must not have a sub selection.`, 3, 18), + }) +} +func TestValidate_ScalarLeafs_ScalarSelectionNotAllowedWithArgs(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ScalarLeafsRule, ` + fragment scalarSelectionsNotAllowedWithArgs on Dog { + doesKnowCommand(dogCommand: SIT) { sinceWhen } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "doesKnowCommand" of type "Boolean" must not have a sub selection.`, 3, 42), + }) +} +func TestValidate_ScalarLeafs_ScalarSelectionNotAllowedWithDirectives(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ScalarLeafsRule, ` + fragment scalarSelectionsNotAllowedWithDirectives on Dog { + name @include(if: true) { isAlsoHumanName } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "name" of type "String" must not have a sub selection.`, 3, 33), + }) +} +func TestValidate_ScalarLeafs_ScalarSelectionNotAllowedWithDirectivesAndArgs(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.ScalarLeafsRule, ` + fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog { + doesKnowCommand(dogCommand: SIT) @include(if: true) { sinceWhen } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Field "doesKnowCommand" of type "Boolean" must not have a sub selection.`, 3, 61), + }) +} diff --git a/rules_unique_argument_names_test.go b/rules_unique_argument_names_test.go new file mode 100644 index 00000000..2c111b80 --- /dev/null +++ b/rules_unique_argument_names_test.go @@ -0,0 +1,112 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_UniqueArgumentNames_NoArgumentsOnField(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field + } + `) +} +func TestValidate_UniqueArgumentNames_NoArgumentsOnDirective(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field + } + `) +} +func TestValidate_UniqueArgumentNames_ArgumentOnField(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field(arg: "value") + } + `) +} +func TestValidate_UniqueArgumentNames_ArgumentOnDirective(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field @directive(arg: "value") + } + `) +} +func TestValidate_UniqueArgumentNames_SameArgumentOnTwoFields(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + one: field(arg: "value") + two: field(arg: "value") + } + `) +} +func TestValidate_UniqueArgumentNames_SameArgumentOnFieldAndDirective(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field(arg: "value") @directive(arg: "value") + } + `) +} +func TestValidate_UniqueArgumentNames_SameArgumentOnTwoDirectives(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field @directive1(arg: "value") @directive2(arg: "value") + } + `) +} +func TestValidate_UniqueArgumentNames_MultipleFieldArguments(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field(arg1: "value", arg2: "value", arg3: "value") + } + `) +} +func TestValidate_UniqueArgumentNames_MultipleDirectiveArguments(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueArgumentNamesRule, ` + { + field @directive(arg1: "value", arg2: "value", arg3: "value") + } + `) +} +func TestValidate_UniqueArgumentNames_DuplicateFieldArguments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueArgumentNamesRule, ` + { + field(arg1: "value", arg1: "value") + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can be only one argument named "arg1".`, 3, 15, 3, 30), + }) +} +func TestValidate_UniqueArgumentNames_ManyDuplicateFieldArguments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueArgumentNamesRule, ` + { + field(arg1: "value", arg1: "value", arg1: "value") + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can be only one argument named "arg1".`, 3, 15, 3, 30), + testutil.RuleError(`There can be only one argument named "arg1".`, 3, 15, 3, 45), + }) +} +func TestValidate_UniqueArgumentNames_DuplicateDirectiveArguments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueArgumentNamesRule, ` + { + field @directive(arg1: "value", arg1: "value") + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can be only one argument named "arg1".`, 3, 26, 3, 41), + }) +} +func TestValidate_UniqueArgumentNames_ManyDuplicateDirectiveArguments(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueArgumentNamesRule, ` + { + field @directive(arg1: "value", arg1: "value", arg1: "value") + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can be only one argument named "arg1".`, 3, 26, 3, 41), + testutil.RuleError(`There can be only one argument named "arg1".`, 3, 26, 3, 56), + }) +} diff --git a/rules_unique_fragment_names_test.go b/rules_unique_fragment_names_test.go new file mode 100644 index 00000000..5cacd5e9 --- /dev/null +++ b/rules_unique_fragment_names_test.go @@ -0,0 +1,95 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_UniqueFragmentNames_NoFragments(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueFragmentNamesRule, ` + { + field + } + `) +} +func TestValidate_UniqueFragmentNames_OneFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueFragmentNamesRule, ` + { + ...fragA + } + + fragment fragA on Type { + field + } + `) +} +func TestValidate_UniqueFragmentNames_ManyFragments(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueFragmentNamesRule, ` + { + ...fragA + ...fragB + ...fragC + } + fragment fragA on Type { + fieldA + } + fragment fragB on Type { + fieldB + } + fragment fragC on Type { + fieldC + } + `) +} +func TestValidate_UniqueFragmentNames_InlineFragmentsAreAlwaysUnique(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueFragmentNamesRule, ` + { + ...on Type { + fieldA + } + ...on Type { + fieldB + } + } + `) +} +func TestValidate_UniqueFragmentNames_FragmentAndOperationNamedTheSame(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueFragmentNamesRule, ` + query Foo { + ...Foo + } + fragment Foo on Type { + field + } + `) +} +func TestValidate_UniqueFragmentNames_FragmentsNamedTheSame(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueFragmentNamesRule, ` + { + ...fragA + } + fragment fragA on Type { + fieldA + } + fragment fragA on Type { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can only be one fragment named "fragA".`, 5, 16, 8, 16), + }) +} +func TestValidate_UniqueFragmentNames_FragmentsNamedTheSameWithoutBeingReferenced(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueFragmentNamesRule, ` + fragment fragA on Type { + fieldA + } + fragment fragA on Type { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can only be one fragment named "fragA".`, 2, 16, 5, 16), + }) +} diff --git a/rules_unique_operation_names_test.go b/rules_unique_operation_names_test.go new file mode 100644 index 00000000..44e59b60 --- /dev/null +++ b/rules_unique_operation_names_test.go @@ -0,0 +1,88 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_UniqueOperationNames_NoOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueOperationNamesRule, ` + fragment fragA on Type { + field + } + `) +} +func TestValidate_UniqueOperationNames_OneAnonOperation(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueOperationNamesRule, ` + { + field + } + `) +} +func TestValidate_UniqueOperationNames_OneNamedOperation(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueOperationNamesRule, ` + query Foo { + field + } + `) +} +func TestValidate_UniqueOperationNames_MultipleOperations(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueOperationNamesRule, ` + query Foo { + field + } + + query Bar { + field + } + `) +} +func TestValidate_UniqueOperationNames_MultipleOperationsOfDifferentTypes(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueOperationNamesRule, ` + query Foo { + field + } + + mutation Bar { + field + } + `) +} +func TestValidate_UniqueOperationNames_FragmentAndOperationNamedTheSame(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.UniqueOperationNamesRule, ` + query Foo { + field + } + + mutation Bar { + field + } + `) +} +func TestValidate_UniqueOperationNames_MultipleOperationsOfSameName(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueOperationNamesRule, ` + query Foo { + fieldA + } + query Foo { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can only be one operation named "Foo".`, 2, 13, 5, 13), + }) +} +func TestValidate_UniqueOperationNames_MultipleOperationsOfSameNameOfDifferentTypes(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.UniqueOperationNamesRule, ` + query Foo { + fieldA + } + mutation Foo { + fieldB + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`There can only be one operation named "Foo".`, 2, 13, 5, 16), + }) +} diff --git a/rules_variables_are_input_types_test.go b/rules_variables_are_input_types_test.go new file mode 100644 index 00000000..fb1d1675 --- /dev/null +++ b/rules_variables_are_input_types_test.go @@ -0,0 +1,28 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_VariablesAreInputTypes_(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesAreInputTypesRule, ` + query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) { + field(a: $a, b: $b, c: $c) + } + `) +} +func TestValidate_VariablesAreInputTypes_1(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesAreInputTypesRule, ` + query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) { + field(a: $a, b: $b, c: $c) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$a" cannot be non-input type "Dog".`, 2, 21), + testutil.RuleError(`Variable "$b" cannot be non-input type "[[CatOrDog!]]!".`, 2, 30), + testutil.RuleError(`Variable "$c" cannot be non-input type "Pet".`, 2, 50), + }) +} diff --git a/rules_variables_in_allowed_position_test.go b/rules_variables_in_allowed_position_test.go new file mode 100644 index 00000000..83ee2aa7 --- /dev/null +++ b/rules_variables_in_allowed_position_test.go @@ -0,0 +1,253 @@ +package graphql_test + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/testutil" +) + +func TestValidate_VariablesInAllowedPosition_BooleanToBoolean(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($booleanArg: Boolean) + { + complicatedArgs { + booleanArgField(booleanArg: $booleanArg) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_BooleanToBooleanWithinFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + fragment booleanArgFrag on ComplicatedArgs { + booleanArgField(booleanArg: $booleanArg) + } + query Query($booleanArg: Boolean) + { + complicatedArgs { + ...booleanArgFrag + } + } + `) + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($booleanArg: Boolean) + { + complicatedArgs { + ...booleanArgFrag + } + } + fragment booleanArgFrag on ComplicatedArgs { + booleanArgField(booleanArg: $booleanArg) + } + `) +} +func TestValidate_VariablesInAllowedPosition_NonNullableBooleanToBoolean(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($nonNullBooleanArg: Boolean!) + { + complicatedArgs { + booleanArgField(booleanArg: $nonNullBooleanArg) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_NonNullableBooleanToBooleanWithinFragment(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + fragment booleanArgFrag on ComplicatedArgs { + booleanArgField(booleanArg: $nonNullBooleanArg) + } + + query Query($nonNullBooleanArg: Boolean!) + { + complicatedArgs { + ...booleanArgFrag + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithDefault(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($intArg: Int = 1) + { + complicatedArgs { + nonNullIntArgField(nonNullIntArg: $intArg) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_ListOfStringToListOfString(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($stringListVar: [String]) + { + complicatedArgs { + stringListArgField(stringListArg: $stringListVar) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_ListOfNonNullableStringToListOfString(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($stringListVar: [String!]) + { + complicatedArgs { + stringListArgField(stringListArg: $stringListVar) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_StringToListOfStringInItemPosition(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($stringVar: String) + { + complicatedArgs { + stringListArgField(stringListArg: [$stringVar]) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_NonNullableStringToListOfStringInItemPosition(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($stringVar: String!) + { + complicatedArgs { + stringListArgField(stringListArg: [$stringVar]) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_ComplexInputToComplexInput(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($complexVar: ComplexInput) + { + complicatedArgs { + complexArgField(complexArg: $ComplexInput) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_ComplexInputToComplexInputInFieldPosition(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($boolVar: Boolean = false) + { + complicatedArgs { + complexArgField(complexArg: {requiredArg: $boolVar}) + } + } + `) +} +func TestValidate_VariablesInAllowedPosition_NonNullableBooleanToNonNullableBooleanInDirective(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($boolVar: Boolean!) + { + dog @include(if: $boolVar) + } + `) +} +func TestValidate_VariablesInAllowedPosition_NonNullableBooleanToNonNullableBooleanInDirectiveInDirectiveWithDefault(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($boolVar: Boolean = false) + { + dog @include(if: $boolVar) + } + `) +} +func TestValidate_VariablesInAllowedPosition_IntToNonNullableInt(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($intArg: Int) + { + complicatedArgs { + nonNullIntArgField(nonNullIntArg: $intArg) + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ + `expecting type "Int!".`, 5, 45), + }) +} +func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` + fragment nonNullIntArgFieldFrag on ComplicatedArgs { + nonNullIntArgField(nonNullIntArg: $intArg) + } + + query Query($intArg: Int) + { + complicatedArgs { + ...nonNullIntArgFieldFrag + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ + `expecting type "Int!".`, 3, 43), + }) +} +func TestValidate_VariablesInAllowedPosition_IntToNonNullableIntWithinNestedFragment(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` + fragment outerFrag on ComplicatedArgs { + ...nonNullIntArgFieldFrag + } + + fragment nonNullIntArgFieldFrag on ComplicatedArgs { + nonNullIntArgField(nonNullIntArg: $intArg) + } + + query Query($intArg: Int) + { + complicatedArgs { + ...outerFrag + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$intArg" of type "Int" used in position `+ + `expecting type "Int!".`, 7, 43), + }) +} +func TestValidate_VariablesInAllowedPosition_StringOverBoolean(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($stringVar: String) + { + complicatedArgs { + booleanArgField(booleanArg: $stringVar) + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ + `expecting type "Boolean".`, 5, 39), + }) +} +func TestValidate_VariablesInAllowedPosition_StringToListOfString(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($stringVar: String) + { + complicatedArgs { + stringListArgField(stringListArg: $stringVar) + } + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ + `expecting type "[String]".`, 5, 45), + }) +} +func TestValidate_VariablesInAllowedPosition_BooleanToNonNullableBooleanInDirective(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($boolVar: Boolean) + { + dog @include(if: $boolVar) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$boolVar" of type "Boolean" used in position `+ + `expecting type "Boolean!".`, 4, 26), + }) +} +func TestValidate_VariablesInAllowedPosition_StringToNonNullableBooleanInDirective(t *testing.T) { + testutil.ExpectFailsRule(t, graphql.VariablesInAllowedPositionRule, ` + query Query($stringVar: String) + { + dog @include(if: $stringVar) + } + `, []gqlerrors.FormattedError{ + testutil.RuleError(`Variable "$stringVar" of type "String" used in position `+ + `expecting type "Boolean!".`, 4, 26), + }) +} diff --git a/scalars.go b/scalars.go index 78505db6..7d22a456 100644 --- a/scalars.go +++ b/scalars.go @@ -98,7 +98,7 @@ var Float *Scalar = NewScalar(ScalarConfig{ return floatValue } } - return float32(0) + return nil }, }) @@ -115,7 +115,7 @@ var String *Scalar = NewScalar(ScalarConfig{ case *ast.StringValue: return valueAST.Value } - return "" + return nil }, }) @@ -157,7 +157,7 @@ var Boolean *Scalar = NewScalar(ScalarConfig{ case *ast.BooleanValue: return valueAST.Value } - return false + return nil }, }) @@ -172,6 +172,6 @@ var ID *Scalar = NewScalar(ScalarConfig{ case *ast.StringValue: return valueAST.Value } - return "" + return nil }, }) diff --git a/schema.go b/schema.go index 94078c78..b2be7bba 100644 --- a/schema.go +++ b/schema.go @@ -104,6 +104,15 @@ func (gq *Schema) Directives() []*Directive { return gq.directives } +func (gq *Schema) Directive(name string) *Directive { + for _, directive := range gq.Directives() { + if directive.Name == name { + return directive + } + } + return nil +} + func (gq *Schema) TypeMap() TypeMap { return gq.typeMap } diff --git a/testutil/rules_test_harness.go b/testutil/rules_test_harness.go new file mode 100644 index 00000000..3acb9094 --- /dev/null +++ b/testutil/rules_test_harness.go @@ -0,0 +1,529 @@ +package testutil + +import ( + "testing" + + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/gqlerrors" + "github.com/graphql-go/graphql/language/location" + "github.com/graphql-go/graphql/language/parser" + "github.com/graphql-go/graphql/language/source" + "reflect" +) + +var defaultRulesTestSchema *graphql.Schema + +func init() { + + var beingInterface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "Being", + Fields: graphql.Fields{ + "name": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "surname": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + }, + }, + }, + }) + var petInterface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "Pet", + 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{ + "SIT": &graphql.EnumValueConfig{ + Value: 0, + }, + "HEEL": &graphql.EnumValueConfig{ + Value: 1, + }, + "DOWN": &graphql.EnumValueConfig{ + Value: 2, + }, + }, + }) + var dogType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Dog", + IsTypeOf: func(value interface{}, info graphql.ResolveInfo) bool { + return true + }, + Fields: graphql.Fields{ + "name": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "surname": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + }, + }, + "nickname": &graphql.Field{ + Type: graphql.String, + }, + "barkVolume": &graphql.Field{ + Type: graphql.Int, + }, + "barks": &graphql.Field{ + Type: graphql.Boolean, + }, + "doesKnowCommand": &graphql.Field{ + Type: graphql.Boolean, + Args: graphql.FieldConfigArgument{ + "dogCommand": &graphql.ArgumentConfig{ + Type: dogCommandEnum, + }, + }, + }, + "isHousetrained": &graphql.Field{ + Type: graphql.Boolean, + Args: graphql.FieldConfigArgument{ + "atOtherHomes": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + DefaultValue: true, + }, + }, + }, + "isAtLocation": &graphql.Field{ + Type: graphql.Boolean, + Args: graphql.FieldConfigArgument{ + "x": &graphql.ArgumentConfig{ + Type: graphql.Int, + }, + "y": &graphql.ArgumentConfig{ + Type: graphql.Int, + }, + }, + }, + }, + Interfaces: []*graphql.Interface{ + beingInterface, + petInterface, + }, + }) + var furColorEnum = graphql.NewEnum(graphql.EnumConfig{ + Name: "FurColor", + Values: graphql.EnumValueConfigMap{ + "BROWN": &graphql.EnumValueConfig{ + Value: 0, + }, + "BLACK": &graphql.EnumValueConfig{ + Value: 1, + }, + "TAN": &graphql.EnumValueConfig{ + Value: 2, + }, + "SPOTTED": &graphql.EnumValueConfig{ + Value: 3, + }, + }, + }) + + var catType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Cat", + IsTypeOf: func(value interface{}, info graphql.ResolveInfo) bool { + return true + }, + Fields: graphql.Fields{ + "name": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "surname": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + }, + }, + "nickname": &graphql.Field{ + Type: graphql.String, + }, + "meowVolume": &graphql.Field{ + Type: graphql.Int, + }, + "meows": &graphql.Field{ + Type: graphql.Boolean, + }, + "furColor": &graphql.Field{ + Type: furColorEnum, + }, + }, + Interfaces: []*graphql.Interface{ + beingInterface, + petInterface, + }, + }) + var catOrDogUnion = graphql.NewUnion(graphql.UnionConfig{ + Name: "CatOrDog", + Types: []*graphql.Object{ + dogType, + catType, + }, + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + // not used for validation + return nil + }, + }) + var intelligentInterface = graphql.NewInterface(graphql.InterfaceConfig{ + Name: "Intelligent", + Fields: graphql.Fields{ + "iq": &graphql.Field{ + Type: graphql.Int, + }, + }, + }) + + var humanType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Human", + IsTypeOf: func(value interface{}, info graphql.ResolveInfo) bool { + return true + }, + Interfaces: []*graphql.Interface{ + beingInterface, + intelligentInterface, + }, + Fields: graphql.Fields{ + "name": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "surname": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + }, + }, + "pets": &graphql.Field{ + Type: graphql.NewList(petInterface), + }, + "iq": &graphql.Field{ + Type: graphql.Int, + }, + }, + }) + + humanType.AddFieldConfig("relatives", &graphql.Field{ + Type: graphql.NewList(humanType), + }) + + var alienType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Alien", + IsTypeOf: func(value interface{}, info graphql.ResolveInfo) bool { + return true + }, + Interfaces: []*graphql.Interface{ + beingInterface, + intelligentInterface, + }, + Fields: graphql.Fields{ + "name": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "surname": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + }, + }, + "iq": &graphql.Field{ + Type: graphql.Int, + }, + "numEyes": &graphql.Field{ + Type: graphql.Int, + }, + }, + }) + var dogOrHumanUnion = graphql.NewUnion(graphql.UnionConfig{ + Name: "DogOrHuman", + Types: []*graphql.Object{ + dogType, + humanType, + }, + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + // not used for validation + return nil + }, + }) + var humanOrAlienUnion = graphql.NewUnion(graphql.UnionConfig{ + Name: "HumanOrAlien", + Types: []*graphql.Object{ + alienType, + humanType, + }, + ResolveType: func(value interface{}, info graphql.ResolveInfo) *graphql.Object { + // not used for validation + return nil + }, + }) + + var complexInputObject = graphql.NewInputObject(graphql.InputObjectConfig{ + Name: "ComplexInput", + Fields: graphql.InputObjectConfigFieldMap{ + "requiredField": &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(graphql.Boolean), + }, + "intField": &graphql.InputObjectFieldConfig{ + Type: graphql.Int, + }, + "stringField": &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + "booleanField": &graphql.InputObjectFieldConfig{ + Type: graphql.Boolean, + }, + "stringListField": &graphql.InputObjectFieldConfig{ + Type: graphql.NewList(graphql.String), + }, + }, + }) + var complicatedArgs = graphql.NewObject(graphql.ObjectConfig{ + Name: "ComplicatedArgs", + // TODO List + // TODO Coercion + // TODO NotNulls + Fields: graphql.Fields{ + "intArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "intArg": &graphql.ArgumentConfig{ + Type: graphql.Int, + }, + }, + }, + "nonNullIntArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "nonNullIntArg": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + }, + }, + "stringArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "stringArg": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + }, + }, + "booleanArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "booleanArg": &graphql.ArgumentConfig{ + Type: graphql.Boolean, + }, + }, + }, + "enumArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "enumArg": &graphql.ArgumentConfig{ + Type: furColorEnum, + }, + }, + }, + "floatArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "floatArg": &graphql.ArgumentConfig{ + Type: graphql.Float, + }, + }, + }, + "idArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "idArg": &graphql.ArgumentConfig{ + Type: graphql.ID, + }, + }, + }, + "stringListArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "stringListArg": &graphql.ArgumentConfig{ + Type: graphql.NewList(graphql.String), + }, + }, + }, + "complexArgField": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "complexArg": &graphql.ArgumentConfig{ + Type: complexInputObject, + }, + }, + }, + "multipleReqs": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "req1": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + "req2": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + }, + }, + "multipleOpts": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "opt1": &graphql.ArgumentConfig{ + Type: graphql.Int, + DefaultValue: 0, + }, + "opt2": &graphql.ArgumentConfig{ + Type: graphql.Int, + DefaultValue: 0, + }, + }, + }, + "multipleOptAndReq": &graphql.Field{ + Type: graphql.String, + Args: graphql.FieldConfigArgument{ + "req1": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + "req2": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + "opt1": &graphql.ArgumentConfig{ + Type: graphql.Int, + DefaultValue: 0, + }, + "opt2": &graphql.ArgumentConfig{ + Type: graphql.Int, + DefaultValue: 0, + }, + }, + }, + }, + }) + queryRoot := graphql.NewObject(graphql.ObjectConfig{ + Name: "QueryRoot", + Fields: graphql.Fields{ + "human": &graphql.Field{ + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.ID, + }, + }, + Type: humanType, + }, + "alien": &graphql.Field{ + Type: alienType, + }, + "dog": &graphql.Field{ + Type: dogType, + }, + "cat": &graphql.Field{ + Type: catType, + }, + "pet": &graphql.Field{ + Type: petInterface, + }, + "catOrDog": &graphql.Field{ + Type: catOrDogUnion, + }, + "dogOrHuman": &graphql.Field{ + Type: dogOrHumanUnion, + }, + "humanOrAlien": &graphql.Field{ + Type: humanOrAlienUnion, + }, + "complicatedArgs": &graphql.Field{ + Type: complicatedArgs, + }, + }, + }) + schema, err := graphql.NewSchema(graphql.SchemaConfig{ + Query: queryRoot, + }) + if err != nil { + panic(err) + } + defaultRulesTestSchema = &schema + +} +func expectValidRule(t *testing.T, schema *graphql.Schema, rules []graphql.ValidationRuleFn, queryString string) { + source := source.NewSource(&source.Source{ + Body: queryString, + }) + AST, err := parser.Parse(parser.ParseParams{Source: source}) + if err != nil { + t.Fatal(err) + } + result := graphql.ValidateDocument(schema, AST, rules) + if len(result.Errors) > 0 { + t.Fatalf("Should validate, got %v", result.Errors) + } + if result.IsValid != true { + t.Fatalf("IsValid should be true, got %v", result.IsValid) + } + +} +func expectInvalidRule(t *testing.T, schema *graphql.Schema, rules []graphql.ValidationRuleFn, queryString string, expectedErrors []gqlerrors.FormattedError) { + source := source.NewSource(&source.Source{ + Body: queryString, + }) + AST, err := parser.Parse(parser.ParseParams{Source: source}) + if err != nil { + t.Fatal(err) + } + result := graphql.ValidateDocument(schema, AST, rules) + if len(result.Errors) != len(expectedErrors) { + t.Fatalf("Should have %v errors, got %v", len(expectedErrors), len(result.Errors)) + } + if result.IsValid != false { + t.Fatalf("IsValid should be false, got %v", result.IsValid) + } + for _, expectedErr := range expectedErrors { + found := false + for _, err := range result.Errors { + if reflect.DeepEqual(expectedErr, err) { + found = true + break + } + } + if found == false { + t.Fatalf("Unexpected result, Diff: %v", Diff(expectedErrors, result.Errors)) + } + } + +} +func ExpectPassesRule(t *testing.T, rule graphql.ValidationRuleFn, queryString string) { + expectValidRule(t, defaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString) +} +func ExpectFailsRule(t *testing.T, rule graphql.ValidationRuleFn, queryString string, expectedErrors []gqlerrors.FormattedError) { + expectInvalidRule(t, defaultRulesTestSchema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) +} +func ExpectFailsRuleWithSchema(t *testing.T, schema *graphql.Schema, rule graphql.ValidationRuleFn, queryString string, expectedErrors []gqlerrors.FormattedError) { + expectInvalidRule(t, schema, []graphql.ValidationRuleFn{rule}, queryString, expectedErrors) +} +func ExpectPassesRuleWithSchema(t *testing.T, schema *graphql.Schema, rule graphql.ValidationRuleFn, queryString string) { + expectValidRule(t, schema, []graphql.ValidationRuleFn{rule}, queryString) +} +func RuleError(message string, locs ...int) gqlerrors.FormattedError { + locations := []location.SourceLocation{} + for i := 0; i < len(locs); i = i + 2 { + line := locs[i] + col := 0 + if i+1 < len(locs) { + col = locs[i+1] + } + locations = append(locations, location.SourceLocation{ + Line: line, + Column: col, + }) + } + return gqlerrors.FormattedError{ + Message: message, + Locations: locations, + } +} diff --git a/type_info.go b/type_info.go new file mode 100644 index 00000000..e7978889 --- /dev/null +++ b/type_info.go @@ -0,0 +1,235 @@ +package graphql + +import ( + "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/kinds" +) + +// TODO: can move TypeInfo to a utils package if there ever is one +/** + * TypeInfo is a utility class which, given a GraphQL schema, can keep track + * of the current field and type definitions at any point in a GraphQL document + * AST during a recursive descent by calling `enter(node)` and `leave(node)`. + */ +type TypeInfo struct { + schema *Schema + typeStack []Output + parentTypeStack []Composite + inputTypeStack []Input + fieldDefStack []*FieldDefinition + directive *Directive + argument *Argument +} + +func NewTypeInfo(schema *Schema) *TypeInfo { + return &TypeInfo{ + schema: schema, + } +} + +func (ti *TypeInfo) Type() Output { + if len(ti.typeStack) > 0 { + return ti.typeStack[len(ti.typeStack)-1] + } + return nil +} + +func (ti *TypeInfo) ParentType() Composite { + if len(ti.parentTypeStack) > 0 { + return ti.parentTypeStack[len(ti.parentTypeStack)-1] + } + return nil +} + +func (ti *TypeInfo) InputType() Input { + if len(ti.inputTypeStack) > 0 { + return ti.inputTypeStack[len(ti.inputTypeStack)-1] + } + return nil +} +func (ti *TypeInfo) FieldDef() *FieldDefinition { + if len(ti.fieldDefStack) > 0 { + return ti.fieldDefStack[len(ti.fieldDefStack)-1] + } + return nil +} + +func (ti *TypeInfo) Directive() *Directive { + return ti.directive +} + +func (ti *TypeInfo) Argument() *Argument { + return ti.argument +} + +func (ti *TypeInfo) Enter(node ast.Node) { + + schema := ti.schema + var ttype Type + switch node := node.(type) { + case *ast.SelectionSet: + namedType := GetNamed(ti.Type()) + var compositeType Composite = nil + if IsCompositeType(namedType) { + compositeType, _ = namedType.(Composite) + } + ti.parentTypeStack = append(ti.parentTypeStack, compositeType) + case *ast.Field: + parentType := ti.ParentType() + var fieldDef *FieldDefinition + if parentType != nil { + fieldDef = TypeInfoFieldDef(*schema, parentType.(Type), node) + } + ti.fieldDefStack = append(ti.fieldDefStack, fieldDef) + if fieldDef != nil { + ti.typeStack = append(ti.typeStack, fieldDef.Type) + } else { + ti.typeStack = append(ti.typeStack, nil) + } + case *ast.Directive: + nameVal := "" + if node.Name != nil { + nameVal = node.Name.Value + } + ti.directive = schema.Directive(nameVal) + case *ast.OperationDefinition: + if node.Operation == "query" { + ttype = schema.QueryType() + } else if node.Operation == "mutation" { + ttype = schema.MutationType() + } + ti.typeStack = append(ti.typeStack, ttype) + case *ast.InlineFragment: + ttype, _ = typeFromAST(*schema, node.TypeCondition) + ti.typeStack = append(ti.typeStack, ttype) + case *ast.FragmentDefinition: + ttype, _ = typeFromAST(*schema, node.TypeCondition) + ti.typeStack = append(ti.typeStack, ttype) + case *ast.VariableDefinition: + ttype, _ = typeFromAST(*schema, node.Type) + ti.inputTypeStack = append(ti.inputTypeStack, ttype) + case *ast.Argument: + nameVal := "" + if node.Name != nil { + nameVal = node.Name.Value + } + var argType Input + var argDef *Argument + directive := ti.Directive() + fieldDef := ti.FieldDef() + if directive != nil { + for _, arg := range directive.Args { + if arg.Name() == nameVal { + argDef = arg + } + } + } else if fieldDef != nil { + for _, arg := range fieldDef.Args { + if arg.Name() == nameVal { + argDef = arg + } + } + } + if argDef != nil { + argType = argDef.Type + } + ti.argument = argDef + ti.inputTypeStack = append(ti.inputTypeStack, argType) + case *ast.ListValue: + listType := GetNullable(ti.InputType()) + if list, ok := listType.(*List); ok { + ti.inputTypeStack = append(ti.inputTypeStack, list.OfType) + } else { + ti.inputTypeStack = append(ti.inputTypeStack, nil) + } + case *ast.ObjectField: + var fieldType Input + objectType := GetNamed(ti.InputType()) + + if objectType, ok := objectType.(*InputObject); ok { + nameVal := "" + if node.Name != nil { + nameVal = node.Name.Value + } + if inputField, ok := objectType.Fields()[nameVal]; ok { + fieldType = inputField.Type + } + } + ti.inputTypeStack = append(ti.inputTypeStack, fieldType) + } +} +func (ti *TypeInfo) Leave(node ast.Node) { + kind := node.GetKind() + switch kind { + case kinds.SelectionSet: + // pop ti.parentTypeStack + _, ti.parentTypeStack = ti.parentTypeStack[len(ti.parentTypeStack)-1], ti.parentTypeStack[:len(ti.parentTypeStack)-1] + case kinds.Field: + // pop ti.fieldDefStack + _, ti.fieldDefStack = ti.fieldDefStack[len(ti.fieldDefStack)-1], ti.fieldDefStack[:len(ti.fieldDefStack)-1] + // pop ti.typeStack + _, ti.typeStack = ti.typeStack[len(ti.typeStack)-1], ti.typeStack[:len(ti.typeStack)-1] + case kinds.Directive: + ti.directive = nil + case kinds.OperationDefinition: + fallthrough + case kinds.InlineFragment: + fallthrough + case kinds.FragmentDefinition: + // pop ti.typeStack + _, ti.typeStack = ti.typeStack[len(ti.typeStack)-1], ti.typeStack[:len(ti.typeStack)-1] + case kinds.VariableDefinition: + // pop ti.inputTypeStack + _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + case kinds.Argument: + ti.argument = nil + // pop ti.inputTypeStack + _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + case kinds.ListValue: + fallthrough + case kinds.ObjectField: + // pop ti.inputTypeStack + _, ti.inputTypeStack = ti.inputTypeStack[len(ti.inputTypeStack)-1], ti.inputTypeStack[:len(ti.inputTypeStack)-1] + } +} + +/** + * Not exactly the same as the executor's definition of FieldDef, in this + * statically evaluated environment we do not always have an Object type, + * and need to handle Interface and Union types. + */ +func TypeInfoFieldDef(schema Schema, parentType Type, fieldAST *ast.Field) *FieldDefinition { + name := "" + if fieldAST.Name != nil { + name = fieldAST.Name.Value + } + if name == SchemaMetaFieldDef.Name && + schema.QueryType() == parentType { + return SchemaMetaFieldDef + } + if name == TypeMetaFieldDef.Name && + schema.QueryType() == parentType { + return TypeMetaFieldDef + } + if name == TypeNameMetaFieldDef.Name { + if _, ok := parentType.(*Object); ok && parentType != nil { + return TypeNameMetaFieldDef + } + if _, ok := parentType.(*Interface); ok && parentType != nil { + return TypeNameMetaFieldDef + } + if _, ok := parentType.(*Union); ok && parentType != nil { + return TypeNameMetaFieldDef + } + } + + if parentType, ok := parentType.(*Object); ok && parentType != nil { + field, _ := parentType.Fields()[name] + return field + } + if parentType, ok := parentType.(*Interface); ok && parentType != nil { + field, _ := parentType.Fields()[name] + return field + } + return nil +} diff --git a/validator.go b/validator.go index 3d1a8e72..2873fd64 100644 --- a/validator.go +++ b/validator.go @@ -3,6 +3,8 @@ package graphql import ( "github.com/graphql-go/graphql/gqlerrors" "github.com/graphql-go/graphql/language/ast" + "github.com/graphql-go/graphql/language/kinds" + "github.com/graphql-go/graphql/language/visitor" ) type ValidationResult struct { @@ -10,7 +12,201 @@ type ValidationResult struct { Errors []gqlerrors.FormattedError } -func ValidateDocument(schema Schema, ast *ast.Document) (vr ValidationResult) { - vr.IsValid = true +func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) (vr ValidationResult) { + if len(rules) == 0 { + rules = SpecifiedRules + } + + vr.IsValid = false + if schema == nil { + vr.Errors = append(vr.Errors, gqlerrors.NewFormattedError("Must provide schema")) + return vr + } + if astDoc == nil { + vr.Errors = append(vr.Errors, gqlerrors.NewFormattedError("Must provide document")) + return vr + } + vr.Errors = visitUsingRules(schema, astDoc, rules) + if len(vr.Errors) == 0 { + vr.IsValid = true + } return vr } + +func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) (errors []gqlerrors.FormattedError) { + typeInfo := NewTypeInfo(schema) + context := NewValidationContext(schema, astDoc, typeInfo) + + var visitInstance func(astNode ast.Node, instance *ValidationRuleInstance) + + visitInstance = func(astNode ast.Node, instance *ValidationRuleInstance) { + visitor.Visit(astNode, &visitor.VisitorOptions{ + Enter: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + switch node := p.Node.(type) { + case ast.Node: + // Collect type information about the current position in the AST. + typeInfo.Enter(node) + + // Do not visit top level fragment definitions if this instance will + // visit those fragments inline because it + // provided `visitSpreadFragments`. + kind := node.GetKind() + + if kind == kinds.FragmentDefinition && + p.Key != nil && instance.VisitSpreadFragments == true { + return visitor.ActionSkip, nil + } + + // Get the visitor function from the validation instance, and if it + // exists, call it with the visitor arguments. + enterFn := visitor.GetVisitFn(instance.VisitorOpts, false, kind) + if enterFn != nil { + action, result = enterFn(p) + } + + // If the visitor returned an error, log it and do not visit any + // deeper nodes. + if err, ok := result.(error); ok && err != nil { + errors = append(errors, gqlerrors.FormatError(err)) + action = visitor.ActionSkip + } + if err, ok := result.([]error); ok && err != nil { + errors = append(errors, gqlerrors.FormatErrors(err...)...) + action = visitor.ActionSkip + } + + // If any validation instances provide the flag `visitSpreadFragments` + // and this node is a fragment spread, visit the fragment definition + // from this point. + if action == visitor.ActionNoChange && result == nil && + instance.VisitSpreadFragments == true && kind == kinds.FragmentSpread { + node, _ := node.(*ast.FragmentSpread) + name := node.Name + nameVal := "" + if name != nil { + nameVal = name.Value + } + fragment := context.Fragment(nameVal) + if fragment != nil { + visitInstance(fragment, instance) + } + } + + // If the result is "false" (ie action === Action.Skip), we're not visiting any descendent nodes, + // but need to update typeInfo. + if action == visitor.ActionSkip { + typeInfo.Leave(node) + } + + } + + return action, result + }, + Leave: func(p visitor.VisitFuncParams) (string, interface{}) { + var action = visitor.ActionNoChange + var result interface{} + switch node := p.Node.(type) { + case ast.Node: + kind := node.GetKind() + + // Get the visitor function from the validation instance, and if it + // exists, call it with the visitor arguments. + leaveFn := visitor.GetVisitFn(instance.VisitorOpts, true, kind) + if leaveFn != nil { + action, result = leaveFn(p) + } + + // If the visitor returned an error, log it and do not visit any + // deeper nodes. + if err, ok := result.(error); ok && err != nil { + errors = append(errors, gqlerrors.FormatError(err)) + action = visitor.ActionSkip + } + if err, ok := result.([]error); ok && err != nil { + errors = append(errors, gqlerrors.FormatErrors(err...)...) + action = visitor.ActionSkip + } + + // Update typeInfo. + typeInfo.Leave(node) + } + return action, result + }, + }, nil) + } + + instances := []*ValidationRuleInstance{} + for _, rule := range rules { + instance := rule(context) + instances = append(instances, instance) + } + for _, instance := range instances { + visitInstance(astDoc, instance) + } + return errors +} + +type ValidationContext struct { + schema *Schema + astDoc *ast.Document + typeInfo *TypeInfo + fragments map[string]*ast.FragmentDefinition +} + +func NewValidationContext(schema *Schema, astDoc *ast.Document, typeInfo *TypeInfo) *ValidationContext { + return &ValidationContext{ + schema: schema, + astDoc: astDoc, + typeInfo: typeInfo, + } +} + +func (ctx *ValidationContext) Schema() *Schema { + return ctx.schema +} +func (ctx *ValidationContext) Document() *ast.Document { + return ctx.astDoc +} + +func (ctx *ValidationContext) Fragment(name string) *ast.FragmentDefinition { + if len(ctx.fragments) == 0 { + if ctx.Document() == nil { + return nil + } + defs := ctx.Document().Definitions + fragments := map[string]*ast.FragmentDefinition{} + for _, def := range defs { + if def, ok := def.(*ast.FragmentDefinition); ok { + defName := "" + if def.Name != nil { + defName = def.Name.Value + } + fragments[defName] = def + } + } + ctx.fragments = fragments + } + f, _ := ctx.fragments[name] + return f +} + +func (ctx *ValidationContext) Type() Output { + return ctx.typeInfo.Type() +} +func (ctx *ValidationContext) ParentType() Composite { + return ctx.typeInfo.ParentType() +} +func (ctx *ValidationContext) InputType() Input { + return ctx.typeInfo.InputType() +} +func (ctx *ValidationContext) FieldDef() *FieldDefinition { + return ctx.typeInfo.FieldDef() +} +func (ctx *ValidationContext) Directive() *Directive { + return ctx.typeInfo.Directive() +} +func (ctx *ValidationContext) Argument() *Argument { + return ctx.typeInfo.Argument() +} diff --git a/values.go b/values.go index d2a0f711..6b3ff169 100644 --- a/values.go +++ b/values.go @@ -177,7 +177,7 @@ func coerceValue(ttype Input, value interface{}) interface{} { // graphql-js/src/utilities.js` // TODO: figure out where to organize utils - +// TODO: change to *Schema func typeFromAST(schema Schema, inputTypeAST ast.Type) (Type, error) { switch inputTypeAST := inputTypeAST.(type) { case *ast.List: