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, InputFieldsDefinition, InputObjectTypeDefinition, InputType,
7    InputTypeReference, InputValueDefinition, SchemaDefinition, TypeDefinitionReference,
8};
9use bluejay_core::executable::{
10    ExecutableDocument, FragmentSpread, OperationDefinition, VariableDefinition, VariableType,
11    VariableTypeReference,
12};
13use bluejay_core::{Argument, AsIter, Indexed, ObjectValue, Value, ValueReference, Variable};
14use itertools::Either;
15use std::collections::{BTreeMap, BTreeSet, HashMap};
16use std::ops::Not;
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) => o.iter().for_each(|(key, value)| {
87                if let Some(ivd) = location.input_value_definition() {
88                    if let BaseInputTypeReference::InputObject(iotd) =
89                        ivd.r#type().base(self.schema_definition)
90                    {
91                        if let Some(ivd) = iotd.input_field_definitions().get(key.as_ref()) {
92                            self.visit_value(value, root, VariableUsageLocation::ObjectField(ivd));
93                        }
94                    }
95                }
96            }),
97            _ => {}
98        }
99    }
100
101    fn operation_definitions_where_fragment_used(
102        &self,
103        fragment_definition: &'a E::FragmentDefinition,
104    ) -> impl Iterator<Item = &'a E::OperationDefinition> {
105        let mut references = BTreeSet::new();
106        self.visit_fragment_references(fragment_definition, &mut references);
107        references
108            .into_iter()
109            .filter_map(|reference| match reference {
110                PathRoot::Operation(o) => Some(o),
111                PathRoot::Fragment(_) => None,
112            })
113    }
114
115    fn visit_fragment_references(
116        &self,
117        fragment_definition: &'a E::FragmentDefinition,
118        visited: &mut BTreeSet<PathRoot<'a, E>>,
119    ) {
120        if let Some(references) = self.fragment_references.get(&Indexed(fragment_definition)) {
121            references.iter().for_each(|reference| {
122                if visited.insert(*reference) {
123                    if let PathRoot::Fragment(f) = reference {
124                        self.visit_fragment_references(f, visited);
125                    }
126                }
127            });
128        }
129    }
130
131    fn is_variable_usage_allowed(
132        &self,
133        variable_definition: &'a E::VariableDefinition,
134        variable_usage: &VariableUsage<'a, E, S>,
135    ) -> bool {
136        let variable_type = variable_definition.r#type().as_ref();
137
138        // filter non-input types to avoid duplicate error
139        if !self.is_input_type(variable_type.name()) {
140            return true;
141        }
142
143        let VariableUsage { location, .. } = variable_usage;
144        let location_type = location.r#type().as_ref(self.schema_definition);
145        let input_value_definition = location.input_value_definition();
146
147        if location_type.is_required() && !variable_type.is_required() {
148            let has_non_null_variable_default_value =
149                matches!(variable_definition.default_value(), Some(v) if !v.as_ref().is_null());
150            let has_location_default_value = matches!(input_value_definition.and_then(InputValueDefinition::default_value), Some(v) if !v.as_ref().is_null());
151
152            if !has_non_null_variable_default_value && !has_location_default_value {
153                false
154            } else {
155                self.are_types_compatible(variable_type, location_type.unwrap_nullable())
156            }
157        } else {
158            self.are_types_compatible(variable_type, location_type)
159        }
160    }
161
162    #[allow(clippy::only_used_in_recursion)] // making it a class method requires some additional lifetime constraints
163    fn are_types_compatible(
164        &self,
165        variable_type: VariableTypeReference<'a, E::VariableType>,
166        location_type: InputTypeReference<'a, S::InputType>,
167    ) -> bool {
168        match (variable_type, location_type) {
169            (
170                VariableTypeReference::List(item_variable_type, variable_required),
171                InputTypeReference::List(item_location_type, location_required),
172            ) if variable_required || !location_required => self.are_types_compatible(
173                item_variable_type.as_ref(),
174                item_location_type.as_ref(self.schema_definition),
175            ),
176            (
177                VariableTypeReference::Named(base_variable_type, variable_required),
178                InputTypeReference::Base(base_location_type, location_required),
179            ) if variable_required || !location_required => {
180                base_location_type.name() == base_variable_type
181            }
182            _ => false,
183        }
184    }
185
186    fn is_input_type(&self, name: &str) -> bool {
187        self.schema_definition
188            .get_type_definition(name)
189            .map_or(false, |tdr| tdr.is_input())
190    }
191}
192
193impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> Rule<'a, E, S>
194    for AllVariableUsagesAllowed<'a, E, S>
195{
196    type Error = Error<'a, E, S>;
197    type Errors = std::vec::IntoIter<Error<'a, E, S>>;
198
199    fn into_errors(self) -> Self::Errors {
200        self.variable_usages
201            .iter()
202            .filter(|(_, variable_usages)| !variable_usages.is_empty())
203            .flat_map(|(root, variable_usages)| {
204                let operation_definitions: Either<std::iter::Once<&'a E::OperationDefinition>, _> =
205                    match root {
206                        PathRoot::Operation(operation_definition) => {
207                            Either::Left(std::iter::once(operation_definition))
208                        }
209                        PathRoot::Fragment(fragment_definition) => Either::Right(
210                            self.operation_definitions_where_fragment_used(fragment_definition),
211                        ),
212                    };
213                operation_definitions.flat_map(|operation_definition| {
214                    variable_usages.iter().filter_map(|variable_usage| {
215                        let VariableUsage { variable, location } = variable_usage;
216                        let variable_definition = operation_definition
217                            .as_ref()
218                            .variable_definitions()
219                            .and_then(|variable_definitions| {
220                                variable_definitions.iter().find(|variable_definition| {
221                                    variable_definition.variable() == variable.name()
222                                })
223                            });
224
225                        variable_definition.and_then(|variable_definition| {
226                            self.is_variable_usage_allowed(variable_definition, variable_usage)
227                                .not()
228                                .then_some(Error::InvalidVariableUsage {
229                                    variable: *variable,
230                                    variable_type: variable_definition.r#type(),
231                                    location_type: location.r#type(),
232                                })
233                        })
234                    })
235                })
236            })
237            .collect::<Vec<Error<'a, E, S>>>()
238            .into_iter()
239    }
240}
241
242enum VariableUsageLocation<'a, S: SchemaDefinition> {
243    Argument(&'a S::InputValueDefinition),
244    ObjectField(&'a S::InputValueDefinition),
245    ListValue(&'a S::InputType),
246}
247
248impl<'a, S: SchemaDefinition> VariableUsageLocation<'a, S> {
249    fn input_value_definition(&self) -> Option<&'a S::InputValueDefinition> {
250        match self {
251            Self::Argument(ivd) => Some(ivd),
252            Self::ObjectField(ivd) => Some(ivd),
253            Self::ListValue(_) => None,
254        }
255    }
256
257    fn r#type(&self) -> &'a S::InputType {
258        match self {
259            Self::Argument(ivd) => ivd.r#type(),
260            Self::ObjectField(ivd) => ivd.r#type(),
261            Self::ListValue(t) => t,
262        }
263    }
264}
265
266struct VariableUsage<'a, E: ExecutableDocument, S: SchemaDefinition> {
267    variable: &'a <E::Value<false> as Value<false>>::Variable,
268    location: VariableUsageLocation<'a, S>,
269}