Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2018 Adam Scarr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
package ast
func arg2map(
defs ArgumentDefinitionList,
args ArgumentList,
vars map[string]any,
) map[string]any {
result := map[string]any{}
var err error
for _, argDef := range defs {
var val any
var hasValue bool
if argValue := args.ForName(argDef.Name); argValue != nil {
if argValue.Value.Kind == Variable {
val, hasValue = vars[argValue.Value.Raw]
} else {
val, err = argValue.Value.Value(vars)
if err != nil {
panic(err)
}
hasValue = true
}
}
if !hasValue && argDef.DefaultValue != nil {
val, err = argDef.DefaultValue.Value(vars)
if err != nil {
panic(err)
}
hasValue = true
}
if hasValue {
result[argDef.Name] = val
}
}
return result
}
+148
View File
@@ -0,0 +1,148 @@
package ast
type FieldList []*FieldDefinition
func (l FieldList) ForName(name string) *FieldDefinition {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type EnumValueList []*EnumValueDefinition
func (l EnumValueList) ForName(name string) *EnumValueDefinition {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type DirectiveList []*Directive
func (l DirectiveList) ForName(name string) *Directive {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
func (l DirectiveList) ForNames(name string) []*Directive {
resp := []*Directive{}
for _, it := range l {
if it.Name == name {
resp = append(resp, it)
}
}
return resp
}
type OperationList []*OperationDefinition
func (l OperationList) ForName(name string) *OperationDefinition {
if name == "" && len(l) == 1 {
return l[0]
}
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type FragmentDefinitionList []*FragmentDefinition
func (l FragmentDefinitionList) ForName(name string) *FragmentDefinition {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type VariableDefinitionList []*VariableDefinition
func (l VariableDefinitionList) ForName(name string) *VariableDefinition {
for _, it := range l {
if it.Variable == name {
return it
}
}
return nil
}
type ArgumentList []*Argument
func (l ArgumentList) ForName(name string) *Argument {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type ArgumentDefinitionList []*ArgumentDefinition
func (l ArgumentDefinitionList) ForName(name string) *ArgumentDefinition {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type SchemaDefinitionList []*SchemaDefinition
type DirectiveDefinitionList []*DirectiveDefinition
func (l DirectiveDefinitionList) ForName(name string) *DirectiveDefinition {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type DefinitionList []*Definition
func (l DefinitionList) ForName(name string) *Definition {
for _, it := range l {
if it.Name == name {
return it
}
}
return nil
}
type OperationTypeDefinitionList []*OperationTypeDefinition
func (l OperationTypeDefinitionList) ForType(name string) *OperationTypeDefinition {
for _, it := range l {
if it.Type == name {
return it
}
}
return nil
}
type ChildValueList []*ChildValue
func (v ChildValueList) ForName(name string) *Value {
for _, f := range v {
if f.Name == name {
return f.Value
}
}
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package ast
import (
"strconv"
"strings"
)
type Comment struct {
Value string
Position *Position
}
func (c *Comment) Text() string {
return strings.TrimPrefix(c.Value, "#")
}
type CommentGroup struct {
List []*Comment
}
func (c *CommentGroup) Dump() string {
if len(c.List) == 0 {
return ""
}
var builder strings.Builder
for _, comment := range c.List {
builder.WriteString(comment.Value)
builder.WriteString("\n")
}
return strconv.Quote(builder.String())
}
+216
View File
@@ -0,0 +1,216 @@
package ast
import (
"encoding/json"
)
func UnmarshalSelectionSet(b []byte) (SelectionSet, error) {
var tmp []json.RawMessage
if err := json.Unmarshal(b, &tmp); err != nil {
return nil, err
}
result := make([]Selection, 0)
for _, item := range tmp {
var field Field
if err := json.Unmarshal(item, &field); err == nil {
result = append(result, &field)
continue
}
var fragmentSpread FragmentSpread
if err := json.Unmarshal(item, &fragmentSpread); err == nil {
result = append(result, &fragmentSpread)
continue
}
var inlineFragment InlineFragment
if err := json.Unmarshal(item, &inlineFragment); err == nil {
result = append(result, &inlineFragment)
continue
}
}
return result, nil
}
func (f *FragmentDefinition) UnmarshalJSON(b []byte) error {
var tmp map[string]json.RawMessage
if err := json.Unmarshal(b, &tmp); err != nil {
return err
}
for k := range tmp {
switch k {
case "Name":
err := json.Unmarshal(tmp[k], &f.Name)
if err != nil {
return err
}
case "VariableDefinition":
err := json.Unmarshal(tmp[k], &f.VariableDefinition)
if err != nil {
return err
}
case "TypeCondition":
err := json.Unmarshal(tmp[k], &f.TypeCondition)
if err != nil {
return err
}
case "Directives":
err := json.Unmarshal(tmp[k], &f.Directives)
if err != nil {
return err
}
case "SelectionSet":
ss, err := UnmarshalSelectionSet(tmp[k])
if err != nil {
return err
}
f.SelectionSet = ss
case "Definition":
err := json.Unmarshal(tmp[k], &f.Definition)
if err != nil {
return err
}
case "Position":
err := json.Unmarshal(tmp[k], &f.Position)
if err != nil {
return err
}
}
}
return nil
}
func (f *InlineFragment) UnmarshalJSON(b []byte) error {
var tmp map[string]json.RawMessage
if err := json.Unmarshal(b, &tmp); err != nil {
return err
}
for k := range tmp {
switch k {
case "TypeCondition":
err := json.Unmarshal(tmp[k], &f.TypeCondition)
if err != nil {
return err
}
case "Directives":
err := json.Unmarshal(tmp[k], &f.Directives)
if err != nil {
return err
}
case "SelectionSet":
ss, err := UnmarshalSelectionSet(tmp[k])
if err != nil {
return err
}
f.SelectionSet = ss
case "ObjectDefinition":
err := json.Unmarshal(tmp[k], &f.ObjectDefinition)
if err != nil {
return err
}
case "Position":
err := json.Unmarshal(tmp[k], &f.Position)
if err != nil {
return err
}
}
}
return nil
}
func (f *OperationDefinition) UnmarshalJSON(b []byte) error {
var tmp map[string]json.RawMessage
if err := json.Unmarshal(b, &tmp); err != nil {
return err
}
for k := range tmp {
switch k {
case "Operation":
err := json.Unmarshal(tmp[k], &f.Operation)
if err != nil {
return err
}
case "Name":
err := json.Unmarshal(tmp[k], &f.Name)
if err != nil {
return err
}
case "VariableDefinitions":
err := json.Unmarshal(tmp[k], &f.VariableDefinitions)
if err != nil {
return err
}
case "Directives":
err := json.Unmarshal(tmp[k], &f.Directives)
if err != nil {
return err
}
case "SelectionSet":
ss, err := UnmarshalSelectionSet(tmp[k])
if err != nil {
return err
}
f.SelectionSet = ss
case "Position":
err := json.Unmarshal(tmp[k], &f.Position)
if err != nil {
return err
}
}
}
return nil
}
func (f *Field) UnmarshalJSON(b []byte) error {
var tmp map[string]json.RawMessage
if err := json.Unmarshal(b, &tmp); err != nil {
return err
}
for k := range tmp {
switch k {
case "Alias":
err := json.Unmarshal(tmp[k], &f.Alias)
if err != nil {
return err
}
case "Name":
err := json.Unmarshal(tmp[k], &f.Name)
if err != nil {
return err
}
case "Arguments":
err := json.Unmarshal(tmp[k], &f.Arguments)
if err != nil {
return err
}
case "Directives":
err := json.Unmarshal(tmp[k], &f.Directives)
if err != nil {
return err
}
case "SelectionSet":
ss, err := UnmarshalSelectionSet(tmp[k])
if err != nil {
return err
}
f.SelectionSet = ss
case "Position":
err := json.Unmarshal(tmp[k], &f.Position)
if err != nil {
return err
}
case "Definition":
err := json.Unmarshal(tmp[k], &f.Definition)
if err != nil {
return err
}
case "ObjectDefinition":
err := json.Unmarshal(tmp[k], &f.ObjectDefinition)
if err != nil {
return err
}
}
}
return nil
}
+107
View File
@@ -0,0 +1,107 @@
package ast
import "slices"
type DefinitionKind string
const (
Scalar DefinitionKind = "SCALAR"
Object DefinitionKind = "OBJECT"
Interface DefinitionKind = "INTERFACE"
Union DefinitionKind = "UNION"
Enum DefinitionKind = "ENUM"
InputObject DefinitionKind = "INPUT_OBJECT"
)
// Definition is the core type definition object, it includes all of the definable types
// but does *not* cover schema or directives.
//
// @vektah: Javascript implementation has different types for all of these, but they are
// more similar than different and don't define any behaviour. I think this style of
// "some hot" struct works better, at least for go.
//
// Type extensions are also represented by this same struct.
type Definition struct {
Kind DefinitionKind
Description string
Name string
Directives DirectiveList
Interfaces []string // object and input object
Fields FieldList // object and input object
Types []string // union
EnumValues EnumValueList // enum
Position *Position `dump:"-" json:"-"`
BuiltIn bool `dump:"-"`
BeforeDescriptionComment *CommentGroup
AfterDescriptionComment *CommentGroup
EndOfDefinitionComment *CommentGroup
}
func (d *Definition) IsLeafType() bool {
return d.Kind == Enum || d.Kind == Scalar
}
func (d *Definition) IsAbstractType() bool {
return d.Kind == Interface || d.Kind == Union
}
func (d *Definition) IsCompositeType() bool {
return d.Kind == Object || d.Kind == Interface || d.Kind == Union
}
func (d *Definition) IsInputType() bool {
return d.Kind == Scalar || d.Kind == Enum || d.Kind == InputObject
}
func (d *Definition) OneOf(types ...string) bool {
return slices.Contains(types, d.Name)
}
type FieldDefinition struct {
Description string
Name string
Arguments ArgumentDefinitionList // only for objects
DefaultValue *Value // only for input objects
Type *Type
Directives DirectiveList
Position *Position `dump:"-" json:"-"`
BeforeDescriptionComment *CommentGroup
AfterDescriptionComment *CommentGroup
}
type ArgumentDefinition struct {
Description string
Name string
DefaultValue *Value
Type *Type
Directives DirectiveList
Position *Position `dump:"-" json:"-"`
BeforeDescriptionComment *CommentGroup
AfterDescriptionComment *CommentGroup
}
type EnumValueDefinition struct {
Description string
Name string
Directives DirectiveList
Position *Position `dump:"-" json:"-"`
BeforeDescriptionComment *CommentGroup
AfterDescriptionComment *CommentGroup
}
type DirectiveDefinition struct {
Description string
Name string
Arguments ArgumentDefinitionList
Locations []DirectiveLocation
IsRepeatable bool
Position *Position `dump:"-" json:"-"`
BeforeDescriptionComment *CommentGroup
AfterDescriptionComment *CommentGroup
}
+46
View File
@@ -0,0 +1,46 @@
package ast
type DirectiveLocation string
const (
// Executable.
LocationQuery DirectiveLocation = `QUERY`
LocationMutation DirectiveLocation = `MUTATION`
LocationSubscription DirectiveLocation = `SUBSCRIPTION`
LocationField DirectiveLocation = `FIELD`
LocationFragmentDefinition DirectiveLocation = `FRAGMENT_DEFINITION`
LocationFragmentSpread DirectiveLocation = `FRAGMENT_SPREAD`
LocationInlineFragment DirectiveLocation = `INLINE_FRAGMENT`
// Type System.
LocationSchema DirectiveLocation = `SCHEMA`
LocationScalar DirectiveLocation = `SCALAR`
LocationObject DirectiveLocation = `OBJECT`
LocationFieldDefinition DirectiveLocation = `FIELD_DEFINITION`
LocationArgumentDefinition DirectiveLocation = `ARGUMENT_DEFINITION`
LocationInterface DirectiveLocation = `INTERFACE`
LocationUnion DirectiveLocation = `UNION`
LocationEnum DirectiveLocation = `ENUM`
LocationEnumValue DirectiveLocation = `ENUM_VALUE`
LocationInputObject DirectiveLocation = `INPUT_OBJECT`
LocationInputFieldDefinition DirectiveLocation = `INPUT_FIELD_DEFINITION`
LocationVariableDefinition DirectiveLocation = `VARIABLE_DEFINITION`
)
type Directive struct {
Name string
Arguments ArgumentList
Position *Position `dump:"-" json:"-"`
// Requires validation
ParentDefinition *Definition
Definition *DirectiveDefinition
Location DirectiveLocation
}
func (d *Directive) ArgumentMap(vars map[string]any) map[string]any {
if d.Definition == nil {
return nil
}
return arg2map(d.Definition.Arguments, d.Arguments, vars)
}
+90
View File
@@ -0,0 +1,90 @@
package ast
type QueryDocument struct {
Operations OperationList
Fragments FragmentDefinitionList
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
type SchemaDocument struct {
Schema SchemaDefinitionList
SchemaExtension SchemaDefinitionList
Directives DirectiveDefinitionList
Definitions DefinitionList
Extensions DefinitionList
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
func (d *SchemaDocument) Merge(other *SchemaDocument) {
d.Schema = append(d.Schema, other.Schema...)
d.SchemaExtension = append(d.SchemaExtension, other.SchemaExtension...)
d.Directives = append(d.Directives, other.Directives...)
d.Definitions = append(d.Definitions, other.Definitions...)
d.Extensions = append(d.Extensions, other.Extensions...)
}
type Schema struct {
Query *Definition
Mutation *Definition
Subscription *Definition
SchemaDirectives DirectiveList
Types map[string]*Definition
Directives map[string]*DirectiveDefinition
PossibleTypes map[string][]*Definition
Implements map[string][]*Definition
Description string
Comment *CommentGroup
}
// AddTypes is the helper to add types definition to the schema.
func (s *Schema) AddTypes(defs ...*Definition) {
if s.Types == nil {
s.Types = make(map[string]*Definition)
}
for _, def := range defs {
s.Types[def.Name] = def
}
}
func (s *Schema) AddPossibleType(name string, def *Definition) {
s.PossibleTypes[name] = append(s.PossibleTypes[name], def)
}
// GetPossibleTypes will enumerate all the definitions for a given interface or union.
func (s *Schema) GetPossibleTypes(def *Definition) []*Definition {
return s.PossibleTypes[def.Name]
}
func (s *Schema) AddImplements(name string, iface *Definition) {
s.Implements[name] = append(s.Implements[name], iface)
}
// GetImplements returns all the interface and union definitions that the given definition
// satisfies.
func (s *Schema) GetImplements(def *Definition) []*Definition {
return s.Implements[def.Name]
}
type SchemaDefinition struct {
Description string
Directives DirectiveList
OperationTypes OperationTypeDefinitionList
Position *Position `dump:"-" json:"-"`
BeforeDescriptionComment *CommentGroup
AfterDescriptionComment *CommentGroup
EndOfDefinitionComment *CommentGroup
}
type OperationTypeDefinition struct {
Operation Operation
Type string
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
+161
View File
@@ -0,0 +1,161 @@
package ast
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
)
// Dump turns ast into a stable string format for assertions in tests.
func Dump(i any) string {
v := reflect.ValueOf(i)
d := dumper{Buffer: &bytes.Buffer{}}
d.dump(v)
return d.String()
}
type dumper struct {
*bytes.Buffer
indent int
}
type Dumpable interface {
Dump() string
}
func (d *dumper) dump(v reflect.Value) {
if dumpable, isDumpable := v.Interface().(Dumpable); isDumpable {
d.WriteString(dumpable.Dump())
return
}
switch v.Kind() {
case reflect.Bool:
if v.Bool() {
d.WriteString("true")
} else {
d.WriteString("false")
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Fprintf(d, "%d", v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fmt.Fprintf(d, "%d", v.Uint())
case reflect.Float32, reflect.Float64:
fmt.Fprintf(d, "%.2f", v.Float())
case reflect.String:
if v.Type().Name() != "string" {
d.WriteString(v.Type().Name() + "(" + strconv.Quote(v.String()) + ")")
} else {
d.WriteString(strconv.Quote(v.String()))
}
case reflect.Array, reflect.Slice:
d.dumpArray(v)
case reflect.Interface, reflect.Ptr:
d.dumpPtr(v)
case reflect.Struct:
d.dumpStruct(v)
default:
panic(fmt.Errorf("unsupported kind: %s\n buf: %s", v.Kind().String(), d.String()))
}
}
func (d *dumper) writeIndent() {
d.WriteString(strings.Repeat(" ", d.indent))
}
func (d *dumper) nl() {
d.WriteByte('\n')
d.writeIndent()
}
func typeName(t reflect.Type) string {
if t.Kind() == reflect.Ptr {
return typeName(t.Elem())
}
return t.Name()
}
func (d *dumper) dumpArray(v reflect.Value) {
d.WriteString("[" + typeName(v.Type().Elem()) + "]")
for i := 0; i < v.Len(); i++ {
d.nl()
d.WriteString("- ")
d.indent++
d.dump(v.Index(i))
d.indent--
}
}
func (d *dumper) dumpStruct(v reflect.Value) {
d.WriteString("<" + v.Type().Name() + ">")
d.indent++
typ := v.Type()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if typ.Field(i).Tag.Get("dump") == "-" {
continue
}
if isZero(f) {
continue
}
d.nl()
d.WriteString(typ.Field(i).Name)
d.WriteString(": ")
d.dump(v.Field(i))
}
d.indent--
}
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
return v.IsNil()
case reflect.Func, reflect.Map:
return v.IsNil()
case reflect.Array, reflect.Slice:
if v.IsNil() {
return true
}
z := true
for i := 0; i < v.Len(); i++ {
z = z && isZero(v.Index(i))
}
return z
case reflect.Struct:
z := true
for i := 0; i < v.NumField(); i++ {
z = z && isZero(v.Field(i))
}
return z
case reflect.String:
return v.String() == ""
case reflect.Bool:
// Never consider Bool field as zero value.
// Always include them in AST dump.
return false
default:
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}
}
func (d *dumper) dumpPtr(v reflect.Value) {
if v.IsNil() {
d.WriteString("nil")
return
}
d.dump(v.Elem())
}
+41
View File
@@ -0,0 +1,41 @@
package ast
type FragmentSpread struct {
Name string
Directives DirectiveList
// Require validation
ObjectDefinition *Definition
Definition *FragmentDefinition
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
type InlineFragment struct {
TypeCondition string
Directives DirectiveList
SelectionSet SelectionSet
// Require validation
ObjectDefinition *Definition
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
type FragmentDefinition struct {
Name string
// Note: fragment variable definitions are experimental and may be changed
// or removed in the future.
VariableDefinition VariableDefinitionList
TypeCondition string
Directives DirectiveList
SelectionSet SelectionSet
// Require validation
Definition *Definition
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
+32
View File
@@ -0,0 +1,32 @@
package ast
type Operation string
const (
Query Operation = "query"
Mutation Operation = "mutation"
Subscription Operation = "subscription"
)
type OperationDefinition struct {
Operation Operation
Name string
VariableDefinitions VariableDefinitionList
Directives DirectiveList
SelectionSet SelectionSet
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
type VariableDefinition struct {
Variable string
Type *Type
DefaultValue *Value
Directives DirectiveList
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
// Requires validation
Definition *Definition
Used bool `dump:"-"`
}
+72
View File
@@ -0,0 +1,72 @@
package ast
import (
"bytes"
"encoding/json"
"fmt"
)
var _ json.Unmarshaler = (*Path)(nil)
type Path []PathElement
type PathElement interface {
isPathElement()
}
var (
_ PathElement = PathIndex(0)
_ PathElement = PathName("")
)
func (path Path) String() string {
if path == nil {
return ""
}
var str bytes.Buffer
for i, v := range path {
switch v := v.(type) {
case PathIndex:
fmt.Fprintf(&str, "[%d]", v)
case PathName:
if i != 0 {
str.WriteByte('.')
}
str.WriteString(string(v))
default:
panic(fmt.Sprintf("unknown type: %T", v))
}
}
return str.String()
}
func (path *Path) UnmarshalJSON(b []byte) error {
var vs []any
err := json.Unmarshal(b, &vs)
if err != nil {
return err
}
*path = make([]PathElement, 0, len(vs))
for _, v := range vs {
switch v := v.(type) {
case string:
*path = append(*path, PathName(v))
case int:
*path = append(*path, PathIndex(v))
case float64:
*path = append(*path, PathIndex(int(v)))
default:
return fmt.Errorf("unknown path element type: %T", v)
}
}
return nil
}
type PathIndex int
func (PathIndex) isPathElement() {}
type PathName string
func (PathName) isPathElement() {}
+44
View File
@@ -0,0 +1,44 @@
package ast
type SelectionSet []Selection
type Selection interface {
isSelection()
GetPosition() *Position
}
func (*Field) isSelection() {}
func (*FragmentSpread) isSelection() {}
func (*InlineFragment) isSelection() {}
func (f *Field) GetPosition() *Position { return f.Position }
func (s *FragmentSpread) GetPosition() *Position { return s.Position }
func (f *InlineFragment) GetPosition() *Position { return f.Position }
type Field struct {
Alias string
Name string
Arguments ArgumentList
Directives DirectiveList
SelectionSet SelectionSet
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
// Require validation
Definition *FieldDefinition
ObjectDefinition *Definition
}
type Argument struct {
Name string
Value *Value
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
func (f *Field) ArgumentMap(vars map[string]any) map[string]any {
if f.Definition == nil {
return nil
}
return arg2map(f.Definition.Arguments, f.Arguments, vars)
}
+19
View File
@@ -0,0 +1,19 @@
package ast
// Source covers a single *.graphql file.
type Source struct {
// Name is the filename of the source
Name string
// Input is the actual contents of the source file
Input string
// BuiltIn indicate whether the source is a part of the specification
BuiltIn bool
}
type Position struct {
Start int // The starting position, in runes, of this token in the input.
End int // The end position, in runes, of this token in the input.
Line int // The line number at the start of this item.
Column int // The column number at the start of this item.
Src *Source // The source document this token belongs to
}
+68
View File
@@ -0,0 +1,68 @@
package ast
func NonNullNamedType(named string, pos *Position) *Type {
return &Type{NamedType: named, NonNull: true, Position: pos}
}
func NamedType(named string, pos *Position) *Type {
return &Type{NamedType: named, NonNull: false, Position: pos}
}
func NonNullListType(elem *Type, pos *Position) *Type {
return &Type{Elem: elem, NonNull: true, Position: pos}
}
func ListType(elem *Type, pos *Position) *Type {
return &Type{Elem: elem, NonNull: false, Position: pos}
}
type Type struct {
NamedType string
Elem *Type
NonNull bool
Position *Position `dump:"-" json:"-"`
}
func (t *Type) Name() string {
if t.NamedType != "" {
return t.NamedType
}
return t.Elem.Name()
}
func (t *Type) String() string {
nn := ""
if t.NonNull {
nn = "!"
}
if t.NamedType != "" {
return t.NamedType + nn
}
return "[" + t.Elem.String() + "]" + nn
}
func (t *Type) IsCompatible(other *Type) bool {
if t.NamedType != other.NamedType {
return false
}
if t.Elem != nil && other.Elem == nil {
return false
}
if t.Elem != nil && !t.Elem.IsCompatible(other.Elem) {
return false
}
if other.NonNull {
return t.NonNull
}
return true
}
func (t *Type) Dump() string {
return t.String()
}
+123
View File
@@ -0,0 +1,123 @@
package ast
import (
"fmt"
"strconv"
"strings"
)
type ValueKind int
const (
Variable ValueKind = iota
IntValue
FloatValue
StringValue
BlockValue
BooleanValue
NullValue
EnumValue
ListValue
ObjectValue
)
type Value struct {
Raw string
Children ChildValueList
Kind ValueKind
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
// Require validation
Definition *Definition
VariableDefinition *VariableDefinition
ExpectedType *Type
ExpectedTypeHasDefault bool
}
type ChildValue struct {
Name string
Value *Value
Position *Position `dump:"-" json:"-"`
Comment *CommentGroup
}
func (v *Value) Value(vars map[string]any) (any, error) {
if v == nil {
return nil, nil
}
switch v.Kind {
case Variable:
if value, ok := vars[v.Raw]; ok {
return value, nil
}
if v.VariableDefinition != nil && v.VariableDefinition.DefaultValue != nil {
return v.VariableDefinition.DefaultValue.Value(vars)
}
return nil, nil
case IntValue:
return strconv.ParseInt(v.Raw, 10, 64)
case FloatValue:
return strconv.ParseFloat(v.Raw, 64)
case StringValue, BlockValue, EnumValue:
return v.Raw, nil
case BooleanValue:
return strconv.ParseBool(v.Raw)
case NullValue:
return nil, nil
case ListValue:
var val []any
for _, elem := range v.Children {
elemVal, err := elem.Value.Value(vars)
if err != nil {
return val, err
}
val = append(val, elemVal)
}
return val, nil
case ObjectValue:
val := map[string]any{}
for _, elem := range v.Children {
elemVal, err := elem.Value.Value(vars)
if err != nil {
return val, err
}
val[elem.Name] = elemVal
}
return val, nil
default:
panic(fmt.Errorf("unknown value kind %d", v.Kind))
}
}
func (v *Value) String() string {
if v == nil {
return "<nil>"
}
switch v.Kind {
case Variable:
return "$" + v.Raw
case IntValue, FloatValue, EnumValue, BooleanValue, NullValue:
return v.Raw
case StringValue, BlockValue:
return strconv.Quote(v.Raw)
case ListValue:
var val []string
for _, elem := range v.Children {
val = append(val, elem.Value.String())
}
return "[" + strings.Join(val, ",") + "]"
case ObjectValue:
var val []string
for _, elem := range v.Children {
val = append(val, elem.Name+":"+elem.Value.String())
}
return "{" + strings.Join(val, ",") + "}"
default:
panic(fmt.Errorf("unknown value kind %d", v.Kind))
}
}
func (v *Value) Dump() string {
return v.String()
}
+198
View File
@@ -0,0 +1,198 @@
package gqlerror
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/ast"
)
// Error is the standard graphql error type described in https://spec.graphql.org/draft/#sec-Errors
type Error struct {
Err error `json:"-"`
Message string `json:"message"`
Path ast.Path `json:"path,omitempty"`
Locations []Location `json:"locations,omitempty"`
Extensions map[string]any `json:"extensions,omitempty"`
Rule string `json:"-"`
}
func (err *Error) SetFile(file string) {
if file == "" {
return
}
if err.Extensions == nil {
err.Extensions = map[string]any{}
}
err.Extensions["file"] = file
}
type Location struct {
Line int `json:"line,omitempty"`
Column int `json:"column,omitempty"`
}
type List []*Error
func (err *Error) Error() string {
var res strings.Builder
if err == nil {
return ""
}
filename, _ := err.Extensions["file"].(string)
if filename == "" {
filename = "input"
}
res.WriteString(filename)
if len(err.Locations) > 0 {
res.WriteByte(':')
res.WriteString(strconv.Itoa(err.Locations[0].Line))
res.WriteByte(':')
res.WriteString(strconv.Itoa(err.Locations[0].Column))
}
res.WriteString(": ")
if ps := err.pathString(); ps != "" {
res.WriteString(ps)
res.WriteByte(' ')
}
res.WriteString(err.Message)
return res.String()
}
func (err *Error) pathString() string {
return err.Path.String()
}
func (err *Error) Unwrap() error {
return err.Err
}
func (err *Error) AsError() error {
if err == nil {
return nil
}
return err
}
func (errs List) Error() string {
var buf strings.Builder
for _, err := range errs {
buf.WriteString(err.Error())
buf.WriteByte('\n')
}
return buf.String()
}
func (errs List) Is(target error) bool {
for _, err := range errs {
if errors.Is(err, target) {
return true
}
}
return false
}
func (errs List) As(target any) bool {
for _, err := range errs {
if errors.As(err, target) {
return true
}
}
return false
}
func (errs List) Unwrap() []error {
l := make([]error, len(errs))
for i, err := range errs {
l[i] = err
}
return l
}
func WrapPath(path ast.Path, err error) *Error {
if err == nil {
return nil
}
return &Error{
Err: err,
Message: err.Error(),
Path: path,
}
}
func Wrap(err error) *Error {
if err == nil {
return nil
}
return &Error{
Err: err,
Message: err.Error(),
}
}
func WrapIfUnwrapped(err error) *Error {
if err == nil {
return nil
}
gqlErr := &Error{}
if errors.As(err, &gqlErr) {
return gqlErr
}
return &Error{
Err: err,
Message: err.Error(),
}
}
func Errorf(message string, args ...any) *Error {
return &Error{
Message: fmt.Sprintf(message, args...),
}
}
func ErrorPathf(path ast.Path, message string, args ...any) *Error {
return &Error{
Message: fmt.Sprintf(message, args...),
Path: path,
}
}
func ErrorPosf(pos *ast.Position, message string, args ...any) *Error {
if pos == nil {
return ErrorLocf(
"",
-1,
-1,
message,
args...,
)
}
return ErrorLocf(
pos.Src.Name,
pos.Line,
pos.Column,
message,
args...,
)
}
func ErrorLocf(file string, line, col int, message string, args ...any) *Error {
var extensions map[string]any
if file != "" {
extensions = map[string]any{"file": file}
}
return &Error{
Message: fmt.Sprintf(message, args...),
Extensions: extensions,
Locations: []Location{
{Line: line, Column: col},
},
}
}
+58
View File
@@ -0,0 +1,58 @@
package lexer
import (
"math"
"strings"
)
// blockStringValue produces the value of a block string from its parsed raw value, similar to
// Coffeescript's block string, Python's docstring trim or Ruby's strip_heredoc.
//
// This implements the GraphQL spec's BlockStringValue() static algorithm.
func blockStringValue(raw string) string {
lines := strings.Split(raw, "\n")
commonIndent := math.MaxInt32
for _, line := range lines {
indent := leadingWhitespace(line)
if indent < len(line) && indent < commonIndent {
commonIndent = indent
if commonIndent == 0 {
break
}
}
}
if commonIndent != math.MaxInt32 && len(lines) > 0 {
for i := 1; i < len(lines); i++ {
if len(lines[i]) < commonIndent {
lines[i] = ""
} else {
lines[i] = lines[i][commonIndent:]
}
}
}
start := 0
end := len(lines)
for start < end && leadingWhitespace(lines[start]) == math.MaxInt32 {
start++
}
for start < end && leadingWhitespace(lines[end-1]) == math.MaxInt32 {
end--
}
return strings.Join(lines[start:end], "\n")
}
func leadingWhitespace(str string) int {
for i, r := range str {
if r != ' ' && r != '\t' {
return i
}
}
// this line is made up entirely of whitespace, its leading whitespace doesnt count.
return math.MaxInt32
}
+582
View File
@@ -0,0 +1,582 @@
package lexer
import (
"bytes"
"slices"
"unicode/utf8"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// Lexer turns graphql request and schema strings into tokens.
type Lexer struct {
*ast.Source
// An offset into the string in bytes
start int
// An offset into the string in runes
startRunes int
// An offset into the string in bytes
end int
// An offset into the string in runes
endRunes int
// the current line number
line int
// An offset into the string in rune
lineStartRunes int
}
func New(src *ast.Source) Lexer {
return Lexer{
Source: src,
line: 1,
}
}
// take one rune from input and advance end.
func (s *Lexer) peek() (rune, int) {
return utf8.DecodeRuneInString(s.Input[s.end:])
}
func (s *Lexer) makeToken(kind Type) (Token, error) {
return s.makeValueToken(kind, s.Input[s.start:s.end])
}
func (s *Lexer) makeValueToken(kind Type, value string) (Token, error) {
return Token{
Kind: kind,
Value: value,
Pos: ast.Position{
Start: s.startRunes,
End: s.endRunes,
Line: s.line,
Column: s.startRunes - s.lineStartRunes + 1,
Src: s.Source,
},
}, nil
}
func (s *Lexer) makeError(format string, args ...any) (Token, *gqlerror.Error) {
column := s.endRunes - s.lineStartRunes + 1
return Token{
Kind: Invalid,
Pos: ast.Position{
Start: s.startRunes,
End: s.endRunes,
Line: s.line,
Column: column,
Src: s.Source,
},
}, gqlerror.ErrorLocf(s.Name, s.line, column, format, args...)
}
// ReadToken gets the next token from the source starting at the given position.
//
// This skips over whitespace and comments until it finds the next lexable
// token, then lexes punctuators immediately or calls the appropriate helper
// function for more complicated tokens.
func (s *Lexer) ReadToken() (Token, error) {
s.ws()
s.start = s.end
s.startRunes = s.endRunes
if s.end >= len(s.Input) {
return s.makeToken(EOF)
}
r := s.Input[s.start]
s.end++
s.endRunes++
switch r {
case '!':
return s.makeValueToken(Bang, "")
case '$':
return s.makeValueToken(Dollar, "")
case '&':
return s.makeValueToken(Amp, "")
case '(':
return s.makeValueToken(ParenL, "")
case ')':
return s.makeValueToken(ParenR, "")
case '.':
if len(s.Input) > s.start+2 && s.Input[s.start:s.start+3] == "..." {
s.end += 2
s.endRunes += 2
return s.makeValueToken(Spread, "")
}
case ':':
return s.makeValueToken(Colon, "")
case '=':
return s.makeValueToken(Equals, "")
case '@':
return s.makeValueToken(At, "")
case '[':
return s.makeValueToken(BracketL, "")
case ']':
return s.makeValueToken(BracketR, "")
case '{':
return s.makeValueToken(BraceL, "")
case '}':
return s.makeValueToken(BraceR, "")
case '|':
return s.makeValueToken(Pipe, "")
case '#':
return s.readComment()
case '_',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z':
return s.readName()
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return s.readNumber()
case '"':
if len(s.Input) > s.start+2 && s.Input[s.start:s.start+3] == `"""` {
return s.readBlockString()
}
return s.readString()
}
s.end--
s.endRunes--
if r < 0x0020 && r != 0x0009 && r != 0x000a && r != 0x000d {
return s.makeError(`Cannot contain the invalid character "\u%04d"`, r)
}
if r == '\'' {
return s.makeError(
`Unexpected single quote character ('), did you mean to use a double quote (")?`,
)
}
return s.makeError(`Cannot parse the unexpected character "%s".`, string(r))
}
// ws reads from body starting at startPosition until it finds a non-whitespace
// or commented character, and updates the token end to include all whitespace.
func (s *Lexer) ws() {
for s.end < len(s.Input) {
switch s.Input[s.end] {
case '\t', ' ', ',':
s.end++
s.endRunes++
case '\n':
s.end++
s.endRunes++
s.line++
s.lineStartRunes = s.endRunes
case '\r':
s.end++
s.endRunes++
s.line++
s.lineStartRunes = s.endRunes
// skip the following newline if its there
if s.end < len(s.Input) && s.Input[s.end] == '\n' {
s.end++
s.endRunes++
}
// byte order mark, given ws is hot path we aren't relying on the unicode package here.
case 0xef:
if s.end+2 < len(s.Input) && s.Input[s.end+1] == 0xBB && s.Input[s.end+2] == 0xBF {
s.end += 3
s.endRunes++
} else {
return
}
default:
return
}
}
}
// readComment from the input
//
// #[\u0009\u0020-\uFFFF]*.
func (s *Lexer) readComment() (Token, error) {
for s.end < len(s.Input) {
r, w := s.peek()
// SourceCharacter but not LineTerminator
if r > 0x001f || r == '\t' {
s.end += w
s.endRunes++
} else {
break
}
}
return s.makeToken(Comment)
}
// readNumber from the input, either a float
// or an int depending on whether a decimal point appears.
//
// Int: -?(0|[1-9][0-9]*)
// Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
func (s *Lexer) readNumber() (Token, error) {
float := false
// backup to the first digit
s.end--
s.endRunes--
s.acceptByte('-')
if s.acceptByte('0') {
if consumed := s.acceptDigits(); consumed != 0 {
s.end -= consumed
s.endRunes -= consumed
return s.makeError("Invalid number, unexpected digit after 0: %s.", s.describeNext())
}
} else {
if consumed := s.acceptDigits(); consumed == 0 {
return s.makeError("Invalid number, expected digit but got: %s.", s.describeNext())
}
}
if s.acceptByte('.') {
float = true
if consumed := s.acceptDigits(); consumed == 0 {
return s.makeError("Invalid number, expected digit but got: %s.", s.describeNext())
}
}
if s.acceptByte('e', 'E') {
float = true
s.acceptByte('-', '+')
if consumed := s.acceptDigits(); consumed == 0 {
return s.makeError("Invalid number, expected digit but got: %s.", s.describeNext())
}
}
if float {
return s.makeToken(Float)
}
return s.makeToken(Int)
}
// acceptByte if it matches any of given bytes, returning true if it found anything.
func (s *Lexer) acceptByte(bytes ...uint8) bool {
if s.end >= len(s.Input) {
return false
}
if slices.Contains(bytes, s.Input[s.end]) {
s.end++
s.endRunes++
return true
}
return false
}
// acceptDigits from the input, returning the number of digits it found.
func (s *Lexer) acceptDigits() int {
consumed := 0
for s.end < len(s.Input) && s.Input[s.end] >= '0' && s.Input[s.end] <= '9' {
s.end++
s.endRunes++
consumed++
}
return consumed
}
// describeNext peeks at the input and returns a human readable string. This should will alloc
// and should only be used in errors.
func (s *Lexer) describeNext() string {
if s.end < len(s.Input) {
return `"` + string(s.Input[s.end]) + `"`
}
return "<EOF>"
}
// readString from the input
//
// "([^"\\\u000A\u000D]|(\\(u[0-9a-fA-F]{4}|["\\/bfnrt])))*".
func (s *Lexer) readString() (Token, error) {
inputLen := len(s.Input)
// this buffer is lazily created only if there are escape characters.
var buf *bytes.Buffer
// skip the opening quote
s.start++
s.startRunes++
for s.end < inputLen {
r := s.Input[s.end]
if r == '\n' || r == '\r' {
break
}
if r < 0x0020 && r != '\t' {
return s.makeError(`Invalid character within String: "\u%04d".`, r)
}
switch r {
default:
char := rune(r)
w := 1
// skip unicode overhead if we are in the ascii range
if r >= 127 {
char, w = utf8.DecodeRuneInString(s.Input[s.end:])
}
s.end += w
s.endRunes++
if buf != nil {
buf.WriteRune(char)
}
case '"':
t, err := s.makeToken(String)
// the token should not include the quotes in its value, but should cover them in its
// position
t.Pos.Start--
t.Pos.End++
if buf != nil {
t.Value = buf.String()
}
// skip the close quote
s.end++
s.endRunes++
return t, err
case '\\':
if s.end+1 >= inputLen {
s.end++
s.endRunes++
return s.makeError(`Invalid character escape sequence.`)
}
if buf == nil {
buf = bytes.NewBufferString(s.Input[s.start:s.end])
}
escape := s.Input[s.end+1]
if escape == 'u' {
if s.end+6 >= inputLen {
s.end++
s.endRunes++
return s.makeError("Invalid character escape sequence: \\%s.", s.Input[s.end:])
}
r, ok := unhex(s.Input[s.end+2 : s.end+6])
if !ok {
s.end++
s.endRunes++
return s.makeError(
"Invalid character escape sequence: \\%s.",
s.Input[s.end:s.end+5],
)
}
buf.WriteRune(r)
s.end += 6
s.endRunes += 6
} else {
switch escape {
case '"', '/', '\\':
buf.WriteByte(escape)
case 'b':
buf.WriteByte('\b')
case 'f':
buf.WriteByte('\f')
case 'n':
buf.WriteByte('\n')
case 'r':
buf.WriteByte('\r')
case 't':
buf.WriteByte('\t')
default:
s.end++
s.endRunes++
return s.makeError("Invalid character escape sequence: \\%s.", string(escape))
}
s.end += 2
s.endRunes += 2
}
}
}
return s.makeError("Unterminated string.")
}
// readBlockString from the input
//
// """("?"?(\\"""|\\(?!=""")|[^"\\]))*""".
func (s *Lexer) readBlockString() (Token, error) {
inputLen := len(s.Input)
var buf bytes.Buffer
// skip the opening quote
s.start += 3
s.startRunes += 3
s.end += 2
s.endRunes += 2
for s.end < inputLen {
r := s.Input[s.end]
// Closing triple quote (""")
if r == '"' {
// Count consecutive quotes
quoteCount := 1
i := s.end + 1
for i < inputLen && s.Input[i] == '"' {
quoteCount++
i++
}
// If we have at least 3 quotes, use the last 3 as the closing quote
if quoteCount >= 3 {
// Add any extra quotes to the buffer (except the last 3)
for range quoteCount - 3 {
buf.WriteByte('"')
}
t, err := s.makeValueToken(BlockString, blockStringValue(buf.String()))
t.Pos.Start -= 3
t.Pos.End += 3
s.end += quoteCount
s.endRunes += quoteCount
return t, err
}
}
// SourceCharacter
if r < 0x0020 && r != '\t' && r != '\n' && r != '\r' {
return s.makeError(`Invalid character within String: "\u%04d".`, r)
}
switch {
case r == '\\' && s.end+4 <= inputLen && s.Input[s.end:s.end+4] == `\"""`:
buf.WriteString(`"""`)
s.end += 4
s.endRunes += 4
case r == '\r':
if s.end+1 < inputLen && s.Input[s.end+1] == '\n' {
s.end++
s.endRunes++
}
buf.WriteByte('\n')
s.end++
s.endRunes++
s.line++
s.lineStartRunes = s.endRunes
default:
char := rune(r)
w := 1
// skip unicode overhead if we are in the ascii range
if r >= 127 {
char, w = utf8.DecodeRuneInString(s.Input[s.end:])
}
s.end += w
s.endRunes++
buf.WriteRune(char)
if r == '\n' {
s.line++
s.lineStartRunes = s.endRunes
}
}
}
return s.makeError("Unterminated string.")
}
func unhex(b string) (v rune, ok bool) {
for _, c := range b {
v <<= 4
switch {
case '0' <= c && c <= '9':
v |= c - '0'
case 'a' <= c && c <= 'f':
v |= c - 'a' + 10
case 'A' <= c && c <= 'F':
v |= c - 'A' + 10
default:
return 0, false
}
}
return v, true
}
// readName from the input
//
// [_A-Za-z][_0-9A-Za-z]*.
func (s *Lexer) readName() (Token, error) {
for s.end < len(s.Input) {
r, w := s.peek()
if (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || r == '_' {
s.end += w
s.endRunes++
} else {
break
}
}
return s.makeToken(Name)
}
+758
View File
@@ -0,0 +1,758 @@
encoding:
- name: disallows uncommon control characters
input: "\u0007"
error:
message: 'Cannot contain the invalid character "\u0007"'
locations: [{line: 1, column: 1}]
- name: accepts BOM header
input: "\uFEFF foo"
tokens:
-
kind: NAME
start: 2
end: 5
value: 'foo'
simple tokens:
- name: records line and column
input: "\n \r\n \r foo\n"
tokens:
-
kind: NAME
start: 8
end: 11
line: 4
column: 3
value: 'foo'
- name: records line and column with comments
input: "\n\n\n#foo\n #bar\n foo\n"
tokens:
-
kind: COMMENT
start: 3
end: 7
line: 4
column: 0
value: '#foo'
-
kind: COMMENT
start: 10
end: 14
line: 5
column: 3
value: '#bar'
-
kind: NAME
start: 17
end: 20
line: 6
column: 3
value: 'foo'
- name: skips whitespace
input: "\n\n foo\n\n\n"
tokens:
-
kind: NAME
start: 6
end: 9
value: 'foo'
- name: skips commas
input: ",,,foo,,,"
tokens:
-
kind: NAME
start: 3
end: 6
value: 'foo'
- name: errors respect whitespace
input: "\n\n ?\n\n\n"
error:
message: 'Cannot parse the unexpected character "?".'
locations: [{line: 3, column: 5}]
string: |
Syntax Error: Cannot parse the unexpected character "?".
GraphQL request (3:5)
2:
3: ?
^
4:
- name: lex reports useful information for dashes in names
input: "a-b"
error:
message: 'Invalid number, expected digit but got: "b".'
locations: [{ line: 1, column: 3 }]
tokens:
-
kind: Name
start: 0
end: 1
value: a
lexes comments:
- name: basic
input: '#simple'
tokens:
-
kind: COMMENT
start: 0
end: 7
value: '#simple'
- name: two lines
input: "#first\n#second"
tokens:
-
kind: COMMENT
start: 0
end: 6
value: "#first"
-
kind: COMMENT
start: 7
end: 14
value: "#second"
- name: whitespace
input: '# white space '
tokens:
-
kind: COMMENT
start: 0
end: 14
value: '# white space '
- name: not escaped
input: '#not escaped \n\r\b\t\f'
tokens:
-
kind: COMMENT
start: 0
end: 23
value: '#not escaped \n\r\b\t\f'
- name: slashes
input: '#slashes \\ \/'
tokens:
-
kind: COMMENT
start: 0
end: 14
value: '#slashes \\ \/'
lexes strings:
- name: basic
input: '"simple"'
tokens:
-
kind: STRING
start: 0
end: 8
value: 'simple'
- name: whitespace
input: '" white space "'
tokens:
-
kind: STRING
start: 0
end: 15
value: ' white space '
- name: quote
input: '"quote \""'
tokens:
-
kind: STRING
start: 0
end: 10
value: 'quote "'
- name: escaped
input: '"escaped \n\r\b\t\f"'
tokens:
-
kind: STRING
start: 0
end: 20
value: "escaped \n\r\b\t\f"
- name: slashes
input: '"slashes \\ \/"'
tokens:
-
kind: STRING
start: 0
end: 15
value: 'slashes \ /'
- name: unicode
input: '"unicode \u1234\u5678\u90AB\uCDEF"'
tokens:
-
kind: STRING
start: 0
end: 34
value: "unicode \u1234\u5678\u90AB\uCDEF"
lex reports useful string errors:
- name: unterminated
input: '"'
error:
message: "Unterminated string."
locations: [{ line: 1, column: 2 }]
- name: no end quote
input: '"no end quote'
error:
message: 'Unterminated string.'
locations: [{ line: 1, column: 14 }]
- name: single quotes
input: "'single quotes'"
error:
message: "Unexpected single quote character ('), did you mean to use a double quote (\")?"
locations: [{ line: 1, column: 1 }]
- name: control characters
input: "\"contains unescaped \u0007 control char\""
error:
message: 'Invalid character within String: "\u0007".'
locations: [{ line: 1, column: 21 }]
- name: null byte
input: "\"null-byte is not \u0000 end of file\""
error:
message: 'Invalid character within String: "\u0000".'
locations: [{ line: 1, column: 19 }]
- name: unterminated newline
input: "\"multi\nline\""
error:
message: 'Unterminated string.'
locations: [{line: 1, column: 7 }]
- name: unterminated carriage return
input: "\"multi\rline\""
error:
message: 'Unterminated string.'
locations: [{ line: 1, column: 7 }]
- name: bad escape character
input: '"bad \z esc"'
error:
message: 'Invalid character escape sequence: \z.'
locations: [{ line: 1, column: 7 }]
- name: hex escape sequence
input: '"bad \x esc"'
error:
message: 'Invalid character escape sequence: \x.'
locations: [{ line: 1, column: 7 }]
- name: short escape sequence
input: '"bad \u1 esc"'
error:
message: 'Invalid character escape sequence: \u1 es.'
locations: [{ line: 1, column: 7 }]
- name: invalid escape sequence 1
input: '"bad \u0XX1 esc"'
error:
message: 'Invalid character escape sequence: \u0XX1.'
locations: [{ line: 1, column: 7 }]
- name: invalid escape sequence 2
input: '"bad \uXXXX esc"'
error:
message: 'Invalid character escape sequence: \uXXXX.'
locations: [{ line: 1, column: 7 }]
- name: invalid escape sequence 3
input: '"bad \uFXXX esc"'
error:
message: 'Invalid character escape sequence: \uFXXX.'
locations: [{ line: 1, column: 7 }]
- name: invalid character escape sequence
input: '"bad \uXXXF esc"'
error:
message: 'Invalid character escape sequence: \uXXXF.'
locations: [{ line: 1, column: 7 }]
lexes block strings:
- name: simple
input: '"""simple"""'
tokens:
-
kind: BLOCK_STRING
start: 0
end: 12
value: 'simple'
- name: white space
input: '""" white space """'
tokens:
-
kind: BLOCK_STRING
start: 0
end: 19
value: ' white space '
- name: contains quote
input: '"""contains " quote"""'
tokens:
-
kind: BLOCK_STRING
start: 0
end: 22
value: 'contains " quote'
- name: contains triplequote
input: "\"\"\"contains \\\"\"\" triplequote\"\"\""
tokens:
-
kind: BLOCK_STRING
start: 0
end: 31
value: 'contains """ triplequote'
- name: multi line
input: "\"\"\"multi\nline\"\"\""
tokens:
-
kind: BLOCK_STRING
start: 0
end: 16
value: "multi\nline"
- name: multi line normalized
input: "\"\"\"multi\rline\r\nnormalized\"\"\""
tokens:
-
kind: BLOCK_STRING
start: 0
end: 28
value: "multi\nline\nnormalized"
- name: unescaped
input: '"""unescaped \n\r\b\t\f\u1234"""'
tokens:
-
kind: BLOCK_STRING
start: 0
end: 32
value: 'unescaped \n\r\b\t\f\u1234'
- name: slashes
input: '"""slashes \\ \/"""'
tokens:
-
kind: BLOCK_STRING
start: 0
end: 19
value: 'slashes \\ \/'
- name: multiple lines
input: |
"""
spans
multiple
lines
"""
tokens:
-
kind: BLOCK_STRING
start: 0
end: 36
value: "spans\n multiple\n lines"
- name: records correct line and column after block string
input: |
"""
some
description
""" foo
tokens:
-
kind: BLOCK_STRING
value: "some\ndescription"
-
kind: NAME
start: 27
end: 30
line: 6
column: 5
value: 'foo'
lex reports useful block string errors:
- name: unterminated string
input: '"""'
error:
message: "Unterminated string."
locations: [{ line: 1, column: 4 }]
- name: unescaped control characters
input: "\"\"\"contains unescaped \u0007 control char\"\"\""
error:
message: 'Invalid character within String: "\u0007".'
locations: [{ line: 1, column: 23 }]
- name: null byte
input: "\"\"\"null-byte is not \u0000 end of file\"\"\""
error:
message: 'Invalid character within String: "\u0000".'
locations: [{ line: 1, column: 21 }]
lexes numbers:
- name: integer
input: "4"
tokens:
-
kind: INT
start: 0
end: 1
value: '4'
- name: float
input: "4.123"
tokens:
-
kind: FLOAT
start: 0
end: 5
value: '4.123'
- name: negative
input: "-4"
tokens:
-
kind: INT
start: 0
end: 2
value: '-4'
- name: nine
input: "9"
tokens:
-
kind: INT
start: 0
end: 1
value: '9'
- name: zero
input: "0"
tokens:
-
kind: INT
start: 0
end: 1
value: '0'
- name: negative float
input: "-4.123"
tokens:
-
kind: FLOAT
start: 0
end: 6
value: '-4.123'
- name: float leading zero
input: "0.123"
tokens:
-
kind: FLOAT
start: 0
end: 5
value: '0.123'
- name: exponent whole
input: "123e4"
tokens:
-
kind: FLOAT
start: 0
end: 5
value: '123e4'
- name: exponent uppercase
input: "123E4"
tokens:
-
kind: FLOAT
start: 0
end: 5
value: '123E4'
- name: exponent negative power
input: "123e-4"
tokens:
-
kind: FLOAT
start: 0
end: 6
value: '123e-4'
- name: exponent positive power
input: "123e+4"
tokens:
-
kind: FLOAT
start: 0
end: 6
value: '123e+4'
- name: exponent negative base
input: "-1.123e4"
tokens:
-
kind: FLOAT
start: 0
end: 8
value: '-1.123e4'
- name: exponent negative base upper
input: "-1.123E4"
tokens:
-
kind: FLOAT
start: 0
end: 8
value: '-1.123E4'
- name: exponent negative base negative power
input: "-1.123e-4"
tokens:
-
kind: FLOAT
start: 0
end: 9
value: '-1.123e-4'
- name: exponent negative base positive power
input: "-1.123e+4"
tokens:
-
kind: FLOAT
start: 0
end: 9
value: '-1.123e+4'
- name: exponent negative base large power
input: "-1.123e4567"
tokens:
-
kind: FLOAT
start: 0
end: 11
value: '-1.123e4567'
lex reports useful number errors:
- name: zero
input: "00"
error:
message: 'Invalid number, unexpected digit after 0: "0".'
locations: [{ line: 1, column: 2 }]
- name: positive
input: "+1"
error:
message: 'Cannot parse the unexpected character "+".'
locations: [{ line: 1, column: 1 }]
- name: trailing dot
input: "1."
error:
message: 'Invalid number, expected digit but got: <EOF>.'
locations: [{ line: 1, column: 3 }]
- name: traililng dot exponent
input: "1.e1"
error:
message: 'Invalid number, expected digit but got: "e".'
locations: [{ line: 1, column: 3 }]
- name: missing leading zero
input: ".123"
error:
message: 'Cannot parse the unexpected character ".".'
locations: [{ line: 1, column: 1 }]
- name: characters
input: "1.A"
error:
message: 'Invalid number, expected digit but got: "A".'
locations: [{ line: 1, column: 3 }]
- name: negative characters
input: "-A"
error:
message: 'Invalid number, expected digit but got: "A".'
locations: [{ line: 1, column: 2 }]
- name: missing exponent
input: '1.0e'
error:
message: 'Invalid number, expected digit but got: <EOF>.'
locations: [{ line: 1, column: 5 }]
- name: character exponent
input: "1.0eA"
error:
message: 'Invalid number, expected digit but got: "A".'
locations: [{ line: 1, column: 5 }]
lexes punctuation:
- name: bang
input: "!"
tokens:
-
kind: BANG
start: 0
end: 1
value: undefined
- name: dollar
input: "$"
tokens:
-
kind: DOLLAR
start: 0
end: 1
value: undefined
- name: open paren
input: "("
tokens:
-
kind: PAREN_L
start: 0
end: 1
value: undefined
- name: close paren
input: ")"
tokens:
-
kind: PAREN_R
start: 0
end: 1
value: undefined
- name: spread
input: "..."
tokens:
-
kind: SPREAD
start: 0
end: 3
value: undefined
- name: colon
input: ":"
tokens:
-
kind: COLON
start: 0
end: 1
value: undefined
- name: equals
input: "="
tokens:
-
kind: EQUALS
start: 0
end: 1
value: undefined
- name: at
input: "@"
tokens:
-
kind: AT
start: 0
end: 1
value: undefined
- name: open bracket
input: "["
tokens:
-
kind: BRACKET_L
start: 0
end: 1
value: undefined
- name: close bracket
input: "]"
tokens:
-
kind: BRACKET_R
start: 0
end: 1
value: undefined
- name: open brace
input: "{"
tokens:
-
kind: BRACE_L
start: 0
end: 1
value: undefined
- name: close brace
input: "}"
tokens:
-
kind: BRACE_R
start: 0
end: 1
value: undefined
- name: pipe
input: "|"
tokens:
-
kind: PIPE
start: 0
end: 1
value: undefined
lex reports useful unknown character error:
- name: not a spread
input: ".."
error:
message: 'Cannot parse the unexpected character ".".'
locations: [{ line: 1, column: 1 }]
- name: question mark
input: "?"
error:
message: 'Cannot parse the unexpected character "?".'
locations: [{ line: 1, column: 1 }]
- name: unicode 203
input: "\u203B"
error:
message: 'Cannot parse the unexpected character "â".'
locations: [{ line: 1, column: 1 }]
- name: unicode 200
input: "\u200b"
error:
message: 'Cannot parse the unexpected character "â".'
locations: [{ line: 1, column: 1 }]
+148
View File
@@ -0,0 +1,148 @@
package lexer
import (
"strconv"
"github.com/vektah/gqlparser/v2/ast"
)
const (
Invalid Type = iota
EOF
Bang
Dollar
Amp
ParenL
ParenR
Spread
Colon
Equals
At
BracketL
BracketR
BraceL
BraceR
Pipe
Name
Int
Float
String
BlockString
Comment
)
func (t Type) Name() string {
switch t {
case Invalid:
return "Invalid"
case EOF:
return "EOF"
case Bang:
return "Bang"
case Dollar:
return "Dollar"
case Amp:
return "Amp"
case ParenL:
return "ParenL"
case ParenR:
return "ParenR"
case Spread:
return "Spread"
case Colon:
return "Colon"
case Equals:
return "Equals"
case At:
return "At"
case BracketL:
return "BracketL"
case BracketR:
return "BracketR"
case BraceL:
return "BraceL"
case BraceR:
return "BraceR"
case Pipe:
return "Pipe"
case Name:
return "Name"
case Int:
return "Int"
case Float:
return "Float"
case String:
return "String"
case BlockString:
return "BlockString"
case Comment:
return "Comment"
}
return "Unknown " + strconv.Itoa(int(t))
}
func (t Type) String() string {
switch t {
case Invalid:
return "<Invalid>"
case EOF:
return "<EOF>"
case Bang:
return "!"
case Dollar:
return "$"
case Amp:
return "&"
case ParenL:
return "("
case ParenR:
return ")"
case Spread:
return "..."
case Colon:
return ":"
case Equals:
return "="
case At:
return "@"
case BracketL:
return "["
case BracketR:
return "]"
case BraceL:
return "{"
case BraceR:
return "}"
case Pipe:
return "|"
case Name:
return "Name"
case Int:
return "Int"
case Float:
return "Float"
case String:
return "String"
case BlockString:
return "BlockString"
case Comment:
return "Comment"
}
return "Unknown " + strconv.Itoa(int(t))
}
// Kind represents a type of token. The types are predefined as constants.
type Type int
type Token struct {
Kind Type // The token type.
Value string // The literal value consumed.
Pos ast.Position // The file and line this token was read from
}
func (t Token) String() string {
if t.Value != "" {
return t.Kind.String() + " " + strconv.Quote(t.Value)
}
return t.Kind.String()
}
+200
View File
@@ -0,0 +1,200 @@
package parser
import (
"strconv"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/lexer"
)
type parser struct {
lexer lexer.Lexer
err error
peeked bool
peekToken lexer.Token
peekError error
prev lexer.Token
comment *ast.CommentGroup
commentConsuming bool
tokenCount int
maxTokenLimit int
}
func (p *parser) SetMaxTokenLimit(maxToken int) {
p.maxTokenLimit = maxToken
}
func (p *parser) consumeComment() (*ast.Comment, bool) {
if p.err != nil {
return nil, false
}
tok := p.peek()
if tok.Kind != lexer.Comment {
return nil, false
}
p.next()
return &ast.Comment{
Value: tok.Value,
Position: &tok.Pos,
}, true
}
func (p *parser) consumeCommentGroup() {
if p.err != nil {
return
}
if p.commentConsuming {
return
}
p.commentConsuming = true
var comments []*ast.Comment
for {
comment, ok := p.consumeComment()
if !ok {
break
}
comments = append(comments, comment)
}
p.comment = &ast.CommentGroup{List: comments}
p.commentConsuming = false
}
func (p *parser) peekPos() *ast.Position {
if p.err != nil {
return nil
}
peek := p.peek()
return &peek.Pos
}
func (p *parser) peek() lexer.Token {
if p.err != nil {
return p.prev
}
if !p.peeked {
p.peekToken, p.peekError = p.lexer.ReadToken()
p.peeked = true
if p.peekToken.Kind == lexer.Comment {
p.consumeCommentGroup()
}
}
return p.peekToken
}
func (p *parser) error(tok lexer.Token, format string, args ...any) {
if p.err != nil {
return
}
p.err = gqlerror.ErrorLocf(tok.Pos.Src.Name, tok.Pos.Line, tok.Pos.Column, format, args...)
}
func (p *parser) next() lexer.Token {
if p.err != nil {
return p.prev
}
// Increment the token count before reading the next token
p.tokenCount++
if p.maxTokenLimit != 0 && p.tokenCount > p.maxTokenLimit {
p.err = gqlerror.Errorf("exceeded token limit of %d", p.maxTokenLimit)
return p.prev
}
if p.peeked {
p.peeked = false
p.comment = nil
p.prev, p.err = p.peekToken, p.peekError
} else {
p.prev, p.err = p.lexer.ReadToken()
if p.prev.Kind == lexer.Comment {
p.consumeCommentGroup()
}
}
return p.prev
}
func (p *parser) expectKeyword(value string) (lexer.Token, *ast.CommentGroup) {
tok := p.peek()
comment := p.comment
if tok.Kind == lexer.Name && tok.Value == value {
return p.next(), comment
}
p.error(tok, "Expected %s, found %s", strconv.Quote(value), tok.String())
return tok, comment
}
func (p *parser) expect(kind lexer.Type) (lexer.Token, *ast.CommentGroup) {
tok := p.peek()
comment := p.comment
if tok.Kind == kind {
return p.next(), comment
}
p.error(tok, "Expected %s, found %s", kind, tok.Kind.String())
return tok, comment
}
func (p *parser) skip(kind lexer.Type) bool {
if p.err != nil {
return false
}
tok := p.peek()
if tok.Kind != kind {
return false
}
p.next()
return true
}
func (p *parser) unexpectedError() {
p.unexpectedToken(p.peek())
}
func (p *parser) unexpectedToken(tok lexer.Token) {
p.error(tok, "Unexpected %s", tok.String())
}
func (p *parser) many(start, end lexer.Type, cb func()) {
hasDef := p.skip(start)
if !hasDef {
return
}
for p.peek().Kind != end && p.err == nil {
cb()
}
p.next()
}
func (p *parser) some(start, end lexer.Type, cb func()) *ast.CommentGroup {
hasDef := p.skip(start)
if !hasDef {
return nil
}
called := false
for p.peek().Kind != end && p.err == nil {
called = true
cb()
}
if !called {
p.error(p.peek(), "expected at least one definition, found %s", p.peek().Kind.String())
return nil
}
comment := p.comment
p.next()
return comment
}
+372
View File
@@ -0,0 +1,372 @@
package parser
import (
. "github.com/vektah/gqlparser/v2/ast" //nolint:staticcheck // bad, yeah
"github.com/vektah/gqlparser/v2/lexer"
)
func ParseQuery(source *Source) (*QueryDocument, error) {
p := parser{
lexer: lexer.New(source),
maxTokenLimit: 0, // 0 means unlimited
}
return p.parseQueryDocument(), p.err
}
func ParseQueryWithTokenLimit(source *Source, maxTokenLimit int) (*QueryDocument, error) {
p := parser{
lexer: lexer.New(source),
maxTokenLimit: maxTokenLimit,
}
return p.parseQueryDocument(), p.err
}
func (p *parser) parseQueryDocument() *QueryDocument {
var doc QueryDocument
for p.peek().Kind != lexer.EOF {
if p.err != nil {
return &doc
}
doc.Position = p.peekPos()
switch p.peek().Kind {
case lexer.Name:
switch p.peek().Value {
case "query", "mutation", "subscription":
doc.Operations = append(doc.Operations, p.parseOperationDefinition())
case "fragment":
doc.Fragments = append(doc.Fragments, p.parseFragmentDefinition())
default:
p.unexpectedError()
}
case lexer.BraceL:
doc.Operations = append(doc.Operations, p.parseOperationDefinition())
default:
p.unexpectedError()
}
}
return &doc
}
func (p *parser) parseOperationDefinition() *OperationDefinition {
if p.peek().Kind == lexer.BraceL {
return &OperationDefinition{
Position: p.peekPos(),
Comment: p.comment,
Operation: Query,
SelectionSet: p.parseRequiredSelectionSet(),
}
}
var od OperationDefinition
od.Position = p.peekPos()
od.Comment = p.comment
od.Operation = p.parseOperationType()
if p.peek().Kind == lexer.Name {
od.Name = p.next().Value
}
od.VariableDefinitions = p.parseVariableDefinitions()
od.Directives = p.parseDirectives(false)
od.SelectionSet = p.parseRequiredSelectionSet()
return &od
}
func (p *parser) parseOperationType() Operation {
tok := p.next()
switch tok.Value {
case "query":
return Query
case "mutation":
return Mutation
case "subscription":
return Subscription
}
p.unexpectedToken(tok)
return ""
}
func (p *parser) parseVariableDefinitions() VariableDefinitionList {
var defs []*VariableDefinition
p.some(lexer.ParenL, lexer.ParenR, func() {
defs = append(defs, p.parseVariableDefinition())
})
return defs
}
func (p *parser) parseVariableDefinition() *VariableDefinition {
var def VariableDefinition
def.Position = p.peekPos()
def.Comment = p.comment
def.Variable = p.parseVariable()
p.expect(lexer.Colon)
def.Type = p.parseTypeReference()
if p.skip(lexer.Equals) {
def.DefaultValue = p.parseValueLiteral(true)
}
def.Directives = p.parseDirectives(false)
return &def
}
func (p *parser) parseVariable() string {
p.expect(lexer.Dollar)
return p.parseName()
}
func (p *parser) parseOptionalSelectionSet() SelectionSet {
var selections []Selection
p.some(lexer.BraceL, lexer.BraceR, func() {
selections = append(selections, p.parseSelection())
})
return selections
}
func (p *parser) parseRequiredSelectionSet() SelectionSet {
if p.peek().Kind != lexer.BraceL {
p.error(p.peek(), "Expected %s, found %s", lexer.BraceL, p.peek().Kind.String())
return nil
}
var selections []Selection
p.some(lexer.BraceL, lexer.BraceR, func() {
selections = append(selections, p.parseSelection())
})
return selections
}
func (p *parser) parseSelection() Selection {
if p.peek().Kind == lexer.Spread {
return p.parseFragment()
}
return p.parseField()
}
func (p *parser) parseField() *Field {
var field Field
field.Position = p.peekPos()
field.Comment = p.comment
field.Alias = p.parseName()
if p.skip(lexer.Colon) {
field.Name = p.parseName()
} else {
field.Name = field.Alias
}
field.Arguments = p.parseArguments(false)
field.Directives = p.parseDirectives(false)
if p.peek().Kind == lexer.BraceL {
field.SelectionSet = p.parseOptionalSelectionSet()
}
return &field
}
func (p *parser) parseArguments(isConst bool) ArgumentList {
var arguments ArgumentList
p.some(lexer.ParenL, lexer.ParenR, func() {
arguments = append(arguments, p.parseArgument(isConst))
})
return arguments
}
func (p *parser) parseArgument(isConst bool) *Argument {
arg := Argument{}
arg.Position = p.peekPos()
arg.Comment = p.comment
arg.Name = p.parseName()
p.expect(lexer.Colon)
arg.Value = p.parseValueLiteral(isConst)
return &arg
}
func (p *parser) parseFragment() Selection {
_, comment := p.expect(lexer.Spread)
if peek := p.peek(); peek.Kind == lexer.Name && peek.Value != "on" {
return &FragmentSpread{
Position: p.peekPos(),
Comment: comment,
Name: p.parseFragmentName(),
Directives: p.parseDirectives(false),
}
}
var def InlineFragment
def.Position = p.peekPos()
def.Comment = comment
if p.peek().Value == "on" {
p.next() // "on"
def.TypeCondition = p.parseName()
}
def.Directives = p.parseDirectives(false)
def.SelectionSet = p.parseRequiredSelectionSet()
return &def
}
func (p *parser) parseFragmentDefinition() *FragmentDefinition {
var def FragmentDefinition
def.Position = p.peekPos()
def.Comment = p.comment
p.expectKeyword("fragment")
def.Name = p.parseFragmentName()
def.VariableDefinition = p.parseVariableDefinitions()
p.expectKeyword("on")
def.TypeCondition = p.parseName()
def.Directives = p.parseDirectives(false)
def.SelectionSet = p.parseRequiredSelectionSet()
return &def
}
func (p *parser) parseFragmentName() string {
if p.peek().Value == "on" {
p.unexpectedError()
return ""
}
return p.parseName()
}
func (p *parser) parseValueLiteral(isConst bool) *Value {
token := p.peek()
var kind ValueKind
switch token.Kind {
case lexer.BracketL:
return p.parseList(isConst)
case lexer.BraceL:
return p.parseObject(isConst)
case lexer.Dollar:
if isConst {
p.unexpectedError()
return nil
}
return &Value{
Position: &token.Pos,
Comment: p.comment,
Raw: p.parseVariable(),
Kind: Variable,
}
case lexer.Int:
kind = IntValue
case lexer.Float:
kind = FloatValue
case lexer.String:
kind = StringValue
case lexer.BlockString:
kind = BlockValue
case lexer.Name:
switch token.Value {
case "true", "false":
kind = BooleanValue
case "null":
kind = NullValue
default:
kind = EnumValue
}
default:
p.unexpectedError()
return nil
}
p.next()
return &Value{Position: &token.Pos, Comment: p.comment, Raw: token.Value, Kind: kind}
}
func (p *parser) parseList(isConst bool) *Value {
var values ChildValueList
pos := p.peekPos()
comment := p.comment
p.many(lexer.BracketL, lexer.BracketR, func() {
values = append(values, &ChildValue{Value: p.parseValueLiteral(isConst)})
})
return &Value{Children: values, Kind: ListValue, Position: pos, Comment: comment}
}
func (p *parser) parseObject(isConst bool) *Value {
var fields ChildValueList
pos := p.peekPos()
comment := p.comment
p.many(lexer.BraceL, lexer.BraceR, func() {
fields = append(fields, p.parseObjectField(isConst))
})
return &Value{Children: fields, Kind: ObjectValue, Position: pos, Comment: comment}
}
func (p *parser) parseObjectField(isConst bool) *ChildValue {
field := ChildValue{}
field.Position = p.peekPos()
field.Comment = p.comment
field.Name = p.parseName()
p.expect(lexer.Colon)
field.Value = p.parseValueLiteral(isConst)
return &field
}
func (p *parser) parseDirectives(isConst bool) []*Directive {
var directives []*Directive
for p.peek().Kind == lexer.At {
if p.err != nil {
break
}
directives = append(directives, p.parseDirective(isConst))
}
return directives
}
func (p *parser) parseDirective(isConst bool) *Directive {
p.expect(lexer.At)
return &Directive{
Position: p.peekPos(),
Name: p.parseName(),
Arguments: p.parseArguments(isConst),
}
}
func (p *parser) parseTypeReference() *Type {
var typ Type
if p.skip(lexer.BracketL) {
typ.Position = p.peekPos()
typ.Elem = p.parseTypeReference()
p.expect(lexer.BracketR)
} else {
typ.Position = p.peekPos()
typ.NamedType = p.parseName()
}
if p.skip(lexer.Bang) {
typ.NonNull = true
}
return &typ
}
func (p *parser) parseName() string {
token, _ := p.expect(lexer.Name)
return token.Value
}
+545
View File
@@ -0,0 +1,545 @@
parser provides useful errors:
- name: unclosed paren
input: '{'
error:
message: "Expected Name, found <EOF>"
locations: [{line: 1, column: 2}]
- name: missing on in fragment
input: |
{ ...MissingOn }
fragment MissingOn Type
error:
message: 'Expected "on", found Name "Type"'
locations: [{ line: 2, column: 20 }]
- name: missing name after alias
input: '{ field: {} }'
error:
message: "Expected Name, found {"
locations: [{ line: 1, column: 10 }]
- name: not an operation
input: 'notanoperation Foo { field }'
error:
message: 'Unexpected Name "notanoperation"'
locations: [{ line: 1, column: 1 }]
- name: a wild splat appears
input: '...'
error:
message: 'Unexpected ...'
locations: [{ line: 1, column: 1}]
variables:
- name: are allowed in args
input: '{ field(complex: { a: { b: [ $var ] } }) }'
- name: are not allowed in default args
input: 'query Foo($x: Complex = { a: { b: [ $var ] } }) { field }'
error:
message: 'Unexpected $'
locations: [{ line: 1, column: 37 }]
- name: can have directives
input: 'query ($withDirective: String @first @second, $withoutDirective: String) { f }'
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
VariableDefinitions: [VariableDefinition]
- <VariableDefinition>
Variable: "withDirective"
Type: String
Directives: [Directive]
- <Directive>
Name: "first"
- <Directive>
Name: "second"
- <VariableDefinition>
Variable: "withoutDirective"
Type: String
SelectionSet: [Selection]
- <Field>
Alias: "f"
Name: "f"
fragments:
- name: can not be named 'on'
input: 'fragment on on on { on }'
error:
message: 'Unexpected Name "on"'
locations: [{ line: 1, column: 10 }]
- name: can not spread fragments called 'on'
input: '{ ...on }'
error:
message: 'Expected Name, found }'
locations: [{ line: 1, column: 9 }]
encoding:
- name: multibyte characters are supported
input: |
# This comment has a ਊ multi-byte character.
{ field(arg: "Has a ਊ multi-byte character.") }
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
SelectionSet: [Selection]
- <Field>
Alias: "field"
Name: "field"
Arguments: [Argument]
- <Argument>
Name: "arg"
Value: "Has a ਊ multi-byte character."
keywords are allowed anywhere a name is:
- name: on
input: |
query on {
... a
... on on { field }
}
fragment a on Type {
on(on: $on)
@on(on: on)
}
- name: subscription
input: |
query subscription {
... subscription
... on subscription { field }
}
fragment subscription on Type {
subscription(subscription: $subscription)
@subscription(subscription: subscription)
}
- name: true
input: |
query true {
... true
... on true { field }
}
fragment true on Type {
true(true: $true)
@true(true: true)
}
operations:
- name: anonymous mutation
input: 'mutation { mutationField }'
- name: named mutation
input: 'mutation Foo { mutationField }'
- name: anonymous subscription
input: 'subscription { subscriptionField }'
- name: named subscription
input: 'subscription Foo { subscriptionField }'
ast:
- name: simple query
input: |
{
node(id: 4) {
id,
name
}
}
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
SelectionSet: [Selection]
- <Field>
Alias: "node"
Name: "node"
Arguments: [Argument]
- <Argument>
Name: "id"
Value: 4
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
- <Field>
Alias: "name"
Name: "name"
- name: nameless query with no variables
input: |
query {
node {
id
}
}
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
SelectionSet: [Selection]
- <Field>
Alias: "node"
Name: "node"
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
- name: fragment defined variables
input: 'fragment a($v: Boolean = false) on t { f(v: $v) }'
ast: |
<QueryDocument>
Fragments: [FragmentDefinition]
- <FragmentDefinition>
Name: "a"
VariableDefinition: [VariableDefinition]
- <VariableDefinition>
Variable: "v"
Type: Boolean
DefaultValue: false
TypeCondition: "t"
SelectionSet: [Selection]
- <Field>
Alias: "f"
Name: "f"
Arguments: [Argument]
- <Argument>
Name: "v"
Value: $v
values:
- name: null
input: '{ f(id: null) }'
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
SelectionSet: [Selection]
- <Field>
Alias: "f"
Name: "f"
Arguments: [Argument]
- <Argument>
Name: "id"
Value: null
- name: strings
input: '{ f(long: """long""", short: "short") } '
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
SelectionSet: [Selection]
- <Field>
Alias: "f"
Name: "f"
Arguments: [Argument]
- <Argument>
Name: "long"
Value: "long"
- <Argument>
Name: "short"
Value: "short"
- name: list
input: '{ f(id: [1,2]) }'
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
SelectionSet: [Selection]
- <Field>
Alias: "f"
Name: "f"
Arguments: [Argument]
- <Argument>
Name: "id"
Value: [1,2]
types:
- name: common types
input: 'query ($string: String, $int: Int, $arr: [Arr], $notnull: [Arr!]!) { f }'
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
VariableDefinitions: [VariableDefinition]
- <VariableDefinition>
Variable: "string"
Type: String
- <VariableDefinition>
Variable: "int"
Type: Int
- <VariableDefinition>
Variable: "arr"
Type: [Arr]
- <VariableDefinition>
Variable: "notnull"
Type: [Arr!]!
SelectionSet: [Selection]
- <Field>
Alias: "f"
Name: "f"
large queries:
- name: kitchen sink
input: |
# Copyright (c) 2015-present, Facebook, Inc.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
query queryName($foo: ComplexType, $site: Site = MOBILE) {
whoever123is: node(id: [123, 456]) {
id ,
... on User @defer {
field2 {
id ,
alias: field1(first:10, after:$foo,) @include(if: $foo) {
id,
...frag
}
}
}
... @skip(unless: $foo) {
id
}
... {
id
}
}
}
mutation likeStory {
like(story: 123) @defer {
story {
id
}
}
}
subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) {
storyLikeSubscribe(input: $input) {
story {
likers {
count
}
likeSentence {
text
}
}
}
}
fragment frag on Friend {
foo(size: $size, bar: $b, obj: {key: "value", block: """
block string uses \"""
"""})
}
{
unnamed(truthy: true, falsey: false, nullish: null),
query
}
ast: |
<QueryDocument>
Operations: [OperationDefinition]
- <OperationDefinition>
Operation: Operation("query")
Name: "queryName"
VariableDefinitions: [VariableDefinition]
- <VariableDefinition>
Variable: "foo"
Type: ComplexType
- <VariableDefinition>
Variable: "site"
Type: Site
DefaultValue: MOBILE
SelectionSet: [Selection]
- <Field>
Alias: "whoever123is"
Name: "node"
Arguments: [Argument]
- <Argument>
Name: "id"
Value: [123,456]
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
- <InlineFragment>
TypeCondition: "User"
Directives: [Directive]
- <Directive>
Name: "defer"
SelectionSet: [Selection]
- <Field>
Alias: "field2"
Name: "field2"
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
- <Field>
Alias: "alias"
Name: "field1"
Arguments: [Argument]
- <Argument>
Name: "first"
Value: 10
- <Argument>
Name: "after"
Value: $foo
Directives: [Directive]
- <Directive>
Name: "include"
Arguments: [Argument]
- <Argument>
Name: "if"
Value: $foo
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
- <FragmentSpread>
Name: "frag"
- <InlineFragment>
Directives: [Directive]
- <Directive>
Name: "skip"
Arguments: [Argument]
- <Argument>
Name: "unless"
Value: $foo
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
- <InlineFragment>
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
Comment: "# Copyright (c) 2015-present, Facebook, Inc.\n#\n# This source code is licensed under the MIT license found in the\n# LICENSE file in the root directory of this source tree.\n"
- <OperationDefinition>
Operation: Operation("mutation")
Name: "likeStory"
SelectionSet: [Selection]
- <Field>
Alias: "like"
Name: "like"
Arguments: [Argument]
- <Argument>
Name: "story"
Value: 123
Directives: [Directive]
- <Directive>
Name: "defer"
SelectionSet: [Selection]
- <Field>
Alias: "story"
Name: "story"
SelectionSet: [Selection]
- <Field>
Alias: "id"
Name: "id"
- <OperationDefinition>
Operation: Operation("subscription")
Name: "StoryLikeSubscription"
VariableDefinitions: [VariableDefinition]
- <VariableDefinition>
Variable: "input"
Type: StoryLikeSubscribeInput
SelectionSet: [Selection]
- <Field>
Alias: "storyLikeSubscribe"
Name: "storyLikeSubscribe"
Arguments: [Argument]
- <Argument>
Name: "input"
Value: $input
SelectionSet: [Selection]
- <Field>
Alias: "story"
Name: "story"
SelectionSet: [Selection]
- <Field>
Alias: "likers"
Name: "likers"
SelectionSet: [Selection]
- <Field>
Alias: "count"
Name: "count"
- <Field>
Alias: "likeSentence"
Name: "likeSentence"
SelectionSet: [Selection]
- <Field>
Alias: "text"
Name: "text"
- <OperationDefinition>
Operation: Operation("query")
SelectionSet: [Selection]
- <Field>
Alias: "unnamed"
Name: "unnamed"
Arguments: [Argument]
- <Argument>
Name: "truthy"
Value: true
- <Argument>
Name: "falsey"
Value: false
- <Argument>
Name: "nullish"
Value: null
- <Field>
Alias: "query"
Name: "query"
Fragments: [FragmentDefinition]
- <FragmentDefinition>
Name: "frag"
TypeCondition: "Friend"
SelectionSet: [Selection]
- <Field>
Alias: "foo"
Name: "foo"
Arguments: [Argument]
- <Argument>
Name: "size"
Value: $size
- <Argument>
Name: "bar"
Value: $b
- <Argument>
Name: "obj"
Value: {key:"value",block:"block string uses \"\"\""}
fuzzer:
- name: 01
input: '{__typename{...}}'
error:
message: 'Expected {, found }'
locations: [{ line: 1, column: 16 }]
- name: 02
input: '{...{__typename{...{}}}}'
error:
message: 'expected at least one definition, found }'
locations: [{ line: 1, column: 21 }]
+635
View File
@@ -0,0 +1,635 @@
package parser
import (
. "github.com/vektah/gqlparser/v2/ast" //nolint:staticcheck // bad, yeah
"github.com/vektah/gqlparser/v2/lexer"
)
func ParseSchemas(inputs ...*Source) (*SchemaDocument, error) {
sd := &SchemaDocument{}
for _, input := range inputs {
inputAst, err := ParseSchema(input)
if err != nil {
return nil, err
}
sd.Merge(inputAst)
}
return sd, nil
}
func ParseSchema(source *Source) (*SchemaDocument, error) {
p := parser{
lexer: lexer.New(source),
maxTokenLimit: 0, // default value is unlimited
}
sd, err := p.parseSchemaDocument(), p.err
if err != nil {
return nil, err
}
for _, def := range sd.Definitions {
def.BuiltIn = source.BuiltIn
}
for _, def := range sd.Extensions {
def.BuiltIn = source.BuiltIn
}
return sd, nil
}
func ParseSchemasWithLimit(maxTokenLimit int, inputs ...*Source) (*SchemaDocument, error) {
sd := &SchemaDocument{}
for _, input := range inputs {
inputAst, err := ParseSchemaWithLimit(input, maxTokenLimit)
if err != nil {
return nil, err
}
sd.Merge(inputAst)
}
return sd, nil
}
func ParseSchemaWithLimit(source *Source, maxTokenLimit int) (*SchemaDocument, error) {
p := parser{
lexer: lexer.New(source),
maxTokenLimit: maxTokenLimit, // 0 is unlimited
}
sd, err := p.parseSchemaDocument(), p.err
if err != nil {
return nil, err
}
for _, def := range sd.Definitions {
def.BuiltIn = source.BuiltIn
}
for _, def := range sd.Extensions {
def.BuiltIn = source.BuiltIn
}
return sd, nil
}
func (p *parser) parseSchemaDocument() *SchemaDocument {
var doc SchemaDocument
doc.Position = p.peekPos()
for p.peek().Kind != lexer.EOF {
if p.err != nil {
return nil
}
var description descriptionWithComment
if p.peek().Kind == lexer.BlockString || p.peek().Kind == lexer.String {
description = p.parseDescription()
}
if p.peek().Kind != lexer.Name {
p.unexpectedError()
break
}
switch p.peek().Value {
case "scalar", "type", "interface", "union", "enum", "input":
doc.Definitions = append(doc.Definitions, p.parseTypeSystemDefinition(description))
case "schema":
doc.Schema = append(doc.Schema, p.parseSchemaDefinition(description))
case "directive":
doc.Directives = append(doc.Directives, p.parseDirectiveDefinition(description))
case "extend":
if description.text != "" {
p.unexpectedToken(p.prev)
}
p.parseTypeSystemExtension(&doc)
default:
p.unexpectedError()
return nil
}
}
// treat end of file comments
doc.Comment = p.comment
return &doc
}
func (p *parser) parseDescription() descriptionWithComment {
token := p.peek()
var desc descriptionWithComment
if token.Kind != lexer.BlockString && token.Kind != lexer.String {
return desc
}
desc.comment = p.comment
desc.text = p.next().Value
return desc
}
func (p *parser) parseTypeSystemDefinition(description descriptionWithComment) *Definition {
tok := p.peek()
if tok.Kind != lexer.Name {
p.unexpectedError()
return nil
}
switch tok.Value {
case "scalar":
return p.parseScalarTypeDefinition(description)
case "type":
return p.parseObjectTypeDefinition(description)
case "interface":
return p.parseInterfaceTypeDefinition(description)
case "union":
return p.parseUnionTypeDefinition(description)
case "enum":
return p.parseEnumTypeDefinition(description)
case "input":
return p.parseInputObjectTypeDefinition(description)
default:
p.unexpectedError()
return nil
}
}
func (p *parser) parseSchemaDefinition(description descriptionWithComment) *SchemaDefinition {
_, comment := p.expectKeyword("schema")
def := SchemaDefinition{}
def.Position = p.peekPos()
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Directives = p.parseDirectives(true)
def.EndOfDefinitionComment = p.some(lexer.BraceL, lexer.BraceR, func() {
def.OperationTypes = append(def.OperationTypes, p.parseOperationTypeDefinition())
})
return &def
}
func (p *parser) parseOperationTypeDefinition() *OperationTypeDefinition {
var op OperationTypeDefinition
op.Position = p.peekPos()
op.Comment = p.comment
op.Operation = p.parseOperationType()
p.expect(lexer.Colon)
op.Type = p.parseName()
return &op
}
func (p *parser) parseScalarTypeDefinition(description descriptionWithComment) *Definition {
_, comment := p.expectKeyword("scalar")
var def Definition
def.Position = p.peekPos()
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Kind = Scalar
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
return &def
}
func (p *parser) parseObjectTypeDefinition(description descriptionWithComment) *Definition {
_, comment := p.expectKeyword("type")
var def Definition
def.Position = p.peekPos()
def.Kind = Object
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Name = p.parseName()
def.Interfaces = p.parseImplementsInterfaces()
def.Directives = p.parseDirectives(true)
def.Fields, def.EndOfDefinitionComment = p.parseFieldsDefinition()
return &def
}
func (p *parser) parseImplementsInterfaces() []string {
var types []string
if p.peek().Value == "implements" {
p.next()
// optional leading ampersand
p.skip(lexer.Amp)
types = append(types, p.parseName())
for p.skip(lexer.Amp) && p.err == nil {
types = append(types, p.parseName())
}
}
return types
}
func (p *parser) parseFieldsDefinition() (FieldList, *CommentGroup) {
var defs FieldList
comment := p.some(lexer.BraceL, lexer.BraceR, func() {
defs = append(defs, p.parseFieldDefinition())
})
return defs, comment
}
func (p *parser) parseFieldDefinition() *FieldDefinition {
var def FieldDefinition
def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
def.BeforeDescriptionComment = desc.comment
def.Description = desc.text
}
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
def.Name = p.parseName()
def.Arguments = p.parseArgumentDefs()
p.expect(lexer.Colon)
def.Type = p.parseTypeReference()
def.Directives = p.parseDirectives(true)
return &def
}
func (p *parser) parseArgumentDefs() ArgumentDefinitionList {
var args ArgumentDefinitionList
p.some(lexer.ParenL, lexer.ParenR, func() {
args = append(args, p.parseArgumentDef())
})
return args
}
func (p *parser) parseArgumentDef() *ArgumentDefinition {
var def ArgumentDefinition
def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
def.BeforeDescriptionComment = desc.comment
def.Description = desc.text
}
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
def.Name = p.parseName()
p.expect(lexer.Colon)
def.Type = p.parseTypeReference()
if p.skip(lexer.Equals) {
def.DefaultValue = p.parseValueLiteral(true)
}
def.Directives = p.parseDirectives(true)
return &def
}
func (p *parser) parseInputValueDef() *FieldDefinition {
var def FieldDefinition
def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
def.BeforeDescriptionComment = desc.comment
def.Description = desc.text
}
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
def.Name = p.parseName()
p.expect(lexer.Colon)
def.Type = p.parseTypeReference()
if p.skip(lexer.Equals) {
def.DefaultValue = p.parseValueLiteral(true)
}
def.Directives = p.parseDirectives(true)
return &def
}
func (p *parser) parseInterfaceTypeDefinition(description descriptionWithComment) *Definition {
_, comment := p.expectKeyword("interface")
var def Definition
def.Position = p.peekPos()
def.Kind = Interface
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Name = p.parseName()
def.Interfaces = p.parseImplementsInterfaces()
def.Directives = p.parseDirectives(true)
def.Fields, def.EndOfDefinitionComment = p.parseFieldsDefinition()
return &def
}
func (p *parser) parseUnionTypeDefinition(description descriptionWithComment) *Definition {
_, comment := p.expectKeyword("union")
var def Definition
def.Position = p.peekPos()
def.Kind = Union
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.Types = p.parseUnionMemberTypes()
return &def
}
func (p *parser) parseUnionMemberTypes() []string {
var types []string
if p.skip(lexer.Equals) {
// optional leading pipe
p.skip(lexer.Pipe)
types = append(types, p.parseName())
for p.skip(lexer.Pipe) && p.err == nil {
types = append(types, p.parseName())
}
}
return types
}
func (p *parser) parseEnumTypeDefinition(description descriptionWithComment) *Definition {
_, comment := p.expectKeyword("enum")
var def Definition
def.Position = p.peekPos()
def.Kind = Enum
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.EnumValues, def.EndOfDefinitionComment = p.parseEnumValuesDefinition()
return &def
}
func (p *parser) parseEnumValuesDefinition() (EnumValueList, *CommentGroup) {
var values EnumValueList
comment := p.some(lexer.BraceL, lexer.BraceR, func() {
values = append(values, p.parseEnumValueDefinition())
})
return values, comment
}
func (p *parser) parseEnumValueDefinition() *EnumValueDefinition {
var def EnumValueDefinition
def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
def.BeforeDescriptionComment = desc.comment
def.Description = desc.text
}
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
return &def
}
func (p *parser) parseInputObjectTypeDefinition(description descriptionWithComment) *Definition {
_, comment := p.expectKeyword("input")
var def Definition
def.Position = p.peekPos()
def.Kind = InputObject
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.Fields, def.EndOfDefinitionComment = p.parseInputFieldsDefinition()
return &def
}
func (p *parser) parseInputFieldsDefinition() (FieldList, *CommentGroup) {
var values FieldList
comment := p.some(lexer.BraceL, lexer.BraceR, func() {
values = append(values, p.parseInputValueDef())
})
return values, comment
}
func (p *parser) parseTypeSystemExtension(doc *SchemaDocument) {
_, comment := p.expectKeyword("extend")
switch p.peek().Value {
case "schema":
doc.SchemaExtension = append(doc.SchemaExtension, p.parseSchemaExtension(comment))
case "scalar":
doc.Extensions = append(doc.Extensions, p.parseScalarTypeExtension(comment))
case "type":
doc.Extensions = append(doc.Extensions, p.parseObjectTypeExtension(comment))
case "interface":
doc.Extensions = append(doc.Extensions, p.parseInterfaceTypeExtension(comment))
case "union":
doc.Extensions = append(doc.Extensions, p.parseUnionTypeExtension(comment))
case "enum":
doc.Extensions = append(doc.Extensions, p.parseEnumTypeExtension(comment))
case "input":
doc.Extensions = append(doc.Extensions, p.parseInputObjectTypeExtension(comment))
default:
p.unexpectedError()
}
}
func (p *parser) parseSchemaExtension(comment *CommentGroup) *SchemaDefinition {
p.expectKeyword("schema")
var def SchemaDefinition
def.Position = p.peekPos()
def.AfterDescriptionComment = comment
def.Directives = p.parseDirectives(true)
def.EndOfDefinitionComment = p.some(lexer.BraceL, lexer.BraceR, func() {
def.OperationTypes = append(def.OperationTypes, p.parseOperationTypeDefinition())
})
if len(def.Directives) == 0 && len(def.OperationTypes) == 0 {
p.unexpectedError()
}
return &def
}
func (p *parser) parseScalarTypeExtension(comment *CommentGroup) *Definition {
p.expectKeyword("scalar")
var def Definition
def.Position = p.peekPos()
def.AfterDescriptionComment = comment
def.Kind = Scalar
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
if len(def.Directives) == 0 {
p.unexpectedError()
}
return &def
}
func (p *parser) parseObjectTypeExtension(comment *CommentGroup) *Definition {
p.expectKeyword("type")
var def Definition
def.Position = p.peekPos()
def.AfterDescriptionComment = comment
def.Kind = Object
def.Name = p.parseName()
def.Interfaces = p.parseImplementsInterfaces()
def.Directives = p.parseDirectives(true)
def.Fields, def.EndOfDefinitionComment = p.parseFieldsDefinition()
if len(def.Interfaces) == 0 && len(def.Directives) == 0 && len(def.Fields) == 0 {
p.unexpectedError()
}
return &def
}
func (p *parser) parseInterfaceTypeExtension(comment *CommentGroup) *Definition {
p.expectKeyword("interface")
var def Definition
def.Position = p.peekPos()
def.AfterDescriptionComment = comment
def.Kind = Interface
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.Fields, def.EndOfDefinitionComment = p.parseFieldsDefinition()
if len(def.Directives) == 0 && len(def.Fields) == 0 {
p.unexpectedError()
}
return &def
}
func (p *parser) parseUnionTypeExtension(comment *CommentGroup) *Definition {
p.expectKeyword("union")
var def Definition
def.Position = p.peekPos()
def.AfterDescriptionComment = comment
def.Kind = Union
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.Types = p.parseUnionMemberTypes()
if len(def.Directives) == 0 && len(def.Types) == 0 {
p.unexpectedError()
}
return &def
}
func (p *parser) parseEnumTypeExtension(comment *CommentGroup) *Definition {
p.expectKeyword("enum")
var def Definition
def.Position = p.peekPos()
def.AfterDescriptionComment = comment
def.Kind = Enum
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.EnumValues, def.EndOfDefinitionComment = p.parseEnumValuesDefinition()
if len(def.Directives) == 0 && len(def.EnumValues) == 0 {
p.unexpectedError()
}
return &def
}
func (p *parser) parseInputObjectTypeExtension(comment *CommentGroup) *Definition {
p.expectKeyword("input")
var def Definition
def.Position = p.peekPos()
def.AfterDescriptionComment = comment
def.Kind = InputObject
def.Name = p.parseName()
def.Directives = p.parseDirectives(false)
def.Fields, def.EndOfDefinitionComment = p.parseInputFieldsDefinition()
if len(def.Directives) == 0 && len(def.Fields) == 0 {
p.unexpectedError()
}
return &def
}
func (p *parser) parseDirectiveDefinition(description descriptionWithComment) *DirectiveDefinition {
_, comment := p.expectKeyword("directive")
p.expect(lexer.At)
var def DirectiveDefinition
def.Position = p.peekPos()
def.BeforeDescriptionComment = description.comment
def.Description = description.text
def.AfterDescriptionComment = comment
def.Name = p.parseName()
def.Arguments = p.parseArgumentDefs()
if peek := p.peek(); peek.Kind == lexer.Name && peek.Value == "repeatable" {
def.IsRepeatable = true
p.skip(lexer.Name)
}
p.expectKeyword("on")
def.Locations = p.parseDirectiveLocations()
return &def
}
func (p *parser) parseDirectiveLocations() []DirectiveLocation {
p.skip(lexer.Pipe)
locations := []DirectiveLocation{p.parseDirectiveLocation()}
for p.skip(lexer.Pipe) && p.err == nil {
locations = append(locations, p.parseDirectiveLocation())
}
return locations
}
func (p *parser) parseDirectiveLocation() DirectiveLocation {
name, _ := p.expect(lexer.Name)
switch name.Value {
case `QUERY`:
return LocationQuery
case `MUTATION`:
return LocationMutation
case `SUBSCRIPTION`:
return LocationSubscription
case `FIELD`:
return LocationField
case `FRAGMENT_DEFINITION`:
return LocationFragmentDefinition
case `FRAGMENT_SPREAD`:
return LocationFragmentSpread
case `INLINE_FRAGMENT`:
return LocationInlineFragment
case `VARIABLE_DEFINITION`:
return LocationVariableDefinition
case `SCHEMA`:
return LocationSchema
case `SCALAR`:
return LocationScalar
case `OBJECT`:
return LocationObject
case `FIELD_DEFINITION`:
return LocationFieldDefinition
case `ARGUMENT_DEFINITION`:
return LocationArgumentDefinition
case `INTERFACE`:
return LocationInterface
case `UNION`:
return LocationUnion
case `ENUM`:
return LocationEnum
case `ENUM_VALUE`:
return LocationEnumValue
case `INPUT_OBJECT`:
return LocationInputObject
case `INPUT_FIELD_DEFINITION`:
return LocationInputFieldDefinition
}
p.unexpectedToken(name)
return ""
}
type descriptionWithComment struct {
text string
comment *CommentGroup
}
+760
View File
@@ -0,0 +1,760 @@
object types:
- name: simple
input: |
type Hello {
world: String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Type: String
- name: with comments
input: |
# Hello
# Hello another
type Hello {
# World
# World another
world: String
# end of type comments
}
# end of file comments
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Type: String
AfterDescriptionComment: "# World\n# World another\n"
AfterDescriptionComment: "# Hello\n# Hello another\n"
EndOfDefinitionComment: "# end of type comments\n"
Comment: "# end of file comments\n"
- name: with comments and description
input: |
# Hello
# Hello another
"type description"
# Hello after description
# Hello after description another
type Hello {
# World
# World another
"field description"
# World after description
# World after description another
world: String
# end of definition coments
# end of definition comments another
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Description: "type description"
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Description: "field description"
Name: "world"
Type: String
BeforeDescriptionComment: "# World\n# World another\n"
AfterDescriptionComment: "# World after description\n# World after description another\n"
BeforeDescriptionComment: "# Hello\n# Hello another\n"
AfterDescriptionComment: "# Hello after description\n# Hello after description another\n"
EndOfDefinitionComment: "# end of definition coments\n# end of definition comments another\n"
- name: with description
input: |
"Description"
type Hello {
world: String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Description: "Description"
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Type: String
- name: with block description
input: |
# Before description comment
"""
Description
"""
# Even with comments between them
type Hello {
world: String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Description: "Description"
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Type: String
BeforeDescriptionComment: "# Before description comment\n"
AfterDescriptionComment: "# Even with comments between them\n"
- name: with field arg
input: |
type Hello {
world(flag: Boolean): String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Arguments: [ArgumentDefinition]
- <ArgumentDefinition>
Name: "flag"
Type: Boolean
Type: String
- name: with field arg and default value
input: |
type Hello {
world(flag: Boolean = true): String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Arguments: [ArgumentDefinition]
- <ArgumentDefinition>
Name: "flag"
DefaultValue: true
Type: Boolean
Type: String
- name: with field list arg
input: |
type Hello {
world(things: [String]): String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Arguments: [ArgumentDefinition]
- <ArgumentDefinition>
Name: "things"
Type: [String]
Type: String
- name: with two args
input: |
type Hello {
world(argOne: Boolean, argTwo: Int): String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Arguments: [ArgumentDefinition]
- <ArgumentDefinition>
Name: "argOne"
Type: Boolean
- <ArgumentDefinition>
Name: "argTwo"
Type: Int
Type: String
- name: must define one or more fields
input: |
type Hello {}
error:
message: "expected at least one definition, found }"
locations: [{ line: 1, column: 13 }]
type extensions:
- name: Object extension
input: |
# comment
extend type Hello {
# comment world
world: String
# end of definition comment
}
ast: |
<SchemaDocument>
Extensions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Type: String
AfterDescriptionComment: "# comment world\n"
AfterDescriptionComment: "# comment\n"
EndOfDefinitionComment: "# end of definition comment\n"
- name: without any fields
input: "extend type Hello implements Greeting"
ast: |
<SchemaDocument>
Extensions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Interfaces: [string]
- "Greeting"
- name: without fields twice
input: |
extend type Hello implements Greeting
extend type Hello implements SecondGreeting
ast: |
<SchemaDocument>
Extensions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Interfaces: [string]
- "Greeting"
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Interfaces: [string]
- "SecondGreeting"
- name: without anything errors
input: "extend type Hello"
error:
message: "Unexpected <EOF>"
locations: [{ line: 1, column: 18 }]
- name: can have descriptions # hmm, this might not be spec compliant...
input: |
"Description"
extend type Hello {
world: String
}
error:
message: 'Unexpected String "Description"'
locations: [{ line: 1, column: 2 }]
- name: can not have descriptions on types
input: |
extend "Description" type Hello {
world: String
}
error:
message: Unexpected String "Description"
locations: [{ line: 1, column: 9 }]
- name: all can have directives
input: |
extend scalar Foo @deprecated
extend type Foo @deprecated
extend interface Foo @deprecated
extend union Foo @deprecated
extend enum Foo @deprecated
extend input Foo @deprecated
ast: |
<SchemaDocument>
Extensions: [Definition]
- <Definition>
Kind: DefinitionKind("SCALAR")
Name: "Foo"
Directives: [Directive]
- <Directive>
Name: "deprecated"
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Foo"
Directives: [Directive]
- <Directive>
Name: "deprecated"
- <Definition>
Kind: DefinitionKind("INTERFACE")
Name: "Foo"
Directives: [Directive]
- <Directive>
Name: "deprecated"
- <Definition>
Kind: DefinitionKind("UNION")
Name: "Foo"
Directives: [Directive]
- <Directive>
Name: "deprecated"
- <Definition>
Kind: DefinitionKind("ENUM")
Name: "Foo"
Directives: [Directive]
- <Directive>
Name: "deprecated"
- <Definition>
Kind: DefinitionKind("INPUT_OBJECT")
Name: "Foo"
Directives: [Directive]
- <Directive>
Name: "deprecated"
schema definition:
- name: simple
input: |
schema {
query: Query
}
ast: |
<SchemaDocument>
Schema: [SchemaDefinition]
- <SchemaDefinition>
OperationTypes: [OperationTypeDefinition]
- <OperationTypeDefinition>
Operation: Operation("query")
Type: "Query"
- name: with comments and description
input: |
# before description comment
"description"
# after description comment
schema {
# before field comment
query: Query
# after field comment
}
ast: |
<SchemaDocument>
Schema: [SchemaDefinition]
- <SchemaDefinition>
Description: "description"
OperationTypes: [OperationTypeDefinition]
- <OperationTypeDefinition>
Operation: Operation("query")
Type: "Query"
Comment: "# before field comment\n"
BeforeDescriptionComment: "# before description comment\n"
AfterDescriptionComment: "# after description comment\n"
EndOfDefinitionComment: "# after field comment\n"
schema extensions:
- name: simple
input: |
extend schema {
mutation: Mutation
}
ast: |
<SchemaDocument>
SchemaExtension: [SchemaDefinition]
- <SchemaDefinition>
OperationTypes: [OperationTypeDefinition]
- <OperationTypeDefinition>
Operation: Operation("mutation")
Type: "Mutation"
- name: with comment and description
input: |
# before extend comment
extend schema {
# before field comment
mutation: Mutation
# after field comment
}
ast: |
<SchemaDocument>
SchemaExtension: [SchemaDefinition]
- <SchemaDefinition>
OperationTypes: [OperationTypeDefinition]
- <OperationTypeDefinition>
Operation: Operation("mutation")
Type: "Mutation"
Comment: "# before field comment\n"
AfterDescriptionComment: "# before extend comment\n"
EndOfDefinitionComment: "# after field comment\n"
- name: directive only
input: "extend schema @directive"
ast: |
<SchemaDocument>
SchemaExtension: [SchemaDefinition]
- <SchemaDefinition>
Directives: [Directive]
- <Directive>
Name: "directive"
- name: without anything errors
input: "extend schema"
error:
message: "Unexpected <EOF>"
locations: [{ line: 1, column: 14}]
inheritance:
- name: single
input: "type Hello implements World { field: String }"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Interfaces: [string]
- "World"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "field"
Type: String
- name: multi
input: "type Hello implements Wo & rld { field: String }"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Interfaces: [string]
- "Wo"
- "rld"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "field"
Type: String
- name: multi with leading amp
input: "type Hello implements & Wo & rld { field: String }"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "Hello"
Interfaces: [string]
- "Wo"
- "rld"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "field"
Type: String
enums:
- name: single value
input: "enum Hello { WORLD }"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("ENUM")
Name: "Hello"
EnumValues: [EnumValueDefinition]
- <EnumValueDefinition>
Name: "WORLD"
- name: double value
input: "enum Hello { WO, RLD }"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("ENUM")
Name: "Hello"
EnumValues: [EnumValueDefinition]
- <EnumValueDefinition>
Name: "WO"
- <EnumValueDefinition>
Name: "RLD"
- name: must define one or more unique enum values
input: |
enum Hello {}
error:
message: "expected at least one definition, found }"
locations: [{ line: 1, column: 13 }]
interface:
- name: simple
input: |
interface Hello {
world: String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("INTERFACE")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Type: String
- name: must define one or more fields
input: |
interface Hello {}
error:
message: "expected at least one definition, found }"
locations: [{ line: 1, column: 18 }]
- name: may define intermediate interfaces
input: |
interface IA {
id: ID!
}
interface IIA implements IA {
id: ID!
}
type A implements IIA {
id: ID!
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("INTERFACE")
Name: "IA"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "id"
Type: ID!
- <Definition>
Kind: DefinitionKind("INTERFACE")
Name: "IIA"
Interfaces: [string]
- "IA"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "id"
Type: ID!
- <Definition>
Kind: DefinitionKind("OBJECT")
Name: "A"
Interfaces: [string]
- "IIA"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "id"
Type: ID!
unions:
- name: simple
input: "union Hello = World"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("UNION")
Name: "Hello"
Types: [string]
- "World"
- name: with two types
input: "union Hello = Wo | Rld"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("UNION")
Name: "Hello"
Types: [string]
- "Wo"
- "Rld"
- name: with leading pipe
input: "union Hello = | Wo | Rld"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("UNION")
Name: "Hello"
Types: [string]
- "Wo"
- "Rld"
- name: cant be empty
input: "union Hello = || Wo | Rld"
error:
message: "Expected Name, found |"
locations: [{ line: 1, column: 16 }]
- name: cant double pipe
input: "union Hello = Wo || Rld"
error:
message: "Expected Name, found |"
locations: [{ line: 1, column: 19 }]
- name: cant have trailing pipe
input: "union Hello = | Wo | Rld |"
error:
message: "Expected Name, found <EOF>"
locations: [{ line: 1, column: 27 }]
scalar:
- name: simple
input: "scalar Hello"
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("SCALAR")
Name: "Hello"
input object:
- name: simple
input: |
input Hello {
world: String
}
ast: |
<SchemaDocument>
Definitions: [Definition]
- <Definition>
Kind: DefinitionKind("INPUT_OBJECT")
Name: "Hello"
Fields: [FieldDefinition]
- <FieldDefinition>
Name: "world"
Type: String
- name: can not have args
input: |
input Hello {
world(foo: Int): String
}
error:
message: "Expected :, found ("
locations: [{ line: 2, column: 8 }]
- name: must define one or more input fields
input: |
input Hello {}
error:
message: "expected at least one definition, found }"
locations: [{ line: 1, column: 14 }]
directives:
- name: simple
input: directive @foo on FIELD
ast: |
<SchemaDocument>
Directives: [DirectiveDefinition]
- <DirectiveDefinition>
Name: "foo"
Locations: [DirectiveLocation]
- DirectiveLocation("FIELD")
IsRepeatable: false
- name: executable
input: |
directive @onQuery on QUERY
directive @onMutation on MUTATION
directive @onSubscription on SUBSCRIPTION
directive @onField on FIELD
directive @onFragmentDefinition on FRAGMENT_DEFINITION
directive @onFragmentSpread on FRAGMENT_SPREAD
directive @onInlineFragment on INLINE_FRAGMENT
directive @onVariableDefinition on VARIABLE_DEFINITION
ast: |
<SchemaDocument>
Directives: [DirectiveDefinition]
- <DirectiveDefinition>
Name: "onQuery"
Locations: [DirectiveLocation]
- DirectiveLocation("QUERY")
IsRepeatable: false
- <DirectiveDefinition>
Name: "onMutation"
Locations: [DirectiveLocation]
- DirectiveLocation("MUTATION")
IsRepeatable: false
- <DirectiveDefinition>
Name: "onSubscription"
Locations: [DirectiveLocation]
- DirectiveLocation("SUBSCRIPTION")
IsRepeatable: false
- <DirectiveDefinition>
Name: "onField"
Locations: [DirectiveLocation]
- DirectiveLocation("FIELD")
IsRepeatable: false
- <DirectiveDefinition>
Name: "onFragmentDefinition"
Locations: [DirectiveLocation]
- DirectiveLocation("FRAGMENT_DEFINITION")
IsRepeatable: false
- <DirectiveDefinition>
Name: "onFragmentSpread"
Locations: [DirectiveLocation]
- DirectiveLocation("FRAGMENT_SPREAD")
IsRepeatable: false
- <DirectiveDefinition>
Name: "onInlineFragment"
Locations: [DirectiveLocation]
- DirectiveLocation("INLINE_FRAGMENT")
IsRepeatable: false
- <DirectiveDefinition>
Name: "onVariableDefinition"
Locations: [DirectiveLocation]
- DirectiveLocation("VARIABLE_DEFINITION")
IsRepeatable: false
- name: repeatable
input: directive @foo repeatable on FIELD
ast: |
<SchemaDocument>
Directives: [DirectiveDefinition]
- <DirectiveDefinition>
Name: "foo"
Locations: [DirectiveLocation]
- DirectiveLocation("FIELD")
IsRepeatable: true
- name: invalid location
input: "directive @foo on FIELD | INCORRECT_LOCATION"
error:
message: 'Unexpected Name "INCORRECT_LOCATION"'
locations: [{ line: 1, column: 27 }]
fuzzer:
- name: 1
input: "type o{d(g:["
error:
message: 'Expected Name, found <EOF>'
locations: [{ line: 1, column: 13 }]
- name: 2
input: "\"\"\"\r"
error:
message: 'Unexpected <Invalid>'
locations: [{ line: 2, column: 1 }]
+24
View File
@@ -0,0 +1,24 @@
package core
import (
"github.com/vektah/gqlparser/v2/gqlerror"
)
type AddErrFunc func(options ...ErrorOption)
type RuleFunc func(observers *Events, addError AddErrFunc)
type Rule struct {
Name string
RuleFunc RuleFunc
}
// NameSorter sorts Rules by name.
// usage: sort.Sort(core.NameSorter(specifiedRules))
type NameSorter []Rule
func (a NameSorter) Len() int { return len(a) }
func (a NameSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a NameSorter) Less(i, j int) bool { return a[i].Name < a[j].Name }
type ErrorOption func(err *gqlerror.Error)
+151
View File
@@ -0,0 +1,151 @@
package core
import (
"bytes"
"fmt"
"math"
"sort"
"strings"
"github.com/agnivade/levenshtein"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
func Message(msg string, args ...any) 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, 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, 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 ...any) ErrorOption {
return func(err *gqlerror.Error) {
err.Message += " Did you mean " + fmt.Sprintf(suggestion, args...) + "?"
}
}
// 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()
}
// 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 = max(int(math.Floor(float64(len(a))*0.4)+1), 1)
return threshold
}
// 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)
}
+308
View File
@@ -0,0 +1,308 @@
package core
import (
"context"
"fmt"
"github.com/vektah/gqlparser/v2/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.ExpectedTypeHasDefault = fieldDef.DefaultValue != nil &&
fieldDef.DefaultValue.Kind != ast.NullValue
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.ExpectedTypeHasDefault = argDef.DefaultValue != nil &&
argDef.DefaultValue.Kind != ast.NullValue
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 infinite 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))
}
}
@@ -0,0 +1,250 @@
# 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
"Directs the executor to defer this fragment when the `if` argument is true or undefined."
directive @defer(
"Deferred when true or undefined."
if: Boolean = true,
"Unique name"
label: String
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
"""
Directs the executor to include this field or fragment only when the `if` argument is true.
"""
directive @include(
"""Included when true."""
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""
Directs the executor to skip this field or fragment when the `if` argument is true.
"""
directive @skip(
"""Skipped when true."""
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""Marks an element of a GraphQL schema as no longer supported."""
directive @deprecated(
"""
Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).
"""
reason: String = "No longer supported"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
"""Exposes a URL that specifies the behavior of this scalar."""
directive @specifiedBy(
"""The URL that specifies the behavior of this scalar."""
url: String!
) on SCALAR
"""
Indicates exactly one field must be supplied and this field must not be `null`.
"""
directive @oneOf on INPUT_OBJECT
"""
A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.
"""
type __Schema {
description: String
"""A list of all types supported by this server."""
types: [__Type!]!
"""The type that query operations will be rooted at."""
queryType: __Type!
"""
If this server supports mutation, the type that mutation operations will be rooted at.
"""
mutationType: __Type
"""
If this server support subscription, the type that subscription operations will be rooted at.
"""
subscriptionType: __Type
"""A list of all directives supported by this server."""
directives: [__Directive!]!
}
"""
The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.
Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.
"""
type __Type {
kind: __TypeKind!
name: String
description: String
specifiedByURL: String
fields(includeDeprecated: Boolean = false): [__Field!]
interfaces: [__Type!]
possibleTypes: [__Type!]
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
inputFields(includeDeprecated: Boolean = false): [__InputValue!]
ofType: __Type
isOneOf: Boolean
}
"""An enum describing what kind of type a given `__Type` is."""
enum __TypeKind {
"""Indicates this type is a scalar."""
SCALAR
"""
Indicates this type is an object. `fields` and `interfaces` are valid fields.
"""
OBJECT
"""
Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.
"""
INTERFACE
"""Indicates this type is a union. `possibleTypes` is a valid field."""
UNION
"""Indicates this type is an enum. `enumValues` is a valid field."""
ENUM
"""
Indicates this type is an input object. `inputFields` is a valid field.
"""
INPUT_OBJECT
"""Indicates this type is a list. `ofType` is a valid field."""
LIST
"""Indicates this type is a non-null. `ofType` is a valid field."""
NON_NULL
}
"""
Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.
"""
type __Field {
name: String!
description: String
args(includeDeprecated: Boolean = false): [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
"""
Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.
"""
type __InputValue {
name: String!
description: String
type: __Type!
"""
A GraphQL-formatted string representing the default value for this input value.
"""
defaultValue: String
isDeprecated: Boolean!
deprecationReason: String
}
"""
One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.
"""
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
"""
A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.
"""
type __Directive {
name: String!
description: String
isRepeatable: Boolean!
locations: [__DirectiveLocation!]!
args(includeDeprecated: Boolean = false): [__InputValue!]!
}
"""
A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.
"""
enum __DirectiveLocation {
"""Location adjacent to a query operation."""
QUERY
"""Location adjacent to a mutation operation."""
MUTATION
"""Location adjacent to a subscription operation."""
SUBSCRIPTION
"""Location adjacent to a field."""
FIELD
"""Location adjacent to a fragment definition."""
FRAGMENT_DEFINITION
"""Location adjacent to a fragment spread."""
FRAGMENT_SPREAD
"""Location adjacent to an inline fragment."""
INLINE_FRAGMENT
"""Location adjacent to a variable definition."""
VARIABLE_DEFINITION
"""Location adjacent to a schema definition."""
SCHEMA
"""Location adjacent to a scalar definition."""
SCALAR
"""Location adjacent to an object type definition."""
OBJECT
"""Location adjacent to a field definition."""
FIELD_DEFINITION
"""Location adjacent to an argument definition."""
ARGUMENT_DEFINITION
"""Location adjacent to an interface definition."""
INTERFACE
"""Location adjacent to a union definition."""
UNION
"""Location adjacent to an enum definition."""
ENUM
"""Location adjacent to an enum value definition."""
ENUM_VALUE
"""Location adjacent to an input object type definition."""
INPUT_OBJECT
"""Location adjacent to an input object field definition."""
INPUT_FIELD_DEFINITION
}
+16
View File
@@ -0,0 +1,16 @@
package validator
import (
_ "embed"
"github.com/vektah/gqlparser/v2/ast"
)
//go:embed imported/prelude.graphql
var preludeGraphql string
var Prelude = &ast.Source{
Name: "prelude.graphql",
Input: preludeGraphql,
BuiltIn: true,
}
@@ -0,0 +1,131 @@
package rules
import (
"fmt"
"sort"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncFieldsOnCorrectType(observers *Events, addError AddErrFunc, disableSuggestion bool) {
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 !disableSuggestion {
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("%s", message),
At(field.Position),
)
})
}
var FieldsOnCorrectTypeRule = Rule{
Name: "FieldsOnCorrectType",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncFieldsOnCorrectType(observers, addError, false)
},
}
var FieldsOnCorrectTypeRuleWithoutSuggestions = Rule{
Name: "FieldsOnCorrectTypeWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncFieldsOnCorrectType(observers, addError, true)
},
}
// Go through all 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)
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 := concatSlice(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 typeA < typeB
})
return suggestedTypes
}
// By employing a full slice expression (slice[low:high:max]),
// where max is set to the slices length,
// we ensure that appending elements results
// in a slice backed by a distinct array.
// This method prevents the shared array issue.
func concatSlice(first, second []string) []string {
n := len(first)
return append(first[:n:n], second...)
}
// 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
}
possibleFieldNames := make([]string, 0, len(parent.Fields))
for _, field := range parent.Fields {
possibleFieldNames = append(possibleFieldNames, field.Name)
}
return SuggestionList(name, possibleFieldNames)
}
@@ -0,0 +1,49 @@
package rules
import (
"fmt"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var FragmentsOnCompositeTypesRule = Rule{
Name: "FragmentsOnCompositeTypes",
RuleFunc: 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("%s", 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("%s", message),
At(fragment.Position),
)
})
},
}
@@ -0,0 +1,93 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncKnownArgumentNames(observers *Events, addError AddErrFunc, disableSuggestion bool) {
// 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
}
if disableSuggestion {
addError(
Message(
`Unknown argument "%s" on field "%s.%s".`,
arg.Name,
field.ObjectDefinition.Name,
field.Name,
),
At(field.Position),
)
} else {
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
}
if disableSuggestion {
addError(
Message(`Unknown argument "%s" on directive "@%s".`, arg.Name, directive.Name),
At(directive.Position),
)
} else {
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),
)
}
}
})
}
var KnownArgumentNamesRule = Rule{
Name: "KnownArgumentNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownArgumentNames(observers, addError, false)
},
}
var KnownArgumentNamesRuleWithoutSuggestions = Rule{
Name: "KnownArgumentNamesWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownArgumentNames(observers, addError, true)
},
}
@@ -0,0 +1,53 @@
package rules
import (
"slices"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var KnownDirectivesRule = Rule{
Name: "KnownDirectives",
RuleFunc: func(observers *Events, addError AddErrFunc) {
type mayNotBeUsedDirective struct {
Name string
Line int
Column int
}
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
}
if slices.Contains(directive.Definition.Locations, 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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var KnownFragmentNamesRule = Rule{
Name: "KnownFragmentNames",
RuleFunc: 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 rules
import (
"fmt"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var KnownRootTypeRule = Rule{
Name: "KnownRootType",
RuleFunc: 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,79 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncKnownTypeNames(observers *Events, addError AddErrFunc, disableSuggestion bool) {
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
}
if disableSuggestion {
addError(
Message(`Unknown type "%s".`, typeName),
At(fragment.Position),
)
} else {
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),
)
}
})
}
var KnownTypeNamesRule = Rule{
Name: "KnownTypeNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownTypeNames(observers, addError, false)
},
}
var KnownTypeNamesRuleWithoutSuggestions = Rule{
Name: "KnownTypeNamesWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownTypeNames(observers, addError, true)
},
}
@@ -0,0 +1,21 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var LoneAnonymousOperationRule = Rule{
Name: "LoneAnonymousOperation",
RuleFunc: 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,93 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
const maxListsDepth = 3
var MaxIntrospectionDepth = Rule{
Name: "MaxIntrospectionDepth",
RuleFunc: func(observers *Events, addError AddErrFunc) {
// Counts the depth of list fields in "__Type" recursively and
// returns `true` if the limit has been reached.
observers.OnField(func(walker *Walker, field *ast.Field) {
if field.Name == "__schema" || field.Name == "__type" {
visitedFragments := make(map[string]bool)
if checkDepthField(field, visitedFragments, 0) {
addError(
Message(`Maximum introspection depth exceeded`),
At(field.Position),
)
}
return
}
})
},
}
func checkDepthSelectionSet(
selectionSet ast.SelectionSet,
visitedFragments map[string]bool,
depth int,
) bool {
for _, child := range selectionSet {
if field, ok := child.(*ast.Field); ok {
if checkDepthField(field, visitedFragments, depth) {
return true
}
}
if fragmentSpread, ok := child.(*ast.FragmentSpread); ok {
if checkDepthFragmentSpread(fragmentSpread, visitedFragments, depth) {
return true
}
}
if inlineFragment, ok := child.(*ast.InlineFragment); ok {
if checkDepthSelectionSet(inlineFragment.SelectionSet, visitedFragments, depth) {
return true
}
}
}
return false
}
func checkDepthField(field *ast.Field, visitedFragments map[string]bool, depth int) bool {
if field.Name == "fields" ||
field.Name == "interfaces" ||
field.Name == "possibleTypes" ||
field.Name == "inputFields" {
depth++
if depth >= maxListsDepth {
return true
}
}
return checkDepthSelectionSet(field.SelectionSet, visitedFragments, depth)
}
func checkDepthFragmentSpread(
fragmentSpread *ast.FragmentSpread,
visitedFragments map[string]bool,
depth int,
) bool {
fragmentName := fragmentSpread.Name
if visited, ok := visitedFragments[fragmentName]; ok && visited {
// Fragment cycles are handled by `NoFragmentCyclesRule`.
return false
}
fragment := fragmentSpread.Definition
if fragment == nil {
// Missing fragments checks are handled by `KnownFragmentNamesRule`.
return false
}
// Rather than following an immutable programming pattern which has
// significant memory and garbage collection overhead, we've opted to
// take a mutable approach for efficiency's sake. Importantly visiting a
// fragment twice is fine, so long as you don't do one visit inside the
// other.
visitedFragments[fragmentName] = true
defer delete(visitedFragments, fragmentName)
return checkDepthSelectionSet(fragment.SelectionSet, visitedFragments, depth)
}
@@ -0,0 +1,99 @@
package rules
import (
"fmt"
"strings"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoFragmentCyclesRule = Rule{
Name: "NoFragmentCycles",
RuleFunc: 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,35 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoUndefinedVariablesRule = Rule{
Name: "NoUndefinedVariables",
RuleFunc: 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,31 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoUnusedFragmentsRule = Rule{
Name: "NoUnusedFragments",
RuleFunc: 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,36 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoUnusedVariablesRule = Rule{
Name: "NoUnusedVariables",
RuleFunc: 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,640 @@
package rules
import (
"bytes"
"fmt"
"reflect"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var OverlappingFieldsCanBeMergedRule = Rule{
Name: "OverlappingFieldsCanBeMerged",
RuleFunc: 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 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, 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, 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, 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,83 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var PossibleFragmentSpreadsRule = Rule{
Name: "PossibleFragmentSpreads",
RuleFunc: 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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var ProvidedRequiredArgumentsRule = Rule{
Name: "ProvidedRequiredArguments",
RuleFunc: 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),
)
}
})
},
}
+126
View File
@@ -0,0 +1,126 @@
package rules
import (
"slices"
"github.com/vektah/gqlparser/v2/validator/core"
)
// Rules manages GraphQL validation rules.
type Rules struct {
rules map[string]core.RuleFunc
ruleNameKeys []string // for deterministic order
}
// NewRules creates a Rules instance with the specified rules.
func NewRules(rs ...core.Rule) *Rules {
r := &Rules{
rules: make(map[string]core.RuleFunc),
}
for _, rule := range rs {
r.AddRule(rule.Name, rule.RuleFunc)
}
return r
}
// NewDefaultRules creates a Rules instance containing the default GraphQL validation rule set.
func NewDefaultRules() *Rules {
rules := []core.Rule{
FieldsOnCorrectTypeRule,
FragmentsOnCompositeTypesRule,
KnownArgumentNamesRule,
KnownDirectivesRule,
KnownFragmentNamesRule,
KnownRootTypeRule,
KnownTypeNamesRule,
LoneAnonymousOperationRule,
MaxIntrospectionDepth,
NoFragmentCyclesRule,
NoUndefinedVariablesRule,
NoUnusedFragmentsRule,
NoUnusedVariablesRule,
OverlappingFieldsCanBeMergedRule,
PossibleFragmentSpreadsRule,
ProvidedRequiredArgumentsRule,
ScalarLeafsRule,
SingleFieldSubscriptionsRule,
UniqueArgumentNamesRule,
UniqueDirectivesPerLocationRule,
UniqueFragmentNamesRule,
UniqueInputFieldNamesRule,
UniqueOperationNamesRule,
UniqueVariableNamesRule,
ValuesOfCorrectTypeRule,
VariablesAreInputTypesRule,
VariablesInAllowedPositionRule,
}
r := NewRules(rules...)
return r
}
// AddRule adds a rule with the specified name and rule function to the rule set.
// If a rule with the same name already exists, it will not be added.
func (r *Rules) AddRule(name string, ruleFunc core.RuleFunc) {
if r.rules == nil {
r.rules = make(map[string]core.RuleFunc)
}
if _, exists := r.rules[name]; !exists {
r.rules[name] = ruleFunc
r.ruleNameKeys = append(r.ruleNameKeys, name)
}
}
// GetInner returns the internal rule map.
// If the map is not initialized, it returns an empty map.
// This returns a copy of the rules map, not the original map.
func (r *Rules) GetInner() map[string]core.RuleFunc {
if r == nil {
return nil // impossible nonsense, hopefully
}
if r.rules == nil {
return make(map[string]core.RuleFunc)
}
rules := make(map[string]core.RuleFunc)
for k, v := range r.rules {
rules[k] = v
}
return rules
}
// RemoveRule removes a rule with the specified name from the rule set.
// If no rule with the specified name exists, it does nothing.
func (r *Rules) RemoveRule(name string) {
if r == nil {
return // impossible nonsense, hopefully
}
if r.rules != nil {
delete(r.rules, name)
}
if len(r.ruleNameKeys) > 0 {
r.ruleNameKeys = slices.DeleteFunc(r.ruleNameKeys, func(s string) bool {
return s == name // delete the name rule key
})
}
}
// ReplaceRule replaces a rule with the specified name with a new rule function.
// If no rule with the specified name exists, it does nothing.
func (r *Rules) ReplaceRule(name string, ruleFunc core.RuleFunc) {
if r == nil {
return // impossible nonsense, hopefully
}
if r.rules == nil {
r.rules = make(map[string]core.RuleFunc)
}
if _, exists := r.rules[name]; exists {
r.rules[name] = ruleFunc
}
}
@@ -0,0 +1,46 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var ScalarLeafsRule = Rule{
Name: "ScalarLeafs",
RuleFunc: 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 rules
import (
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var SingleFieldSubscriptionsRule = Rule{
Name: "SingleFieldSubscriptions",
RuleFunc: 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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueArgumentNamesRule = Rule{
Name: "UniqueArgumentNames",
RuleFunc: 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,29 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueDirectivesPerLocationRule = Rule{
Name: "UniqueDirectivesPerLocation",
RuleFunc: 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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueFragmentNamesRule = Rule{
Name: "UniqueFragmentNames",
RuleFunc: 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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueInputFieldNamesRule = Rule{
Name: "UniqueInputFieldNames",
RuleFunc: 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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueOperationNamesRule = Rule{
Name: "UniqueOperationNames",
RuleFunc: 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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueVariableNamesRule = Rule{
Name: "UniqueVariableNames",
RuleFunc: 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,307 @@
package rules
import (
"errors"
"fmt"
"strconv"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncValuesOfCorrectType(observers *Events, addError AddErrFunc, disableSuggestion bool) {
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 {
if disableSuggestion {
addError(
Message(
`Enum "%s" cannot represent non-enum value: %s.`,
value.ExpectedType.String(),
value.String(),
),
At(value.Position),
)
} else {
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 {
if disableSuggestion {
addError(
unexpectedTypeMessageOnly(value),
At(value.Position),
)
} else {
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 {
if disableSuggestion {
addError(
Message(
`Value "%s" does not exist in "%s" enum.`,
value.String(),
value.ExpectedType.String(),
),
At(value.Position),
)
} else {
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 _, directive := range value.Definition.Directives {
if directive.Name == "oneOf" {
func() {
if len(value.Children) != 1 {
addError(
Message(
`OneOf Input Object "%s" must specify exactly one key.`,
value.Definition.Name,
),
At(value.Position),
)
return
}
fieldValue := value.Children[0].Value
isNullLiteral := fieldValue == nil || fieldValue.Kind == ast.NullValue
if isNullLiteral {
addError(
Message(
`Field "%s.%s" must be non-null.`,
value.Definition.Name,
value.Definition.Fields[0].Name,
),
At(fieldValue.Position),
)
return
}
isVariable := fieldValue.Kind == ast.Variable
if isVariable {
variableName := fieldValue.VariableDefinition.Variable
isNullableVariable := !fieldValue.VariableDefinition.Type.NonNull
if isNullableVariable {
addError(
Message(
`Variable "%s" must be non-nullable to be used for OneOf Input Object "%s".`,
variableName,
value.Definition.Name,
),
At(fieldValue.Position),
)
}
}
}()
}
}
for _, fieldValue := range value.Children {
if value.Definition.Fields.ForName(fieldValue.Name) == nil {
if disableSuggestion {
addError(
Message(
`Field "%s" is not defined by type "%s".`,
fieldValue.Name,
value.Definition.Name,
),
At(fieldValue.Position),
)
} else {
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))
}
})
}
var ValuesOfCorrectTypeRule = Rule{
Name: "ValuesOfCorrectType",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncValuesOfCorrectType(observers, addError, false)
},
}
var ValuesOfCorrectTypeRuleWithoutSuggestions = Rule{
Name: "ValuesOfCorrectTypeWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncValuesOfCorrectType(observers, addError, true)
},
}
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 rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var VariablesAreInputTypesRule = Rule{
Name: "VariablesAreInputTypes",
RuleFunc: 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,48 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var VariablesInAllowedPositionRule = Rule{
Name: "VariablesInAllowedPosition",
RuleFunc: 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 the expected type has a default, the given variable can be null
if value.ExpectedTypeHasDefault {
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),
)
}
})
},
}
+660
View File
@@ -0,0 +1,660 @@
package validator
import (
"slices"
"sort"
"strconv"
"strings"
. "github.com/vektah/gqlparser/v2/ast" //nolint:staticcheck // bad, yeah
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/parser"
)
func LoadSchema(inputs ...*Source) (*Schema, error) {
sd, err := parser.ParseSchemas(inputs...)
if err != nil {
return nil, gqlerror.WrapIfUnwrapped(err)
}
return ValidateSchemaDocument(sd)
}
func ValidateSchemaDocument(sd *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 sd.Definitions {
if schema.Types[def.Name] != nil {
return nil, gqlerror.ErrorPosf(def.Position, "Cannot redeclare type %s.", def.Name)
}
schema.Types[def.Name] = sd.Definitions[i]
}
defs := append(DefinitionList{}, sd.Definitions...)
for _, ext := range sd.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 sd.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", "defer", "oneOf": // 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] = sd.Directives[i]
}
if len(sd.Schema) > 1 {
return nil, gqlerror.ErrorPosf(
sd.Schema[1].Position,
"Cannot have multiple schema entry points, consider schema extensions instead.",
)
}
if len(sd.Schema) == 1 {
schema.Description = sd.Schema[0].Description
for _, entrypoint := range sd.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
}
}
if err := validateDirectives(
&schema,
sd.Schema[0].Directives,
LocationSchema,
nil,
); err != nil {
return nil, err
}
schema.SchemaDirectives = append(schema.SchemaDirectives, sd.Schema[0].Directives...)
}
for _, ext := range sd.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 := validateDirectives(&schema, ext.Directives, LocationSchema, nil); err != nil {
return nil, err
}
schema.SchemaDirectives = append(schema.SchemaDirectives, ext.Directives...)
}
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(sd.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,
)
}
}
if err := validateDirectives(
schema,
value.Directives,
LocationEnumValue,
nil,
); err != nil {
return err
}
}
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,
)
}
dirDefinition := schema.Directives[dir.Name]
if dirDefinition == nil {
return gqlerror.ErrorPosf(dir.Position, "Undefined directive %s.", dir.Name)
}
validKind := slices.Contains(dirDefinition.Locations, location)
if !validKind {
return gqlerror.ErrorPosf(
dir.Position,
"Directive %s is not applicable on %s.",
dir.Name,
location,
)
}
for _, arg := range dir.Arguments {
if dirDefinition.Arguments.ForName(arg.Name) == nil {
return gqlerror.ErrorPosf(
arg.Position,
"Undefined argument %s for directive %s.",
arg.Name,
dir.Name,
)
}
}
for _, schemaArg := range dirDefinition.Arguments {
if schemaArg.Type.NonNull && schemaArg.DefaultValue == nil {
if arg := dir.Arguments.ForName(
schemaArg.Name,
); arg == nil ||
arg.Value.Kind == NullValue {
return gqlerror.ErrorPosf(
dir.Position,
"Argument %s for directive %s cannot be null.",
schemaArg.Name,
dir.Name,
)
}
}
}
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://spec.graphql.org/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 {
return slices.Contains(slice, want)
}
func isCovariant(schema *Schema, required, 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 {
return slices.Contains(valid, kind)
}
func kindList(kinds ...DefinitionKind) string {
s := make([]string, len(kinds))
for i, k := range kinds {
s[i] = string(k)
}
return strings.Join(s, ", ")
}
+731
View File
@@ -0,0 +1,731 @@
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: field argument list must not be empty
input: |
type FooBar {
foo(): ID
}
error:
message: 'expected at least one definition, found )'
locations: [{line: 2, column: 7}]
- 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 (type)
input: |
type User @foo {
name: String
}
error:
message: "Undefined directive foo."
locations: [{line: 1, column: 12}]
- name: must be declared (field)
input: |
type User {
name: String @foo
}
error:
message: "Undefined directive foo."
locations: [{line: 2, column: 17}]
- name: must be declared (enum)
input: |
enum Unit {
METER @foo
}
error:
message: "Undefined directive foo."
locations: [{line: 2, column: 10}]
- 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 }
- name: Invalid directive argument not allowed
input: |
directive @foo(bla: Int!) on FIELD_DEFINITION
type P {f: Int @foo(foobla: 11)}
error:
message: 'Undefined argument foobla for directive foo.'
locations: [{line: 2, column: 21}]
- name: non-null argument must be provided
input: |
directive @foo(bla: Int!) on FIELD_DEFINITION
type P {f: Int @foo }
error:
message: 'Argument bla for directive foo cannot be null.'
locations: [{line: 2, column: 17}]
- name: non-null argument must not be null
input: |
directive @foo(bla: Int!) on FIELD_DEFINITION
type P {f: Int @foo(bla: null) }
error:
message: 'Argument bla for directive foo cannot be null.'
locations: [{line: 2, column: 17}]
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}]
+163
View File
@@ -0,0 +1,163 @@
package validator
import (
"sort"
//nolint:staticcheck // bad, yeah
. "github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/validator/core"
validatorrules "github.com/vektah/gqlparser/v2/validator/rules"
)
type (
AddErrFunc = core.AddErrFunc
RuleFunc = core.RuleFunc
Rule = core.Rule
Events = core.Events
ErrorOption = core.ErrorOption
Walker = core.Walker
)
var (
Message = core.Message
QuotedOrList = core.QuotedOrList
OrList = core.OrList
)
// Walk is an alias for core.Walk.
func Walk(schema *Schema, document *QueryDocument, observers *Events) {
core.Walk(schema, document, observers)
}
var specifiedRules []Rule
func init() {
// Initialize specifiedRules with default rules
defaultRules := validatorrules.NewDefaultRules()
for name, ruleFunc := range defaultRules.GetInner() {
specifiedRules = append(specifiedRules, Rule{Name: name, RuleFunc: ruleFunc})
// ensure initial default is in deterministic order
sort.Sort(core.NameSorter(specifiedRules))
}
}
// AddRule adds a rule to the rule set.
// ruleFunc is called once each time `Validate` is executed.
func AddRule(name string, ruleFunc RuleFunc) {
specifiedRules = append(specifiedRules, Rule{Name: name, RuleFunc: ruleFunc})
}
// RemoveRule removes an existing rule from the rule set
// if one of the same name exists.
// The rule set is global, so it is not safe for concurrent changes.
func RemoveRule(name string) {
var result []Rule //nolint:prealloc // using initialized with len(rules) produces a race condition
for _, r := range specifiedRules {
if r.Name == name {
continue
}
result = append(result, r)
}
specifiedRules = result
}
// ReplaceRule replaces an existing rule from the rule set
// if one of the same name exists.
// If no match is found, it will add a new rule to the rule set.
// The rule set is global, so it is not safe for concurrent changes.
func ReplaceRule(name string, ruleFunc RuleFunc) {
var found bool
var result []Rule //nolint:prealloc // using initialized with len(rules) produces a race condition
for _, r := range specifiedRules {
if r.Name == name {
found = true
result = append(result, Rule{Name: name, RuleFunc: ruleFunc})
continue
}
result = append(result, r)
}
if !found {
specifiedRules = append(specifiedRules, Rule{Name: name, RuleFunc: ruleFunc})
return
}
specifiedRules = result
}
// Deprecated: use ValidateWithRules instead.
func Validate(schema *Schema, doc *QueryDocument, rules ...Rule) gqlerror.List {
if rules == nil {
rules = specifiedRules
}
var errs gqlerror.List
if schema == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as Schema is nil"))
}
if doc == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as QueryDocument is nil"))
}
if len(errs) > 0 {
return errs
}
observers := &core.Events{}
for i := range rules {
rule := rules[i]
rule.RuleFunc(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
}
func ValidateWithRules(
schema *Schema,
doc *QueryDocument,
rules *validatorrules.Rules,
) gqlerror.List {
if rules == nil {
rules = validatorrules.NewDefaultRules()
}
var errs gqlerror.List
if schema == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as Schema is nil"))
}
if doc == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as QueryDocument is nil"))
}
if len(errs) > 0 {
return errs
}
observers := &core.Events{}
var currentRules []Rule //nolint:prealloc // would require extra local refs for len
for name, ruleFunc := range rules.GetInner() {
currentRules = append(currentRules, Rule{Name: name, RuleFunc: ruleFunc})
// ensure deterministic order evaluation
sort.Sort(core.NameSorter(currentRules))
}
for _, currentRule := range currentRules {
currentRule.RuleFunc(observers, func(options ...ErrorOption) {
err := &gqlerror.Error{
Rule: currentRule.Name,
}
for _, o := range options {
o(err)
}
errs = append(errs, err)
})
}
Walk(schema, doc, observers)
return errs
}
+285
View File
@@ -0,0 +1,285 @@
package validator
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
//nolint:staticcheck // We do not care about capitalized error strings
var ErrUnexpectedType = errors.New("Unexpected Type")
// VariableValues coerces and validates variable values.
func VariableValues(
schema *ast.Schema,
op *ast.OperationDefinition,
variables map[string]any,
) (map[string]any, error) {
coercedVars := map[string]any{}
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 {
switch v.Type.NamedType {
case "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)
case "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, not a %s", def.Name, val.Kind())
}
// 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
}