Skip to content

Commit b6bb960

Browse files
authored
feat: add unique variable names rule (#26)
1 parent 4c546ce commit b6bb960

File tree

4 files changed

+102
-4
lines changed

4 files changed

+102
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ cargo add graphql-tools
5656
- [x] NoUnusedFragments
5757
- [x] PossibleFragmentSpreads
5858
- [x] NoFragmentCycles
59-
- [ ] UniqueVariableNames
59+
- [x] UniqueVariableNames
6060
- [x] NoUndefinedVariables
6161
- [x] NoUnusedVariables
6262
- [ ] KnownDirectives

src/validation/rules/defaults.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use crate::validation::validate::ValidationPlan;
33
use super::{
44
FieldsOnCorrectType, FragmentsOnCompositeTypes, KnownArgumentNames, KnownFragmentNames,
55
KnownTypeNames, LeafFieldSelections, LoneAnonymousOperation, NoFragmentsCycle,
6-
NoUndefinedVariables, NoUnusedFragments, OverlappingFieldsCanBeMerged, PossibleFragmentSpreads,
7-
ProvidedRequiredArguments, SingleFieldSubscriptions, UniqueArgumentNames, UniqueFragmentNames,
8-
UniqueOperationNames, VariablesAreInputTypes, NoUnusedVariables,
6+
NoUndefinedVariables, NoUnusedFragments, NoUnusedVariables, OverlappingFieldsCanBeMerged,
7+
PossibleFragmentSpreads, ProvidedRequiredArguments, SingleFieldSubscriptions,
8+
UniqueArgumentNames, UniqueFragmentNames, UniqueOperationNames, UniqueVariableNames,
9+
VariablesAreInputTypes,
910
};
1011

1112
pub fn default_rules_validation_plan() -> ValidationPlan {
@@ -29,6 +30,7 @@ pub fn default_rules_validation_plan() -> ValidationPlan {
2930
plan.add_rule(Box::new(NoUndefinedVariables {}));
3031
plan.add_rule(Box::new(KnownArgumentNames {}));
3132
plan.add_rule(Box::new(UniqueArgumentNames {}));
33+
plan.add_rule(Box::new(UniqueVariableNames {}));
3234
plan.add_rule(Box::new(ProvidedRequiredArguments {}));
3335

3436
plan

src/validation/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod single_field_subscriptions;
1919
pub mod unique_argument_names;
2020
pub mod unique_fragment_names;
2121
pub mod unique_operation_names;
22+
pub mod unique_variable_names;
2223
pub mod variables_are_input_types;
2324

2425
pub use self::defaults::*;
@@ -41,4 +42,5 @@ pub use self::single_field_subscriptions::*;
4142
pub use self::unique_argument_names::*;
4243
pub use self::unique_fragment_names::*;
4344
pub use self::unique_operation_names::*;
45+
pub use self::unique_variable_names::*;
4446
pub use self::variables_are_input_types::*;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use std::collections::HashSet;
2+
3+
use super::ValidationRule;
4+
use crate::static_graphql::query::*;
5+
use crate::validation::utils::{ValidationError, ValidationErrorContext};
6+
use crate::{
7+
ast::{ext::AstWithVariables, QueryVisitor},
8+
validation::utils::ValidationContext,
9+
};
10+
11+
/// Unique variable names
12+
///
13+
/// A GraphQL operation is only valid if all its variables are uniquely named.
14+
///
15+
/// See https://spec.graphql.org/draft/#sec-Variable-Uniqueness
16+
pub struct UniqueVariableNames;
17+
18+
struct UniqueVariableNamesHelper<'a> {
19+
error_context: ValidationErrorContext<'a>,
20+
}
21+
22+
impl<'a> UniqueVariableNamesHelper<'a> {
23+
fn new(validation_context: &'a ValidationContext<'a>) -> Self {
24+
UniqueVariableNamesHelper {
25+
error_context: ValidationErrorContext::new(validation_context),
26+
}
27+
}
28+
}
29+
30+
impl<'a> QueryVisitor<UniqueVariableNamesHelper<'a>> for UniqueVariableNames {
31+
fn leave_operation_definition(
32+
&self,
33+
node: &OperationDefinition,
34+
visitor_context: &mut UniqueVariableNamesHelper<'a>,
35+
) {
36+
let variables = node.get_variables();
37+
38+
let mut seen_variables: HashSet<String> = HashSet::new();
39+
40+
variables.iter().for_each(|var| {
41+
if seen_variables.contains(&var.name) {
42+
visitor_context.error_context.report_error(ValidationError {
43+
locations: vec![],
44+
message: format!("There can only be one variable named \"${}\".", var.name),
45+
});
46+
} else {
47+
seen_variables.insert(var.name.clone());
48+
}
49+
})
50+
}
51+
}
52+
53+
impl ValidationRule for UniqueVariableNames {
54+
fn validate<'a>(&self, ctx: &ValidationContext) -> Vec<ValidationError> {
55+
let mut helper = UniqueVariableNamesHelper::new(&ctx);
56+
self.visit_document(&ctx.operation.clone(), &mut helper);
57+
58+
helper.error_context.errors
59+
}
60+
}
61+
62+
#[test]
63+
fn unique_variable_names() {
64+
use crate::validation::test_utils::*;
65+
66+
let mut plan = create_plan_from_rule(Box::new(UniqueVariableNames {}));
67+
let errors = test_operation_without_schema(
68+
"query A($x: Int, $y: String) { __typename }
69+
query B($x: String, $y: Int) { __typename }",
70+
&mut plan,
71+
);
72+
73+
assert_eq!(get_messages(&errors).len(), 0);
74+
}
75+
76+
#[test]
77+
fn duplicate_variable_names() {
78+
use crate::validation::test_utils::*;
79+
80+
let mut plan = create_plan_from_rule(Box::new(UniqueVariableNames {}));
81+
let errors = test_operation_without_schema(
82+
"query A($x: Int, $x: Int, $x: String) { __typename }
83+
query B($y: String, $y: Int) { __typename }
84+
query C($z: Int, $z: Int) { __typename }",
85+
&mut plan,
86+
);
87+
88+
let messages = get_messages(&errors);
89+
90+
assert_eq!(messages.len(), 4);
91+
assert!(messages.contains(&&"There can only be one variable named \"$x\".".to_owned()));
92+
assert!(messages.contains(&&"There can only be one variable named \"$y\".".to_owned()));
93+
assert!(messages.contains(&&"There can only be one variable named \"$z\".".to_owned()));
94+
}

0 commit comments

Comments
 (0)