graphql_tools/validation/rules/
variables_in_allowed_position.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::{
4    ast::{
5        visit_document, AstNodeWithName, OperationVisitor, OperationVisitorContext,
6        SchemaDocumentExtension,
7    },
8    static_graphql::query::{Type, Value, VariableDefinition},
9    validation::utils::{ValidationError, ValidationErrorContext},
10};
11
12use super::ValidationRule;
13
14/// Variables in allowed position
15///
16/// Variable usages must be compatible with the arguments they are passed to.
17///
18/// See https://spec.graphql.org/draft/#sec-All-Variable-Usages-are-Allowed
19#[derive(Default)]
20pub struct VariablesInAllowedPosition<'a> {
21    spreads: HashMap<Scope<'a>, HashSet<&'a str>>,
22    variable_usages: HashMap<Scope<'a>, Vec<(&'a str, &'a Type)>>,
23    variable_defs: HashMap<Scope<'a>, Vec<&'a VariableDefinition>>,
24    current_scope: Option<Scope<'a>>,
25}
26
27impl<'a> VariablesInAllowedPosition<'a> {
28    pub fn new() -> Self {
29        VariablesInAllowedPosition {
30            spreads: HashMap::new(),
31            variable_usages: HashMap::new(),
32            variable_defs: HashMap::new(),
33            current_scope: None,
34        }
35    }
36
37    fn collect_incorrect_usages(
38        &self,
39        from: &Scope<'a>,
40        var_defs: &Vec<&VariableDefinition>,
41        visitor_context: &mut OperationVisitorContext,
42        user_context: &mut ValidationErrorContext,
43        visited: &mut HashSet<Scope<'a>>,
44    ) {
45        if visited.contains(from) {
46            return;
47        }
48
49        visited.insert(from.clone());
50
51        if let Some(usages) = self.variable_usages.get(from) {
52            for (var_name, var_type) in usages {
53                if let Some(var_def) = var_defs.iter().find(|var_def| var_def.name == *var_name) {
54                    let expected_type = match (&var_def.default_value, &var_def.var_type) {
55                        (Some(_), Type::ListType(inner)) => Type::NonNullType(inner.clone()),
56                        (Some(default_value), Type::NamedType(_)) => {
57                            if let Value::Null = default_value {
58                                var_def.var_type.clone()
59                            } else {
60                                Type::NonNullType(Box::new(var_def.var_type.clone()))
61                            }
62                        }
63                        (_, t) => t.clone(),
64                    };
65
66                    if !visitor_context.schema.is_subtype(&expected_type, var_type) {
67                        user_context.report_error(ValidationError {
68                          error_code: self.error_code(),
69                            message: format!("Variable \"${}\" of type \"{}\" used in position expecting type \"{}\".",
70                                var_name,
71                                expected_type,
72                                var_type,
73                            ),
74                            locations: vec![var_def.position],
75                        });
76                    }
77                }
78            }
79        }
80
81        if let Some(spreads) = self.spreads.get(from) {
82            for spread in spreads {
83                self.collect_incorrect_usages(
84                    &Scope::Fragment(spread),
85                    var_defs,
86                    visitor_context,
87                    user_context,
88                    visited,
89                );
90            }
91        }
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96pub enum Scope<'a> {
97    Operation(Option<&'a str>),
98    Fragment(&'a str),
99}
100
101impl<'a> OperationVisitor<'a, ValidationErrorContext> for VariablesInAllowedPosition<'a> {
102    fn leave_document(
103        &mut self,
104        visitor_context: &mut OperationVisitorContext<'a>,
105        user_context: &mut ValidationErrorContext,
106        _: &crate::static_graphql::query::Document,
107    ) {
108        for (op_scope, var_defs) in &self.variable_defs {
109            self.collect_incorrect_usages(
110                op_scope,
111                var_defs,
112                visitor_context,
113                user_context,
114                &mut HashSet::new(),
115            );
116        }
117    }
118
119    fn enter_fragment_definition(
120        &mut self,
121        _: &mut OperationVisitorContext<'a>,
122        _: &mut ValidationErrorContext,
123        fragment_definition: &'a crate::static_graphql::query::FragmentDefinition,
124    ) {
125        self.current_scope = Some(Scope::Fragment(&fragment_definition.name));
126    }
127
128    fn enter_operation_definition(
129        &mut self,
130        _: &mut OperationVisitorContext<'a>,
131        _: &mut ValidationErrorContext,
132        operation_definition: &'a crate::static_graphql::query::OperationDefinition,
133    ) {
134        self.current_scope = Some(Scope::Operation(operation_definition.node_name()));
135    }
136
137    fn enter_fragment_spread(
138        &mut self,
139        _: &mut OperationVisitorContext<'a>,
140        _: &mut ValidationErrorContext,
141        fragment_spread: &'a crate::static_graphql::query::FragmentSpread,
142    ) {
143        if let Some(scope) = &self.current_scope {
144            self.spreads
145                .entry(scope.clone())
146                .or_default()
147                .insert(&fragment_spread.fragment_name);
148        }
149    }
150
151    fn enter_variable_definition(
152        &mut self,
153        _: &mut OperationVisitorContext<'a>,
154        _: &mut ValidationErrorContext,
155        variable_definition: &'a VariableDefinition,
156    ) {
157        if let Some(ref scope) = self.current_scope {
158            self.variable_defs
159                .entry(scope.clone())
160                .or_default()
161                .push(variable_definition);
162        }
163    }
164
165    fn enter_variable_value(
166        &mut self,
167        visitor_context: &mut OperationVisitorContext<'a>,
168        _: &mut ValidationErrorContext,
169        variable_name: &'a str,
170    ) {
171        if let (Some(scope), Some(input_type)) = (
172            &self.current_scope,
173            visitor_context.current_input_type_literal(),
174        ) {
175            self.variable_usages
176                .entry(scope.clone())
177                .or_default()
178                .push((variable_name, input_type));
179        }
180    }
181}
182
183impl<'v> ValidationRule for VariablesInAllowedPosition<'v> {
184    fn error_code<'a>(&self) -> &'a str {
185        "VariablesInAllowedPosition"
186    }
187
188    fn validate(
189        &self,
190        ctx: &mut OperationVisitorContext,
191        error_collector: &mut ValidationErrorContext,
192    ) {
193        visit_document(
194            &mut VariablesInAllowedPosition::new(),
195            ctx.operation,
196            ctx,
197            error_collector,
198        );
199    }
200}
201
202#[test]
203fn boolean_to_boolean() {
204    use crate::validation::test_utils::*;
205
206    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
207    let errors = test_operation_with_schema(
208        "query Query($booleanArg: Boolean)
209        {
210          complicatedArgs {
211            booleanArgField(booleanArg: $booleanArg)
212          }
213        }",
214        TEST_SCHEMA,
215        &mut plan,
216    );
217
218    assert_eq!(get_messages(&errors).len(), 0);
219}
220
221#[test]
222fn boolean_to_boolean_within_fragment() {
223    use crate::validation::test_utils::*;
224
225    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
226    let errors = test_operation_with_schema(
227        "fragment booleanArgFrag on ComplicatedArgs {
228          booleanArgField(booleanArg: $booleanArg)
229        }
230        query Query($booleanArg: Boolean)
231        {
232          complicatedArgs {
233            ...booleanArgFrag
234          }
235        }",
236        TEST_SCHEMA,
237        &mut plan,
238    );
239
240    assert_eq!(get_messages(&errors).len(), 0);
241
242    let errors = test_operation_with_schema(
243        "query Query($booleanArg: Boolean)
244      {
245        complicatedArgs {
246          ...booleanArgFrag
247        }
248      }
249      fragment booleanArgFrag on ComplicatedArgs {
250        booleanArgField(booleanArg: $booleanArg)
251      }",
252        TEST_SCHEMA,
253        &mut plan,
254    );
255
256    assert_eq!(get_messages(&errors).len(), 0);
257}
258
259#[test]
260fn boolean_nonnull_to_boolean() {
261    use crate::validation::test_utils::*;
262
263    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
264    let errors = test_operation_with_schema(
265        "query Query($nonNullBooleanArg: Boolean!)
266        {
267          complicatedArgs {
268            booleanArgField(booleanArg: $nonNullBooleanArg)
269          }
270        }",
271        TEST_SCHEMA,
272        &mut plan,
273    );
274
275    assert_eq!(get_messages(&errors).len(), 0);
276}
277
278#[test]
279fn string_list_to_string_list() {
280    use crate::validation::test_utils::*;
281
282    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
283    let errors = test_operation_with_schema(
284        "query Query($stringListVar: [String])
285        {
286          complicatedArgs {
287            stringListArgField(stringListArg: $stringListVar)
288          }
289        }",
290        TEST_SCHEMA,
291        &mut plan,
292    );
293
294    assert_eq!(get_messages(&errors).len(), 0);
295}
296
297#[test]
298fn string_list_nonnull_to_string_list() {
299    use crate::validation::test_utils::*;
300
301    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
302    let errors = test_operation_with_schema(
303        "query Query($stringListVar: [String!])
304        {
305          complicatedArgs {
306            stringListArgField(stringListArg: $stringListVar)
307          }
308        }",
309        TEST_SCHEMA,
310        &mut plan,
311    );
312
313    assert_eq!(get_messages(&errors).len(), 0);
314}
315
316#[test]
317fn string_to_string_list_in_item_position() {
318    use crate::validation::test_utils::*;
319
320    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
321    let errors = test_operation_with_schema(
322        "query Query($stringVar: String)
323        {
324          complicatedArgs {
325            stringListArgField(stringListArg: [$stringVar])
326          }
327        }",
328        TEST_SCHEMA,
329        &mut plan,
330    );
331
332    assert_eq!(get_messages(&errors).len(), 0);
333}
334
335#[test]
336fn string_nonnull_to_string_list_in_item_position() {
337    use crate::validation::test_utils::*;
338
339    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
340    let errors = test_operation_with_schema(
341        "query Query($stringVar: String!)
342        {
343          complicatedArgs {
344            stringListArgField(stringListArg: [$stringVar])
345          }
346        }",
347        TEST_SCHEMA,
348        &mut plan,
349    );
350
351    assert_eq!(get_messages(&errors).len(), 0);
352}
353
354#[test]
355fn complexinput_to_complexinput() {
356    use crate::validation::test_utils::*;
357
358    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
359    let errors = test_operation_with_schema(
360        "query Query($complexVar: ComplexInput)
361        {
362          complicatedArgs {
363            complexArgField(complexArg: $complexVar)
364          }
365        }",
366        TEST_SCHEMA,
367        &mut plan,
368    );
369
370    assert_eq!(get_messages(&errors).len(), 0);
371}
372
373#[test]
374fn complexinput_to_complexinput_in_field_position() {
375    use crate::validation::test_utils::*;
376
377    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
378    let errors = test_operation_with_schema(
379        "query Query($boolVar: Boolean = false)
380        {
381          complicatedArgs {
382            complexArgField(complexArg: { requiredArg: $boolVar })
383          }
384        }",
385        TEST_SCHEMA,
386        &mut plan,
387    );
388
389    let messages = get_messages(&errors);
390    assert_eq!(messages.len(), 0);
391}
392
393#[test]
394fn boolean_nonnull_to_boolean_nonnull_in_directive() {
395    use crate::validation::test_utils::*;
396
397    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
398    let errors = test_operation_with_schema(
399        "query Query($boolVar: Boolean!)
400        {
401          dog @include(if: $boolVar)
402        }",
403        TEST_SCHEMA,
404        &mut plan,
405    );
406
407    assert_eq!(get_messages(&errors).len(), 0);
408}
409
410#[test]
411fn int_to_int_nonnull() {
412    use crate::validation::test_utils::*;
413
414    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
415    let errors = test_operation_with_schema(
416        "query Query($intArg: Int) {
417          complicatedArgs {
418            nonNullIntArgField(nonNullIntArg: $intArg)
419          }
420        }",
421        TEST_SCHEMA,
422        &mut plan,
423    );
424
425    let messages = get_messages(&errors);
426    assert_eq!(messages.len(), 1);
427    assert_eq!(
428        messages,
429        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
430    )
431}
432
433#[test]
434fn int_to_int_nonnull_within_fragment() {
435    use crate::validation::test_utils::*;
436
437    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
438    let errors = test_operation_with_schema(
439        "fragment nonNullIntArgFieldFrag on ComplicatedArgs {
440          nonNullIntArgField(nonNullIntArg: $intArg)
441        }
442        query Query($intArg: Int) {
443          complicatedArgs {
444            ...nonNullIntArgFieldFrag
445          }
446        }",
447        TEST_SCHEMA,
448        &mut plan,
449    );
450
451    let messages = get_messages(&errors);
452    assert_eq!(messages.len(), 1);
453    assert_eq!(
454        messages,
455        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
456    )
457}
458
459#[test]
460fn int_to_int_nonnull_within_nested_fragment() {
461    use crate::validation::test_utils::*;
462
463    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
464    let errors = test_operation_with_schema(
465        "fragment outerFrag on ComplicatedArgs {
466          ...nonNullIntArgFieldFrag
467        }
468        fragment nonNullIntArgFieldFrag on ComplicatedArgs {
469          nonNullIntArgField(nonNullIntArg: $intArg)
470        }
471        query Query($intArg: Int) {
472          complicatedArgs {
473            ...outerFrag
474          }
475        }",
476        TEST_SCHEMA,
477        &mut plan,
478    );
479
480    let messages = get_messages(&errors);
481    assert_eq!(messages.len(), 1);
482    assert_eq!(
483        messages,
484        vec!["Variable \"$intArg\" of type \"Int\" used in position expecting type \"Int!\"."]
485    )
486}
487
488#[test]
489fn string_over_boolean() {
490    use crate::validation::test_utils::*;
491
492    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
493    let errors = test_operation_with_schema(
494        "query Query($stringVar: String) {
495          complicatedArgs {
496            booleanArgField(booleanArg: $stringVar)
497          }
498        }",
499        TEST_SCHEMA,
500        &mut plan,
501    );
502
503    let messages = get_messages(&errors);
504    assert_eq!(messages.len(), 1);
505    assert_eq!(
506        messages,
507        vec![
508      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"Boolean\"."
509    ]
510    )
511}
512
513#[test]
514fn string_over_string_list() {
515    use crate::validation::test_utils::*;
516
517    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
518    let errors = test_operation_with_schema(
519        "query Query($stringVar: String) {
520          complicatedArgs {
521            stringListArgField(stringListArg: $stringVar)
522          }
523        }",
524        TEST_SCHEMA,
525        &mut plan,
526    );
527
528    let messages = get_messages(&errors);
529    assert_eq!(messages.len(), 1);
530    assert_eq!(
531        messages,
532        vec![
533      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"[String]\"."
534    ]
535    )
536}
537
538#[test]
539fn boolean_to_boolean_nonnull_in_directive() {
540    use crate::validation::test_utils::*;
541
542    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
543    let errors = test_operation_with_schema(
544        "query Query($boolVar: Boolean) {
545          dog @include(if: $boolVar)
546        }",
547        TEST_SCHEMA,
548        &mut plan,
549    );
550
551    let messages = get_messages(&errors);
552    assert_eq!(messages.len(), 1);
553    assert_eq!(
554        messages,
555        vec![
556      "Variable \"$boolVar\" of type \"Boolean\" used in position expecting type \"Boolean!\"."
557    ]
558    )
559}
560
561#[test]
562fn string_to_boolean_nonnull_in_directive() {
563    use crate::validation::test_utils::*;
564
565    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
566    let errors = test_operation_with_schema(
567        "query Query($stringVar: String) {
568          dog @include(if: $stringVar)
569        }",
570        TEST_SCHEMA,
571        &mut plan,
572    );
573
574    let messages = get_messages(&errors);
575    assert_eq!(messages.len(), 1);
576    assert_eq!(
577        messages,
578        vec![
579      "Variable \"$stringVar\" of type \"String\" used in position expecting type \"Boolean!\"."
580    ]
581    )
582}
583
584#[test]
585fn string_list_to_string_nonnull_list() {
586    use crate::validation::test_utils::*;
587
588    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
589    let errors = test_operation_with_schema(
590        "query Query($stringListVar: [String])
591        {
592          complicatedArgs {
593            stringListNonNullArgField(stringListNonNullArg: $stringListVar)
594          }
595        }",
596        TEST_SCHEMA,
597        &mut plan,
598    );
599
600    let messages = get_messages(&errors);
601    assert_eq!(messages.len(), 1);
602    assert_eq!(messages, vec![
603      "Variable \"$stringListVar\" of type \"[String]\" used in position expecting type \"[String!]\"."
604    ])
605}
606
607#[test]
608fn int_to_int_non_null_with_null_default_value() {
609    use crate::validation::test_utils::*;
610
611    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
612    let errors = test_operation_with_schema(
613        "query Query($intVar: Int = null) {
614          complicatedArgs {
615            nonNullIntArgField(nonNullIntArg: $intVar)
616          }
617        }",
618        TEST_SCHEMA,
619        &mut plan,
620    );
621
622    let messages = get_messages(&errors);
623    assert_eq!(messages.len(), 1);
624    assert_eq!(
625        messages,
626        vec!["Variable \"$intVar\" of type \"Int\" used in position expecting type \"Int!\"."]
627    )
628}
629
630#[test]
631fn int_to_int_non_null_with_default_value() {
632    use crate::validation::test_utils::*;
633
634    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
635    let errors = test_operation_with_schema(
636        "query Query($intVar: Int = 1) {
637          complicatedArgs {
638            nonNullIntArgField(nonNullIntArg: $intVar)
639          }
640        }",
641        TEST_SCHEMA,
642        &mut plan,
643    );
644
645    let messages = get_messages(&errors);
646    assert_eq!(messages.len(), 0);
647}
648
649#[test]
650fn int_to_int_non_null_where_argument_with_default_value() {
651    use crate::validation::test_utils::*;
652
653    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
654    let errors = test_operation_with_schema(
655        "query Query($intVar: Int) {
656          complicatedArgs {
657            nonNullFieldWithDefault(nonNullIntArg: $intVar)
658          }
659        }",
660        TEST_SCHEMA,
661        &mut plan,
662    );
663
664    let messages = get_messages(&errors);
665    assert_eq!(messages.len(), 0);
666}
667
668#[test]
669fn boolean_to_boolean_non_null_with_default_value() {
670    use crate::validation::test_utils::*;
671
672    let mut plan = create_plan_from_rule(Box::new(VariablesInAllowedPosition::new()));
673    let errors = test_operation_with_schema(
674        "query Query($boolVar: Boolean = false) {
675          dog @include(if: $boolVar)
676        }",
677        TEST_SCHEMA,
678        &mut plan,
679    );
680
681    let messages = get_messages(&errors);
682    assert_eq!(messages.len(), 0);
683}