bluejay_validator/executable/operation/analyzers/
query_depth.rs1use 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}