bluejay_validator/executable/operation/analyzers/
input_size.rs

1use crate::executable::{
2    operation::{Analyzer, VariableValues, Visitor},
3    Cache,
4};
5use bluejay_core::{
6    definition::SchemaDefinition,
7    executable::{ExecutableDocument, OperationDefinition, VariableDefinition},
8    Argument, AsIter, ObjectValue, Value, ValueReference, Variable,
9};
10
11#[derive(Clone)]
12/// Represents an argument or input-field that exceeds
13/// the maximum allowed list-size.
14pub struct Offender {
15    pub size: usize,
16    pub name: String,
17}
18
19#[derive(Clone)]
20/// The [InputSize] visitor will check all arguments and object-fields
21/// for list-values, when it sees a list-value that exceeds the maximum
22/// allowed list-size we will add it to the list of offenders.
23/// As output we'll return an array of [Offender].
24pub struct InputSize<'a, E: ExecutableDocument, VV: VariableValues> {
25    offenders: Vec<Offender>,
26    max_length: usize,
27    variable_values: &'a VV,
28    variable_definitions: Option<&'a E::VariableDefinitions>,
29}
30
31impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Visitor<'a, E, S, VV>
32    for InputSize<'a, E, VV>
33{
34    type ExtraInfo = usize;
35
36    fn new(
37        op: &'a E::OperationDefinition,
38        _s: &'a S,
39        variables: &'a VV,
40        _: &'a Cache<'a, E, S>,
41        max_length: Self::ExtraInfo,
42    ) -> Self {
43        Self {
44            max_length,
45            offenders: vec![],
46            variable_values: variables,
47            variable_definitions: op.as_ref().variable_definitions(),
48        }
49    }
50
51    fn visit_variable_argument(
52        &mut self,
53        argument: &'a <E as ExecutableDocument>::Argument<false>,
54        _input_value_definition: &'a S::InputValueDefinition,
55    ) {
56        find_input_size_offenders_arguments::<E, VV, false>(
57            self.max_length,
58            &mut self.offenders,
59            self.variable_values,
60            self.variable_definitions,
61            argument.name().to_string(),
62            argument.value(),
63        );
64    }
65}
66
67/// Will go over all the arguments on a field and check whether the value is a list,
68/// when it is a list it will check the input-size. When the input-size exceeds the maximum
69/// allowed length we'll flag it as an offending argument, when it does not we'll traverse
70/// deeper to find potential Objects contained within the list. When we enconter an object
71/// we'll traverse deeper to find object-fields that contain lists as a value.
72fn find_input_size_offenders_arguments<
73    E: ExecutableDocument,
74    VV: VariableValues,
75    const CONST: bool,
76>(
77    max_length: usize,
78    offenders: &mut Vec<Offender>,
79    variable_values: &VV,
80    variable_definitions: Option<&E::VariableDefinitions>,
81    argument_name: String,
82    argument_value: &<E as bluejay_core::executable::ExecutableDocument>::Value<CONST>,
83) {
84    match argument_value.as_ref() {
85        ValueReference::List(list) => {
86            let list_length = list.len();
87            if list_length > max_length {
88                offenders.push(Offender {
89                    size: list_length,
90                    name: argument_name,
91                })
92            } else {
93                list.iter().enumerate().for_each(|(index, item)| {
94                    find_input_size_offenders_arguments::<E, VV, CONST>(
95                        max_length,
96                        offenders,
97                        variable_values,
98                        variable_definitions,
99                        format!("{}.{}", argument_name, index),
100                        item,
101                    );
102                })
103            }
104        }
105        ValueReference::Object(obj) => {
106            obj.iter().for_each(|(key, value)| {
107                find_input_size_offenders_arguments::<E, VV, CONST>(
108                    max_length,
109                    offenders,
110                    variable_values,
111                    variable_definitions,
112                    format!("{}.{}", argument_name, key.as_ref()),
113                    value,
114                );
115            });
116        }
117        ValueReference::Variable(var) => {
118            let name = var.name();
119            let variable = variable_values.get(name);
120            if let Some(value) = variable {
121                find_input_size_offenders_variables::<E, VV>(
122                    max_length,
123                    offenders,
124                    argument_name,
125                    value,
126                );
127            } else {
128                let variable_definition = variable_definitions.map(|variable_definitions| {
129                    variable_definitions
130                        .iter()
131                        .find(|def| def.variable() == argument_name)
132                });
133                if let Some(Some(var_def)) = variable_definition {
134                    let default_value = var_def.default_value();
135                    if let Some(default_value) = default_value {
136                        find_input_size_offenders_arguments::<E, VV, true>(
137                            max_length,
138                            offenders,
139                            variable_values,
140                            variable_definitions,
141                            argument_name,
142                            default_value,
143                        );
144                    }
145                }
146            }
147        }
148        _ => {}
149    };
150}
151
152/// Similar to [find_input_size_offenders_arguments] however, it is specialised to traversing
153/// variable-values.
154fn find_input_size_offenders_variables<E: ExecutableDocument, VV: VariableValues>(
155    max_length: usize,
156    offenders: &mut Vec<Offender>,
157    argument_name: String,
158    argument_value: &VV::Value,
159) {
160    match argument_value.as_ref() {
161        ValueReference::List(list) => {
162            let list_length = list.len();
163            if list_length > max_length {
164                offenders.push(Offender {
165                    size: list_length,
166                    name: argument_name,
167                })
168            } else {
169                list.iter().enumerate().for_each(|(index, item)| {
170                    find_input_size_offenders_variables::<E, VV>(
171                        max_length,
172                        offenders,
173                        format!("{}.{}", argument_name, index),
174                        item,
175                    );
176                })
177            }
178        }
179        ValueReference::Object(obj) => {
180            obj.iter().for_each(|(key, value)| {
181                find_input_size_offenders_variables::<E, VV>(
182                    max_length,
183                    offenders,
184                    format!("{}.{}", argument_name, key.as_ref()),
185                    value,
186                );
187            });
188        }
189        _ => {}
190    };
191}
192
193impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Analyzer<'a, E, S, VV>
194    for InputSize<'a, E, VV>
195{
196    type Output = Vec<Offender>;
197
198    fn into_output(self) -> Self::Output {
199        self.offenders
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::{InputSize, Offender};
206    use crate::executable::{operation::Orchestrator, Cache};
207    use bluejay_parser::ast::{
208        definition::{
209            DefaultContext, DefinitionDocument, SchemaDefinition as ParserSchemaDefinition,
210        },
211        executable::ExecutableDocument as ParserExecutableDocument,
212        Parse,
213    };
214    use serde_json::{Map as JsonMap, Value as JsonValue};
215
216    const TEST_SCHEMA: &str = r#"
217        directive @test(y: [String]) on FIELD_DEFINITION
218
219        input ObjectList {
220            property: [String]
221        }
222        type Query {
223          simple(x: [String]): String!
224          object(x: ObjectList): String!
225          list_object(x: [ObjectList]): String!
226        }
227        schema {
228          query: Query
229        }
230    "#;
231
232    fn analyze_input_size(query: &str, variables: serde_json::Value) -> Vec<Offender> {
233        let definition_document: DefinitionDocument<'_, DefaultContext> =
234            DefinitionDocument::parse(TEST_SCHEMA).expect("Schema had parse errors");
235        let schema_definition =
236            ParserSchemaDefinition::try_from(&definition_document).expect("Schema had errors");
237        let executable_document = ParserExecutableDocument::parse(query)
238            .unwrap_or_else(|_| panic!("Document had parse errors"));
239        let cache = Cache::new(&executable_document, &schema_definition);
240        let variables = variables.as_object().expect("Variables must be an object");
241        Orchestrator::<_, _, JsonMap<String, JsonValue>, InputSize<_, _>>::analyze(
242            &executable_document,
243            &schema_definition,
244            None,
245            variables,
246            &cache,
247            1,
248        )
249        .unwrap()
250    }
251
252    #[test]
253    fn simple_size() {
254        let result =
255            analyze_input_size(r#"query { simple(x: ["x", "y"])} "#, serde_json::json!({}));
256        let result = result.first().unwrap();
257        assert_eq!(result.size, 2);
258        assert_eq!(result.name, "x");
259    }
260
261    #[test]
262    fn simple_directive_size() {
263        let result = analyze_input_size(
264            r#"query { simple(x: []) @test(y: ["x", "y"])} "#,
265            serde_json::json!({}),
266        );
267        let result = result.first().unwrap();
268        assert_eq!(result.size, 2);
269        assert_eq!(result.name, "y");
270    }
271
272    #[test]
273    fn simple_size_variable() {
274        let result = analyze_input_size(
275            r#"query ($x: [String]) { simple(x: $x)} "#,
276            serde_json::json!({ "x": ["x", "y"] }),
277        );
278        let result = result.first().unwrap();
279        assert_eq!(result.size, 2);
280        assert_eq!(result.name, "x");
281    }
282
283    #[test]
284    fn simple_size_variable_default_value() {
285        let result = analyze_input_size(
286            r#"query ($x: [String] = ["x", "y"]) { simple(x: $x)} "#,
287            serde_json::json!({}),
288        );
289        let result = result.first().unwrap();
290        assert_eq!(result.size, 2);
291        assert_eq!(result.name, "x");
292    }
293
294    #[test]
295    fn object_size() {
296        let result = analyze_input_size(
297            r#"query { object(x: { property: ["x", "y"] })} "#,
298            serde_json::json!({}),
299        );
300        let result = result.first().unwrap();
301        assert_eq!(result.size, 2);
302        assert_eq!(result.name, "x.property");
303    }
304
305    #[test]
306    fn object_size_variable() {
307        let result = analyze_input_size(
308            r#"query($x: ObjectList) { object(x: $x)} "#,
309            serde_json::json!({ "x": { "property": ["x", "y"] } }),
310        );
311        let result = result.first().unwrap();
312        assert_eq!(result.size, 2);
313        assert_eq!(result.name, "x.property");
314    }
315
316    #[test]
317    fn list_object_size() {
318        let result = analyze_input_size(
319            r#"query { list_object(x: [{ property: ["x", "y"] }, { property: ["x", "y"] }])} "#,
320            serde_json::json!({}),
321        );
322        assert_eq!(result.len(), 1);
323        let first = result.first().unwrap();
324        assert_eq!(first.size, 2);
325        assert_eq!(first.name, "x");
326    }
327
328    #[test]
329    fn list_object_size_variable() {
330        let result = analyze_input_size(
331            r#"query($x: [ObjectList]) { list_object(x: $x)} "#,
332            serde_json::json!({ "x": [{ "property": ["x", "y"] }, { "property": ["x", "y"] }] }),
333        );
334        let result = result.first().unwrap();
335        assert_eq!(result.size, 2);
336        assert_eq!(result.name, "x");
337    }
338
339    #[test]
340    fn list_nested_object_size() {
341        let result = analyze_input_size(
342            r#"query { list_object(x: [{ property: ["x", "y"] }])} "#,
343            serde_json::json!({}),
344        );
345        assert_eq!(result.len(), 1);
346        let first = result.first().unwrap();
347        assert_eq!(first.size, 2);
348        assert_eq!(first.name, "x.0.property");
349    }
350
351    #[test]
352    fn list_nested_object_size_variable() {
353        let result = analyze_input_size(
354            r#"query($x: [ObjectList]) { list_object(x: $x)} "#,
355            serde_json::json!({ "x": [{ "property": ["x", "y"] }] }),
356        );
357        let result = result.first().unwrap();
358        assert_eq!(result.size, 2);
359        assert_eq!(result.name, "x.0.property");
360    }
361}