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)
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}