Initial QSfera import
This commit is contained in:
+19
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
+250
@@ -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
@@ -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,
|
||||
}
|
||||
Generated
Vendored
+131
@@ -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 slice’s 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)
|
||||
}
|
||||
Generated
Vendored
+49
@@ -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),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+93
@@ -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)
|
||||
},
|
||||
}
|
||||
+53
@@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+21
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
+37
@@ -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))
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
+79
@@ -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)
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+21
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+93
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+99
@@ -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
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+31
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+640
@@ -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
|
||||
}
|
||||
Generated
Vendored
+83
@@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+64
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
+46
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+88
@@ -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
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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]++
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+29
@@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+29
@@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+26
@@ -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]++
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+307
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+30
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+48
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user