switch to go vendoring

This commit is contained in:
Michael Barz
2023-04-19 20:24:34 +02:00
parent 632fa05ef9
commit afc6ed1e41
8527 changed files with 3004916 additions and 2 deletions
@@ -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...) + "?"
}
}
@@ -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()
}
@@ -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,
}
@@ -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
}
@@ -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)
}
@@ -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),
)
})
})
}
@@ -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),
)
}
})
})
}
@@ -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
}
})
})
}
@@ -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),
)
}
})
})
}
@@ -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))
}
})
})
}
@@ -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),
)
})
})
}
@@ -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),
)
}
})
})
}
@@ -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
}
@@ -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),
)
}
})
})
}
@@ -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),
)
}
})
})
}
@@ -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),
)
}
}
})
})
}
@@ -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
}
@@ -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),
)
})
})
})
}
@@ -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),
)
}
})
})
}
@@ -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),
)
}
})
})
}
@@ -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
}
@@ -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]++
}
}
@@ -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
}
})
})
}
@@ -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
})
})
}
@@ -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
}
})
})
}
@@ -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
})
})
}
@@ -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]++
}
})
})
}
@@ -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())
}
}
@@ -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),
)
}
}
})
})
}
@@ -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),
)
}
})
})
}
@@ -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, ", ")
}
@@ -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}]
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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))
}
}