1package graphql
2
3import (
4 "github.com/graphql-go/graphql/gqlerrors"
5 "github.com/graphql-go/graphql/language/ast"
6 "github.com/graphql-go/graphql/language/kinds"
7 "github.com/graphql-go/graphql/language/visitor"
8)
9
10type ValidationResult struct {
11 IsValid bool
12 Errors []gqlerrors.FormattedError
13}
14
15/**
16 * Implements the "Validation" section of the spec.
17 *
18 * Validation runs synchronously, returning an array of encountered errors, or
19 * an empty array if no errors were encountered and the document is valid.
20 *
21 * A list of specific validation rules may be provided. If not provided, the
22 * default list of rules defined by the GraphQL specification will be used.
23 *
24 * Each validation rules is a function which returns a visitor
25 * (see the language/visitor API). Visitor methods are expected to return
26 * GraphQLErrors, or Arrays of GraphQLErrors when invalid.
27 */
28
29func ValidateDocument(schema *Schema, astDoc *ast.Document, rules []ValidationRuleFn) (vr ValidationResult) {
30 if len(rules) == 0 {
31 rules = SpecifiedRules
32 }
33
34 vr.IsValid = false
35 if schema == nil {
36 vr.Errors = append(vr.Errors, gqlerrors.NewFormattedError("Must provide schema"))
37 return vr
38 }
39 if astDoc == nil {
40 vr.Errors = append(vr.Errors, gqlerrors.NewFormattedError("Must provide document"))
41 return vr
42 }
43
44 typeInfo := NewTypeInfo(&TypeInfoConfig{
45 Schema: schema,
46 })
47 vr.Errors = VisitUsingRules(schema, typeInfo, astDoc, rules)
48 if len(vr.Errors) == 0 {
49 vr.IsValid = true
50 }
51 return vr
52}
53
54// VisitUsingRules This uses a specialized visitor which runs multiple visitors in parallel,
55// while maintaining the visitor skip and break API.
56//
57// @internal
58// Had to expose it to unit test experimental customizable validation feature,
59// but not meant for public consumption
60func VisitUsingRules(schema *Schema, typeInfo *TypeInfo, astDoc *ast.Document, rules []ValidationRuleFn) []gqlerrors.FormattedError {
61
62 context := NewValidationContext(schema, astDoc, typeInfo)
63 visitors := []*visitor.VisitorOptions{}
64
65 for _, rule := range rules {
66 instance := rule(context)
67 visitors = append(visitors, instance.VisitorOpts)
68 }
69
70 // Visit the whole document with each instance of all provided rules.
71 visitor.Visit(astDoc, visitor.VisitWithTypeInfo(typeInfo, visitor.VisitInParallel(visitors...)), nil)
72 return context.Errors()
73}
74
75type HasSelectionSet interface {
76 GetKind() string
77 GetLoc() *ast.Location
78 GetSelectionSet() *ast.SelectionSet
79}
80
81var _ HasSelectionSet = (*ast.OperationDefinition)(nil)
82var _ HasSelectionSet = (*ast.FragmentDefinition)(nil)
83
84type VariableUsage struct {
85 Node *ast.Variable
86 Type Input
87}
88
89type ValidationContext struct {
90 schema *Schema
91 astDoc *ast.Document
92 typeInfo *TypeInfo
93 errors []gqlerrors.FormattedError
94 fragments map[string]*ast.FragmentDefinition
95 variableUsages map[HasSelectionSet][]*VariableUsage
96 recursiveVariableUsages map[*ast.OperationDefinition][]*VariableUsage
97 recursivelyReferencedFragments map[*ast.OperationDefinition][]*ast.FragmentDefinition
98 fragmentSpreads map[*ast.SelectionSet][]*ast.FragmentSpread
99}
100
101func NewValidationContext(schema *Schema, astDoc *ast.Document, typeInfo *TypeInfo) *ValidationContext {
102 return &ValidationContext{
103 schema: schema,
104 astDoc: astDoc,
105 typeInfo: typeInfo,
106 fragments: map[string]*ast.FragmentDefinition{},
107 variableUsages: map[HasSelectionSet][]*VariableUsage{},
108 recursiveVariableUsages: map[*ast.OperationDefinition][]*VariableUsage{},
109 recursivelyReferencedFragments: map[*ast.OperationDefinition][]*ast.FragmentDefinition{},
110 fragmentSpreads: map[*ast.SelectionSet][]*ast.FragmentSpread{},
111 }
112}
113
114func (ctx *ValidationContext) ReportError(err error) {
115 formattedErr := gqlerrors.FormatError(err)
116 ctx.errors = append(ctx.errors, formattedErr)
117}
118func (ctx *ValidationContext) Errors() []gqlerrors.FormattedError {
119 return ctx.errors
120}
121
122func (ctx *ValidationContext) Schema() *Schema {
123 return ctx.schema
124}
125func (ctx *ValidationContext) Document() *ast.Document {
126 return ctx.astDoc
127}
128func (ctx *ValidationContext) Fragment(name string) *ast.FragmentDefinition {
129 if len(ctx.fragments) == 0 {
130 if ctx.Document() == nil {
131 return nil
132 }
133 defs := ctx.Document().Definitions
134 fragments := map[string]*ast.FragmentDefinition{}
135 for _, def := range defs {
136 if def, ok := def.(*ast.FragmentDefinition); ok {
137 defName := ""
138 if def.Name != nil {
139 defName = def.Name.Value
140 }
141 fragments[defName] = def
142 }
143 }
144 ctx.fragments = fragments
145 }
146 f, _ := ctx.fragments[name]
147 return f
148}
149func (ctx *ValidationContext) FragmentSpreads(node *ast.SelectionSet) []*ast.FragmentSpread {
150 if spreads, ok := ctx.fragmentSpreads[node]; ok && spreads != nil {
151 return spreads
152 }
153
154 spreads := []*ast.FragmentSpread{}
155 setsToVisit := []*ast.SelectionSet{node}
156
157 for {
158 if len(setsToVisit) == 0 {
159 break
160 }
161 var set *ast.SelectionSet
162 // pop
163 set, setsToVisit = setsToVisit[len(setsToVisit)-1], setsToVisit[:len(setsToVisit)-1]
164 if set.Selections != nil {
165 for _, selection := range set.Selections {
166 switch selection := selection.(type) {
167 case *ast.FragmentSpread:
168 spreads = append(spreads, selection)
169 case *ast.Field:
170 if selection.SelectionSet != nil {
171 setsToVisit = append(setsToVisit, selection.SelectionSet)
172 }
173 case *ast.InlineFragment:
174 if selection.SelectionSet != nil {
175 setsToVisit = append(setsToVisit, selection.SelectionSet)
176 }
177 }
178 }
179 }
180 ctx.fragmentSpreads[node] = spreads
181 }
182 return spreads
183}
184
185func (ctx *ValidationContext) RecursivelyReferencedFragments(operation *ast.OperationDefinition) []*ast.FragmentDefinition {
186 if fragments, ok := ctx.recursivelyReferencedFragments[operation]; ok && fragments != nil {
187 return fragments
188 }
189
190 fragments := []*ast.FragmentDefinition{}
191 collectedNames := map[string]bool{}
192 nodesToVisit := []*ast.SelectionSet{operation.SelectionSet}
193
194 for {
195 if len(nodesToVisit) == 0 {
196 break
197 }
198
199 var node *ast.SelectionSet
200
201 node, nodesToVisit = nodesToVisit[len(nodesToVisit)-1], nodesToVisit[:len(nodesToVisit)-1]
202 spreads := ctx.FragmentSpreads(node)
203 for _, spread := range spreads {
204 fragName := ""
205 if spread.Name != nil {
206 fragName = spread.Name.Value
207 }
208 if res, ok := collectedNames[fragName]; !ok || !res {
209 collectedNames[fragName] = true
210 fragment := ctx.Fragment(fragName)
211 if fragment != nil {
212 fragments = append(fragments, fragment)
213 nodesToVisit = append(nodesToVisit, fragment.SelectionSet)
214 }
215 }
216
217 }
218 }
219
220 ctx.recursivelyReferencedFragments[operation] = fragments
221 return fragments
222}
223func (ctx *ValidationContext) VariableUsages(node HasSelectionSet) []*VariableUsage {
224 if usages, ok := ctx.variableUsages[node]; ok && usages != nil {
225 return usages
226 }
227 usages := []*VariableUsage{}
228 typeInfo := NewTypeInfo(&TypeInfoConfig{
229 Schema: ctx.schema,
230 })
231
232 visitor.Visit(node, visitor.VisitWithTypeInfo(typeInfo, &visitor.VisitorOptions{
233 KindFuncMap: map[string]visitor.NamedVisitFuncs{
234 kinds.VariableDefinition: {
235 Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
236 return visitor.ActionSkip, nil
237 },
238 },
239 kinds.Variable: {
240 Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
241 if node, ok := p.Node.(*ast.Variable); ok && node != nil {
242 usages = append(usages, &VariableUsage{
243 Node: node,
244 Type: typeInfo.InputType(),
245 })
246 }
247 return visitor.ActionNoChange, nil
248 },
249 },
250 },
251 }), nil)
252
253 ctx.variableUsages[node] = usages
254 return usages
255}
256func (ctx *ValidationContext) RecursiveVariableUsages(operation *ast.OperationDefinition) []*VariableUsage {
257 if usages, ok := ctx.recursiveVariableUsages[operation]; ok && usages != nil {
258 return usages
259 }
260 usages := ctx.VariableUsages(operation)
261
262 fragments := ctx.RecursivelyReferencedFragments(operation)
263 for _, fragment := range fragments {
264 fragmentUsages := ctx.VariableUsages(fragment)
265 usages = append(usages, fragmentUsages...)
266 }
267
268 ctx.recursiveVariableUsages[operation] = usages
269 return usages
270}
271func (ctx *ValidationContext) Type() Output {
272 return ctx.typeInfo.Type()
273}
274func (ctx *ValidationContext) ParentType() Composite {
275 return ctx.typeInfo.ParentType()
276}
277func (ctx *ValidationContext) InputType() Input {
278 return ctx.typeInfo.InputType()
279}
280func (ctx *ValidationContext) FieldDef() *FieldDefinition {
281 return ctx.typeInfo.FieldDef()
282}
283func (ctx *ValidationContext) Directive() *Directive {
284 return ctx.typeInfo.Directive()
285}
286func (ctx *ValidationContext) Argument() *Argument {
287 return ctx.typeInfo.Argument()
288}