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