Skip to content

Commit b7a61a5

Browse files
saihajdotansimha
andauthored
feat: UniqueDirectivesPerLocation (#30)
Co-authored-by: Dotan Simha <[email protected]>
1 parent 63106e3 commit b7a61a5

File tree

5 files changed

+351
-31
lines changed

5 files changed

+351
-31
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ cargo add graphql-tools
6060
- [x] NoUndefinedVariables
6161
- [x] NoUnusedVariables
6262
- [x] KnownDirectives
63-
- [ ] UniqueDirectivesPerLocation
63+
- [x] UniqueDirectivesPerLocation
6464
- [x] KnownArgumentNames
6565
- [x] UniqueArgumentNames
6666
- [x] ValuesOfCorrectType

src/validation/rules/defaults.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use super::{
55
KnownFragmentNames, KnownTypeNames, LeafFieldSelections, LoneAnonymousOperation,
66
NoFragmentsCycle, NoUndefinedVariables, NoUnusedFragments, NoUnusedVariables,
77
OverlappingFieldsCanBeMerged, PossibleFragmentSpreads, ProvidedRequiredArguments,
8-
SingleFieldSubscriptions, UniqueArgumentNames, UniqueFragmentNames, UniqueOperationNames,
9-
UniqueVariableNames, ValuesOfCorrectType, VariablesAreInputTypes, VariablesInAllowedPosition,
8+
SingleFieldSubscriptions, UniqueArgumentNames, UniqueDirectivesPerLocation,
9+
UniqueFragmentNames, UniqueOperationNames, UniqueVariableNames, ValuesOfCorrectType,
10+
VariablesAreInputTypes, VariablesInAllowedPosition,
1011
};
1112

1213
pub fn default_rules_validation_plan() -> ValidationPlan {
@@ -35,6 +36,7 @@ pub fn default_rules_validation_plan() -> ValidationPlan {
3536
plan.add_rule(Box::new(KnownDirectives::new()));
3637
plan.add_rule(Box::new(VariablesInAllowedPosition::new()));
3738
plan.add_rule(Box::new(ValuesOfCorrectType::new()));
39+
plan.add_rule(Box::new(UniqueDirectivesPerLocation::new()));
3840

3941
plan
4042
}

src/validation/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod possible_fragment_spreads;
1818
pub mod provided_required_arguments;
1919
pub mod single_field_subscriptions;
2020
pub mod unique_argument_names;
21+
pub mod unique_directives_per_location;
2122
pub mod unique_fragment_names;
2223
pub mod unique_operation_names;
2324
pub mod unique_variable_names;
@@ -45,6 +46,7 @@ pub use self::possible_fragment_spreads::*;
4546
pub use self::provided_required_arguments::*;
4647
pub use self::single_field_subscriptions::*;
4748
pub use self::unique_argument_names::*;
49+
pub use self::unique_directives_per_location::*;
4850
pub use self::unique_fragment_names::*;
4951
pub use self::unique_operation_names::*;
5052
pub use self::unique_variable_names::*;
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
use std::collections::HashSet;
2+
3+
use super::ValidationRule;
4+
use crate::ast::OperationDefinitionExtension;
5+
use crate::static_graphql::query::{
6+
Directive, Field, FragmentDefinition, FragmentSpread, InlineFragment, OperationDefinition,
7+
};
8+
use crate::{
9+
ast::{visit_document, OperationVisitor, OperationVisitorContext},
10+
validation::utils::{ValidationError, ValidationErrorContext},
11+
};
12+
13+
/// Unique directive names per location
14+
///
15+
/// A GraphQL document is only valid if all non-repeatable directives at
16+
/// a given location are uniquely named.
17+
///
18+
/// See https://spec.graphql.org/draft/#sec-Directives-Are-Unique-Per-Location
19+
pub struct UniqueDirectivesPerLocation {}
20+
21+
impl UniqueDirectivesPerLocation {
22+
pub fn new() -> Self {
23+
UniqueDirectivesPerLocation {}
24+
}
25+
26+
pub fn check_duplicate_directive(
27+
&self,
28+
ctx: &mut OperationVisitorContext,
29+
err_context: &mut ValidationErrorContext,
30+
directives: &[Directive],
31+
) {
32+
let mut exists = HashSet::new();
33+
34+
for directive in directives {
35+
if let Some(meta_directive) = ctx.directives.get(&directive.name) {
36+
if !meta_directive.repeatable {
37+
if exists.contains(&directive.name) {
38+
err_context.report_error(ValidationError {
39+
locations: vec![directive.position],
40+
message: format!("Duplicate directive \"{}\"", &directive.name),
41+
});
42+
43+
continue;
44+
}
45+
46+
exists.insert(directive.name.clone());
47+
}
48+
}
49+
}
50+
}
51+
}
52+
53+
impl<'a> OperationVisitor<'a, ValidationErrorContext> for UniqueDirectivesPerLocation {
54+
fn enter_operation_definition(
55+
&mut self,
56+
ctx: &mut OperationVisitorContext<'a>,
57+
err_ctx: &mut ValidationErrorContext,
58+
operation: &OperationDefinition,
59+
) {
60+
self.check_duplicate_directive(ctx, err_ctx, operation.directives());
61+
}
62+
63+
fn enter_field(
64+
&mut self,
65+
ctx: &mut OperationVisitorContext<'a>,
66+
err_ctx: &mut ValidationErrorContext,
67+
field: &Field,
68+
) {
69+
self.check_duplicate_directive(ctx, err_ctx, &field.directives);
70+
}
71+
72+
fn enter_fragment_definition(
73+
&mut self,
74+
ctx: &mut OperationVisitorContext<'a>,
75+
err_ctx: &mut ValidationErrorContext,
76+
fragment: &FragmentDefinition,
77+
) {
78+
self.check_duplicate_directive(ctx, err_ctx, &fragment.directives);
79+
}
80+
81+
fn enter_fragment_spread(
82+
&mut self,
83+
ctx: &mut OperationVisitorContext<'a>,
84+
err_ctx: &mut ValidationErrorContext,
85+
fragment_spread: &FragmentSpread,
86+
) {
87+
self.check_duplicate_directive(ctx, err_ctx, &fragment_spread.directives)
88+
}
89+
90+
fn enter_inline_fragment(
91+
&mut self,
92+
ctx: &mut OperationVisitorContext<'a>,
93+
err_ctx: &mut ValidationErrorContext,
94+
inline_fragment: &InlineFragment,
95+
) {
96+
self.check_duplicate_directive(ctx, err_ctx, &inline_fragment.directives)
97+
}
98+
}
99+
100+
impl ValidationRule for UniqueDirectivesPerLocation {
101+
fn validate<'a>(
102+
&self,
103+
ctx: &'a mut OperationVisitorContext,
104+
error_collector: &mut ValidationErrorContext,
105+
) {
106+
visit_document(
107+
&mut UniqueDirectivesPerLocation::new(),
108+
&ctx.operation,
109+
ctx,
110+
error_collector,
111+
);
112+
}
113+
}
114+
115+
#[test]
116+
fn no_directives() {
117+
use crate::validation::test_utils::*;
118+
119+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
120+
let errors = test_operation_with_schema(
121+
"fragment Test on Type {
122+
field
123+
}",
124+
&TEST_SCHEMA,
125+
&mut plan,
126+
);
127+
128+
assert_eq!(get_messages(&errors).len(), 0);
129+
}
130+
131+
#[test]
132+
fn unique_directives_in_different_locations() {
133+
use crate::validation::test_utils::*;
134+
135+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
136+
let errors = test_operation_with_schema(
137+
"fragment Test on Type @directiveA {
138+
field @directiveB
139+
}",
140+
&TEST_SCHEMA,
141+
&mut plan,
142+
);
143+
144+
assert_eq!(get_messages(&errors).len(), 0);
145+
}
146+
147+
#[test]
148+
fn unique_directives_in_same_location() {
149+
use crate::validation::test_utils::*;
150+
151+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
152+
let errors = test_operation_with_schema(
153+
"fragment Test on Type @directiveA @directiveB {
154+
field @directiveA @directiveB
155+
}",
156+
&TEST_SCHEMA,
157+
&mut plan,
158+
);
159+
160+
assert_eq!(get_messages(&errors).len(), 0);
161+
}
162+
163+
#[test]
164+
fn same_directives_in_different_locations() {
165+
use crate::validation::test_utils::*;
166+
167+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
168+
let errors = test_operation_with_schema(
169+
"fragment Test on Type @directiveA {
170+
field @directiveA
171+
}",
172+
&TEST_SCHEMA,
173+
&mut plan,
174+
);
175+
176+
assert_eq!(get_messages(&errors).len(), 0);
177+
}
178+
179+
#[test]
180+
fn same_directives_in_similar_locations() {
181+
use crate::validation::test_utils::*;
182+
183+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
184+
let errors = test_operation_with_schema(
185+
"fragment Test on Type {
186+
field @directive
187+
field @directive
188+
}",
189+
&TEST_SCHEMA,
190+
&mut plan,
191+
);
192+
193+
assert_eq!(get_messages(&errors).len(), 0);
194+
}
195+
196+
#[test]
197+
fn repeatable_directives_in_same_location() {
198+
use crate::validation::test_utils::*;
199+
200+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
201+
let errors = test_operation_with_schema(
202+
"fragment Test on Type @repeatable @repeatable {
203+
field @repeatable @repeatable
204+
}",
205+
&TEST_SCHEMA,
206+
&mut plan,
207+
);
208+
209+
assert_eq!(get_messages(&errors).len(), 0);
210+
}
211+
212+
#[test]
213+
fn unknown_directives_must_be_ignored() {
214+
use crate::validation::test_utils::*;
215+
216+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
217+
let errors = test_operation_with_schema(
218+
"fragment Test on Type @repeatable @repeatable {
219+
field @repeatable @repeatable
220+
}",
221+
&TEST_SCHEMA,
222+
&mut plan,
223+
);
224+
225+
assert_eq!(get_messages(&errors).len(), 0);
226+
}
227+
228+
#[test]
229+
fn duplicate_directives_in_one_location() {
230+
use crate::validation::test_utils::*;
231+
232+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
233+
let errors = test_operation_with_schema(
234+
"fragment Test on Type {
235+
field @onField @onField
236+
}",
237+
&TEST_SCHEMA,
238+
&mut plan,
239+
);
240+
let messages = get_messages(&errors);
241+
assert_eq!(messages.len(), 1);
242+
assert_eq!(messages, vec!["Duplicate directive \"onField\""])
243+
}
244+
245+
#[test]
246+
fn many_duplicate_directives_in_one_location() {
247+
use crate::validation::test_utils::*;
248+
249+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
250+
let errors = test_operation_with_schema(
251+
"fragment Test on Type {
252+
field @onField @onField @onField
253+
}",
254+
&TEST_SCHEMA,
255+
&mut plan,
256+
);
257+
let messages = get_messages(&errors);
258+
assert_eq!(messages.len(), 2);
259+
assert_eq!(
260+
messages,
261+
vec![
262+
"Duplicate directive \"onField\"",
263+
"Duplicate directive \"onField\""
264+
]
265+
)
266+
}
267+
268+
#[test]
269+
fn different_duplicate_directives_in_one_location() {
270+
use crate::validation::test_utils::*;
271+
272+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
273+
let errors = test_operation_with_schema(
274+
"fragment Test on Type {
275+
field @onField @testDirective @onField @testDirective
276+
}",
277+
&TEST_SCHEMA,
278+
&mut plan,
279+
);
280+
let messages = get_messages(&errors);
281+
assert_eq!(messages.len(), 2);
282+
assert_eq!(
283+
messages,
284+
vec![
285+
"Duplicate directive \"onField\"",
286+
"Duplicate directive \"testDirective\""
287+
]
288+
)
289+
}
290+
291+
#[test]
292+
fn duplicate_directives_in_many_location() {
293+
use crate::validation::test_utils::*;
294+
295+
let mut plan = create_plan_from_rule(Box::new(UniqueDirectivesPerLocation::new()));
296+
let errors = test_operation_with_schema(
297+
"fragment Test on Type @onFragmentDefinition @onFragmentDefinition {
298+
field @onField @onField
299+
}",
300+
&TEST_SCHEMA,
301+
&mut plan,
302+
);
303+
let messages = get_messages(&errors);
304+
assert_eq!(messages.len(), 2);
305+
assert_eq!(
306+
messages,
307+
vec![
308+
"Duplicate directive \"onFragmentDefinition\"",
309+
"Duplicate directive \"onField\""
310+
]
311+
)
312+
}

0 commit comments

Comments
 (0)