Skip to main content

bluejay_validator/executable/operation/analyzers/
query_depth.rs

1use crate::executable::{
2    operation::{Analyzer, VariableValues, Visitor},
3    Cache,
4};
5use bluejay_core::definition::{SchemaDefinition, TypeDefinitionReference};
6use bluejay_core::executable::ExecutableDocument;
7use std::cmp::max;
8
9pub struct QueryDepth {
10    current_depth: usize,
11    max_depth: usize,
12}
13
14impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Visitor<'a, E, S, VV>
15    for QueryDepth
16{
17    type ExtraInfo = ();
18
19    fn new(
20        _: &'a E::OperationDefinition,
21        _s: &'a S,
22        _: &'a VV,
23        _: &'a Cache<'a, E, S>,
24        _: Self::ExtraInfo,
25    ) -> Self {
26        Self {
27            current_depth: 0,
28            max_depth: 0,
29        }
30    }
31
32    fn visit_field(
33        &mut self,
34        _field: &'a <E as ExecutableDocument>::Field,
35        _field_definition: &'a S::FieldDefinition,
36        _scoped_type: TypeDefinitionReference<'a, S::TypeDefinition>,
37        included: bool,
38    ) {
39        if included {
40            self.current_depth += 1;
41            self.max_depth = max(self.max_depth, self.current_depth);
42        }
43    }
44
45    fn leave_field(
46        &mut self,
47        _field: &'a <E as ExecutableDocument>::Field,
48        _field_definition: &'a S::FieldDefinition,
49        _scoped_type: TypeDefinitionReference<'a, S::TypeDefinition>,
50        included: bool,
51    ) {
52        if included {
53            self.current_depth -= 1;
54        }
55    }
56}
57
58impl<E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Analyzer<'_, E, S, VV>
59    for QueryDepth
60{
61    type Output = usize;
62
63    fn into_output(self) -> Self::Output {
64        self.max_depth
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::QueryDepth;
71    use crate::executable::{operation::Orchestrator, Cache};
72    use bluejay_parser::ast::{
73        definition::{
74            DefaultContext, DefinitionDocument, SchemaDefinition as ParserSchemaDefinition,
75        },
76        executable::ExecutableDocument as ParserExecutableDocument,
77        Parse,
78    };
79    use serde_json::{Map as JsonMap, Value as JsonValue};
80
81    type DepthAnalyzer<'a, E, S> = Orchestrator<'a, E, S, JsonMap<String, JsonValue>, QueryDepth>;
82
83    const TEST_SCHEMA: &str = r#"
84        type Query {
85          node: Node
86          thing: Thing!
87          ping: String!
88        }
89        interface Node {
90          id: ID!
91        }
92        type Product implements Node {
93            id: ID!
94            title: String!
95            things: [Thing]!
96        }
97        type Thing implements Node {
98          id: ID!
99          title: String!
100          parent: Thing!
101        }
102        schema {
103          query: Query
104        }
105    "#;
106
107    fn check_depth(
108        source: &str,
109        operation_name: Option<&str>,
110        variables: JsonValue,
111        expected_depth: usize,
112    ) {
113        let definition_document: DefinitionDocument<'_, DefaultContext> =
114            DefinitionDocument::parse(TEST_SCHEMA)
115                .result
116                .expect("Schema had parse errors");
117        let schema_definition =
118            ParserSchemaDefinition::try_from(&definition_document).expect("Schema had errors");
119        let executable_document = ParserExecutableDocument::parse(source)
120            .result
121            .unwrap_or_else(|_| panic!("Document had parse errors"));
122        let cache = Cache::new(&executable_document, &schema_definition);
123        let variables = variables.as_object().expect("Variables must be an object");
124        let depth = DepthAnalyzer::analyze(
125            &executable_document,
126            &schema_definition,
127            operation_name,
128            variables,
129            &cache,
130            (),
131        )
132        .unwrap();
133
134        assert_eq!(depth, expected_depth);
135    }
136
137    #[test]
138    fn basic_depth_metrics() {
139        check_depth(r#"{ ping }"#, None, serde_json::json!({}), 1);
140        check_depth(r#"{ thing { id } }"#, None, serde_json::json!({}), 2);
141        check_depth(
142            r#"{ thing { parent { id } } }"#,
143            None,
144            serde_json::json!({}),
145            3,
146        );
147        check_depth(
148            r#"{
149                thing { parent { parent { id } } }
150                thing { parent { id } }
151            }"#,
152            None,
153            serde_json::json!({}),
154            4,
155        );
156    }
157
158    #[test]
159    fn depth_with_operation_context() {
160        check_depth(
161            r#"
162            query D2{
163                thing { title }
164            }
165            query D4 {
166                d4: thing { parent { parent { id } } }
167            }"#,
168            Some("D2"),
169            serde_json::json!({}),
170            2,
171        );
172    }
173
174    #[test]
175    fn depth_with_inline_fragments() {
176        check_depth(
177            r#"{
178                node {
179                    ...on Product { title }
180                    ...on Thing { parent { title } }
181                }
182            }"#,
183            None,
184            serde_json::json!({}),
185            3,
186        );
187    }
188
189    #[test]
190    fn depth_with_fragment_spreads() {
191        check_depth(
192            r#"{
193                node {
194                    ...ProductAttrs
195                    ...ThingAttrs
196                }
197            }
198            fragment ProductAttrs on Product {
199                title
200            }
201            fragment ThingAttrs on Thing {
202                parent { title }
203            }
204            "#,
205            None,
206            serde_json::json!({}),
207            3,
208        );
209    }
210
211    #[test]
212    fn depth_with_const_skip_include_fields() {
213        check_depth(
214            r#"{
215                d1: ping
216                d2: thing @skip(if: false) { title }
217                d4: thing @skip(if: true) { parent { parent { id } } }
218            }"#,
219            None,
220            serde_json::json!({}),
221            2,
222        );
223
224        check_depth(
225            r#"{
226                d1: ping
227                d2: thing @include(if: true) { title }
228                d4: thing @include(if: false) { parent { parent { id } } }
229            }"#,
230            None,
231            serde_json::json!({}),
232            2,
233        );
234    }
235
236    #[test]
237    fn depth_with_variable_skip_include_fields() {
238        check_depth(
239            r#"query($no: Boolean, $yes: Boolean) {
240                d1: ping
241                d2: thing @skip(if: $no) { title }
242                d4: thing @skip(if: $yes) { parent { parent { id } } }
243            }"#,
244            None,
245            serde_json::json!({ "no": false, "yes": true }),
246            2,
247        );
248
249        check_depth(
250            r#"query($no: Boolean, $yes: Boolean) {
251                d1: ping
252                d2: thing @include(if: $yes) { title }
253                d4: thing @include(if: $no) { parent { parent { id } } }
254            }"#,
255            None,
256            serde_json::json!({ "no": false, "yes": true }),
257            2,
258        );
259    }
260
261    #[test]
262    fn depth_with_default_variable_skip_include_fields() {
263        check_depth(
264            r#"query($no: Boolean = false, $yes: Boolean = true) {
265                d1: ping
266                d2: thing @skip(if: $no) { title }
267                d4: thing @skip(if: $yes) { parent { parent { id } } }
268            }"#,
269            None,
270            serde_json::json!({}),
271            2,
272        );
273    }
274
275    #[test]
276    fn depth_with_skip_include_fragments() {
277        check_depth(
278            r#"query {
279                d1: ping
280                ...@skip(if: false) { d2: thing { title } }
281                ...@skip(if: true) { d4: thing { parent { parent { id } } } }
282            }"#,
283            None,
284            serde_json::json!({}),
285            2,
286        );
287
288        check_depth(
289            r#"query {
290                d1: ping
291                ...D2 @skip(if: false)
292                ...D4 @skip(if: true)
293            }
294            fragment D2 on Query {
295                d2: thing { title }
296            }
297            fragment D4 on Query {
298                d4: thing { parent { parent { id } } }
299            }"#,
300            None,
301            serde_json::json!({}),
302            2,
303        );
304    }
305}