graphql_tools/validation/rules/
known_directives.rs

1use super::ValidationRule;
2use crate::ast::{visit_document, OperationVisitor, OperationVisitorContext};
3use crate::static_graphql::query::{
4    Directive, Field, FragmentDefinition, InlineFragment, OperationDefinition,
5};
6use crate::static_graphql::schema::DirectiveLocation;
7use crate::validation::utils::{ValidationError, ValidationErrorContext};
8
9/// Known Directives
10///
11/// A GraphQL document is only valid if all `@directives` are known by the
12/// schema and legally positioned.
13///
14/// See https://spec.graphql.org/draft/#sec-Directives-Are-Defined
15pub struct KnownDirectives {
16    recent_location: Option<DirectiveLocation>,
17}
18
19impl Default for KnownDirectives {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl KnownDirectives {
26    pub fn new() -> Self {
27        KnownDirectives {
28            recent_location: None,
29        }
30    }
31}
32
33impl<'a> OperationVisitor<'a, ValidationErrorContext> for KnownDirectives {
34    fn enter_operation_definition(
35        &mut self,
36        _: &mut OperationVisitorContext<'a>,
37        _: &mut ValidationErrorContext,
38        operation_definition: &crate::static_graphql::query::OperationDefinition,
39    ) {
40        self.recent_location = Some(match operation_definition {
41            OperationDefinition::Mutation(_) => DirectiveLocation::Mutation,
42            OperationDefinition::Query(_) => DirectiveLocation::Query,
43            OperationDefinition::SelectionSet(_) => DirectiveLocation::Query,
44            OperationDefinition::Subscription(_) => DirectiveLocation::Subscription,
45        })
46    }
47
48    fn leave_operation_definition(
49        &mut self,
50        _: &mut OperationVisitorContext<'a>,
51        _: &mut ValidationErrorContext,
52        _: &OperationDefinition,
53    ) {
54        self.recent_location = None;
55    }
56
57    fn enter_field(
58        &mut self,
59        _: &mut OperationVisitorContext<'a>,
60        _: &mut ValidationErrorContext,
61        _: &Field,
62    ) {
63        self.recent_location = Some(DirectiveLocation::Field);
64    }
65
66    fn leave_field(
67        &mut self,
68        _: &mut OperationVisitorContext<'a>,
69        _: &mut ValidationErrorContext,
70        _: &Field,
71    ) {
72        self.recent_location = None;
73    }
74
75    fn enter_fragment_definition(
76        &mut self,
77        _: &mut OperationVisitorContext<'a>,
78        _: &mut ValidationErrorContext,
79        _: &FragmentDefinition,
80    ) {
81        self.recent_location = Some(DirectiveLocation::FragmentDefinition);
82    }
83
84    fn leave_fragment_definition(
85        &mut self,
86        _: &mut OperationVisitorContext<'a>,
87        _: &mut ValidationErrorContext,
88        _: &FragmentDefinition,
89    ) {
90        self.recent_location = None;
91    }
92
93    fn enter_fragment_spread(
94        &mut self,
95        _: &mut OperationVisitorContext<'a>,
96        _: &mut ValidationErrorContext,
97        _: &crate::static_graphql::query::FragmentSpread,
98    ) {
99        self.recent_location = Some(DirectiveLocation::FragmentSpread);
100    }
101
102    fn leave_fragment_spread(
103        &mut self,
104        _: &mut OperationVisitorContext<'a>,
105        _: &mut ValidationErrorContext,
106        _: &crate::static_graphql::query::FragmentSpread,
107    ) {
108        self.recent_location = None;
109    }
110
111    fn enter_inline_fragment(
112        &mut self,
113        _: &mut OperationVisitorContext<'a>,
114        _: &mut ValidationErrorContext,
115        _: &InlineFragment,
116    ) {
117        self.recent_location = Some(DirectiveLocation::InlineFragment);
118    }
119
120    fn leave_inline_fragment(
121        &mut self,
122        _: &mut OperationVisitorContext<'a>,
123        _: &mut ValidationErrorContext,
124        _: &InlineFragment,
125    ) {
126        self.recent_location = None;
127    }
128
129    fn enter_directive(
130        &mut self,
131        visitor_context: &mut OperationVisitorContext<'a>,
132        user_context: &mut ValidationErrorContext,
133        directive: &Directive,
134    ) {
135        if let Some(directive_type) = visitor_context.directives.get(&directive.name) {
136            if let Some(current_location) = &self.recent_location {
137                if !directive_type
138                    .locations
139                    .iter()
140                    .any(|l| l == current_location)
141                {
142                    user_context.report_error(ValidationError {
143                        error_code: self.error_code(),
144                        locations: vec![directive.position],
145                        message: format!(
146                            "Directive \"@{}\" may not be used on {}",
147                            directive.name,
148                            current_location.as_str()
149                        ),
150                    });
151                }
152            }
153        } else {
154            user_context.report_error(ValidationError {
155                error_code: self.error_code(),
156                locations: vec![directive.position],
157                message: format!("Unknown directive \"@{}\".", directive.name),
158            });
159        }
160    }
161}
162
163impl ValidationRule for KnownDirectives {
164    fn error_code<'a>(&self) -> &'a str {
165        "KnownDirectives"
166    }
167
168    fn validate(
169        &self,
170        ctx: &mut OperationVisitorContext,
171        error_collector: &mut ValidationErrorContext,
172    ) {
173        visit_document(
174            &mut KnownDirectives::new(),
175            ctx.operation,
176            ctx,
177            error_collector,
178        );
179    }
180}
181
182#[test]
183fn no_directives() {
184    use crate::validation::test_utils::*;
185
186    let mut plan = create_plan_from_rule(Box::new(KnownDirectives::new()));
187    let errors = test_operation_with_schema(
188        "query Foo {
189          name
190          ...Frag
191        }
192  
193        fragment Frag on Dog {
194          name
195        }",
196        TEST_SCHEMA,
197        &mut plan,
198    );
199
200    assert_eq!(get_messages(&errors).len(), 0);
201}
202
203#[test]
204fn standard_directives() {
205    use crate::validation::test_utils::*;
206
207    let mut plan = create_plan_from_rule(Box::new(KnownDirectives::new()));
208    let errors = test_operation_with_schema(
209        "{
210          human @skip(if: false) {
211            name
212            pets {
213              ... on Dog @include(if: true) {
214                name
215              }
216            }
217          }
218        }",
219        TEST_SCHEMA,
220        &mut plan,
221    );
222
223    assert_eq!(get_messages(&errors).len(), 0);
224}
225
226#[test]
227fn unknown_directive() {
228    use crate::validation::test_utils::*;
229
230    let mut plan = create_plan_from_rule(Box::new(KnownDirectives::new()));
231    let errors = test_operation_with_schema(
232        "{
233          human @unknown(directive: \"value\") {
234            name
235          }
236        }",
237        TEST_SCHEMA,
238        &mut plan,
239    );
240
241    assert_eq!(get_messages(&errors).len(), 1);
242}
243
244#[test]
245fn many_unknown_directives() {
246    use crate::validation::test_utils::*;
247
248    let mut plan = create_plan_from_rule(Box::new(KnownDirectives::new()));
249    let errors = test_operation_with_schema(
250        "{
251          __typename @unknown
252          human @unknown {
253            name
254            pets @unknown {
255              name
256            }
257          }
258        }",
259        TEST_SCHEMA,
260        &mut plan,
261    );
262
263    assert_eq!(get_messages(&errors).len(), 3);
264}
265
266#[test]
267fn well_placed_directives() {
268    use crate::validation::test_utils::*;
269
270    let mut plan = create_plan_from_rule(Box::new(KnownDirectives::new()));
271    let errors = test_operation_with_schema(
272        "
273        # TODO: update once this is released https://github.com/graphql-rust/graphql-parser/issues/60 query ($var: Boolean @onVariableDefinition)
274        query ($var: Boolean) @onQuery {
275          human @onField {
276            ...Frag @onFragmentSpread
277            ... @onInlineFragment {
278              name @onField
279            }
280          }
281        }
282  
283        mutation @onMutation {
284          someField @onField
285        }
286  
287        subscription @onSubscription {
288          someField @onField
289        }
290  
291        fragment Frag on Human @onFragmentDefinition {
292          name @onField
293        }",
294        TEST_SCHEMA,
295        &mut plan,
296    );
297
298    assert_eq!(get_messages(&errors).len(), 0);
299}
300
301#[test]
302fn misplaced_directives() {
303    use crate::validation::test_utils::*;
304
305    let mut plan = create_plan_from_rule(Box::new(KnownDirectives::new()));
306    let errors = test_operation_with_schema(
307        "  query ($var: Boolean) @onMutation {
308      human @onQuery {
309        ...Frag @onQuery
310        ... @onQuery {
311          name @onQuery
312        }
313      }
314    }
315
316    mutation @onQuery {
317      someField @onQuery
318    }
319
320    subscription @onQuery {
321      someField @onQuery
322    }
323
324    fragment Frag on Human @onQuery {
325      name @onQuery
326    }",
327        TEST_SCHEMA,
328        &mut plan,
329    );
330
331    assert_eq!(get_messages(&errors).len(), 11);
332}