switch to go vendoring
This commit is contained in:
+55
@@ -0,0 +1,55 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/gqlerror"
|
||||
)
|
||||
|
||||
type ErrorOption func(err *gqlerror.Error)
|
||||
|
||||
func Message(msg string, args ...interface{}) ErrorOption {
|
||||
return func(err *gqlerror.Error) {
|
||||
err.Message += fmt.Sprintf(msg, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func At(position *ast.Position) ErrorOption {
|
||||
return func(err *gqlerror.Error) {
|
||||
if position == nil {
|
||||
return
|
||||
}
|
||||
err.Locations = append(err.Locations, gqlerror.Location{
|
||||
Line: position.Line,
|
||||
Column: position.Column,
|
||||
})
|
||||
if position.Src.Name != "" {
|
||||
err.SetFile(position.Src.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SuggestListQuoted(prefix string, typed string, suggestions []string) ErrorOption {
|
||||
suggested := SuggestionList(typed, suggestions)
|
||||
return func(err *gqlerror.Error) {
|
||||
if len(suggested) > 0 {
|
||||
err.Message += " " + prefix + " " + QuotedOrList(suggested...) + "?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SuggestListUnquoted(prefix string, typed string, suggestions []string) ErrorOption {
|
||||
suggested := SuggestionList(typed, suggestions)
|
||||
return func(err *gqlerror.Error) {
|
||||
if len(suggested) > 0 {
|
||||
err.Message += " " + prefix + " " + OrList(suggested...) + "?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Suggestf(suggestion string, args ...interface{}) ErrorOption {
|
||||
return func(err *gqlerror.Error) {
|
||||
err.Message += " Did you mean " + fmt.Sprintf(suggestion, args...) + "?"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
package validator
|
||||
|
||||
import "bytes"
|
||||
|
||||
// Given [ A, B, C ] return '"A", "B", or "C"'.
|
||||
func QuotedOrList(items ...string) string {
|
||||
itemsQuoted := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
itemsQuoted[i] = `"` + item + `"`
|
||||
}
|
||||
return OrList(itemsQuoted...)
|
||||
}
|
||||
|
||||
// Given [ A, B, C ] return 'A, B, or C'.
|
||||
func OrList(items ...string) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if len(items) > 5 {
|
||||
items = items[:5]
|
||||
}
|
||||
if len(items) == 2 {
|
||||
buf.WriteString(items[0])
|
||||
buf.WriteString(" or ")
|
||||
buf.WriteString(items[1])
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
for i, item := range items {
|
||||
if i != 0 {
|
||||
if i == len(items)-1 {
|
||||
buf.WriteString(", or ")
|
||||
} else {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
}
|
||||
buf.WriteString(item)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
)
|
||||
|
||||
//go:embed prelude.graphql
|
||||
var preludeGraphql string
|
||||
|
||||
var Prelude = &ast.Source{
|
||||
Name: "prelude.graphql",
|
||||
Input: preludeGraphql,
|
||||
BuiltIn: true,
|
||||
}
|
||||
Generated
Vendored
+121
@@ -0,0 +1,121 @@
|
||||
# This file defines all the implicitly declared types that are required by the graphql spec. It is implicitly included by calls to LoadSchema
|
||||
|
||||
"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1."
|
||||
scalar Int
|
||||
|
||||
"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point)."
|
||||
scalar Float
|
||||
|
||||
"The `String`scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."
|
||||
scalar String
|
||||
|
||||
"The `Boolean` scalar type represents `true` or `false`."
|
||||
scalar Boolean
|
||||
|
||||
"""The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as "4") or integer (such as 4) input value will be accepted as an ID."""
|
||||
scalar ID
|
||||
|
||||
"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument."
|
||||
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
|
||||
|
||||
"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument."
|
||||
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
|
||||
|
||||
"The @deprecated built-in directive is used within the type system definition language to indicate deprecated portions of a GraphQL service's schema, such as deprecated fields on a type, arguments on a field, input fields on an input type, or values of an enum type."
|
||||
directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
|
||||
|
||||
"The @specifiedBy built-in directive is used within the type system definition language to provide a scalar specification URL for specifying the behavior of custom scalar types."
|
||||
directive @specifiedBy(url: String!) on SCALAR
|
||||
|
||||
type __Schema {
|
||||
description: String
|
||||
types: [__Type!]!
|
||||
queryType: __Type!
|
||||
mutationType: __Type
|
||||
subscriptionType: __Type
|
||||
directives: [__Directive!]!
|
||||
}
|
||||
|
||||
type __Type {
|
||||
kind: __TypeKind!
|
||||
name: String
|
||||
description: String
|
||||
# must be non-null for OBJECT and INTERFACE, otherwise null.
|
||||
fields(includeDeprecated: Boolean = false): [__Field!]
|
||||
# must be non-null for OBJECT and INTERFACE, otherwise null.
|
||||
interfaces: [__Type!]
|
||||
# must be non-null for INTERFACE and UNION, otherwise null.
|
||||
possibleTypes: [__Type!]
|
||||
# must be non-null for ENUM, otherwise null.
|
||||
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
|
||||
# must be non-null for INPUT_OBJECT, otherwise null.
|
||||
inputFields: [__InputValue!]
|
||||
# must be non-null for NON_NULL and LIST, otherwise null.
|
||||
ofType: __Type
|
||||
# may be non-null for custom SCALAR, otherwise null.
|
||||
specifiedByURL: String
|
||||
}
|
||||
|
||||
type __Field {
|
||||
name: String!
|
||||
description: String
|
||||
args: [__InputValue!]!
|
||||
type: __Type!
|
||||
isDeprecated: Boolean!
|
||||
deprecationReason: String
|
||||
}
|
||||
|
||||
type __InputValue {
|
||||
name: String!
|
||||
description: String
|
||||
type: __Type!
|
||||
defaultValue: String
|
||||
}
|
||||
|
||||
type __EnumValue {
|
||||
name: String!
|
||||
description: String
|
||||
isDeprecated: Boolean!
|
||||
deprecationReason: String
|
||||
}
|
||||
|
||||
enum __TypeKind {
|
||||
SCALAR
|
||||
OBJECT
|
||||
INTERFACE
|
||||
UNION
|
||||
ENUM
|
||||
INPUT_OBJECT
|
||||
LIST
|
||||
NON_NULL
|
||||
}
|
||||
|
||||
type __Directive {
|
||||
name: String!
|
||||
description: String
|
||||
locations: [__DirectiveLocation!]!
|
||||
args: [__InputValue!]!
|
||||
isRepeatable: Boolean!
|
||||
}
|
||||
|
||||
enum __DirectiveLocation {
|
||||
QUERY
|
||||
MUTATION
|
||||
SUBSCRIPTION
|
||||
FIELD
|
||||
FRAGMENT_DEFINITION
|
||||
FRAGMENT_SPREAD
|
||||
INLINE_FRAGMENT
|
||||
VARIABLE_DEFINITION
|
||||
SCHEMA
|
||||
SCALAR
|
||||
OBJECT
|
||||
FIELD_DEFINITION
|
||||
ARGUMENT_DEFINITION
|
||||
INTERFACE
|
||||
UNION
|
||||
ENUM
|
||||
ENUM_VALUE
|
||||
INPUT_OBJECT
|
||||
INPUT_FIELD_DEFINITION
|
||||
}
|
||||
vendor/github.com/open-policy-agent/opa/internal/gqlparser/validator/rules/fields_on_correct_type.go
Generated
Vendored
+97
@@ -0,0 +1,97 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("FieldsOnCorrectType", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnField(func(walker *Walker, field *ast.Field) {
|
||||
if field.ObjectDefinition == nil || field.Definition != nil {
|
||||
return
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(`Cannot query field "%s" on type "%s".`, field.Name, field.ObjectDefinition.Name)
|
||||
|
||||
if suggestedTypeNames := getSuggestedTypeNames(walker, field.ObjectDefinition, field.Name); suggestedTypeNames != nil {
|
||||
message += " Did you mean to use an inline fragment on " + QuotedOrList(suggestedTypeNames...) + "?"
|
||||
} else if suggestedFieldNames := getSuggestedFieldNames(field.ObjectDefinition, field.Name); suggestedFieldNames != nil {
|
||||
message += " Did you mean " + QuotedOrList(suggestedFieldNames...) + "?"
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(message),
|
||||
At(field.Position),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Go through all of the implementations of type, as well as the interfaces
|
||||
// that they implement. If any of those types include the provided field,
|
||||
// suggest them, sorted by how often the type is referenced, starting
|
||||
// with Interfaces.
|
||||
func getSuggestedTypeNames(walker *Walker, parent *ast.Definition, name string) []string {
|
||||
if !parent.IsAbstractType() {
|
||||
return nil
|
||||
}
|
||||
|
||||
possibleTypes := walker.Schema.GetPossibleTypes(parent)
|
||||
var suggestedObjectTypes = make([]string, 0, len(possibleTypes))
|
||||
var suggestedInterfaceTypes []string
|
||||
interfaceUsageCount := map[string]int{}
|
||||
|
||||
for _, possibleType := range possibleTypes {
|
||||
field := possibleType.Fields.ForName(name)
|
||||
if field == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
suggestedObjectTypes = append(suggestedObjectTypes, possibleType.Name)
|
||||
|
||||
for _, possibleInterface := range possibleType.Interfaces {
|
||||
interfaceField := walker.Schema.Types[possibleInterface]
|
||||
if interfaceField != nil && interfaceField.Fields.ForName(name) != nil {
|
||||
if interfaceUsageCount[possibleInterface] == 0 {
|
||||
suggestedInterfaceTypes = append(suggestedInterfaceTypes, possibleInterface)
|
||||
}
|
||||
interfaceUsageCount[possibleInterface]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suggestedTypes := append(suggestedInterfaceTypes, suggestedObjectTypes...)
|
||||
|
||||
sort.SliceStable(suggestedTypes, func(i, j int) bool {
|
||||
typeA, typeB := suggestedTypes[i], suggestedTypes[j]
|
||||
diff := interfaceUsageCount[typeB] - interfaceUsageCount[typeA]
|
||||
if diff != 0 {
|
||||
return diff < 0
|
||||
}
|
||||
return strings.Compare(typeA, typeB) < 0
|
||||
})
|
||||
|
||||
return suggestedTypes
|
||||
}
|
||||
|
||||
// For the field name provided, determine if there are any similar field names
|
||||
// that may be the result of a typo.
|
||||
func getSuggestedFieldNames(parent *ast.Definition, name string) []string {
|
||||
if parent.Kind != ast.Object && parent.Kind != ast.Interface {
|
||||
return nil
|
||||
}
|
||||
|
||||
var possibleFieldNames = make([]string, 0, len(parent.Fields))
|
||||
for _, field := range parent.Fields {
|
||||
possibleFieldNames = append(possibleFieldNames, field.Name)
|
||||
}
|
||||
|
||||
return SuggestionList(name, possibleFieldNames)
|
||||
}
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("FragmentsOnCompositeTypes", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
|
||||
fragmentType := walker.Schema.Types[inlineFragment.TypeCondition]
|
||||
if fragmentType == nil || fragmentType.IsCompositeType() {
|
||||
return
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(`Fragment cannot condition on non composite type "%s".`, inlineFragment.TypeCondition)
|
||||
|
||||
addError(
|
||||
Message(message),
|
||||
At(inlineFragment.Position),
|
||||
)
|
||||
})
|
||||
|
||||
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
|
||||
if fragment.Definition == nil || fragment.TypeCondition == "" || fragment.Definition.IsCompositeType() {
|
||||
return
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(`Fragment "%s" cannot condition on non composite type "%s".`, fragment.Name, fragment.TypeCondition)
|
||||
|
||||
addError(
|
||||
Message(message),
|
||||
At(fragment.Position),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("KnownArgumentNames", func(observers *Events, addError AddErrFunc) {
|
||||
// A GraphQL field is only valid if all supplied arguments are defined by that field.
|
||||
observers.OnField(func(walker *Walker, field *ast.Field) {
|
||||
if field.Definition == nil || field.ObjectDefinition == nil {
|
||||
return
|
||||
}
|
||||
for _, arg := range field.Arguments {
|
||||
def := field.Definition.Arguments.ForName(arg.Name)
|
||||
if def != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, argDef := range field.Definition.Arguments {
|
||||
suggestions = append(suggestions, argDef.Name)
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Unknown argument "%s" on field "%s.%s".`, arg.Name, field.ObjectDefinition.Name, field.Name),
|
||||
SuggestListQuoted("Did you mean", arg.Name, suggestions),
|
||||
At(field.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
|
||||
if directive.Definition == nil {
|
||||
return
|
||||
}
|
||||
for _, arg := range directive.Arguments {
|
||||
def := directive.Definition.Arguments.ForName(arg.Name)
|
||||
if def != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, argDef := range directive.Definition.Arguments {
|
||||
suggestions = append(suggestions, argDef.Name)
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Unknown argument "%s" on directive "@%s".`, arg.Name, directive.Name),
|
||||
SuggestListQuoted("Did you mean", arg.Name, suggestions),
|
||||
At(directive.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("KnownDirectives", func(observers *Events, addError AddErrFunc) {
|
||||
type mayNotBeUsedDirective struct {
|
||||
Name string
|
||||
Line int
|
||||
Column int
|
||||
}
|
||||
var seen = map[mayNotBeUsedDirective]bool{}
|
||||
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
|
||||
if directive.Definition == nil {
|
||||
addError(
|
||||
Message(`Unknown directive "@%s".`, directive.Name),
|
||||
At(directive.Position),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for _, loc := range directive.Definition.Locations {
|
||||
if loc == directive.Location {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// position must be exists if directive.Definition != nil
|
||||
tmp := mayNotBeUsedDirective{
|
||||
Name: directive.Name,
|
||||
Line: directive.Position.Line,
|
||||
Column: directive.Position.Column,
|
||||
}
|
||||
|
||||
if !seen[tmp] {
|
||||
addError(
|
||||
Message(`Directive "@%s" may not be used on %s.`, directive.Name, directive.Location),
|
||||
At(directive.Position),
|
||||
)
|
||||
seen[tmp] = true
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("KnownFragmentNames", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnFragmentSpread(func(walker *Walker, fragmentSpread *ast.FragmentSpread) {
|
||||
if fragmentSpread.Definition == nil {
|
||||
addError(
|
||||
Message(`Unknown fragment "%s".`, fragmentSpread.Name),
|
||||
At(fragmentSpread.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("KnownRootType", func(observers *Events, addError AddErrFunc) {
|
||||
// A query's root must be a valid type. Surprisingly, this isn't
|
||||
// checked anywhere else!
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
var def *ast.Definition
|
||||
switch operation.Operation {
|
||||
case ast.Query, "":
|
||||
def = walker.Schema.Query
|
||||
case ast.Mutation:
|
||||
def = walker.Schema.Mutation
|
||||
case ast.Subscription:
|
||||
def = walker.Schema.Subscription
|
||||
default:
|
||||
// This shouldn't even parse; if it did we probably need to
|
||||
// update this switch block to add the new operation type.
|
||||
panic(fmt.Sprintf(`got unknown operation type "%s"`, operation.Operation))
|
||||
}
|
||||
if def == nil {
|
||||
addError(
|
||||
Message(`Schema does not support operation type "%s"`, operation.Operation),
|
||||
At(operation.Position))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("KnownTypeNames", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnVariable(func(walker *Walker, variable *ast.VariableDefinition) {
|
||||
typeName := variable.Type.Name()
|
||||
typdef := walker.Schema.Types[typeName]
|
||||
if typdef != nil {
|
||||
return
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Unknown type "%s".`, typeName),
|
||||
At(variable.Position),
|
||||
)
|
||||
})
|
||||
|
||||
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
|
||||
typedName := inlineFragment.TypeCondition
|
||||
if typedName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
def := walker.Schema.Types[typedName]
|
||||
if def != nil {
|
||||
return
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Unknown type "%s".`, typedName),
|
||||
At(inlineFragment.Position),
|
||||
)
|
||||
})
|
||||
|
||||
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
|
||||
typeName := fragment.TypeCondition
|
||||
def := walker.Schema.Types[typeName]
|
||||
if def != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var possibleTypes []string
|
||||
for _, t := range walker.Schema.Types {
|
||||
possibleTypes = append(possibleTypes, t.Name)
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Unknown type "%s".`, typeName),
|
||||
SuggestListQuoted("Did you mean", typeName, possibleTypes),
|
||||
At(fragment.Position),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("LoneAnonymousOperation", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
if operation.Name == "" && len(walker.Document.Operations) > 1 {
|
||||
addError(
|
||||
Message(`This anonymous operation must be the only defined operation.`),
|
||||
At(operation.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("NoFragmentCycles", func(observers *Events, addError AddErrFunc) {
|
||||
visitedFrags := make(map[string]bool)
|
||||
|
||||
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
|
||||
var spreadPath []*ast.FragmentSpread
|
||||
spreadPathIndexByName := make(map[string]int)
|
||||
|
||||
var recursive func(fragment *ast.FragmentDefinition)
|
||||
recursive = func(fragment *ast.FragmentDefinition) {
|
||||
if visitedFrags[fragment.Name] {
|
||||
return
|
||||
}
|
||||
|
||||
visitedFrags[fragment.Name] = true
|
||||
|
||||
spreadNodes := getFragmentSpreads(fragment.SelectionSet)
|
||||
if len(spreadNodes) == 0 {
|
||||
return
|
||||
}
|
||||
spreadPathIndexByName[fragment.Name] = len(spreadPath)
|
||||
|
||||
for _, spreadNode := range spreadNodes {
|
||||
spreadName := spreadNode.Name
|
||||
|
||||
cycleIndex, ok := spreadPathIndexByName[spreadName]
|
||||
|
||||
spreadPath = append(spreadPath, spreadNode)
|
||||
if !ok {
|
||||
spreadFragment := walker.Document.Fragments.ForName(spreadName)
|
||||
if spreadFragment != nil {
|
||||
recursive(spreadFragment)
|
||||
}
|
||||
} else {
|
||||
cyclePath := spreadPath[cycleIndex : len(spreadPath)-1]
|
||||
var fragmentNames []string
|
||||
for _, fs := range cyclePath {
|
||||
fragmentNames = append(fragmentNames, fmt.Sprintf(`"%s"`, fs.Name))
|
||||
}
|
||||
var via string
|
||||
if len(fragmentNames) != 0 {
|
||||
via = fmt.Sprintf(" via %s", strings.Join(fragmentNames, ", "))
|
||||
}
|
||||
addError(
|
||||
Message(`Cannot spread fragment "%s" within itself%s.`, spreadName, via),
|
||||
At(spreadNode.Position),
|
||||
)
|
||||
}
|
||||
|
||||
spreadPath = spreadPath[:len(spreadPath)-1]
|
||||
}
|
||||
|
||||
delete(spreadPathIndexByName, fragment.Name)
|
||||
}
|
||||
|
||||
recursive(fragment)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func getFragmentSpreads(node ast.SelectionSet) []*ast.FragmentSpread {
|
||||
var spreads []*ast.FragmentSpread
|
||||
|
||||
setsToVisit := []ast.SelectionSet{node}
|
||||
|
||||
for len(setsToVisit) != 0 {
|
||||
set := setsToVisit[len(setsToVisit)-1]
|
||||
setsToVisit = setsToVisit[:len(setsToVisit)-1]
|
||||
|
||||
for _, selection := range set {
|
||||
switch selection := selection.(type) {
|
||||
case *ast.FragmentSpread:
|
||||
spreads = append(spreads, selection)
|
||||
case *ast.Field:
|
||||
setsToVisit = append(setsToVisit, selection.SelectionSet)
|
||||
case *ast.InlineFragment:
|
||||
setsToVisit = append(setsToVisit, selection.SelectionSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spreads
|
||||
}
|
||||
vendor/github.com/open-policy-agent/opa/internal/gqlparser/validator/rules/no_undefined_variables.go
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("NoUndefinedVariables", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnValue(func(walker *Walker, value *ast.Value) {
|
||||
if walker.CurrentOperation == nil || value.Kind != ast.Variable || value.VariableDefinition != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if walker.CurrentOperation.Name != "" {
|
||||
addError(
|
||||
Message(`Variable "%s" is not defined by operation "%s".`, value, walker.CurrentOperation.Name),
|
||||
At(value.Position),
|
||||
)
|
||||
} else {
|
||||
addError(
|
||||
Message(`Variable "%s" is not defined.`, value),
|
||||
At(value.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("NoUnusedFragments", func(observers *Events, addError AddErrFunc) {
|
||||
|
||||
inFragmentDefinition := false
|
||||
fragmentNameUsed := make(map[string]bool)
|
||||
|
||||
observers.OnFragmentSpread(func(walker *Walker, fragmentSpread *ast.FragmentSpread) {
|
||||
if !inFragmentDefinition {
|
||||
fragmentNameUsed[fragmentSpread.Name] = true
|
||||
}
|
||||
})
|
||||
|
||||
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
|
||||
inFragmentDefinition = true
|
||||
if !fragmentNameUsed[fragment.Name] {
|
||||
addError(
|
||||
Message(`Fragment "%s" is never used.`, fragment.Name),
|
||||
At(fragment.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("NoUnusedVariables", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
for _, varDef := range operation.VariableDefinitions {
|
||||
if varDef.Used {
|
||||
continue
|
||||
}
|
||||
|
||||
if operation.Name != "" {
|
||||
addError(
|
||||
Message(`Variable "$%s" is never used in operation "%s".`, varDef.Variable, operation.Name),
|
||||
At(varDef.Position),
|
||||
)
|
||||
} else {
|
||||
addError(
|
||||
Message(`Variable "$%s" is never used.`, varDef.Variable),
|
||||
At(varDef.Position),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+562
@@ -0,0 +1,562 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
AddRule("OverlappingFieldsCanBeMerged", func(observers *Events, addError AddErrFunc) {
|
||||
/**
|
||||
* Algorithm:
|
||||
*
|
||||
* Conflicts occur when two fields exist in a query which will produce the same
|
||||
* response name, but represent differing values, thus creating a conflict.
|
||||
* The algorithm below finds all conflicts via making a series of comparisons
|
||||
* between fields. In order to compare as few fields as possible, this makes
|
||||
* a series of comparisons "within" sets of fields and "between" sets of fields.
|
||||
*
|
||||
* Given any selection set, a collection produces both a set of fields by
|
||||
* also including all inline fragments, as well as a list of fragments
|
||||
* referenced by fragment spreads.
|
||||
*
|
||||
* A) Each selection set represented in the document first compares "within" its
|
||||
* collected set of fields, finding any conflicts between every pair of
|
||||
* overlapping fields.
|
||||
* Note: This is the *only time* that a the fields "within" a set are compared
|
||||
* to each other. After this only fields "between" sets are compared.
|
||||
*
|
||||
* B) Also, if any fragment is referenced in a selection set, then a
|
||||
* comparison is made "between" the original set of fields and the
|
||||
* referenced fragment.
|
||||
*
|
||||
* C) Also, if multiple fragments are referenced, then comparisons
|
||||
* are made "between" each referenced fragment.
|
||||
*
|
||||
* D) When comparing "between" a set of fields and a referenced fragment, first
|
||||
* a comparison is made between each field in the original set of fields and
|
||||
* each field in the the referenced set of fields.
|
||||
*
|
||||
* E) Also, if any fragment is referenced in the referenced selection set,
|
||||
* then a comparison is made "between" the original set of fields and the
|
||||
* referenced fragment (recursively referring to step D).
|
||||
*
|
||||
* F) When comparing "between" two fragments, first a comparison is made between
|
||||
* each field in the first referenced set of fields and each field in the the
|
||||
* second referenced set of fields.
|
||||
*
|
||||
* G) Also, any fragments referenced by the first must be compared to the
|
||||
* second, and any fragments referenced by the second must be compared to the
|
||||
* first (recursively referring to step F).
|
||||
*
|
||||
* H) When comparing two fields, if both have selection sets, then a comparison
|
||||
* is made "between" both selection sets, first comparing the set of fields in
|
||||
* the first selection set with the set of fields in the second.
|
||||
*
|
||||
* I) Also, if any fragment is referenced in either selection set, then a
|
||||
* comparison is made "between" the other set of fields and the
|
||||
* referenced fragment.
|
||||
*
|
||||
* J) Also, if two fragments are referenced in both selection sets, then a
|
||||
* comparison is made "between" the two fragments.
|
||||
*
|
||||
*/
|
||||
|
||||
m := &overlappingFieldsCanBeMergedManager{
|
||||
comparedFragmentPairs: pairSet{data: make(map[string]map[string]bool)},
|
||||
}
|
||||
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
m.walker = walker
|
||||
conflicts := m.findConflictsWithinSelectionSet(operation.SelectionSet)
|
||||
for _, conflict := range conflicts {
|
||||
conflict.addFieldsConflictMessage(addError)
|
||||
}
|
||||
})
|
||||
observers.OnField(func(walker *Walker, field *ast.Field) {
|
||||
if walker.CurrentOperation == nil {
|
||||
// When checking both Operation and Fragment, errors are duplicated when processing FragmentDefinition referenced from Operation
|
||||
return
|
||||
}
|
||||
m.walker = walker
|
||||
conflicts := m.findConflictsWithinSelectionSet(field.SelectionSet)
|
||||
for _, conflict := range conflicts {
|
||||
conflict.addFieldsConflictMessage(addError)
|
||||
}
|
||||
})
|
||||
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
|
||||
m.walker = walker
|
||||
conflicts := m.findConflictsWithinSelectionSet(inlineFragment.SelectionSet)
|
||||
for _, conflict := range conflicts {
|
||||
conflict.addFieldsConflictMessage(addError)
|
||||
}
|
||||
})
|
||||
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
|
||||
m.walker = walker
|
||||
conflicts := m.findConflictsWithinSelectionSet(fragment.SelectionSet)
|
||||
for _, conflict := range conflicts {
|
||||
conflict.addFieldsConflictMessage(addError)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type pairSet struct {
|
||||
data map[string]map[string]bool
|
||||
}
|
||||
|
||||
func (pairSet *pairSet) Add(a *ast.FragmentSpread, b *ast.FragmentSpread, areMutuallyExclusive bool) {
|
||||
add := func(a *ast.FragmentSpread, b *ast.FragmentSpread) {
|
||||
m := pairSet.data[a.Name]
|
||||
if m == nil {
|
||||
m = make(map[string]bool)
|
||||
pairSet.data[a.Name] = m
|
||||
}
|
||||
m[b.Name] = areMutuallyExclusive
|
||||
}
|
||||
add(a, b)
|
||||
add(b, a)
|
||||
}
|
||||
|
||||
func (pairSet *pairSet) Has(a *ast.FragmentSpread, b *ast.FragmentSpread, areMutuallyExclusive bool) bool {
|
||||
am, ok := pairSet.data[a.Name]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
result, ok := am[b.Name]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// areMutuallyExclusive being false is a superset of being true,
|
||||
// hence if we want to know if this PairSet "has" these two with no
|
||||
// exclusivity, we have to ensure it was added as such.
|
||||
if !areMutuallyExclusive {
|
||||
return !result
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type sequentialFieldsMap struct {
|
||||
// We can't use map[string][]*ast.Field. because map is not stable...
|
||||
seq []string
|
||||
data map[string][]*ast.Field
|
||||
}
|
||||
|
||||
type fieldIterateEntry struct {
|
||||
ResponseName string
|
||||
Fields []*ast.Field
|
||||
}
|
||||
|
||||
func (m *sequentialFieldsMap) Push(responseName string, field *ast.Field) {
|
||||
fields, ok := m.data[responseName]
|
||||
if !ok {
|
||||
m.seq = append(m.seq, responseName)
|
||||
}
|
||||
fields = append(fields, field)
|
||||
m.data[responseName] = fields
|
||||
}
|
||||
|
||||
func (m *sequentialFieldsMap) Get(responseName string) ([]*ast.Field, bool) {
|
||||
fields, ok := m.data[responseName]
|
||||
return fields, ok
|
||||
}
|
||||
|
||||
func (m *sequentialFieldsMap) Iterator() [][]*ast.Field {
|
||||
fieldsList := make([][]*ast.Field, 0, len(m.seq))
|
||||
for _, responseName := range m.seq {
|
||||
fields := m.data[responseName]
|
||||
fieldsList = append(fieldsList, fields)
|
||||
}
|
||||
return fieldsList
|
||||
}
|
||||
|
||||
func (m *sequentialFieldsMap) KeyValueIterator() []*fieldIterateEntry {
|
||||
fieldEntriesList := make([]*fieldIterateEntry, 0, len(m.seq))
|
||||
for _, responseName := range m.seq {
|
||||
fields := m.data[responseName]
|
||||
fieldEntriesList = append(fieldEntriesList, &fieldIterateEntry{
|
||||
ResponseName: responseName,
|
||||
Fields: fields,
|
||||
})
|
||||
}
|
||||
return fieldEntriesList
|
||||
}
|
||||
|
||||
type conflictMessageContainer struct {
|
||||
Conflicts []*ConflictMessage
|
||||
}
|
||||
|
||||
type ConflictMessage struct {
|
||||
Message string
|
||||
ResponseName string
|
||||
Names []string
|
||||
SubMessage []*ConflictMessage
|
||||
Position *ast.Position
|
||||
}
|
||||
|
||||
func (m *ConflictMessage) String(buf *bytes.Buffer) {
|
||||
if len(m.SubMessage) == 0 {
|
||||
buf.WriteString(m.Message)
|
||||
return
|
||||
}
|
||||
|
||||
for idx, subMessage := range m.SubMessage {
|
||||
buf.WriteString(`subfields "`)
|
||||
buf.WriteString(subMessage.ResponseName)
|
||||
buf.WriteString(`" conflict because `)
|
||||
subMessage.String(buf)
|
||||
if idx != len(m.SubMessage)-1 {
|
||||
buf.WriteString(" and ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ConflictMessage) addFieldsConflictMessage(addError AddErrFunc) {
|
||||
var buf bytes.Buffer
|
||||
m.String(&buf)
|
||||
addError(
|
||||
Message(`Fields "%s" conflict because %s. Use different aliases on the fields to fetch both if this was intentional.`, m.ResponseName, buf.String()),
|
||||
At(m.Position),
|
||||
)
|
||||
}
|
||||
|
||||
type overlappingFieldsCanBeMergedManager struct {
|
||||
walker *Walker
|
||||
|
||||
// per walker
|
||||
comparedFragmentPairs pairSet
|
||||
// cachedFieldsAndFragmentNames interface{}
|
||||
|
||||
// per selectionSet
|
||||
comparedFragments map[string]bool
|
||||
}
|
||||
|
||||
func (m *overlappingFieldsCanBeMergedManager) findConflictsWithinSelectionSet(selectionSet ast.SelectionSet) []*ConflictMessage {
|
||||
if len(selectionSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fieldsMap, fragmentSpreads := getFieldsAndFragmentNames(selectionSet)
|
||||
|
||||
var conflicts conflictMessageContainer
|
||||
|
||||
// (A) Find find all conflicts "within" the fieldMap of this selection set.
|
||||
// Note: this is the *only place* `collectConflictsWithin` is called.
|
||||
m.collectConflictsWithin(&conflicts, fieldsMap)
|
||||
|
||||
m.comparedFragments = make(map[string]bool)
|
||||
for idx, fragmentSpreadA := range fragmentSpreads {
|
||||
// (B) Then collect conflicts between these fieldMap and those represented by
|
||||
// each spread fragment name found.
|
||||
m.collectConflictsBetweenFieldsAndFragment(&conflicts, false, fieldsMap, fragmentSpreadA)
|
||||
|
||||
for _, fragmentSpreadB := range fragmentSpreads[idx+1:] {
|
||||
// (C) Then compare this fragment with all other fragments found in this
|
||||
// selection set to collect conflicts between fragments spread together.
|
||||
// This compares each item in the list of fragment names to every other
|
||||
// item in that same list (except for itself).
|
||||
m.collectConflictsBetweenFragments(&conflicts, false, fragmentSpreadA, fragmentSpreadB)
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts.Conflicts
|
||||
}
|
||||
|
||||
func (m *overlappingFieldsCanBeMergedManager) collectConflictsBetweenFieldsAndFragment(conflicts *conflictMessageContainer, areMutuallyExclusive bool, fieldsMap *sequentialFieldsMap, fragmentSpread *ast.FragmentSpread) {
|
||||
if m.comparedFragments[fragmentSpread.Name] {
|
||||
return
|
||||
}
|
||||
m.comparedFragments[fragmentSpread.Name] = true
|
||||
|
||||
if fragmentSpread.Definition == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fieldsMapB, fragmentSpreads := getFieldsAndFragmentNames(fragmentSpread.Definition.SelectionSet)
|
||||
|
||||
// Do not compare a fragment's fieldMap to itself.
|
||||
if reflect.DeepEqual(fieldsMap, fieldsMapB) {
|
||||
return
|
||||
}
|
||||
|
||||
// (D) First collect any conflicts between the provided collection of fields
|
||||
// and the collection of fields represented by the given fragment.
|
||||
m.collectConflictsBetween(conflicts, areMutuallyExclusive, fieldsMap, fieldsMapB)
|
||||
|
||||
// (E) Then collect any conflicts between the provided collection of fields
|
||||
// and any fragment names found in the given fragment.
|
||||
baseFragmentSpread := fragmentSpread
|
||||
for _, fragmentSpread := range fragmentSpreads {
|
||||
if fragmentSpread.Name == baseFragmentSpread.Name {
|
||||
continue
|
||||
}
|
||||
m.collectConflictsBetweenFieldsAndFragment(conflicts, areMutuallyExclusive, fieldsMap, fragmentSpread)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *overlappingFieldsCanBeMergedManager) collectConflictsBetweenFragments(conflicts *conflictMessageContainer, areMutuallyExclusive bool, fragmentSpreadA *ast.FragmentSpread, fragmentSpreadB *ast.FragmentSpread) {
|
||||
|
||||
var check func(fragmentSpreadA *ast.FragmentSpread, fragmentSpreadB *ast.FragmentSpread)
|
||||
check = func(fragmentSpreadA *ast.FragmentSpread, fragmentSpreadB *ast.FragmentSpread) {
|
||||
|
||||
if fragmentSpreadA.Name == fragmentSpreadB.Name {
|
||||
return
|
||||
}
|
||||
|
||||
if m.comparedFragmentPairs.Has(fragmentSpreadA, fragmentSpreadB, areMutuallyExclusive) {
|
||||
return
|
||||
}
|
||||
m.comparedFragmentPairs.Add(fragmentSpreadA, fragmentSpreadB, areMutuallyExclusive)
|
||||
|
||||
if fragmentSpreadA.Definition == nil {
|
||||
return
|
||||
}
|
||||
if fragmentSpreadB.Definition == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fieldsMapA, fragmentSpreadsA := getFieldsAndFragmentNames(fragmentSpreadA.Definition.SelectionSet)
|
||||
fieldsMapB, fragmentSpreadsB := getFieldsAndFragmentNames(fragmentSpreadB.Definition.SelectionSet)
|
||||
|
||||
// (F) First, collect all conflicts between these two collections of fields
|
||||
// (not including any nested fragments).
|
||||
m.collectConflictsBetween(conflicts, areMutuallyExclusive, fieldsMapA, fieldsMapB)
|
||||
|
||||
// (G) Then collect conflicts between the first fragment and any nested
|
||||
// fragments spread in the second fragment.
|
||||
for _, fragmentSpread := range fragmentSpreadsB {
|
||||
check(fragmentSpreadA, fragmentSpread)
|
||||
}
|
||||
// (G) Then collect conflicts between the second fragment and any nested
|
||||
// fragments spread in the first fragment.
|
||||
for _, fragmentSpread := range fragmentSpreadsA {
|
||||
check(fragmentSpread, fragmentSpreadB)
|
||||
}
|
||||
}
|
||||
|
||||
check(fragmentSpreadA, fragmentSpreadB)
|
||||
}
|
||||
|
||||
func (m *overlappingFieldsCanBeMergedManager) findConflictsBetweenSubSelectionSets(areMutuallyExclusive bool, selectionSetA ast.SelectionSet, selectionSetB ast.SelectionSet) *conflictMessageContainer {
|
||||
var conflicts conflictMessageContainer
|
||||
|
||||
fieldsMapA, fragmentSpreadsA := getFieldsAndFragmentNames(selectionSetA)
|
||||
fieldsMapB, fragmentSpreadsB := getFieldsAndFragmentNames(selectionSetB)
|
||||
|
||||
// (H) First, collect all conflicts between these two collections of field.
|
||||
m.collectConflictsBetween(&conflicts, areMutuallyExclusive, fieldsMapA, fieldsMapB)
|
||||
|
||||
// (I) Then collect conflicts between the first collection of fields and
|
||||
// those referenced by each fragment name associated with the second.
|
||||
for _, fragmentSpread := range fragmentSpreadsB {
|
||||
m.comparedFragments = make(map[string]bool)
|
||||
m.collectConflictsBetweenFieldsAndFragment(&conflicts, areMutuallyExclusive, fieldsMapA, fragmentSpread)
|
||||
}
|
||||
|
||||
// (I) Then collect conflicts between the second collection of fields and
|
||||
// those referenced by each fragment name associated with the first.
|
||||
for _, fragmentSpread := range fragmentSpreadsA {
|
||||
m.comparedFragments = make(map[string]bool)
|
||||
m.collectConflictsBetweenFieldsAndFragment(&conflicts, areMutuallyExclusive, fieldsMapB, fragmentSpread)
|
||||
}
|
||||
|
||||
// (J) Also collect conflicts between any fragment names by the first and
|
||||
// fragment names by the second. This compares each item in the first set of
|
||||
// names to each item in the second set of names.
|
||||
for _, fragmentSpreadA := range fragmentSpreadsA {
|
||||
for _, fragmentSpreadB := range fragmentSpreadsB {
|
||||
m.collectConflictsBetweenFragments(&conflicts, areMutuallyExclusive, fragmentSpreadA, fragmentSpreadB)
|
||||
}
|
||||
}
|
||||
|
||||
if len(conflicts.Conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &conflicts
|
||||
}
|
||||
|
||||
func (m *overlappingFieldsCanBeMergedManager) collectConflictsWithin(conflicts *conflictMessageContainer, fieldsMap *sequentialFieldsMap) {
|
||||
for _, fields := range fieldsMap.Iterator() {
|
||||
for idx, fieldA := range fields {
|
||||
for _, fieldB := range fields[idx+1:] {
|
||||
conflict := m.findConflict(false, fieldA, fieldB)
|
||||
if conflict != nil {
|
||||
conflicts.Conflicts = append(conflicts.Conflicts, conflict)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *overlappingFieldsCanBeMergedManager) collectConflictsBetween(conflicts *conflictMessageContainer, parentFieldsAreMutuallyExclusive bool, fieldsMapA *sequentialFieldsMap, fieldsMapB *sequentialFieldsMap) {
|
||||
for _, fieldsEntryA := range fieldsMapA.KeyValueIterator() {
|
||||
fieldsB, ok := fieldsMapB.Get(fieldsEntryA.ResponseName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, fieldA := range fieldsEntryA.Fields {
|
||||
for _, fieldB := range fieldsB {
|
||||
conflict := m.findConflict(parentFieldsAreMutuallyExclusive, fieldA, fieldB)
|
||||
if conflict != nil {
|
||||
conflicts.Conflicts = append(conflicts.Conflicts, conflict)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *overlappingFieldsCanBeMergedManager) findConflict(parentFieldsAreMutuallyExclusive bool, fieldA *ast.Field, fieldB *ast.Field) *ConflictMessage {
|
||||
if fieldA.ObjectDefinition == nil || fieldB.ObjectDefinition == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
areMutuallyExclusive := parentFieldsAreMutuallyExclusive
|
||||
if !areMutuallyExclusive {
|
||||
tmp := fieldA.ObjectDefinition.Name != fieldB.ObjectDefinition.Name
|
||||
tmp = tmp && fieldA.ObjectDefinition.Kind == ast.Object
|
||||
tmp = tmp && fieldB.ObjectDefinition.Kind == ast.Object
|
||||
tmp = tmp && fieldA.Definition != nil && fieldB.Definition != nil
|
||||
areMutuallyExclusive = tmp
|
||||
}
|
||||
|
||||
fieldNameA := fieldA.Name
|
||||
if fieldA.Alias != "" {
|
||||
fieldNameA = fieldA.Alias
|
||||
}
|
||||
|
||||
if !areMutuallyExclusive {
|
||||
// Two aliases must refer to the same field.
|
||||
if fieldA.Name != fieldB.Name {
|
||||
return &ConflictMessage{
|
||||
ResponseName: fieldNameA,
|
||||
Message: fmt.Sprintf(`"%s" and "%s" are different fields`, fieldA.Name, fieldB.Name),
|
||||
Position: fieldB.Position,
|
||||
}
|
||||
}
|
||||
|
||||
// Two field calls must have the same arguments.
|
||||
if !sameArguments(fieldA.Arguments, fieldB.Arguments) {
|
||||
return &ConflictMessage{
|
||||
ResponseName: fieldNameA,
|
||||
Message: "they have differing arguments",
|
||||
Position: fieldB.Position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fieldA.Definition != nil && fieldB.Definition != nil && doTypesConflict(m.walker, fieldA.Definition.Type, fieldB.Definition.Type) {
|
||||
return &ConflictMessage{
|
||||
ResponseName: fieldNameA,
|
||||
Message: fmt.Sprintf(`they return conflicting types "%s" and "%s"`, fieldA.Definition.Type.String(), fieldB.Definition.Type.String()),
|
||||
Position: fieldB.Position,
|
||||
}
|
||||
}
|
||||
|
||||
// Collect and compare sub-fields. Use the same "visited fragment names" list
|
||||
// for both collections so fields in a fragment reference are never
|
||||
// compared to themselves.
|
||||
conflicts := m.findConflictsBetweenSubSelectionSets(areMutuallyExclusive, fieldA.SelectionSet, fieldB.SelectionSet)
|
||||
if conflicts == nil {
|
||||
return nil
|
||||
}
|
||||
return &ConflictMessage{
|
||||
ResponseName: fieldNameA,
|
||||
SubMessage: conflicts.Conflicts,
|
||||
Position: fieldB.Position,
|
||||
}
|
||||
}
|
||||
|
||||
func sameArguments(args1 []*ast.Argument, args2 []*ast.Argument) bool {
|
||||
if len(args1) != len(args2) {
|
||||
return false
|
||||
}
|
||||
for _, arg1 := range args1 {
|
||||
var matched bool
|
||||
for _, arg2 := range args2 {
|
||||
if arg1.Name == arg2.Name && sameValue(arg1.Value, arg2.Value) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sameValue(value1 *ast.Value, value2 *ast.Value) bool {
|
||||
if value1.Kind != value2.Kind {
|
||||
return false
|
||||
}
|
||||
if value1.Raw != value2.Raw {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func doTypesConflict(walker *Walker, type1 *ast.Type, type2 *ast.Type) bool {
|
||||
if type1.Elem != nil {
|
||||
if type2.Elem != nil {
|
||||
return doTypesConflict(walker, type1.Elem, type2.Elem)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if type2.Elem != nil {
|
||||
return true
|
||||
}
|
||||
if type1.NonNull && !type2.NonNull {
|
||||
return true
|
||||
}
|
||||
if !type1.NonNull && type2.NonNull {
|
||||
return true
|
||||
}
|
||||
|
||||
t1 := walker.Schema.Types[type1.NamedType]
|
||||
t2 := walker.Schema.Types[type2.NamedType]
|
||||
if (t1.Kind == ast.Scalar || t1.Kind == ast.Enum) && (t2.Kind == ast.Scalar || t2.Kind == ast.Enum) {
|
||||
return t1.Name != t2.Name
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getFieldsAndFragmentNames(selectionSet ast.SelectionSet) (*sequentialFieldsMap, []*ast.FragmentSpread) {
|
||||
fieldsMap := sequentialFieldsMap{
|
||||
data: make(map[string][]*ast.Field),
|
||||
}
|
||||
var fragmentSpreads []*ast.FragmentSpread
|
||||
|
||||
var walk func(selectionSet ast.SelectionSet)
|
||||
walk = func(selectionSet ast.SelectionSet) {
|
||||
for _, selection := range selectionSet {
|
||||
switch selection := selection.(type) {
|
||||
case *ast.Field:
|
||||
responseName := selection.Name
|
||||
if selection.Alias != "" {
|
||||
responseName = selection.Alias
|
||||
}
|
||||
fieldsMap.Push(responseName, selection)
|
||||
|
||||
case *ast.InlineFragment:
|
||||
walk(selection.SelectionSet)
|
||||
|
||||
case *ast.FragmentSpread:
|
||||
fragmentSpreads = append(fragmentSpreads, selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(selectionSet)
|
||||
|
||||
return &fieldsMap, fragmentSpreads
|
||||
}
|
||||
Generated
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("PossibleFragmentSpreads", func(observers *Events, addError AddErrFunc) {
|
||||
|
||||
validate := func(walker *Walker, parentDef *ast.Definition, fragmentName string, emitError func()) {
|
||||
if parentDef == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var parentDefs []*ast.Definition
|
||||
switch parentDef.Kind {
|
||||
case ast.Object:
|
||||
parentDefs = []*ast.Definition{parentDef}
|
||||
case ast.Interface, ast.Union:
|
||||
parentDefs = walker.Schema.GetPossibleTypes(parentDef)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
fragmentDefType := walker.Schema.Types[fragmentName]
|
||||
if fragmentDefType == nil {
|
||||
return
|
||||
}
|
||||
if !fragmentDefType.IsCompositeType() {
|
||||
// checked by FragmentsOnCompositeTypes
|
||||
return
|
||||
}
|
||||
fragmentDefs := walker.Schema.GetPossibleTypes(fragmentDefType)
|
||||
|
||||
for _, fragmentDef := range fragmentDefs {
|
||||
for _, parentDef := range parentDefs {
|
||||
if parentDef.Name == fragmentDef.Name {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emitError()
|
||||
}
|
||||
|
||||
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
|
||||
validate(walker, inlineFragment.ObjectDefinition, inlineFragment.TypeCondition, func() {
|
||||
addError(
|
||||
Message(`Fragment cannot be spread here as objects of type "%s" can never be of type "%s".`, inlineFragment.ObjectDefinition.Name, inlineFragment.TypeCondition),
|
||||
At(inlineFragment.Position),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
observers.OnFragmentSpread(func(walker *Walker, fragmentSpread *ast.FragmentSpread) {
|
||||
if fragmentSpread.Definition == nil {
|
||||
return
|
||||
}
|
||||
validate(walker, fragmentSpread.ObjectDefinition, fragmentSpread.Definition.TypeCondition, func() {
|
||||
addError(
|
||||
Message(`Fragment "%s" cannot be spread here as objects of type "%s" can never be of type "%s".`, fragmentSpread.Name, fragmentSpread.ObjectDefinition.Name, fragmentSpread.Definition.TypeCondition),
|
||||
At(fragmentSpread.Position),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("ProvidedRequiredArguments", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnField(func(walker *Walker, field *ast.Field) {
|
||||
if field.Definition == nil {
|
||||
return
|
||||
}
|
||||
|
||||
argDef:
|
||||
for _, argDef := range field.Definition.Arguments {
|
||||
if !argDef.Type.NonNull {
|
||||
continue
|
||||
}
|
||||
if argDef.DefaultValue != nil {
|
||||
continue
|
||||
}
|
||||
for _, arg := range field.Arguments {
|
||||
if arg.Name == argDef.Name {
|
||||
continue argDef
|
||||
}
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Field "%s" argument "%s" of type "%s" is required, but it was not provided.`, field.Name, argDef.Name, argDef.Type.String()),
|
||||
At(field.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
|
||||
if directive.Definition == nil {
|
||||
return
|
||||
}
|
||||
|
||||
argDef:
|
||||
for _, argDef := range directive.Definition.Arguments {
|
||||
if !argDef.Type.NonNull {
|
||||
continue
|
||||
}
|
||||
if argDef.DefaultValue != nil {
|
||||
continue
|
||||
}
|
||||
for _, arg := range directive.Arguments {
|
||||
if arg.Name == argDef.Name {
|
||||
continue argDef
|
||||
}
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Directive "@%s" argument "%s" of type "%s" is required, but it was not provided.`, directive.Definition.Name, argDef.Name, argDef.Type.String()),
|
||||
At(directive.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("ScalarLeafs", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnField(func(walker *Walker, field *ast.Field) {
|
||||
if field.Definition == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fieldType := walker.Schema.Types[field.Definition.Type.Name()]
|
||||
if fieldType == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fieldType.IsLeafType() && len(field.SelectionSet) > 0 {
|
||||
addError(
|
||||
Message(`Field "%s" must not have a selection since type "%s" has no subfields.`, field.Name, fieldType.Name),
|
||||
At(field.Position),
|
||||
)
|
||||
}
|
||||
|
||||
if !fieldType.IsLeafType() && len(field.SelectionSet) == 0 {
|
||||
addError(
|
||||
Message(`Field "%s" of type "%s" must have a selection of subfields.`, field.Name, field.Definition.Type.String()),
|
||||
Suggestf(`"%s { ... }"`, field.Name),
|
||||
At(field.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("SingleFieldSubscriptions", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
if walker.Schema.Subscription == nil || operation.Operation != ast.Subscription {
|
||||
return
|
||||
}
|
||||
|
||||
fields := retrieveTopFieldNames(operation.SelectionSet)
|
||||
|
||||
name := "Anonymous Subscription"
|
||||
if operation.Name != "" {
|
||||
name = `Subscription ` + strconv.Quote(operation.Name)
|
||||
}
|
||||
|
||||
if len(fields) > 1 {
|
||||
addError(
|
||||
Message(`%s must select only one top level field.`, name),
|
||||
At(fields[1].position),
|
||||
)
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
if strings.HasPrefix(field.name, "__") {
|
||||
addError(
|
||||
Message(`%s must not select an introspection top level field.`, name),
|
||||
At(field.position),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type topField struct {
|
||||
name string
|
||||
position *ast.Position
|
||||
}
|
||||
|
||||
func retrieveTopFieldNames(selectionSet ast.SelectionSet) []*topField {
|
||||
fields := []*topField{}
|
||||
inFragmentRecursive := map[string]bool{}
|
||||
var walk func(selectionSet ast.SelectionSet)
|
||||
walk = func(selectionSet ast.SelectionSet) {
|
||||
for _, selection := range selectionSet {
|
||||
switch selection := selection.(type) {
|
||||
case *ast.Field:
|
||||
fields = append(fields, &topField{
|
||||
name: selection.Name,
|
||||
position: selection.GetPosition(),
|
||||
})
|
||||
case *ast.InlineFragment:
|
||||
walk(selection.SelectionSet)
|
||||
case *ast.FragmentSpread:
|
||||
if selection.Definition == nil {
|
||||
return
|
||||
}
|
||||
fragment := selection.Definition.Name
|
||||
if !inFragmentRecursive[fragment] {
|
||||
inFragmentRecursive[fragment] = true
|
||||
walk(selection.Definition.SelectionSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(selectionSet)
|
||||
|
||||
seen := make(map[string]bool, len(fields))
|
||||
uniquedFields := make([]*topField, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if !seen[field.name] {
|
||||
uniquedFields = append(uniquedFields, field)
|
||||
}
|
||||
seen[field.name] = true
|
||||
}
|
||||
return uniquedFields
|
||||
}
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("UniqueArgumentNames", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnField(func(walker *Walker, field *ast.Field) {
|
||||
checkUniqueArgs(field.Arguments, addError)
|
||||
})
|
||||
|
||||
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
|
||||
checkUniqueArgs(directive.Arguments, addError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func checkUniqueArgs(args ast.ArgumentList, addError AddErrFunc) {
|
||||
knownArgNames := map[string]int{}
|
||||
|
||||
for _, arg := range args {
|
||||
if knownArgNames[arg.Name] == 1 {
|
||||
addError(
|
||||
Message(`There can be only one argument named "%s".`, arg.Name),
|
||||
At(arg.Position),
|
||||
)
|
||||
}
|
||||
|
||||
knownArgNames[arg.Name]++
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("UniqueDirectivesPerLocation", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnDirectiveList(func(walker *Walker, directives []*ast.Directive) {
|
||||
seen := map[string]bool{}
|
||||
|
||||
for _, dir := range directives {
|
||||
if dir.Name != "repeatable" && seen[dir.Name] {
|
||||
addError(
|
||||
Message(`The directive "@%s" can only be used once at this location.`, dir.Name),
|
||||
At(dir.Position),
|
||||
)
|
||||
}
|
||||
seen[dir.Name] = true
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("UniqueFragmentNames", func(observers *Events, addError AddErrFunc) {
|
||||
seenFragments := map[string]bool{}
|
||||
|
||||
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
|
||||
if seenFragments[fragment.Name] {
|
||||
addError(
|
||||
Message(`There can be only one fragment named "%s".`, fragment.Name),
|
||||
At(fragment.Position),
|
||||
)
|
||||
}
|
||||
seenFragments[fragment.Name] = true
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("UniqueInputFieldNames", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnValue(func(walker *Walker, value *ast.Value) {
|
||||
if value.Kind != ast.ObjectValue {
|
||||
return
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
for _, field := range value.Children {
|
||||
if seen[field.Name] {
|
||||
addError(
|
||||
Message(`There can be only one input field named "%s".`, field.Name),
|
||||
At(field.Position),
|
||||
)
|
||||
}
|
||||
seen[field.Name] = true
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
vendor/github.com/open-policy-agent/opa/internal/gqlparser/validator/rules/unique_operation_names.go
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("UniqueOperationNames", func(observers *Events, addError AddErrFunc) {
|
||||
seen := map[string]bool{}
|
||||
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
if seen[operation.Name] {
|
||||
addError(
|
||||
Message(`There can be only one operation named "%s".`, operation.Name),
|
||||
At(operation.Position),
|
||||
)
|
||||
}
|
||||
seen[operation.Name] = true
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("UniqueVariableNames", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
seen := map[string]int{}
|
||||
for _, def := range operation.VariableDefinitions {
|
||||
// add the same error only once per a variable.
|
||||
if seen[def.Variable] == 1 {
|
||||
addError(
|
||||
Message(`There can be only one variable named "$%s".`, def.Variable),
|
||||
At(def.Position),
|
||||
)
|
||||
}
|
||||
seen[def.Variable]++
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
vendor/github.com/open-policy-agent/opa/internal/gqlparser/validator/rules/values_of_correct_type.go
Generated
Vendored
+170
@@ -0,0 +1,170 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("ValuesOfCorrectType", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnValue(func(walker *Walker, value *ast.Value) {
|
||||
if value.Definition == nil || value.ExpectedType == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if value.Kind == ast.NullValue && value.ExpectedType.NonNull {
|
||||
addError(
|
||||
Message(`Expected value of type "%s", found %s.`, value.ExpectedType.String(), value.String()),
|
||||
At(value.Position),
|
||||
)
|
||||
}
|
||||
|
||||
if value.Definition.Kind == ast.Scalar {
|
||||
// Skip custom validating scalars
|
||||
if !value.Definition.OneOf("Int", "Float", "String", "Boolean", "ID") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var possibleEnums []string
|
||||
if value.Definition.Kind == ast.Enum {
|
||||
for _, val := range value.Definition.EnumValues {
|
||||
possibleEnums = append(possibleEnums, val.Name)
|
||||
}
|
||||
}
|
||||
|
||||
rawVal, err := value.Value(nil)
|
||||
if err != nil {
|
||||
unexpectedTypeMessage(addError, value)
|
||||
}
|
||||
|
||||
switch value.Kind {
|
||||
case ast.NullValue:
|
||||
return
|
||||
case ast.ListValue:
|
||||
if value.ExpectedType.Elem == nil {
|
||||
unexpectedTypeMessage(addError, value)
|
||||
return
|
||||
}
|
||||
|
||||
case ast.IntValue:
|
||||
if !value.Definition.OneOf("Int", "Float", "ID") {
|
||||
unexpectedTypeMessage(addError, value)
|
||||
}
|
||||
|
||||
case ast.FloatValue:
|
||||
if !value.Definition.OneOf("Float") {
|
||||
unexpectedTypeMessage(addError, value)
|
||||
}
|
||||
|
||||
case ast.StringValue, ast.BlockValue:
|
||||
if value.Definition.Kind == ast.Enum {
|
||||
rawValStr := fmt.Sprint(rawVal)
|
||||
addError(
|
||||
Message(`Enum "%s" cannot represent non-enum value: %s.`, value.ExpectedType.String(), value.String()),
|
||||
SuggestListQuoted("Did you mean the enum value", rawValStr, possibleEnums),
|
||||
At(value.Position),
|
||||
)
|
||||
} else if !value.Definition.OneOf("String", "ID") {
|
||||
unexpectedTypeMessage(addError, value)
|
||||
}
|
||||
|
||||
case ast.EnumValue:
|
||||
if value.Definition.Kind != ast.Enum {
|
||||
rawValStr := fmt.Sprint(rawVal)
|
||||
addError(
|
||||
unexpectedTypeMessageOnly(value),
|
||||
SuggestListUnquoted("Did you mean the enum value", rawValStr, possibleEnums),
|
||||
At(value.Position),
|
||||
)
|
||||
} else if value.Definition.EnumValues.ForName(value.Raw) == nil {
|
||||
rawValStr := fmt.Sprint(rawVal)
|
||||
addError(
|
||||
Message(`Value "%s" does not exist in "%s" enum.`, value.String(), value.ExpectedType.String()),
|
||||
SuggestListQuoted("Did you mean the enum value", rawValStr, possibleEnums),
|
||||
At(value.Position),
|
||||
)
|
||||
}
|
||||
|
||||
case ast.BooleanValue:
|
||||
if !value.Definition.OneOf("Boolean") {
|
||||
unexpectedTypeMessage(addError, value)
|
||||
}
|
||||
|
||||
case ast.ObjectValue:
|
||||
|
||||
for _, field := range value.Definition.Fields {
|
||||
if field.Type.NonNull {
|
||||
fieldValue := value.Children.ForName(field.Name)
|
||||
if fieldValue == nil && field.DefaultValue == nil {
|
||||
addError(
|
||||
Message(`Field "%s.%s" of required type "%s" was not provided.`, value.Definition.Name, field.Name, field.Type.String()),
|
||||
At(value.Position),
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, fieldValue := range value.Children {
|
||||
if value.Definition.Fields.ForName(fieldValue.Name) == nil {
|
||||
var suggestions []string
|
||||
for _, fieldValue := range value.Definition.Fields {
|
||||
suggestions = append(suggestions, fieldValue.Name)
|
||||
}
|
||||
|
||||
addError(
|
||||
Message(`Field "%s" is not defined by type "%s".`, fieldValue.Name, value.Definition.Name),
|
||||
SuggestListQuoted("Did you mean", fieldValue.Name, suggestions),
|
||||
At(fieldValue.Position),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
case ast.Variable:
|
||||
return
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("unhandled %T", value))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func unexpectedTypeMessage(addError AddErrFunc, v *ast.Value) {
|
||||
addError(
|
||||
unexpectedTypeMessageOnly(v),
|
||||
At(v.Position),
|
||||
)
|
||||
}
|
||||
|
||||
func unexpectedTypeMessageOnly(v *ast.Value) ErrorOption {
|
||||
switch v.ExpectedType.String() {
|
||||
case "Int", "Int!":
|
||||
if _, err := strconv.ParseInt(v.Raw, 10, 32); err != nil && errors.Is(err, strconv.ErrRange) {
|
||||
return Message(`Int cannot represent non 32-bit signed integer value: %s`, v.String())
|
||||
}
|
||||
return Message(`Int cannot represent non-integer value: %s`, v.String())
|
||||
case "String", "String!", "[String]":
|
||||
return Message(`String cannot represent a non string value: %s`, v.String())
|
||||
case "Boolean", "Boolean!":
|
||||
return Message(`Boolean cannot represent a non boolean value: %s`, v.String())
|
||||
case "Float", "Float!":
|
||||
return Message(`Float cannot represent non numeric value: %s`, v.String())
|
||||
case "ID", "ID!":
|
||||
return Message(`ID cannot represent a non-string and non-integer value: %s`, v.String())
|
||||
//case "Enum":
|
||||
// return Message(`Enum "%s" cannot represent non-enum value: %s`, v.ExpectedType.String(), v.String())
|
||||
default:
|
||||
if v.Definition.Kind == ast.Enum {
|
||||
return Message(`Enum "%s" cannot represent non-enum value: %s.`, v.ExpectedType.String(), v.String())
|
||||
}
|
||||
return Message(`Expected value of type "%s", found %s.`, v.ExpectedType.String(), v.String())
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("VariablesAreInputTypes", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
|
||||
for _, def := range operation.VariableDefinitions {
|
||||
if def.Definition == nil {
|
||||
continue
|
||||
}
|
||||
if !def.Definition.IsInputType() {
|
||||
addError(
|
||||
Message(
|
||||
`Variable "$%s" cannot be non-input type "%s".`,
|
||||
def.Variable,
|
||||
def.Type.String(),
|
||||
),
|
||||
At(def.Position),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
|
||||
//nolint:revive // Validator rules each use dot imports for convenience.
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/validator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddRule("VariablesInAllowedPosition", func(observers *Events, addError AddErrFunc) {
|
||||
observers.OnValue(func(walker *Walker, value *ast.Value) {
|
||||
if value.Kind != ast.Variable || value.ExpectedType == nil || value.VariableDefinition == nil || walker.CurrentOperation == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tmp := *value.ExpectedType
|
||||
|
||||
// todo: move me into walk
|
||||
// If there is a default non nullable types can be null
|
||||
if value.VariableDefinition.DefaultValue != nil && value.VariableDefinition.DefaultValue.Kind != ast.NullValue {
|
||||
if value.ExpectedType.NonNull {
|
||||
tmp.NonNull = false
|
||||
}
|
||||
}
|
||||
|
||||
if !value.VariableDefinition.Type.IsCompatible(&tmp) {
|
||||
addError(
|
||||
Message(
|
||||
`Variable "%s" of type "%s" used in position expecting type "%s".`,
|
||||
value,
|
||||
value.VariableDefinition.Type.String(),
|
||||
value.ExpectedType.String(),
|
||||
),
|
||||
At(value.Position),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
//nolint:revive
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/gqlerror"
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/parser"
|
||||
)
|
||||
|
||||
func LoadSchema(inputs ...*Source) (*Schema, error) {
|
||||
ast, err := parser.ParseSchemas(inputs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ValidateSchemaDocument(ast)
|
||||
}
|
||||
|
||||
func ValidateSchemaDocument(ast *SchemaDocument) (*Schema, error) {
|
||||
schema := Schema{
|
||||
Types: map[string]*Definition{},
|
||||
Directives: map[string]*DirectiveDefinition{},
|
||||
PossibleTypes: map[string][]*Definition{},
|
||||
Implements: map[string][]*Definition{},
|
||||
}
|
||||
|
||||
for i, def := range ast.Definitions {
|
||||
if schema.Types[def.Name] != nil {
|
||||
return nil, gqlerror.ErrorPosf(def.Position, "Cannot redeclare type %s.", def.Name)
|
||||
}
|
||||
schema.Types[def.Name] = ast.Definitions[i]
|
||||
}
|
||||
|
||||
defs := append(DefinitionList{}, ast.Definitions...)
|
||||
|
||||
for _, ext := range ast.Extensions {
|
||||
def := schema.Types[ext.Name]
|
||||
if def == nil {
|
||||
schema.Types[ext.Name] = &Definition{
|
||||
Kind: ext.Kind,
|
||||
Name: ext.Name,
|
||||
Position: ext.Position,
|
||||
}
|
||||
def = schema.Types[ext.Name]
|
||||
defs = append(defs, def)
|
||||
}
|
||||
|
||||
if def.Kind != ext.Kind {
|
||||
return nil, gqlerror.ErrorPosf(ext.Position, "Cannot extend type %s because the base type is a %s, not %s.", ext.Name, def.Kind, ext.Kind)
|
||||
}
|
||||
|
||||
def.Directives = append(def.Directives, ext.Directives...)
|
||||
def.Interfaces = append(def.Interfaces, ext.Interfaces...)
|
||||
def.Fields = append(def.Fields, ext.Fields...)
|
||||
def.Types = append(def.Types, ext.Types...)
|
||||
def.EnumValues = append(def.EnumValues, ext.EnumValues...)
|
||||
}
|
||||
|
||||
for _, def := range defs {
|
||||
switch def.Kind {
|
||||
case Union:
|
||||
for _, t := range def.Types {
|
||||
schema.AddPossibleType(def.Name, schema.Types[t])
|
||||
schema.AddImplements(t, def)
|
||||
}
|
||||
case InputObject, Object:
|
||||
for _, intf := range def.Interfaces {
|
||||
schema.AddPossibleType(intf, def)
|
||||
schema.AddImplements(def.Name, schema.Types[intf])
|
||||
}
|
||||
schema.AddPossibleType(def.Name, def)
|
||||
case Interface:
|
||||
for _, intf := range def.Interfaces {
|
||||
schema.AddPossibleType(intf, def)
|
||||
schema.AddImplements(def.Name, schema.Types[intf])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, dir := range ast.Directives {
|
||||
if schema.Directives[dir.Name] != nil {
|
||||
// While the spec says SDL must not (§3.5) explicitly define builtin
|
||||
// scalars, it may (§3.13) define builtin directives. Here we check for
|
||||
// that, and reject doubly-defined directives otherwise.
|
||||
switch dir.Name {
|
||||
case "include", "skip", "deprecated", "specifiedBy": // the builtins
|
||||
// In principle here we might want to validate that the
|
||||
// directives are the same. But they might not be, if the
|
||||
// server has an older spec than we do. (Plus, validating this
|
||||
// is a lot of work.) So we just keep the first one we saw.
|
||||
// That's an arbitrary choice, but in theory the only way it
|
||||
// fails is if the server is using features newer than this
|
||||
// version of gqlparser, in which case they're in trouble
|
||||
// anyway.
|
||||
default:
|
||||
return nil, gqlerror.ErrorPosf(dir.Position, "Cannot redeclare directive %s.", dir.Name)
|
||||
}
|
||||
}
|
||||
schema.Directives[dir.Name] = ast.Directives[i]
|
||||
}
|
||||
|
||||
if len(ast.Schema) > 1 {
|
||||
return nil, gqlerror.ErrorPosf(ast.Schema[1].Position, "Cannot have multiple schema entry points, consider schema extensions instead.")
|
||||
}
|
||||
|
||||
if len(ast.Schema) == 1 {
|
||||
schema.Description = ast.Schema[0].Description
|
||||
for _, entrypoint := range ast.Schema[0].OperationTypes {
|
||||
def := schema.Types[entrypoint.Type]
|
||||
if def == nil {
|
||||
return nil, gqlerror.ErrorPosf(entrypoint.Position, "Schema root %s refers to a type %s that does not exist.", entrypoint.Operation, entrypoint.Type)
|
||||
}
|
||||
switch entrypoint.Operation {
|
||||
case Query:
|
||||
schema.Query = def
|
||||
case Mutation:
|
||||
schema.Mutation = def
|
||||
case Subscription:
|
||||
schema.Subscription = def
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range ast.SchemaExtension {
|
||||
for _, entrypoint := range ext.OperationTypes {
|
||||
def := schema.Types[entrypoint.Type]
|
||||
if def == nil {
|
||||
return nil, gqlerror.ErrorPosf(entrypoint.Position, "Schema root %s refers to a type %s that does not exist.", entrypoint.Operation, entrypoint.Type)
|
||||
}
|
||||
switch entrypoint.Operation {
|
||||
case Query:
|
||||
schema.Query = def
|
||||
case Mutation:
|
||||
schema.Mutation = def
|
||||
case Subscription:
|
||||
schema.Subscription = def
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateTypeDefinitions(&schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateDirectiveDefinitions(&schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Inferred root operation type names should be performed only when a `schema` directive is
|
||||
// **not** provided, when it is, `Mutation` and `Subscription` becomes valid types and are not
|
||||
// assigned as a root operation on the schema.
|
||||
if len(ast.Schema) == 0 {
|
||||
if schema.Query == nil && schema.Types["Query"] != nil {
|
||||
schema.Query = schema.Types["Query"]
|
||||
}
|
||||
|
||||
if schema.Mutation == nil && schema.Types["Mutation"] != nil {
|
||||
schema.Mutation = schema.Types["Mutation"]
|
||||
}
|
||||
|
||||
if schema.Subscription == nil && schema.Types["Subscription"] != nil {
|
||||
schema.Subscription = schema.Types["Subscription"]
|
||||
}
|
||||
}
|
||||
|
||||
if schema.Query != nil {
|
||||
schema.Query.Fields = append(
|
||||
schema.Query.Fields,
|
||||
&FieldDefinition{
|
||||
Name: "__schema",
|
||||
Type: NonNullNamedType("__Schema", nil),
|
||||
},
|
||||
&FieldDefinition{
|
||||
Name: "__type",
|
||||
Type: NamedType("__Type", nil),
|
||||
Arguments: ArgumentDefinitionList{
|
||||
{Name: "name", Type: NonNullNamedType("String", nil)},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return &schema, nil
|
||||
}
|
||||
|
||||
func validateTypeDefinitions(schema *Schema) *gqlerror.Error {
|
||||
types := make([]string, 0, len(schema.Types))
|
||||
for typ := range schema.Types {
|
||||
types = append(types, typ)
|
||||
}
|
||||
sort.Strings(types)
|
||||
for _, typ := range types {
|
||||
err := validateDefinition(schema, schema.Types[typ])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDirectiveDefinitions(schema *Schema) *gqlerror.Error {
|
||||
directives := make([]string, 0, len(schema.Directives))
|
||||
for directive := range schema.Directives {
|
||||
directives = append(directives, directive)
|
||||
}
|
||||
sort.Strings(directives)
|
||||
for _, directive := range directives {
|
||||
err := validateDirective(schema, schema.Directives[directive])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDirective(schema *Schema, def *DirectiveDefinition) *gqlerror.Error {
|
||||
if err := validateName(def.Position, def.Name); err != nil {
|
||||
// now, GraphQL spec doesn't have reserved directive name
|
||||
return err
|
||||
}
|
||||
|
||||
return validateArgs(schema, def.Arguments, def)
|
||||
}
|
||||
|
||||
func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
|
||||
for _, field := range def.Fields {
|
||||
if err := validateName(field.Position, field.Name); err != nil {
|
||||
// now, GraphQL spec doesn't have reserved field name
|
||||
return err
|
||||
}
|
||||
if err := validateTypeRef(schema, field.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateArgs(schema, field.Arguments, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
wantDirLocation := LocationFieldDefinition
|
||||
if def.Kind == InputObject {
|
||||
wantDirLocation = LocationInputFieldDefinition
|
||||
}
|
||||
if err := validateDirectives(schema, field.Directives, wantDirLocation, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, typ := range def.Types {
|
||||
typDef := schema.Types[typ]
|
||||
if typDef == nil {
|
||||
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(typ))
|
||||
}
|
||||
if !isValidKind(typDef.Kind, Object) {
|
||||
return gqlerror.ErrorPosf(def.Position, "%s type %s must be %s.", def.Kind, strconv.Quote(typ), kindList(Object))
|
||||
}
|
||||
}
|
||||
|
||||
for _, intf := range def.Interfaces {
|
||||
if err := validateImplements(schema, def, intf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch def.Kind {
|
||||
case Object, Interface:
|
||||
if len(def.Fields) == 0 {
|
||||
return gqlerror.ErrorPosf(def.Position, "%s %s: must define one or more fields.", def.Kind, def.Name)
|
||||
}
|
||||
for _, field := range def.Fields {
|
||||
if typ, ok := schema.Types[field.Type.Name()]; ok {
|
||||
if !isValidKind(typ.Kind, Scalar, Object, Interface, Union, Enum) {
|
||||
return gqlerror.ErrorPosf(field.Position, "%s %s: field must be one of %s.", def.Kind, def.Name, kindList(Scalar, Object, Interface, Union, Enum))
|
||||
}
|
||||
}
|
||||
}
|
||||
case Enum:
|
||||
if len(def.EnumValues) == 0 {
|
||||
return gqlerror.ErrorPosf(def.Position, "%s %s: must define one or more unique enum values.", def.Kind, def.Name)
|
||||
}
|
||||
for _, value := range def.EnumValues {
|
||||
for _, nonEnum := range [3]string{"true", "false", "null"} {
|
||||
if value.Name == nonEnum {
|
||||
return gqlerror.ErrorPosf(def.Position, "%s %s: non-enum value %s.", def.Kind, def.Name, value.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
case InputObject:
|
||||
if len(def.Fields) == 0 {
|
||||
return gqlerror.ErrorPosf(def.Position, "%s %s: must define one or more input fields.", def.Kind, def.Name)
|
||||
}
|
||||
for _, field := range def.Fields {
|
||||
if typ, ok := schema.Types[field.Type.Name()]; ok {
|
||||
if !isValidKind(typ.Kind, Scalar, Enum, InputObject) {
|
||||
return gqlerror.ErrorPosf(field.Position, "%s %s: field must be one of %s.", typ.Kind, field.Name, kindList(Scalar, Enum, InputObject))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx, field1 := range def.Fields {
|
||||
for _, field2 := range def.Fields[idx+1:] {
|
||||
if field1.Name == field2.Name {
|
||||
return gqlerror.ErrorPosf(field2.Position, "Field %s.%s can only be defined once.", def.Name, field2.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !def.BuiltIn {
|
||||
// GraphQL spec has reserved type names a lot!
|
||||
err := validateName(def.Position, def.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return validateDirectives(schema, def.Directives, DirectiveLocation(def.Kind), nil)
|
||||
}
|
||||
|
||||
func validateTypeRef(schema *Schema, typ *Type) *gqlerror.Error {
|
||||
if schema.Types[typ.Name()] == nil {
|
||||
return gqlerror.ErrorPosf(typ.Position, "Undefined type %s.", typ.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateArgs(schema *Schema, args ArgumentDefinitionList, currentDirective *DirectiveDefinition) *gqlerror.Error {
|
||||
for _, arg := range args {
|
||||
if err := validateName(arg.Position, arg.Name); err != nil {
|
||||
// now, GraphQL spec doesn't have reserved argument name
|
||||
return err
|
||||
}
|
||||
if err := validateTypeRef(schema, arg.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
def := schema.Types[arg.Type.Name()]
|
||||
if !def.IsInputType() {
|
||||
return gqlerror.ErrorPosf(
|
||||
arg.Position,
|
||||
"cannot use %s as argument %s because %s is not a valid input type",
|
||||
arg.Type.String(),
|
||||
arg.Name,
|
||||
def.Kind,
|
||||
)
|
||||
}
|
||||
if err := validateDirectives(schema, arg.Directives, LocationArgumentDefinition, currentDirective); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDirectives(schema *Schema, dirs DirectiveList, location DirectiveLocation, currentDirective *DirectiveDefinition) *gqlerror.Error {
|
||||
for _, dir := range dirs {
|
||||
if err := validateName(dir.Position, dir.Name); err != nil {
|
||||
// now, GraphQL spec doesn't have reserved directive name
|
||||
return err
|
||||
}
|
||||
if currentDirective != nil && dir.Name == currentDirective.Name {
|
||||
return gqlerror.ErrorPosf(dir.Position, "Directive %s cannot refer to itself.", currentDirective.Name)
|
||||
}
|
||||
if schema.Directives[dir.Name] == nil {
|
||||
return gqlerror.ErrorPosf(dir.Position, "Undefined directive %s.", dir.Name)
|
||||
}
|
||||
validKind := false
|
||||
for _, dirLocation := range schema.Directives[dir.Name].Locations {
|
||||
if dirLocation == location {
|
||||
validKind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validKind {
|
||||
return gqlerror.ErrorPosf(dir.Position, "Directive %s is not applicable on %s.", dir.Name, location)
|
||||
}
|
||||
dir.Definition = schema.Directives[dir.Name]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateImplements(schema *Schema, def *Definition, intfName string) *gqlerror.Error {
|
||||
// see validation rules at the bottom of
|
||||
// https://facebook.github.io/graphql/October2021/#sec-Objects
|
||||
intf := schema.Types[intfName]
|
||||
if intf == nil {
|
||||
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intfName))
|
||||
}
|
||||
if intf.Kind != Interface {
|
||||
return gqlerror.ErrorPosf(def.Position, "%s is a non interface type %s.", strconv.Quote(intfName), intf.Kind)
|
||||
}
|
||||
for _, requiredField := range intf.Fields {
|
||||
foundField := def.Fields.ForName(requiredField.Name)
|
||||
if foundField == nil {
|
||||
return gqlerror.ErrorPosf(def.Position,
|
||||
`For %s to implement %s it must have a field called %s.`,
|
||||
def.Name, intf.Name, requiredField.Name,
|
||||
)
|
||||
}
|
||||
|
||||
if !isCovariant(schema, requiredField.Type, foundField.Type) {
|
||||
return gqlerror.ErrorPosf(foundField.Position,
|
||||
`For %s to implement %s the field %s must have type %s.`,
|
||||
def.Name, intf.Name, requiredField.Name, requiredField.Type.String(),
|
||||
)
|
||||
}
|
||||
|
||||
for _, requiredArg := range requiredField.Arguments {
|
||||
foundArg := foundField.Arguments.ForName(requiredArg.Name)
|
||||
if foundArg == nil {
|
||||
return gqlerror.ErrorPosf(foundField.Position,
|
||||
`For %s to implement %s the field %s must have the same arguments but it is missing %s.`,
|
||||
def.Name, intf.Name, requiredField.Name, requiredArg.Name,
|
||||
)
|
||||
}
|
||||
|
||||
if !requiredArg.Type.IsCompatible(foundArg.Type) {
|
||||
return gqlerror.ErrorPosf(foundArg.Position,
|
||||
`For %s to implement %s the field %s must have the same arguments but %s has the wrong type.`,
|
||||
def.Name, intf.Name, requiredField.Name, requiredArg.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
for _, foundArgs := range foundField.Arguments {
|
||||
if requiredField.Arguments.ForName(foundArgs.Name) == nil && foundArgs.Type.NonNull && foundArgs.DefaultValue == nil {
|
||||
return gqlerror.ErrorPosf(foundArgs.Position,
|
||||
`For %s to implement %s any additional arguments on %s must be optional or have a default value but %s is required.`,
|
||||
def.Name, intf.Name, foundField.Name, foundArgs.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return validateTypeImplementsAncestors(schema, def, intfName)
|
||||
}
|
||||
|
||||
// validateTypeImplementsAncestors
|
||||
// https://github.com/graphql/graphql-js/blob/47bd8c8897c72d3efc17ecb1599a95cee6bac5e8/src/type/validate.ts#L428
|
||||
func validateTypeImplementsAncestors(schema *Schema, def *Definition, intfName string) *gqlerror.Error {
|
||||
intf := schema.Types[intfName]
|
||||
if intf == nil {
|
||||
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intfName))
|
||||
}
|
||||
for _, transitive := range intf.Interfaces {
|
||||
if !containsString(def.Interfaces, transitive) {
|
||||
if transitive == def.Name {
|
||||
return gqlerror.ErrorPosf(def.Position,
|
||||
`Type %s cannot implement %s because it would create a circular reference.`,
|
||||
def.Name, intfName,
|
||||
)
|
||||
}
|
||||
return gqlerror.ErrorPosf(def.Position,
|
||||
`Type %s must implement %s because it is implemented by %s.`,
|
||||
def.Name, transitive, intfName,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsString(slice []string, want string) bool {
|
||||
for _, str := range slice {
|
||||
if want == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isCovariant(schema *Schema, required *Type, actual *Type) bool {
|
||||
if required.NonNull && !actual.NonNull {
|
||||
return false
|
||||
}
|
||||
|
||||
if required.NamedType != "" {
|
||||
if required.NamedType == actual.NamedType {
|
||||
return true
|
||||
}
|
||||
for _, pt := range schema.PossibleTypes[required.NamedType] {
|
||||
if pt.Name == actual.NamedType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if required.Elem != nil && actual.Elem == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return isCovariant(schema, required.Elem, actual.Elem)
|
||||
}
|
||||
|
||||
func validateName(pos *Position, name string) *gqlerror.Error {
|
||||
if strings.HasPrefix(name, "__") {
|
||||
return gqlerror.ErrorPosf(pos, `Name "%s" must not begin with "__", which is reserved by GraphQL introspection.`, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidKind(kind DefinitionKind, valid ...DefinitionKind) bool {
|
||||
for _, k := range valid {
|
||||
if kind == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func kindList(kinds ...DefinitionKind) string {
|
||||
s := make([]string, len(kinds))
|
||||
for i, k := range kinds {
|
||||
s[i] = string(k)
|
||||
}
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
Generated
Vendored
+678
@@ -0,0 +1,678 @@
|
||||
types:
|
||||
- name: cannot be redeclared
|
||||
input: |
|
||||
type A {
|
||||
name: String
|
||||
}
|
||||
type A {
|
||||
name: String
|
||||
}
|
||||
error:
|
||||
message: "Cannot redeclare type A."
|
||||
locations: [{line: 4, column: 6}]
|
||||
- name: cannot be duplicated field at same definition 1
|
||||
input: |
|
||||
type A {
|
||||
name: String
|
||||
name: String
|
||||
}
|
||||
error:
|
||||
message: "Field A.name can only be defined once."
|
||||
locations: [{line: 3, column: 3}]
|
||||
- name: cannot be duplicated field at same definition 2
|
||||
input: |
|
||||
type A {
|
||||
name: String
|
||||
}
|
||||
extend type A {
|
||||
name: String
|
||||
}
|
||||
error:
|
||||
message: "Field A.name can only be defined once."
|
||||
locations: [{line: 5, column: 3}]
|
||||
- name: cannot be duplicated field at same definition 3
|
||||
input: |
|
||||
type A {
|
||||
name: String
|
||||
}
|
||||
extend type A {
|
||||
age: Int
|
||||
age: Int
|
||||
}
|
||||
error:
|
||||
message: "Field A.age can only be defined once."
|
||||
locations: [{line: 6, column: 3}]
|
||||
|
||||
object types:
|
||||
- name: must define one or more fields
|
||||
input: |
|
||||
directive @D on OBJECT
|
||||
|
||||
# This pattern rejected by parser
|
||||
# type InvalidObject1 {}
|
||||
|
||||
type InvalidObject2 @D
|
||||
|
||||
type ValidObject {
|
||||
id: ID
|
||||
}
|
||||
extend type ValidObject @D
|
||||
extend type ValidObject {
|
||||
b: Int
|
||||
}
|
||||
error:
|
||||
message: 'OBJECT InvalidObject2: must define one or more fields.'
|
||||
locations: [{line: 6, column: 6}]
|
||||
- name: check reserved names on type name
|
||||
input: |
|
||||
type __FooBar {
|
||||
id: ID
|
||||
}
|
||||
error:
|
||||
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
locations: [{line: 1, column: 6}]
|
||||
- name: check reserved names on type field
|
||||
input: |
|
||||
type FooBar {
|
||||
__id: ID
|
||||
}
|
||||
error:
|
||||
message: 'Name "__id" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
locations: [{line: 2, column: 3}]
|
||||
|
||||
- name: check reserved names on type field argument
|
||||
input: |
|
||||
type FooBar {
|
||||
foo(__bar: ID): ID
|
||||
}
|
||||
error:
|
||||
message: 'Name "__bar" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
locations: [{line: 2, column: 7}]
|
||||
|
||||
- name: must not allow input object as field type
|
||||
input: |
|
||||
input Input {
|
||||
id: ID
|
||||
}
|
||||
type Query {
|
||||
input: Input!
|
||||
}
|
||||
error:
|
||||
message: 'OBJECT Query: field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.'
|
||||
locations: [{line: 5, column: 3}]
|
||||
|
||||
interfaces:
|
||||
- name: must exist
|
||||
input: |
|
||||
type Thing implements Object {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
Things: [Thing!]!
|
||||
}
|
||||
error:
|
||||
message: 'Undefined type "Object".'
|
||||
locations: [{line: 1, column: 6}]
|
||||
|
||||
- name: must be an interface
|
||||
input: |
|
||||
type Thing implements Object {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
Things: [Thing!]!
|
||||
}
|
||||
|
||||
type Object {
|
||||
name: String
|
||||
}
|
||||
error:
|
||||
message: '"Object" is a non interface type OBJECT.'
|
||||
locations: [{line: 1, column: 6}]
|
||||
|
||||
- name: must define one or more fields
|
||||
input: |
|
||||
directive @D on INTERFACE
|
||||
|
||||
# This pattern rejected by parser
|
||||
# interface InvalidInterface1 {}
|
||||
|
||||
interface InvalidInterface2 @D
|
||||
|
||||
interface ValidInterface {
|
||||
id: ID
|
||||
}
|
||||
extend interface ValidInterface @D
|
||||
extend interface ValidInterface {
|
||||
b: Int
|
||||
}
|
||||
error:
|
||||
message: 'INTERFACE InvalidInterface2: must define one or more fields.'
|
||||
locations: [{line: 6, column: 11}]
|
||||
|
||||
- name: check reserved names on type name
|
||||
input: |
|
||||
interface __FooBar {
|
||||
id: ID
|
||||
}
|
||||
error:
|
||||
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
locations: [{line: 1, column: 11}]
|
||||
|
||||
- name: must not allow input object as field type
|
||||
input: |
|
||||
input Input {
|
||||
id: ID
|
||||
}
|
||||
type Query {
|
||||
foo: Foo!
|
||||
}
|
||||
interface Foo {
|
||||
input: Input!
|
||||
}
|
||||
error:
|
||||
message: 'INTERFACE Foo: field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.'
|
||||
locations: [{line: 8, column: 3}]
|
||||
|
||||
- name: must have all fields from interface
|
||||
input: |
|
||||
type Bar implements BarInterface {
|
||||
someField: Int!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
id: ID!
|
||||
}
|
||||
error:
|
||||
message: 'For Bar to implement BarInterface it must have a field called id.'
|
||||
locations: [{line: 1, column: 6}]
|
||||
|
||||
- name: must have same type of fields
|
||||
input: |
|
||||
type Bar implements BarInterface {
|
||||
id: Int!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
id: ID!
|
||||
}
|
||||
error:
|
||||
message: 'For Bar to implement BarInterface the field id must have type ID!.'
|
||||
locations: [{line: 2, column: 5}]
|
||||
|
||||
- name: must have all required arguments
|
||||
input: |
|
||||
type Bar implements BarInterface {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
id(ff: Int!): ID!
|
||||
}
|
||||
error:
|
||||
message: 'For Bar to implement BarInterface the field id must have the same arguments but it is missing ff.'
|
||||
locations: [{line: 2, column: 5}]
|
||||
|
||||
- name: must have same argument types
|
||||
input: |
|
||||
type Bar implements BarInterface {
|
||||
id(ff: ID!): ID!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
id(ff: Int!): ID!
|
||||
}
|
||||
error:
|
||||
message: 'For Bar to implement BarInterface the field id must have the same arguments but ff has the wrong type.'
|
||||
locations: [{line: 2, column: 8}]
|
||||
|
||||
- name: may defined additional nullable arguments
|
||||
input: |
|
||||
type Bar implements BarInterface {
|
||||
id(opt: Int): ID!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
- name: may defined additional required arguments with defaults
|
||||
input: |
|
||||
type Bar implements BarInterface {
|
||||
id(opt: Int! = 1): ID!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
- name: must not define additional required arguments without defaults
|
||||
input: |
|
||||
type Bar implements BarInterface {
|
||||
id(opt: Int!): ID!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
id: ID!
|
||||
}
|
||||
error:
|
||||
message: 'For Bar to implement BarInterface any additional arguments on id must be optional or have a default value but opt is required.'
|
||||
locations: [{line: 2, column: 8}]
|
||||
|
||||
- name: can have covariant argument types
|
||||
input: |
|
||||
union U = A|B
|
||||
|
||||
type A { name: String }
|
||||
type B { name: String }
|
||||
|
||||
type Bar implements BarInterface {
|
||||
f: A!
|
||||
}
|
||||
|
||||
interface BarInterface {
|
||||
f: U!
|
||||
}
|
||||
|
||||
- name: may define intermediate interfaces
|
||||
input: |
|
||||
interface IA {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
interface IIA implements IA {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type A implements IIA & IA {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
- name: Type Foo must implement Baz because it is implemented by Bar
|
||||
input: |
|
||||
interface Baz {
|
||||
baz: String
|
||||
}
|
||||
|
||||
interface Bar implements Baz {
|
||||
bar: String
|
||||
baz: String
|
||||
}
|
||||
|
||||
type Foo implements Bar {
|
||||
foo: String
|
||||
bar: String
|
||||
baz: String
|
||||
}
|
||||
error:
|
||||
message: 'Type Foo must implement Baz because it is implemented by Bar.'
|
||||
locations: [{line: 10, column: 6}]
|
||||
|
||||
- name: circular reference error
|
||||
input: |
|
||||
interface Circular1 implements Circular2 {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
interface Circular2 implements Circular1 {
|
||||
id: ID!
|
||||
}
|
||||
error:
|
||||
message: 'Type Circular1 cannot implement Circular2 because it would create a circular reference.'
|
||||
locations: [{line: 1, column: 11}]
|
||||
|
||||
inputs:
|
||||
- name: must define one or more input fields
|
||||
input: |
|
||||
directive @D on INPUT_OBJECT
|
||||
|
||||
# This pattern rejected by parser
|
||||
# input InvalidInput1 {}
|
||||
|
||||
input InvalidInput2 @D
|
||||
|
||||
input ValidInput {
|
||||
id: ID
|
||||
}
|
||||
extend input ValidInput @D
|
||||
extend input ValidInput {
|
||||
b: Int
|
||||
}
|
||||
error:
|
||||
message: 'INPUT_OBJECT InvalidInput2: must define one or more input fields.'
|
||||
locations: [{line: 6, column: 7}]
|
||||
- name: check reserved names on type name
|
||||
input: |
|
||||
input __FooBar {
|
||||
id: ID
|
||||
}
|
||||
error:
|
||||
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
locations: [{line: 1, column: 7}]
|
||||
|
||||
- name: fields cannot be Objects
|
||||
input: |
|
||||
type Object { id: ID }
|
||||
input Foo { a: Object! }
|
||||
error:
|
||||
message: 'OBJECT a: field must be one of SCALAR, ENUM, INPUT_OBJECT.'
|
||||
locations: [{line: 2, column: 13}]
|
||||
|
||||
- name: fields cannot be Interfaces
|
||||
input: |
|
||||
interface Interface { id: ID! }
|
||||
input Foo { a: Interface! }
|
||||
error:
|
||||
message: 'INTERFACE a: field must be one of SCALAR, ENUM, INPUT_OBJECT.'
|
||||
locations: [{line: 2, column: 13}]
|
||||
|
||||
- name: fields cannot be Unions
|
||||
input: |
|
||||
type Object { id: ID }
|
||||
union Union = Object
|
||||
input Foo { a: Union! }
|
||||
error:
|
||||
message: 'UNION a: field must be one of SCALAR, ENUM, INPUT_OBJECT.'
|
||||
locations: [{line: 3, column: 13}]
|
||||
|
||||
args:
|
||||
- name: Valid arg types
|
||||
input: |
|
||||
input Input { id: ID }
|
||||
enum Enum { A }
|
||||
scalar Scalar
|
||||
|
||||
type Query {
|
||||
f(a: Input, b: Scalar, c: Enum): Boolean!
|
||||
}
|
||||
|
||||
- name: Objects not allowed
|
||||
input: |
|
||||
type Object { id: ID }
|
||||
type Query { f(a: Object): Boolean! }
|
||||
|
||||
error:
|
||||
message: 'cannot use Object as argument a because OBJECT is not a valid input type'
|
||||
locations: [{line: 2, column: 16}]
|
||||
|
||||
- name: Union not allowed
|
||||
input: |
|
||||
type Object { id: ID }
|
||||
union Union = Object
|
||||
type Query { f(a: Union): Boolean! }
|
||||
|
||||
error:
|
||||
message: 'cannot use Union as argument a because UNION is not a valid input type'
|
||||
locations: [{line: 3, column: 16}]
|
||||
|
||||
- name: Interface not allowed
|
||||
input: |
|
||||
interface Interface { id: ID }
|
||||
type Query { f(a: Interface): Boolean! }
|
||||
|
||||
error:
|
||||
message: 'cannot use Interface as argument a because INTERFACE is not a valid input type'
|
||||
locations: [{line: 2, column: 16}]
|
||||
|
||||
enums:
|
||||
- name: must define one or more unique enum values
|
||||
input: |
|
||||
directive @D on ENUM
|
||||
|
||||
# This pattern rejected by parser
|
||||
# enum InvalidEmum1 {}
|
||||
|
||||
enum InvalidEnum2 @D
|
||||
|
||||
enum ValidEnum {
|
||||
FOO
|
||||
}
|
||||
extend enum ValidEnum @D
|
||||
extend enum ValidEnum {
|
||||
BAR
|
||||
}
|
||||
error:
|
||||
message: 'ENUM InvalidEnum2: must define one or more unique enum values.'
|
||||
locations: [{line: 6, column: 6}]
|
||||
- name: check reserved names on type name
|
||||
input: |
|
||||
enum __FooBar {
|
||||
A
|
||||
B
|
||||
}
|
||||
error:
|
||||
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
locations: [{line: 1, column: 6}]
|
||||
|
||||
unions:
|
||||
- name: union types must be defined
|
||||
input: |
|
||||
union Foo = Bar | Baz
|
||||
type Bar {
|
||||
id: ID
|
||||
}
|
||||
error:
|
||||
message: "Undefined type \"Baz\"."
|
||||
locations: [{line: 1, column: 7}]
|
||||
- name: union types must be objects
|
||||
input: |
|
||||
union Foo = Baz
|
||||
interface Baz {
|
||||
id: ID
|
||||
}
|
||||
error:
|
||||
message: "UNION type \"Baz\" must be OBJECT."
|
||||
locations: [{line: 1, column: 7}]
|
||||
|
||||
- name: unions of pure type extensions are valid
|
||||
input: |
|
||||
|
||||
type Review {
|
||||
body: String!
|
||||
author: User! @provides(fields: "username")
|
||||
product: Product!
|
||||
}
|
||||
|
||||
extend type User @key(fields: "id") {
|
||||
id: ID! @external
|
||||
reviews: [Review]
|
||||
}
|
||||
|
||||
extend type Product @key(fields: "upc") {
|
||||
upc: String! @external
|
||||
reviews: [Review]
|
||||
}
|
||||
|
||||
union Foo = User | Product
|
||||
scalar _Any
|
||||
scalar _FieldSet
|
||||
directive @external on FIELD_DEFINITION
|
||||
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
|
||||
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
|
||||
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
|
||||
directive @extends on OBJECT
|
||||
|
||||
|
||||
|
||||
type extensions:
|
||||
- name: can extend non existant types
|
||||
input: |
|
||||
extend type A {
|
||||
name: String
|
||||
}
|
||||
|
||||
|
||||
- name: cannot extend incorret type existant types
|
||||
input: |
|
||||
scalar A
|
||||
extend type A {
|
||||
name: String
|
||||
}
|
||||
error:
|
||||
message: "Cannot extend type A because the base type is a SCALAR, not OBJECT."
|
||||
locations: [{line: 2, column: 13}]
|
||||
|
||||
directives:
|
||||
- name: cannot redeclare directives
|
||||
input: |
|
||||
directive @A on FIELD_DEFINITION
|
||||
directive @A on FIELD_DEFINITION
|
||||
error:
|
||||
message: "Cannot redeclare directive A."
|
||||
locations: [{line: 2, column: 12}]
|
||||
|
||||
- name: can redeclare builtin directives
|
||||
input: |
|
||||
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
|
||||
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
|
||||
|
||||
- name: must be declared
|
||||
input: |
|
||||
type User {
|
||||
name: String @foo
|
||||
}
|
||||
error:
|
||||
message: "Undefined directive foo."
|
||||
locations: [{line: 2, column: 17}]
|
||||
|
||||
- name: cannot be self-referential
|
||||
input: |
|
||||
directive @A(foo: Int! @A) on FIELD_DEFINITION
|
||||
error:
|
||||
message: "Directive A cannot refer to itself."
|
||||
locations: [{line: 1, column: 25}]
|
||||
- name: check reserved names on type name
|
||||
input: |
|
||||
directive @__A on FIELD_DEFINITION
|
||||
error:
|
||||
message: 'Name "__A" must not begin with "__", which is reserved by GraphQL introspection.'
|
||||
locations: [{line: 1, column: 12}]
|
||||
|
||||
- name: Valid arg types
|
||||
input: |
|
||||
input Input { id: ID }
|
||||
enum Enum { A }
|
||||
scalar Scalar
|
||||
|
||||
directive @A(a: Input, b: Scalar, c: Enum) on FIELD_DEFINITION
|
||||
|
||||
- name: Objects not allowed
|
||||
input: |
|
||||
type Object { id: ID }
|
||||
directive @A(a: Object) on FIELD_DEFINITION
|
||||
|
||||
error:
|
||||
message: 'cannot use Object as argument a because OBJECT is not a valid input type'
|
||||
locations: [{line: 2, column: 14}]
|
||||
|
||||
- name: Union not allowed
|
||||
input: |
|
||||
type Object { id: ID }
|
||||
union Union = Object
|
||||
directive @A(a: Union) on FIELD_DEFINITION
|
||||
|
||||
error:
|
||||
message: 'cannot use Union as argument a because UNION is not a valid input type'
|
||||
locations: [{line: 3, column: 14}]
|
||||
|
||||
- name: Interface not allowed
|
||||
input: |
|
||||
interface Interface { id: ID }
|
||||
directive @A(a: Interface) on FIELD_DEFINITION
|
||||
|
||||
error:
|
||||
message: 'cannot use Interface as argument a because INTERFACE is not a valid input type'
|
||||
locations: [{line: 2, column: 14}]
|
||||
|
||||
- name: Invalid location usage not allowed
|
||||
input: |
|
||||
directive @test on FIELD_DEFINITION
|
||||
input I1 @test { f: String }
|
||||
|
||||
error:
|
||||
message: 'Directive test is not applicable on INPUT_OBJECT.'
|
||||
locations: [{line: 2, column: 11}]
|
||||
|
||||
- name: Valid location usage
|
||||
input: |
|
||||
directive @testInputField on INPUT_FIELD_DEFINITION
|
||||
directive @testField on FIELD_DEFINITION
|
||||
directive @inp on INPUT_OBJECT
|
||||
input I1 @inp { f: String @testInputField }
|
||||
type P { name: String @testField }
|
||||
interface I { id: ID @testField }
|
||||
|
||||
|
||||
entry points:
|
||||
- name: multiple schema entry points
|
||||
input: |
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
scalar Query
|
||||
error:
|
||||
message: "Cannot have multiple schema entry points, consider schema extensions instead."
|
||||
locations: [{line: 4, column: 8}]
|
||||
|
||||
- name: Undefined schema entrypoint
|
||||
input: |
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
error:
|
||||
message: "Schema root query refers to a type Query that does not exist."
|
||||
locations: [{line: 2, column: 3}]
|
||||
|
||||
entry point extensions:
|
||||
- name: Undefined schema entrypoint
|
||||
input: |
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
scalar Query
|
||||
extend schema {
|
||||
mutation: Mutation
|
||||
}
|
||||
error:
|
||||
message: "Schema root mutation refers to a type Mutation that does not exist."
|
||||
locations: [{line: 6, column: 3}]
|
||||
|
||||
type references:
|
||||
- name: Field types
|
||||
input: |
|
||||
type User {
|
||||
posts: Post
|
||||
}
|
||||
error:
|
||||
message: "Undefined type Post."
|
||||
locations: [{line: 2, column: 10}]
|
||||
|
||||
- name: Arg types
|
||||
input: |
|
||||
type User {
|
||||
posts(foo: FooBar): String
|
||||
}
|
||||
error:
|
||||
message: "Undefined type FooBar."
|
||||
locations: [{line: 2, column: 14}]
|
||||
|
||||
- name: Directive arg types
|
||||
input: |
|
||||
directive @Foo(foo: FooBar) on FIELD_DEFINITION
|
||||
|
||||
error:
|
||||
message: "Undefined type FooBar."
|
||||
locations: [{line: 1, column: 21}]
|
||||
|
||||
- name: Invalid enum value
|
||||
input: |
|
||||
enum Enum { true }
|
||||
|
||||
error:
|
||||
message: "ENUM Enum: non-enum value true."
|
||||
locations: [{line: 1, column: 6}]
|
||||
Generated
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/agnivade/levenshtein"
|
||||
)
|
||||
|
||||
// Given an invalid input string and a list of valid options, returns a filtered
|
||||
// list of valid options sorted based on their similarity with the input.
|
||||
func SuggestionList(input string, options []string) []string {
|
||||
var results []string
|
||||
optionsByDistance := map[string]int{}
|
||||
|
||||
for _, option := range options {
|
||||
distance := lexicalDistance(input, option)
|
||||
threshold := calcThreshold(input)
|
||||
if distance <= threshold {
|
||||
results = append(results, option)
|
||||
optionsByDistance[option] = distance
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return optionsByDistance[results[i]] < optionsByDistance[results[j]]
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
func calcThreshold(a string) (threshold int) {
|
||||
// the logic is copied from here
|
||||
// https://github.com/graphql/graphql-js/blob/47bd8c8897c72d3efc17ecb1599a95cee6bac5e8/src/jsutils/suggestionList.ts#L14
|
||||
threshold = int(math.Floor(float64(len(a))*0.4) + 1)
|
||||
|
||||
if threshold < 1 {
|
||||
threshold = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Computes the lexical distance between strings A and B.
|
||||
//
|
||||
// The "distance" between two strings is given by counting the minimum number
|
||||
// of edits needed to transform string A into string B. An edit can be an
|
||||
// insertion, deletion, or substitution of a single character, or a swap of two
|
||||
// adjacent characters.
|
||||
//
|
||||
// Includes a custom alteration from Damerau-Levenshtein to treat case changes
|
||||
// as a single edit which helps identify mis-cased values with an edit distance
|
||||
// of 1.
|
||||
//
|
||||
// This distance can be useful for detecting typos in input or sorting
|
||||
func lexicalDistance(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
|
||||
a = strings.ToLower(a)
|
||||
b = strings.ToLower(b)
|
||||
|
||||
// Any case change counts as a single edit
|
||||
if a == b {
|
||||
return 1
|
||||
}
|
||||
|
||||
return levenshtein.ComputeDistance(a, b)
|
||||
}
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
//nolint:revive
|
||||
. "github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/gqlerror"
|
||||
)
|
||||
|
||||
type AddErrFunc func(options ...ErrorOption)
|
||||
|
||||
type ruleFunc func(observers *Events, addError AddErrFunc)
|
||||
|
||||
type rule struct {
|
||||
name string
|
||||
rule ruleFunc
|
||||
}
|
||||
|
||||
var rules []rule
|
||||
|
||||
// addRule to rule set.
|
||||
// f is called once each time `Validate` is executed.
|
||||
func AddRule(name string, f ruleFunc) {
|
||||
rules = append(rules, rule{name: name, rule: f})
|
||||
}
|
||||
|
||||
func Validate(schema *Schema, doc *QueryDocument) gqlerror.List {
|
||||
var errs gqlerror.List
|
||||
|
||||
observers := &Events{}
|
||||
for i := range rules {
|
||||
rule := rules[i]
|
||||
rule.rule(observers, func(options ...ErrorOption) {
|
||||
err := &gqlerror.Error{
|
||||
Rule: rule.name,
|
||||
}
|
||||
for _, o := range options {
|
||||
o(err)
|
||||
}
|
||||
errs = append(errs, err)
|
||||
})
|
||||
}
|
||||
|
||||
Walk(schema, doc, observers)
|
||||
return errs
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/gqlerror"
|
||||
)
|
||||
|
||||
var ErrUnexpectedType = fmt.Errorf("Unexpected Type")
|
||||
|
||||
// VariableValues coerces and validates variable values
|
||||
func VariableValues(schema *ast.Schema, op *ast.OperationDefinition, variables map[string]interface{}) (map[string]interface{}, error) {
|
||||
coercedVars := map[string]interface{}{}
|
||||
|
||||
validator := varValidator{
|
||||
path: ast.Path{ast.PathName("variable")},
|
||||
schema: schema,
|
||||
}
|
||||
|
||||
for _, v := range op.VariableDefinitions {
|
||||
validator.path = append(validator.path, ast.PathName(v.Variable))
|
||||
|
||||
if !v.Definition.IsInputType() {
|
||||
return nil, gqlerror.ErrorPathf(validator.path, "must an input type")
|
||||
}
|
||||
|
||||
val, hasValue := variables[v.Variable]
|
||||
|
||||
if !hasValue {
|
||||
if v.DefaultValue != nil {
|
||||
var err error
|
||||
val, err = v.DefaultValue.Value(nil)
|
||||
if err != nil {
|
||||
return nil, gqlerror.WrapPath(validator.path, err)
|
||||
}
|
||||
hasValue = true
|
||||
} else if v.Type.NonNull {
|
||||
return nil, gqlerror.ErrorPathf(validator.path, "must be defined")
|
||||
}
|
||||
}
|
||||
|
||||
if hasValue {
|
||||
if val == nil {
|
||||
if v.Type.NonNull {
|
||||
return nil, gqlerror.ErrorPathf(validator.path, "cannot be null")
|
||||
}
|
||||
coercedVars[v.Variable] = nil
|
||||
} else {
|
||||
rv := reflect.ValueOf(val)
|
||||
|
||||
jsonNumber, isJSONNumber := val.(json.Number)
|
||||
if isJSONNumber {
|
||||
if v.Type.NamedType == "Int" {
|
||||
n, err := jsonNumber.Int64()
|
||||
if err != nil {
|
||||
return nil, gqlerror.ErrorPathf(validator.path, "cannot use value %d as %s", n, v.Type.NamedType)
|
||||
}
|
||||
rv = reflect.ValueOf(n)
|
||||
} else if v.Type.NamedType == "Float" {
|
||||
f, err := jsonNumber.Float64()
|
||||
if err != nil {
|
||||
return nil, gqlerror.ErrorPathf(validator.path, "cannot use value %f as %s", f, v.Type.NamedType)
|
||||
}
|
||||
rv = reflect.ValueOf(f)
|
||||
|
||||
}
|
||||
}
|
||||
if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
rval, err := validator.validateVarType(v.Type, rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
coercedVars[v.Variable] = rval.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
validator.path = validator.path[0 : len(validator.path)-1]
|
||||
}
|
||||
return coercedVars, nil
|
||||
}
|
||||
|
||||
type varValidator struct {
|
||||
path ast.Path
|
||||
schema *ast.Schema
|
||||
}
|
||||
|
||||
func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) (reflect.Value, *gqlerror.Error) {
|
||||
currentPath := v.path
|
||||
resetPath := func() {
|
||||
v.path = currentPath
|
||||
}
|
||||
defer resetPath()
|
||||
if typ.Elem != nil {
|
||||
if val.Kind() != reflect.Slice {
|
||||
// GraphQL spec says that non-null values should be coerced to an array when possible.
|
||||
// Hence if the value is not a slice, we create a slice and add val to it.
|
||||
slc := reflect.MakeSlice(reflect.SliceOf(val.Type()), 0, 0)
|
||||
slc = reflect.Append(slc, val)
|
||||
val = slc
|
||||
}
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
resetPath()
|
||||
v.path = append(v.path, ast.PathIndex(i))
|
||||
field := val.Index(i)
|
||||
if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface {
|
||||
if typ.Elem.NonNull && field.IsNil() {
|
||||
return val, gqlerror.ErrorPathf(v.path, "cannot be null")
|
||||
}
|
||||
field = field.Elem()
|
||||
}
|
||||
_, err := v.validateVarType(typ.Elem, field)
|
||||
if err != nil {
|
||||
return val, err
|
||||
}
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
def := v.schema.Types[typ.NamedType]
|
||||
if def == nil {
|
||||
panic(fmt.Errorf("missing def for %s", typ.NamedType))
|
||||
}
|
||||
|
||||
if !typ.NonNull && !val.IsValid() {
|
||||
// If the type is not null and we got a invalid value namely null/nil, then it's valid
|
||||
return val, nil
|
||||
}
|
||||
|
||||
switch def.Kind {
|
||||
case ast.Enum:
|
||||
kind := val.Type().Kind()
|
||||
if kind != reflect.Int && kind != reflect.Int32 && kind != reflect.Int64 && kind != reflect.String {
|
||||
return val, gqlerror.ErrorPathf(v.path, "enums must be ints or strings")
|
||||
}
|
||||
isValidEnum := false
|
||||
for _, enumVal := range def.EnumValues {
|
||||
if strings.EqualFold(val.String(), enumVal.Name) {
|
||||
isValidEnum = true
|
||||
}
|
||||
}
|
||||
if !isValidEnum {
|
||||
return val, gqlerror.ErrorPathf(v.path, "%s is not a valid %s", val.String(), def.Name)
|
||||
}
|
||||
return val, nil
|
||||
case ast.Scalar:
|
||||
kind := val.Type().Kind()
|
||||
switch typ.NamedType {
|
||||
case "Int":
|
||||
if kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 || kind == reflect.Float32 || kind == reflect.Float64 || IsValidIntString(val, kind) {
|
||||
return val, nil
|
||||
}
|
||||
case "Float":
|
||||
if kind == reflect.Float32 || kind == reflect.Float64 || kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 || IsValidFloatString(val, kind) {
|
||||
return val, nil
|
||||
}
|
||||
case "String":
|
||||
if kind == reflect.String {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
case "Boolean":
|
||||
if kind == reflect.Bool {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
case "ID":
|
||||
if kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 || kind == reflect.String {
|
||||
return val, nil
|
||||
}
|
||||
default:
|
||||
// assume custom scalars are ok
|
||||
return val, nil
|
||||
}
|
||||
return val, gqlerror.ErrorPathf(v.path, "cannot use %s as %s", kind.String(), typ.NamedType)
|
||||
case ast.InputObject:
|
||||
if val.Kind() != reflect.Map {
|
||||
return val, gqlerror.ErrorPathf(v.path, "must be a %s", def.Name)
|
||||
}
|
||||
|
||||
// check for unknown fields
|
||||
for _, name := range val.MapKeys() {
|
||||
val.MapIndex(name)
|
||||
fieldDef := def.Fields.ForName(name.String())
|
||||
resetPath()
|
||||
v.path = append(v.path, ast.PathName(name.String()))
|
||||
|
||||
switch {
|
||||
case name.String() == "__typename":
|
||||
continue
|
||||
case fieldDef == nil:
|
||||
return val, gqlerror.ErrorPathf(v.path, "unknown field")
|
||||
}
|
||||
}
|
||||
|
||||
for _, fieldDef := range def.Fields {
|
||||
resetPath()
|
||||
v.path = append(v.path, ast.PathName(fieldDef.Name))
|
||||
|
||||
field := val.MapIndex(reflect.ValueOf(fieldDef.Name))
|
||||
if !field.IsValid() {
|
||||
if fieldDef.Type.NonNull {
|
||||
if fieldDef.DefaultValue != nil {
|
||||
var err error
|
||||
_, err = fieldDef.DefaultValue.Value(nil)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return val, gqlerror.ErrorPathf(v.path, "must be defined")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface {
|
||||
if fieldDef.Type.NonNull && field.IsNil() {
|
||||
return val, gqlerror.ErrorPathf(v.path, "cannot be null")
|
||||
}
|
||||
//allow null object field and skip it
|
||||
if !fieldDef.Type.NonNull && field.IsNil() {
|
||||
continue
|
||||
}
|
||||
field = field.Elem()
|
||||
}
|
||||
cval, err := v.validateVarType(fieldDef.Type, field)
|
||||
if err != nil {
|
||||
return val, err
|
||||
}
|
||||
val.SetMapIndex(reflect.ValueOf(fieldDef.Name), cval)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported type %s", def.Kind))
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func IsValidIntString(val reflect.Value, kind reflect.Kind) bool {
|
||||
if kind != reflect.String {
|
||||
return false
|
||||
}
|
||||
_, e := strconv.ParseInt(fmt.Sprintf("%v", val.Interface()), 10, 64)
|
||||
|
||||
return e == nil
|
||||
}
|
||||
|
||||
func IsValidFloatString(val reflect.Value, kind reflect.Kind) bool {
|
||||
if kind != reflect.String {
|
||||
return false
|
||||
}
|
||||
_, e := strconv.ParseFloat(fmt.Sprintf("%v", val.Interface()), 64)
|
||||
return e == nil
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/gqlparser/ast"
|
||||
)
|
||||
|
||||
type Events struct {
|
||||
operationVisitor []func(walker *Walker, operation *ast.OperationDefinition)
|
||||
field []func(walker *Walker, field *ast.Field)
|
||||
fragment []func(walker *Walker, fragment *ast.FragmentDefinition)
|
||||
inlineFragment []func(walker *Walker, inlineFragment *ast.InlineFragment)
|
||||
fragmentSpread []func(walker *Walker, fragmentSpread *ast.FragmentSpread)
|
||||
directive []func(walker *Walker, directive *ast.Directive)
|
||||
directiveList []func(walker *Walker, directives []*ast.Directive)
|
||||
value []func(walker *Walker, value *ast.Value)
|
||||
variable []func(walker *Walker, variable *ast.VariableDefinition)
|
||||
}
|
||||
|
||||
func (o *Events) OnOperation(f func(walker *Walker, operation *ast.OperationDefinition)) {
|
||||
o.operationVisitor = append(o.operationVisitor, f)
|
||||
}
|
||||
func (o *Events) OnField(f func(walker *Walker, field *ast.Field)) {
|
||||
o.field = append(o.field, f)
|
||||
}
|
||||
func (o *Events) OnFragment(f func(walker *Walker, fragment *ast.FragmentDefinition)) {
|
||||
o.fragment = append(o.fragment, f)
|
||||
}
|
||||
func (o *Events) OnInlineFragment(f func(walker *Walker, inlineFragment *ast.InlineFragment)) {
|
||||
o.inlineFragment = append(o.inlineFragment, f)
|
||||
}
|
||||
func (o *Events) OnFragmentSpread(f func(walker *Walker, fragmentSpread *ast.FragmentSpread)) {
|
||||
o.fragmentSpread = append(o.fragmentSpread, f)
|
||||
}
|
||||
func (o *Events) OnDirective(f func(walker *Walker, directive *ast.Directive)) {
|
||||
o.directive = append(o.directive, f)
|
||||
}
|
||||
func (o *Events) OnDirectiveList(f func(walker *Walker, directives []*ast.Directive)) {
|
||||
o.directiveList = append(o.directiveList, f)
|
||||
}
|
||||
func (o *Events) OnValue(f func(walker *Walker, value *ast.Value)) {
|
||||
o.value = append(o.value, f)
|
||||
}
|
||||
func (o *Events) OnVariable(f func(walker *Walker, variable *ast.VariableDefinition)) {
|
||||
o.variable = append(o.variable, f)
|
||||
}
|
||||
|
||||
func Walk(schema *ast.Schema, document *ast.QueryDocument, observers *Events) {
|
||||
w := Walker{
|
||||
Observers: observers,
|
||||
Schema: schema,
|
||||
Document: document,
|
||||
}
|
||||
|
||||
w.walk()
|
||||
}
|
||||
|
||||
type Walker struct {
|
||||
Context context.Context
|
||||
Observers *Events
|
||||
Schema *ast.Schema
|
||||
Document *ast.QueryDocument
|
||||
|
||||
validatedFragmentSpreads map[string]bool
|
||||
CurrentOperation *ast.OperationDefinition
|
||||
}
|
||||
|
||||
func (w *Walker) walk() {
|
||||
for _, child := range w.Document.Operations {
|
||||
w.validatedFragmentSpreads = make(map[string]bool)
|
||||
w.walkOperation(child)
|
||||
}
|
||||
for _, child := range w.Document.Fragments {
|
||||
w.validatedFragmentSpreads = make(map[string]bool)
|
||||
w.walkFragment(child)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Walker) walkOperation(operation *ast.OperationDefinition) {
|
||||
w.CurrentOperation = operation
|
||||
for _, varDef := range operation.VariableDefinitions {
|
||||
varDef.Definition = w.Schema.Types[varDef.Type.Name()]
|
||||
for _, v := range w.Observers.variable {
|
||||
v(w, varDef)
|
||||
}
|
||||
if varDef.DefaultValue != nil {
|
||||
varDef.DefaultValue.ExpectedType = varDef.Type
|
||||
varDef.DefaultValue.Definition = w.Schema.Types[varDef.Type.Name()]
|
||||
}
|
||||
}
|
||||
|
||||
var def *ast.Definition
|
||||
var loc ast.DirectiveLocation
|
||||
switch operation.Operation {
|
||||
case ast.Query, "":
|
||||
def = w.Schema.Query
|
||||
loc = ast.LocationQuery
|
||||
case ast.Mutation:
|
||||
def = w.Schema.Mutation
|
||||
loc = ast.LocationMutation
|
||||
case ast.Subscription:
|
||||
def = w.Schema.Subscription
|
||||
loc = ast.LocationSubscription
|
||||
}
|
||||
|
||||
for _, varDef := range operation.VariableDefinitions {
|
||||
if varDef.DefaultValue != nil {
|
||||
w.walkValue(varDef.DefaultValue)
|
||||
}
|
||||
w.walkDirectives(varDef.Definition, varDef.Directives, ast.LocationVariableDefinition)
|
||||
}
|
||||
|
||||
w.walkDirectives(def, operation.Directives, loc)
|
||||
w.walkSelectionSet(def, operation.SelectionSet)
|
||||
|
||||
for _, v := range w.Observers.operationVisitor {
|
||||
v(w, operation)
|
||||
}
|
||||
w.CurrentOperation = nil
|
||||
}
|
||||
|
||||
func (w *Walker) walkFragment(it *ast.FragmentDefinition) {
|
||||
def := w.Schema.Types[it.TypeCondition]
|
||||
|
||||
it.Definition = def
|
||||
|
||||
w.walkDirectives(def, it.Directives, ast.LocationFragmentDefinition)
|
||||
w.walkSelectionSet(def, it.SelectionSet)
|
||||
|
||||
for _, v := range w.Observers.fragment {
|
||||
v(w, it)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Walker) walkDirectives(parentDef *ast.Definition, directives []*ast.Directive, location ast.DirectiveLocation) {
|
||||
for _, dir := range directives {
|
||||
def := w.Schema.Directives[dir.Name]
|
||||
dir.Definition = def
|
||||
dir.ParentDefinition = parentDef
|
||||
dir.Location = location
|
||||
|
||||
for _, arg := range dir.Arguments {
|
||||
var argDef *ast.ArgumentDefinition
|
||||
if def != nil {
|
||||
argDef = def.Arguments.ForName(arg.Name)
|
||||
}
|
||||
|
||||
w.walkArgument(argDef, arg)
|
||||
}
|
||||
|
||||
for _, v := range w.Observers.directive {
|
||||
v(w, dir)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range w.Observers.directiveList {
|
||||
v(w, directives)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Walker) walkValue(value *ast.Value) {
|
||||
if value.Kind == ast.Variable && w.CurrentOperation != nil {
|
||||
value.VariableDefinition = w.CurrentOperation.VariableDefinitions.ForName(value.Raw)
|
||||
if value.VariableDefinition != nil {
|
||||
value.VariableDefinition.Used = true
|
||||
}
|
||||
}
|
||||
|
||||
if value.Kind == ast.ObjectValue {
|
||||
for _, child := range value.Children {
|
||||
if value.Definition != nil {
|
||||
fieldDef := value.Definition.Fields.ForName(child.Name)
|
||||
if fieldDef != nil {
|
||||
child.Value.ExpectedType = fieldDef.Type
|
||||
child.Value.Definition = w.Schema.Types[fieldDef.Type.Name()]
|
||||
}
|
||||
}
|
||||
w.walkValue(child.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if value.Kind == ast.ListValue {
|
||||
for _, child := range value.Children {
|
||||
if value.ExpectedType != nil && value.ExpectedType.Elem != nil {
|
||||
child.Value.ExpectedType = value.ExpectedType.Elem
|
||||
child.Value.Definition = value.Definition
|
||||
}
|
||||
|
||||
w.walkValue(child.Value)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range w.Observers.value {
|
||||
v(w, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Walker) walkArgument(argDef *ast.ArgumentDefinition, arg *ast.Argument) {
|
||||
if argDef != nil {
|
||||
arg.Value.ExpectedType = argDef.Type
|
||||
arg.Value.Definition = w.Schema.Types[argDef.Type.Name()]
|
||||
}
|
||||
|
||||
w.walkValue(arg.Value)
|
||||
}
|
||||
|
||||
func (w *Walker) walkSelectionSet(parentDef *ast.Definition, it ast.SelectionSet) {
|
||||
for _, child := range it {
|
||||
w.walkSelection(parentDef, child)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Walker) walkSelection(parentDef *ast.Definition, it ast.Selection) {
|
||||
switch it := it.(type) {
|
||||
case *ast.Field:
|
||||
var def *ast.FieldDefinition
|
||||
if it.Name == "__typename" {
|
||||
def = &ast.FieldDefinition{
|
||||
Name: "__typename",
|
||||
Type: ast.NamedType("String", nil),
|
||||
}
|
||||
} else if parentDef != nil {
|
||||
def = parentDef.Fields.ForName(it.Name)
|
||||
}
|
||||
|
||||
it.Definition = def
|
||||
it.ObjectDefinition = parentDef
|
||||
|
||||
var nextParentDef *ast.Definition
|
||||
if def != nil {
|
||||
nextParentDef = w.Schema.Types[def.Type.Name()]
|
||||
}
|
||||
|
||||
for _, arg := range it.Arguments {
|
||||
var argDef *ast.ArgumentDefinition
|
||||
if def != nil {
|
||||
argDef = def.Arguments.ForName(arg.Name)
|
||||
}
|
||||
|
||||
w.walkArgument(argDef, arg)
|
||||
}
|
||||
|
||||
w.walkDirectives(nextParentDef, it.Directives, ast.LocationField)
|
||||
w.walkSelectionSet(nextParentDef, it.SelectionSet)
|
||||
|
||||
for _, v := range w.Observers.field {
|
||||
v(w, it)
|
||||
}
|
||||
|
||||
case *ast.InlineFragment:
|
||||
it.ObjectDefinition = parentDef
|
||||
|
||||
nextParentDef := parentDef
|
||||
if it.TypeCondition != "" {
|
||||
nextParentDef = w.Schema.Types[it.TypeCondition]
|
||||
}
|
||||
|
||||
w.walkDirectives(nextParentDef, it.Directives, ast.LocationInlineFragment)
|
||||
w.walkSelectionSet(nextParentDef, it.SelectionSet)
|
||||
|
||||
for _, v := range w.Observers.inlineFragment {
|
||||
v(w, it)
|
||||
}
|
||||
|
||||
case *ast.FragmentSpread:
|
||||
def := w.Document.Fragments.ForName(it.Name)
|
||||
it.Definition = def
|
||||
it.ObjectDefinition = parentDef
|
||||
|
||||
var nextParentDef *ast.Definition
|
||||
if def != nil {
|
||||
nextParentDef = w.Schema.Types[def.TypeCondition]
|
||||
}
|
||||
|
||||
w.walkDirectives(nextParentDef, it.Directives, ast.LocationFragmentSpread)
|
||||
|
||||
if def != nil && !w.validatedFragmentSpreads[def.Name] {
|
||||
// prevent inifinite recursion
|
||||
w.validatedFragmentSpreads[def.Name] = true
|
||||
w.walkSelectionSet(nextParentDef, def.SelectionSet)
|
||||
}
|
||||
|
||||
for _, v := range w.Observers.fragmentSpread {
|
||||
v(w, it)
|
||||
}
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported %T", it))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user