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).expect("Schema had parse errors");
115        let schema_definition =
116            ParserSchemaDefinition::try_from(&definition_document).expect("Schema had errors");
117        let executable_document = ParserExecutableDocument::parse(source)
118            .unwrap_or_else(|_| panic!("Document had parse errors"));
119        let cache = Cache::new(&executable_document, &schema_definition);
120        let variables = variables.as_object().expect("Variables must be an object");
121        let depth = DepthAnalyzer::analyze(
122            &executable_document,
123            &schema_definition,
124            operation_name,
125            variables,
126            &cache,
127            (),
128        )
129        .unwrap();
130
131        assert_eq!(depth, expected_depth);
132    }
133
134    #[test]
135    fn basic_depth_metrics() {
136        check_depth(r#"{ ping }"#, None, serde_json::json!({}), 1);
137        check_depth(r#"{ thing { id } }"#, None, serde_json::json!({}), 2);
138        check_depth(
139            r#"{ thing { parent { id } } }"#,
140            None,
141            serde_json::json!({}),
142            3,
143        );
144        check_depth(
145            r#"{
146                thing { parent { parent { id } } }
147                thing { parent { id } }
148            }"#,
149            None,
150            serde_json::json!({}),
151            4,
152        );
153    }
154
155    #[test]
156    fn depth_with_operation_context() {
157        check_depth(
158            r#"
159            query D2{
160                thing { title }
161            }
162            query D4 {
163                d4: thing { parent { parent { id } } }
164            }"#,
165            Some("D2"),
166            serde_json::json!({}),
167            2,
168        );
169    }
170
171    #[test]
172    fn depth_with_inline_fragments() {
173        check_depth(
174            r#"{
175                node {
176                    ...on Product { title }
177                    ...on Thing { parent { title } }
178                }
179            }"#,
180            None,
181            serde_json::json!({}),
182            3,
183        );
184    }
185
186    #[test]
187    fn depth_with_fragment_spreads() {
188        check_depth(
189            r#"{
190                node {
191                    ...ProductAttrs
192                    ...ThingAttrs
193                }
194            }
195            fragment ProductAttrs on Product {
196                title
197            }
198            fragment ThingAttrs on Thing {
199                parent { title }
200            }
201            "#,
202            None,
203            serde_json::json!({}),
204            3,
205        );
206    }
207
208    #[test]
209    fn depth_with_const_skip_include_fields() {
210        check_depth(
211            r#"{
212                d1: ping
213                d2: thing @skip(if: false) { title }
214                d4: thing @skip(if: true) { parent { parent { id } } }
215            }"#,
216            None,
217            serde_json::json!({}),
218            2,
219        );
220
221        check_depth(
222            r#"{
223                d1: ping
224                d2: thing @include(if: true) { title }
225                d4: thing @include(if: false) { parent { parent { id } } }
226            }"#,
227            None,
228            serde_json::json!({}),
229            2,
230        );
231    }
232
233    #[test]
234    fn depth_with_variable_skip_include_fields() {
235        check_depth(
236            r#"query($no: Boolean, $yes: Boolean) {
237                d1: ping
238                d2: thing @skip(if: $no) { title }
239                d4: thing @skip(if: $yes) { parent { parent { id } } }
240            }"#,
241            None,
242            serde_json::json!({ "no": false, "yes": true }),
243            2,
244        );
245
246        check_depth(
247            r#"query($no: Boolean, $yes: Boolean) {
248                d1: ping
249                d2: thing @include(if: $yes) { title }
250                d4: thing @include(if: $no) { parent { parent { id } } }
251            }"#,
252            None,
253            serde_json::json!({ "no": false, "yes": true }),
254            2,
255        );
256    }
257
258    #[test]
259    fn depth_with_default_variable_skip_include_fields() {
260        check_depth(
261            r#"query($no: Boolean = false, $yes: Boolean = true) {
262                d1: ping
263                d2: thing @skip(if: $no) { title }
264                d4: thing @skip(if: $yes) { parent { parent { id } } }
265            }"#,
266            None,
267            serde_json::json!({}),
268            2,
269        );
270    }
271
272    #[test]
273    fn depth_with_skip_include_fragments() {
274        check_depth(
275            r#"query {
276                d1: ping
277                ...@skip(if: false) { d2: thing { title } }
278                ...@skip(if: true) { d4: thing { parent { parent { id } } } }
279            }"#,
280            None,
281            serde_json::json!({}),
282            2,
283        );
284
285        check_depth(
286            r#"query {
287                d1: ping
288                ...D2 @skip(if: false)
289                ...D4 @skip(if: true)
290            }
291            fragment D2 on Query {
292                d2: thing { title }
293            }
294            fragment D4 on Query {
295                d4: thing { parent { parent { id } } }
296            }"#,
297            None,
298            serde_json::json!({}),
299            2,
300        );
301    }
302}