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