Skip to main content

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)
235                .result
236                .expect("Schema had parse errors");
237        let schema_definition =
238            ParserSchemaDefinition::try_from(&definition_document).expect("Schema had errors");
239        let executable_document = ParserExecutableDocument::parse(query)
240            .result
241            .unwrap_or_else(|_| panic!("Document had parse errors"));
242        let cache = Cache::new(&executable_document, &schema_definition);
243        let variables = variables.as_object().expect("Variables must be an object");
244        Orchestrator::<_, _, JsonMap<String, JsonValue>, InputSize<_, _>>::analyze(
245            &executable_document,
246            &schema_definition,
247            None,
248            variables,
249            &cache,
250            1,
251        )
252        .unwrap()
253    }
254
255    #[test]
256    fn simple_size() {
257        let result =
258            analyze_input_size(r#"query { simple(x: ["x", "y"])} "#, serde_json::json!({}));
259        let result = result.first().unwrap();
260        assert_eq!(result.size, 2);
261        assert_eq!(result.name, "x");
262    }
263
264    #[test]
265    fn simple_directive_size() {
266        let result = analyze_input_size(
267            r#"query { simple(x: []) @test(y: ["x", "y"])} "#,
268            serde_json::json!({}),
269        );
270        let result = result.first().unwrap();
271        assert_eq!(result.size, 2);
272        assert_eq!(result.name, "y");
273    }
274
275    #[test]
276    fn simple_size_variable() {
277        let result = analyze_input_size(
278            r#"query ($x: [String]) { simple(x: $x)} "#,
279            serde_json::json!({ "x": ["x", "y"] }),
280        );
281        let result = result.first().unwrap();
282        assert_eq!(result.size, 2);
283        assert_eq!(result.name, "x");
284    }
285
286    #[test]
287    fn simple_size_variable_default_value() {
288        let result = analyze_input_size(
289            r#"query ($x: [String] = ["x", "y"]) { simple(x: $x)} "#,
290            serde_json::json!({}),
291        );
292        let result = result.first().unwrap();
293        assert_eq!(result.size, 2);
294        assert_eq!(result.name, "x");
295    }
296
297    #[test]
298    fn object_size() {
299        let result = analyze_input_size(
300            r#"query { object(x: { property: ["x", "y"] })} "#,
301            serde_json::json!({}),
302        );
303        let result = result.first().unwrap();
304        assert_eq!(result.size, 2);
305        assert_eq!(result.name, "x.property");
306    }
307
308    #[test]
309    fn object_size_variable() {
310        let result = analyze_input_size(
311            r#"query($x: ObjectList) { object(x: $x)} "#,
312            serde_json::json!({ "x": { "property": ["x", "y"] } }),
313        );
314        let result = result.first().unwrap();
315        assert_eq!(result.size, 2);
316        assert_eq!(result.name, "x.property");
317    }
318
319    #[test]
320    fn list_object_size() {
321        let result = analyze_input_size(
322            r#"query { list_object(x: [{ property: ["x", "y"] }, { property: ["x", "y"] }])} "#,
323            serde_json::json!({}),
324        );
325        assert_eq!(result.len(), 1);
326        let first = result.first().unwrap();
327        assert_eq!(first.size, 2);
328        assert_eq!(first.name, "x");
329    }
330
331    #[test]
332    fn list_object_size_variable() {
333        let result = analyze_input_size(
334            r#"query($x: [ObjectList]) { list_object(x: $x)} "#,
335            serde_json::json!({ "x": [{ "property": ["x", "y"] }, { "property": ["x", "y"] }] }),
336        );
337        let result = result.first().unwrap();
338        assert_eq!(result.size, 2);
339        assert_eq!(result.name, "x");
340    }
341
342    #[test]
343    fn list_nested_object_size() {
344        let result = analyze_input_size(
345            r#"query { list_object(x: [{ property: ["x", "y"] }])} "#,
346            serde_json::json!({}),
347        );
348        assert_eq!(result.len(), 1);
349        let first = result.first().unwrap();
350        assert_eq!(first.size, 2);
351        assert_eq!(first.name, "x.0.property");
352    }
353
354    #[test]
355    fn list_nested_object_size_variable() {
356        let result = analyze_input_size(
357            r#"query($x: [ObjectList]) { list_object(x: $x)} "#,
358            serde_json::json!({ "x": [{ "property": ["x", "y"] }] }),
359        );
360        let result = result.first().unwrap();
361        assert_eq!(result.size, 2);
362        assert_eq!(result.name, "x.0.property");
363    }
364}