bluejay_validator/executable/document/rules/
all_variable_usages_allowed.rs

1use crate::executable::{
2    document::{Error, Path, PathRoot, Rule, Visitor},
3    Cache,
4};
5use bluejay_core::definition::{
6    BaseInputTypeReference, HasDirectives, InputFieldsDefinition, InputObjectTypeDefinition,
7    InputType, InputTypeReference, InputValueDefinition, SchemaDefinition, TypeDefinitionReference,
8};
9use bluejay_core::executable::{
10    ExecutableDocument, FragmentSpread, OperationDefinition, VariableDefinition, VariableType,
11    VariableTypeReference,
12};
13use bluejay_core::Directive;
14use bluejay_core::{Argument, AsIter, Indexed, ObjectValue, Value, ValueReference, Variable};
15use itertools::Either;
16use std::collections::{BTreeMap, BTreeSet, HashMap};
17
18pub struct AllVariableUsagesAllowed<'a, E: ExecutableDocument, S: SchemaDefinition> {
19    fragment_references: HashMap<Indexed<'a, E::FragmentDefinition>, BTreeSet<PathRoot<'a, E>>>,
20    variable_usages: BTreeMap<PathRoot<'a, E>, Vec<VariableUsage<'a, E, S>>>,
21    cache: &'a Cache<'a, E, S>,
22    schema_definition: &'a S,
23}
24
25impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> Visitor<'a, E, S>
26    for AllVariableUsagesAllowed<'a, E, S>
27{
28    fn new(_: &'a E, schema_definition: &'a S, cache: &'a Cache<'a, E, S>) -> Self {
29        Self {
30            fragment_references: HashMap::new(),
31            variable_usages: BTreeMap::new(),
32            cache,
33            schema_definition,
34        }
35    }
36
37    fn visit_variable_argument(
38        &mut self,
39        argument: &'a <E as ExecutableDocument>::Argument<false>,
40        input_value_definition: &'a <S as SchemaDefinition>::InputValueDefinition,
41        path: &Path<'a, E>,
42    ) {
43        self.visit_value(
44            argument.value(),
45            *path.root(),
46            VariableUsageLocation::Argument(input_value_definition),
47        );
48    }
49
50    fn visit_fragment_spread(
51        &mut self,
52        fragment_spread: &'a E::FragmentSpread,
53        _: TypeDefinitionReference<'a, S::TypeDefinition>,
54        path: &Path<'a, E>,
55    ) {
56        if let Some(fragment_definition) = self.cache.fragment_definition(fragment_spread.name()) {
57            self.fragment_references
58                .entry(Indexed(fragment_definition))
59                .or_default()
60                .insert(*path.root());
61        }
62    }
63}
64
65impl<'a, E: ExecutableDocument, S: SchemaDefinition> AllVariableUsagesAllowed<'a, E, S> {
66    fn visit_value(
67        &mut self,
68        value: &'a <E as ExecutableDocument>::Value<false>,
69        root: PathRoot<'a, E>,
70        location: VariableUsageLocation<'a, S>,
71    ) {
72        match value.as_ref() {
73            ValueReference::Variable(variable) => {
74                self.variable_usages
75                    .entry(root)
76                    .or_default()
77                    .push(VariableUsage { variable, location });
78            }
79            ValueReference::List(l) => l.iter().for_each(|value| {
80                if let InputTypeReference::List(inner, _) =
81                    location.r#type().as_ref(self.schema_definition)
82                {
83                    self.visit_value(value, root, VariableUsageLocation::ListValue(inner));
84                }
85            }),
86            ValueReference::Object(o) => {
87                if let BaseInputTypeReference::InputObject(iotd) =
88                    location.r#type().base(self.schema_definition)
89                {
90                    o.iter().for_each(|(key, value)| {
91                        if let Some(ivd) = iotd.input_field_definitions().get(key.as_ref()) {
92                            self.visit_value(
93                                value,
94                                root,
95                                VariableUsageLocation::ObjectField {
96                                    input_value_definition: ivd,
97                                    parent_type: iotd,
98                                },
99                            );
100                        }
101                    });
102                }
103            }
104            _ => {}
105        }
106    }
107
108    fn operation_definitions_where_fragment_used(
109        &self,
110        fragment_definition: &'a E::FragmentDefinition,
111    ) -> impl Iterator<Item = &'a E::OperationDefinition> {
112        let mut references = BTreeSet::new();
113        self.visit_fragment_references(fragment_definition, &mut references);
114        references
115            .into_iter()
116            .filter_map(|reference| match reference {
117                PathRoot::Operation(o) => Some(o),
118                PathRoot::Fragment(_) => None,
119            })
120    }
121
122    fn visit_fragment_references(
123        &self,
124        fragment_definition: &'a E::FragmentDefinition,
125        visited: &mut BTreeSet<PathRoot<'a, E>>,
126    ) {
127        if let Some(references) = self.fragment_references.get(&Indexed(fragment_definition)) {
128            references.iter().for_each(|reference| {
129                if visited.insert(*reference) {
130                    if let PathRoot::Fragment(f) = reference {
131                        self.visit_fragment_references(f, visited);
132                    }
133                }
134            });
135        }
136    }
137
138    fn validate_variable_usage(
139        &self,
140        variable_definition: &'a E::VariableDefinition,
141        variable_usage: &VariableUsage<'a, E, S>,
142    ) -> Result<(), Error<'a, E, S>> {
143        let variable_type = variable_definition.r#type().as_ref();
144
145        // filter non-input types to avoid duplicate error
146        if !self.is_input_type(variable_type.name()) {
147            return Ok(());
148        }
149
150        let VariableUsage { location, .. } = variable_usage;
151        let location_type = location.r#type().as_ref(self.schema_definition);
152        let input_value_definition = location.input_value_definition();
153        let is_nested_one_of = location.is_nested_one_of();
154
155        let is_compatible = if location_type.is_required() && !variable_type.is_required() {
156            let has_non_null_variable_default_value =
157                matches!(variable_definition.default_value(), Some(v) if !v.as_ref().is_null());
158            let has_location_default_value = matches!(input_value_definition.and_then(InputValueDefinition::default_value), Some(v) if !v.as_ref().is_null());
159
160            if !has_non_null_variable_default_value && !has_location_default_value {
161                false
162            } else {
163                self.are_types_compatible(variable_type, location_type.unwrap_nullable())
164            }
165        } else {
166            self.are_types_compatible(variable_type, location_type)
167        };
168
169        if !is_compatible {
170            Err(Error::InvalidVariableUsage {
171                variable: variable_usage.variable,
172                variable_type: variable_definition.r#type(),
173                location_type: location.r#type(),
174            })
175        } else if is_nested_one_of && !variable_type.is_required() {
176            Err(Error::InvalidOneOfVariableUsage {
177                variable: variable_usage.variable,
178                variable_type: variable_definition.r#type(),
179                parent_type_name: location.parent_type_name().unwrap(),
180            })
181        } else {
182            Ok(())
183        }
184    }
185
186    #[allow(clippy::only_used_in_recursion)] // making it a class method requires some additional lifetime constraints
187    fn are_types_compatible(
188        &self,
189        variable_type: VariableTypeReference<'a, E::VariableType>,
190        location_type: InputTypeReference<'a, S::InputType>,
191    ) -> bool {
192        match (variable_type, location_type) {
193            (
194                VariableTypeReference::List(item_variable_type, variable_required),
195                InputTypeReference::List(item_location_type, location_required),
196            ) if variable_required || !location_required => self.are_types_compatible(
197                item_variable_type.as_ref(),
198                item_location_type.as_ref(self.schema_definition),
199            ),
200            (
201                VariableTypeReference::Named(base_variable_type, variable_required),
202                InputTypeReference::Base(base_location_type, location_required),
203            ) if variable_required || !location_required => {
204                base_location_type.name() == base_variable_type
205            }
206            _ => false,
207        }
208    }
209
210    fn is_input_type(&self, name: &str) -> bool {
211        self.schema_definition
212            .get_type_definition(name)
213            .is_some_and(|tdr| tdr.is_input())
214    }
215}
216
217impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> Rule<'a, E, S>
218    for AllVariableUsagesAllowed<'a, E, S>
219{
220    type Error = Error<'a, E, S>;
221    type Errors = std::vec::IntoIter<Error<'a, E, S>>;
222
223    fn into_errors(self) -> Self::Errors {
224        self.variable_usages
225            .iter()
226            .filter(|(_, variable_usages)| !variable_usages.is_empty())
227            .flat_map(|(root, variable_usages)| {
228                let operation_definitions: Either<std::iter::Once<&'a E::OperationDefinition>, _> =
229                    match root {
230                        PathRoot::Operation(operation_definition) => {
231                            Either::Left(std::iter::once(operation_definition))
232                        }
233                        PathRoot::Fragment(fragment_definition) => Either::Right(
234                            self.operation_definitions_where_fragment_used(fragment_definition),
235                        ),
236                    };
237                operation_definitions.flat_map(|operation_definition| {
238                    variable_usages.iter().filter_map(|variable_usage| {
239                        let VariableUsage { variable, .. } = variable_usage;
240                        let variable_definition = operation_definition
241                            .as_ref()
242                            .variable_definitions()
243                            .and_then(|variable_definitions| {
244                                variable_definitions.iter().find(|variable_definition| {
245                                    variable_definition.variable() == variable.name()
246                                })
247                            });
248
249                        variable_definition.and_then(|variable_definition| {
250                            self.validate_variable_usage(variable_definition, variable_usage)
251                                .err()
252                        })
253                    })
254                })
255            })
256            .collect::<Vec<Error<'a, E, S>>>()
257            .into_iter()
258    }
259}
260
261#[derive(Debug)]
262enum VariableUsageLocation<'a, S: SchemaDefinition> {
263    Argument(&'a S::InputValueDefinition),
264    ObjectField {
265        input_value_definition: &'a S::InputValueDefinition,
266        parent_type: &'a S::InputObjectTypeDefinition,
267    },
268    ListValue(&'a S::InputType),
269}
270
271impl<'a, S: SchemaDefinition> VariableUsageLocation<'a, S> {
272    fn input_value_definition(&self) -> Option<&'a S::InputValueDefinition> {
273        match self {
274            Self::Argument(ivd) => Some(ivd),
275            Self::ObjectField {
276                input_value_definition,
277                ..
278            } => Some(input_value_definition),
279            Self::ListValue(_) => None,
280        }
281    }
282
283    fn r#type(&self) -> &'a S::InputType {
284        match self {
285            Self::Argument(ivd) => ivd.r#type(),
286            Self::ObjectField {
287                input_value_definition,
288                ..
289            } => input_value_definition.r#type(),
290            Self::ListValue(t) => t,
291        }
292    }
293
294    fn is_nested_one_of(&self) -> bool {
295        match self {
296            Self::ObjectField { parent_type, .. } => parent_type
297                .directives()
298                .and_then(|d| d.iter().find(|d| d.name() == "oneOf"))
299                .is_some(),
300            _ => false,
301        }
302    }
303
304    fn parent_type_name(&self) -> Option<&'a str> {
305        match self {
306            Self::ObjectField { parent_type, .. } => Some(parent_type.name()),
307            _ => None,
308        }
309    }
310}
311
312struct VariableUsage<'a, E: ExecutableDocument, S: SchemaDefinition> {
313    variable: &'a <E::Value<false> as Value<false>>::Variable,
314    location: VariableUsageLocation<'a, S>,
315}