1use anyhow::anyhow;
2use anyhow::Error;
3use graphql_tools::parser::minify_query_document;
4use graphql_tools::parser::schema::InputObjectType;
5use moka::sync::Cache;
6use std::cmp::Ordering;
7use std::collections::BTreeMap;
8use std::collections::HashMap;
9use std::collections::HashSet;
10
11use graphql_tools::ast::{
12 visit_document, OperationTransformer, OperationVisitor, OperationVisitorContext, Transformed,
13 TransformedValue,
14};
15use graphql_tools::parser::parse_query;
16use graphql_tools::parser::query::{
17 Definition, Directive, Document, Field, FragmentDefinition, Number, OperationDefinition,
18 Selection, SelectionSet, Text, Type, Value, VariableDefinition,
19};
20use graphql_tools::parser::schema::{Document as SchemaDocument, TypeDefinition};
21
22struct SchemaCoordinatesContext<'a> {
23 pub schema_coordinates: HashSet<String>,
24 pub used_input_fields: HashSet<&'a str>,
25 pub input_values_provided: HashMap<String, usize>,
26 pub used_variables: HashSet<&'a str>,
27 pub variables_with_defaults: HashSet<&'a str>,
28 error: Option<Error>,
29}
30
31impl SchemaCoordinatesContext<'_> {
32 fn is_corrupted(&self) -> bool {
33 self.error.is_some()
34 }
35}
36
37pub fn collect_schema_coordinates(
38 document: &Document<'static, String>,
39 schema: &SchemaDocument<'static, String>,
40) -> Result<HashSet<String>, Error> {
41 let mut ctx = SchemaCoordinatesContext {
42 schema_coordinates: HashSet::new(),
43 used_input_fields: HashSet::new(),
44 input_values_provided: HashMap::new(),
45 used_variables: HashSet::new(),
46 variables_with_defaults: HashSet::new(),
47 error: None,
48 };
49 let mut visit_context = OperationVisitorContext::new(document, schema);
50 let mut visitor = SchemaCoordinatesVisitor {
51 visited_input_object_types: HashSet::new(),
52 };
53
54 visit_document(&mut visitor, document, &mut visit_context, &mut ctx);
55
56 if let Some(error) = ctx.error {
57 Err(error)
58 } else {
59 for type_name in ctx.used_input_fields {
60 visitor.collect_nested_input_type(schema, type_name, &mut ctx.schema_coordinates);
61 }
62
63 Ok(ctx.schema_coordinates)
64 }
65}
66
67fn is_builtin_scalar(type_name: &str) -> bool {
68 matches!(type_name, "String" | "Int" | "Float" | "Boolean" | "ID")
69}
70
71fn mark_as_used(ctx: &mut SchemaCoordinatesContext, id: &str) {
72 if let Some(count) = ctx.input_values_provided.get_mut(id) {
73 if *count > 0 {
74 *count -= 1;
75 ctx.schema_coordinates.insert(format!("{}!", id));
76 }
77 }
78 ctx.schema_coordinates.insert(id.to_string());
79}
80
81fn count_input_value_provided(ctx: &mut SchemaCoordinatesContext, id: &str) {
82 let counter = ctx.input_values_provided.entry(id.to_string()).or_insert(0);
83 *counter += 1;
84}
85
86fn value_exists(v: &Value<String>) -> bool {
87 !matches!(v, Value::Null)
88}
89
90struct SchemaCoordinatesVisitor<'a> {
91 visited_input_object_types: HashSet<&'a str>,
92}
93
94impl<'a> SchemaCoordinatesVisitor<'a> {
95 fn process_default_value(
96 info: &OperationVisitorContext<'a>,
97 ctx: &mut SchemaCoordinatesContext,
98 type_name: &str,
99 value: &Value<String>,
100 ) {
101 match value {
102 Value::Object(obj) => {
103 if let Some(TypeDefinition::InputObject(input_obj)) =
104 info.schema.type_by_name(type_name)
105 {
106 for (field_name, field_value) in obj {
107 if let Some(field_def) =
108 input_obj.fields.iter().find(|f| &f.name == field_name)
109 {
110 let coordinate = format!("{}.{}", type_name, field_name);
111
112 ctx.schema_coordinates.insert(format!("{}!", coordinate));
114 ctx.schema_coordinates.insert(coordinate);
115
116 let field_type_name = Self::resolve_type_name(&field_def.value_type);
118 Self::process_default_value(info, ctx, field_type_name, field_value);
119 }
120 }
121 }
122 }
123 Value::List(values) => {
124 for val in values {
125 Self::process_default_value(info, ctx, type_name, val);
126 }
127 }
128 Value::Enum(enum_value) => {
129 let enum_coordinate = format!("{}.{}", type_name, enum_value);
130 ctx.schema_coordinates.insert(enum_coordinate);
131 }
132 _ => {
133 }
135 }
136 }
137
138 fn resolve_type_name(t: &'a Type<String>) -> &'a str {
139 match t {
140 Type::NamedType(value) => value.as_str(),
141 Type::ListType(t) => Self::resolve_type_name(t),
142 Type::NonNullType(t) => Self::resolve_type_name(t),
143 }
144 }
145
146 fn resolve_references(
147 &self,
148 schema: &'a SchemaDocument<'static, String>,
149 type_name: &'a str,
150 ) -> Option<Vec<&'a str>> {
151 let mut visited_types = Vec::new();
152 Self::_resolve_references(schema, type_name, &mut visited_types);
153 Some(visited_types)
154 }
155
156 fn _resolve_references(
157 schema: &'a SchemaDocument<'static, String>,
158 type_name: &'a str,
159 visited_types: &mut Vec<&'a str>,
160 ) {
161 if visited_types.contains(&type_name) {
162 return;
163 }
164
165 visited_types.push(type_name);
166
167 let named_type = schema.type_by_name(type_name);
168
169 if let Some(TypeDefinition::InputObject(input_type)) = named_type {
170 for field in &input_type.fields {
171 let field_type = Self::resolve_type_name(&field.value_type);
172 Self::_resolve_references(schema, field_type, visited_types);
173 }
174 }
175 }
176
177 fn collect_nested_input_type(
178 &mut self,
179 schema: &'a SchemaDocument<'static, String>,
180 input_type_name: &'a str,
181 coordinates: &mut HashSet<String>,
182 ) {
183 if let Some(input_type_def) = schema.type_by_name(input_type_name) {
184 match input_type_def {
185 TypeDefinition::Scalar(scalar_def) => {
186 coordinates.insert(scalar_def.name.clone());
187 }
188 TypeDefinition::InputObject(nested_input_type) => {
189 self.collect_nested_input_fields(schema, nested_input_type, coordinates);
190 }
191 TypeDefinition::Enum(enum_type) => {
192 for value in &enum_type.values {
193 coordinates.insert(format!("{}.{}", enum_type.name, value.name));
194 }
195 }
196 _ => {}
197 }
198 } else if is_builtin_scalar(input_type_name) {
199 coordinates.insert(input_type_name.to_string());
201 }
202 }
203
204 fn collect_nested_input_fields(
205 &mut self,
206 schema: &'a SchemaDocument<'static, String>,
207 input_type: &'a InputObjectType<'static, String>,
208 coordinates: &mut HashSet<String>,
209 ) {
210 if self
211 .visited_input_object_types
212 .contains(&input_type.name.as_str())
213 {
214 return;
215 }
216 self.visited_input_object_types
217 .insert(input_type.name.as_str());
218 for field in &input_type.fields {
219 let field_coordinate = format!("{}.{}", input_type.name, field.name);
220 coordinates.insert(field_coordinate);
221
222 let field_type_name = field.value_type.inner_type();
223
224 self.collect_nested_input_type(schema, field_type_name, coordinates);
225 }
226 }
227}
228
229impl<'a> OperationVisitor<'a, SchemaCoordinatesContext<'a>> for SchemaCoordinatesVisitor<'a> {
230 fn enter_variable_value(
231 &mut self,
232 _info: &mut OperationVisitorContext<'a>,
233 ctx: &mut SchemaCoordinatesContext<'a>,
234 name: &'a str,
235 ) {
236 ctx.used_variables.insert(name);
237 }
238
239 fn enter_field(
240 &mut self,
241 info: &mut OperationVisitorContext<'a>,
242 ctx: &mut SchemaCoordinatesContext,
243 field: &Field<'static, String>,
244 ) {
245 if ctx.is_corrupted() {
246 return;
247 }
248
249 let field_name = field.name.to_string();
250
251 if let Some(parent_type) = info.current_parent_type() {
252 let parent_name = parent_type.name();
253
254 ctx.schema_coordinates
255 .insert(format!("{}.{}", parent_name, field_name));
256
257 if let Some(field_def) = parent_type.field_by_name(&field_name) {
258 let field_output_type = info.schema.type_by_name(field_def.field_type.inner_type());
260 if let Some(TypeDefinition::Enum(enum_type)) = field_output_type {
261 for value in &enum_type.values {
262 ctx.schema_coordinates.insert(format!(
263 "{}.{}",
264 enum_type.name.as_str(),
265 value.name
266 ));
267 }
268 }
269 }
270 } else {
271 ctx.error = Some(anyhow!(
272 "Unable to find parent type of '{}' field",
273 field.name
274 ))
275 }
276 }
277
278 fn enter_variable_definition(
279 &mut self,
280 info: &mut OperationVisitorContext<'a>,
281 ctx: &mut SchemaCoordinatesContext<'a>,
282 var: &'a graphql_tools::static_graphql::query::VariableDefinition,
283 ) {
284 if ctx.is_corrupted() {
285 return;
286 }
287
288 if var.default_value.is_some() {
289 ctx.variables_with_defaults.insert(var.name.as_str());
290 }
291
292 let type_name = Self::resolve_type_name(&var.var_type);
293
294 if let Some(inner_types) = self.resolve_references(info.schema, type_name) {
295 for inner_type in inner_types {
296 ctx.used_input_fields.insert(inner_type);
297 }
298 }
299
300 ctx.used_input_fields.insert(type_name);
301
302 if let Some(default_value) = &var.default_value {
303 Self::process_default_value(info, ctx, type_name, default_value);
304 }
305 }
306
307 fn enter_argument(
308 &mut self,
309 info: &mut OperationVisitorContext<'a>,
310 ctx: &mut SchemaCoordinatesContext<'a>,
311 arg: &(String, Value<'static, String>),
312 ) {
313 if ctx.is_corrupted() {
314 return;
315 }
316
317 if info.current_parent_type().is_none() {
318 ctx.error = Some(anyhow!(
319 "Unable to find parent type of '{}' argument",
320 arg.0.clone()
321 ));
322 return;
323 }
324
325 let parent_type = info.current_parent_type().unwrap();
326 let type_name = parent_type.name();
327 let field = info.current_field();
328
329 if let Some(field) = field {
330 let field_name = field.name.clone();
331 let (arg_name, arg_value) = arg;
332
333 let coordinate = format!("{type_name}.{field_name}.{arg_name}");
334
335 let has_value = match arg_value {
336 Value::Null => false,
337 Value::Variable(var_name) => {
338 ctx.variables_with_defaults.contains(var_name.as_str())
339 }
340 _ => true,
341 };
342
343 if has_value {
344 count_input_value_provided(ctx, &coordinate);
345 }
346 mark_as_used(ctx, &coordinate);
347 if let Some(field_def) = parent_type.field_by_name(&field_name) {
348 if let Some(arg_def) = field_def.arguments.iter().find(|a| &a.name == arg_name) {
349 let arg_type_name = Self::resolve_type_name(&arg_def.value_type);
350
351 match arg_value {
352 Value::Enum(value) => {
353 let value_str: String = value.to_string();
354 ctx.schema_coordinates
355 .insert(format!("{arg_type_name}.{value_str}").to_string());
356 }
357 Value::List(_) => {
358 }
360 Value::Object(_) => {
361 if let Some(TypeDefinition::Scalar(_)) =
364 info.schema.type_by_name(arg_type_name)
365 {
366 ctx.schema_coordinates.insert(arg_type_name.to_string());
367 }
368 }
370 Value::Variable(_) => {
371 }
373 _ => {
374 if is_builtin_scalar(arg_type_name) {
377 ctx.schema_coordinates.insert(arg_type_name.to_string());
378 } else if let Some(TypeDefinition::Scalar(_)) =
379 info.schema.type_by_name(arg_type_name)
380 {
381 ctx.schema_coordinates.insert(arg_type_name.to_string());
382 }
383 }
384 }
385 }
386 }
387 }
388 }
389
390 fn enter_list_value(
391 &mut self,
392 info: &mut OperationVisitorContext<'a>,
393 ctx: &mut SchemaCoordinatesContext,
394 values: &Vec<Value<'static, String>>,
395 ) {
396 if ctx.is_corrupted() {
397 return;
398 }
399
400 if let Some(input_type) = info.current_input_type() {
401 let coordinate = input_type.name().to_string();
402 for value in values {
403 match value {
404 Value::Enum(value) => {
405 let value_str = value.to_string();
406 ctx.schema_coordinates
407 .insert(format!("{}.{}", coordinate, value_str));
408 }
409 Value::Object(_) => {
410 }
412 Value::List(_) => {
413 }
415 Value::Variable(_) => {
416 }
418 _ => {
419 if is_builtin_scalar(&coordinate) {
421 ctx.schema_coordinates.insert(coordinate.clone());
422 } else if let Some(TypeDefinition::Scalar(_)) =
423 info.schema.type_by_name(&coordinate)
424 {
425 ctx.schema_coordinates.insert(coordinate.clone());
426 }
427 }
428 }
429 }
430 }
431 }
432
433 fn enter_object_value(
434 &mut self,
435 info: &mut OperationVisitorContext<'a>,
436 ctx: &mut SchemaCoordinatesContext,
437 object_value: &BTreeMap<String, graphql_tools::static_graphql::query::Value>,
438 ) {
439 if let Some(TypeDefinition::InputObject(input_object_def)) = info.current_input_type() {
440 object_value.iter().for_each(|(name, value)| {
441 if let Some(field) = input_object_def
442 .fields
443 .iter()
444 .find(|field| field.name.eq(name))
445 {
446 let coordinate = format!("{}.{}", input_object_def.name, field.name);
447
448 let has_value = match value {
449 Value::Variable(var_name) => {
450 ctx.variables_with_defaults.contains(var_name.as_str())
451 }
452 _ => value_exists(value),
453 };
454
455 ctx.schema_coordinates.insert(coordinate.clone());
456 if has_value {
457 ctx.schema_coordinates.insert(format!("{coordinate}!"));
458 }
459
460 mark_as_used(ctx, &coordinate);
461
462 let field_type_name = field.value_type.inner_type();
463
464 match value {
465 Value::Enum(value) => {
466 let value_str = value.to_string();
467 ctx.schema_coordinates
468 .insert(format!("{field_type_name}.{value_str}").to_string());
469 }
470 Value::List(_) => {
471 }
473 Value::Object(_) => {
474 if let Some(TypeDefinition::Scalar(_)) =
476 info.schema.type_by_name(field_type_name)
477 {
478 ctx.schema_coordinates.insert(field_type_name.to_string());
479 }
480 }
482 Value::Variable(_) => {
483 if is_builtin_scalar(field_type_name) {
486 ctx.schema_coordinates.insert(field_type_name.to_string());
487 } else if let Some(TypeDefinition::Scalar(_)) =
488 info.schema.type_by_name(field_type_name)
489 {
490 ctx.schema_coordinates.insert(field_type_name.to_string());
491 }
492 }
493 Value::Null => {
494 if let Some(TypeDefinition::InputObject(nested_input_obj)) =
497 info.schema.type_by_name(field_type_name)
498 {
499 self.collect_nested_input_fields(
500 info.schema,
501 nested_input_obj,
502 &mut ctx.schema_coordinates,
503 );
504 }
505 }
506 _ => {
507 if is_builtin_scalar(field_type_name) {
509 ctx.schema_coordinates.insert(field_type_name.to_string());
510 } else if let Some(TypeDefinition::Scalar(_)) =
511 info.schema.type_by_name(field_type_name)
512 {
513 ctx.schema_coordinates.insert(field_type_name.to_string());
514 }
515 }
516 }
517 }
518 });
519 }
520 }
521}
522
523struct StripLiteralsTransformer {}
524
525impl<'a, T: Text<'a> + Clone> OperationTransformer<'a, T> for StripLiteralsTransformer {
526 fn transform_value(&mut self, node: &Value<'a, T>) -> TransformedValue<Value<'a, T>> {
527 match node {
528 Value::Float(_) => TransformedValue::Replace(Value::Float(0.0)),
529 Value::Int(_) => TransformedValue::Replace(Value::Int(Number::from(0))),
530 Value::String(_) => TransformedValue::Replace(Value::String(String::from(""))),
531 Value::Variable(_) => TransformedValue::Keep,
532 Value::Boolean(_) => TransformedValue::Keep,
533 Value::Null => TransformedValue::Keep,
534 Value::Enum(_) => TransformedValue::Keep,
535 Value::List(val) => {
536 let items: Vec<Value<'a, T>> = val
537 .iter()
538 .map(|item| self.transform_value(item).replace_or_else(|| item.clone()))
539 .collect();
540
541 TransformedValue::Replace(Value::List(items))
542 }
543 Value::Object(fields) => {
544 let fields: BTreeMap<T::Value, Value<'a, T>> = fields
545 .iter()
546 .map(|field| {
547 let (name, value) = field;
548 let new_value = self
549 .transform_value(value)
550 .replace_or_else(|| value.clone());
551 (name.clone(), new_value)
552 })
553 .collect();
554
555 TransformedValue::Replace(Value::Object(fields))
556 }
557 }
558 }
559
560 fn transform_field(
561 &mut self,
562 field: &graphql_tools::parser::query::Field<'a, T>,
563 ) -> Transformed<graphql_tools::parser::query::Selection<'a, T>> {
564 let selection_set = self.transform_selection_set(&field.selection_set);
565 let arguments = self.transform_arguments(&field.arguments);
566 let directives = self.transform_directives(&field.directives);
567
568 Transformed::Replace(Selection::Field(Field {
569 arguments: arguments.replace_or_else(|| field.arguments.clone()),
570 directives: directives.replace_or_else(|| field.directives.clone()),
571 selection_set: SelectionSet {
572 items: selection_set.replace_or_else(|| field.selection_set.items.clone()),
573 span: field.selection_set.span,
574 },
575 position: field.position,
576 alias: None,
577 name: field.name.clone(),
578 }))
579 }
580}
581
582#[derive(Hash, Eq, PartialEq, Clone, Copy)]
583pub struct PointerAddress(usize);
584
585impl PointerAddress {
586 pub fn new<T>(ptr: &T) -> Self {
587 let ptr_address: usize = unsafe { std::mem::transmute(ptr) };
588 Self(ptr_address)
589 }
590}
591
592type Seen<'s, T> = HashMap<PointerAddress, Transformed<Selection<'s, T>>>;
593
594pub struct SortSelectionsTransform<'s, T: Text<'s> + Clone> {
595 seen: Seen<'s, T>,
596}
597
598impl<'s, T: Text<'s> + Clone> Default for SortSelectionsTransform<'s, T> {
599 fn default() -> Self {
600 Self::new()
601 }
602}
603
604impl<'s, T: Text<'s> + Clone> SortSelectionsTransform<'s, T> {
605 pub fn new() -> Self {
606 Self {
607 seen: Default::default(),
608 }
609 }
610}
611
612impl<'s, T: Text<'s> + Clone> OperationTransformer<'s, T> for SortSelectionsTransform<'s, T> {
613 fn transform_document(
614 &mut self,
615 document: &Document<'s, T>,
616 ) -> TransformedValue<Document<'s, T>> {
617 let mut next_definitions = self
618 .transform_list(&document.definitions, Self::transform_definition)
619 .replace_or_else(|| document.definitions.to_vec());
620 next_definitions.sort_unstable_by(|a, b| self.compare_definitions(a, b));
621 TransformedValue::Replace(Document {
622 definitions: next_definitions,
623 })
624 }
625
626 fn transform_selection_set(
627 &mut self,
628 selections: &SelectionSet<'s, T>,
629 ) -> TransformedValue<Vec<Selection<'s, T>>> {
630 let mut next_selections = self
631 .transform_list(&selections.items, Self::transform_selection)
632 .replace_or_else(|| selections.items.to_vec());
633 next_selections.sort_unstable_by(|a, b| self.compare_selections(a, b));
634 TransformedValue::Replace(next_selections)
635 }
636
637 fn transform_directives(
638 &mut self,
639 directives: &[Directive<'s, T>],
640 ) -> TransformedValue<Vec<Directive<'s, T>>> {
641 let mut next_directives = self
642 .transform_list(directives, Self::transform_directive)
643 .replace_or_else(|| directives.to_vec());
644 next_directives.sort_unstable_by(|a, b| self.compare_directives(a, b));
645 TransformedValue::Replace(next_directives)
646 }
647
648 fn transform_arguments(
649 &mut self,
650 arguments: &[(T::Value, Value<'s, T>)],
651 ) -> TransformedValue<Vec<(T::Value, Value<'s, T>)>> {
652 let mut next_arguments = self
653 .transform_list(arguments, Self::transform_argument)
654 .replace_or_else(|| arguments.to_vec());
655 next_arguments.sort_unstable_by(|a, b| self.compare_arguments(a, b));
656 TransformedValue::Replace(next_arguments)
657 }
658
659 fn transform_variable_definitions(
660 &mut self,
661 variable_definitions: &Vec<VariableDefinition<'s, T>>,
662 ) -> TransformedValue<Vec<VariableDefinition<'s, T>>> {
663 let mut next_variable_definitions = self
664 .transform_list(variable_definitions, Self::transform_variable_definition)
665 .replace_or_else(|| variable_definitions.to_vec());
666 next_variable_definitions.sort_unstable_by(|a, b| self.compare_variable_definitions(a, b));
667 TransformedValue::Replace(next_variable_definitions)
668 }
669
670 fn transform_fragment(
671 &mut self,
672 fragment: &FragmentDefinition<'s, T>,
673 ) -> Transformed<FragmentDefinition<'s, T>> {
674 let mut directives = fragment.directives.clone();
675 directives.sort_unstable_by_key(|var| var.name.clone());
676
677 let selections = self.transform_selection_set(&fragment.selection_set);
678
679 Transformed::Replace(FragmentDefinition {
680 selection_set: SelectionSet {
681 items: selections.replace_or_else(|| fragment.selection_set.items.clone()),
682 span: fragment.selection_set.span,
683 },
684 directives,
685 name: fragment.name.clone(),
686 position: fragment.position,
687 type_condition: fragment.type_condition.clone(),
688 })
689 }
690
691 fn transform_selection(
692 &mut self,
693 selection: &Selection<'s, T>,
694 ) -> Transformed<Selection<'s, T>> {
695 match selection {
696 Selection::InlineFragment(selection) => {
697 let key = PointerAddress::new(selection);
698 if let Some(prev) = self.seen.get(&key) {
699 return prev.clone();
700 }
701 let transformed = self.transform_inline_fragment(selection);
702 self.seen.insert(key, transformed.clone());
703 transformed
704 }
705 Selection::Field(field) => {
706 let key = PointerAddress::new(field);
707 if let Some(prev) = self.seen.get(&key) {
708 return prev.clone();
709 }
710 let transformed = self.transform_field(field);
711 self.seen.insert(key, transformed.clone());
712 transformed
713 }
714 Selection::FragmentSpread(_) => Transformed::Keep,
715 }
716 }
717}
718
719impl<'s, T: Text<'s> + Clone> SortSelectionsTransform<'s, T> {
720 fn compare_definitions(&self, a: &Definition<'s, T>, b: &Definition<'s, T>) -> Ordering {
721 match (a, b) {
722 (Definition::Operation(_), Definition::Operation(_)) => Ordering::Equal,
724 (Definition::Fragment(a), Definition::Fragment(b)) => a.name.cmp(&b.name),
726 _ => definition_kind_ordering(a).cmp(&definition_kind_ordering(b)),
728 }
729 }
730
731 fn compare_selections(&self, a: &Selection<'s, T>, b: &Selection<'s, T>) -> Ordering {
732 match (a, b) {
733 (Selection::Field(a), Selection::Field(b)) => a.name.cmp(&b.name),
734 (Selection::FragmentSpread(a), Selection::FragmentSpread(b)) => {
735 a.fragment_name.cmp(&b.fragment_name)
736 }
737 _ => {
738 let a_ordering = selection_kind_ordering(a);
739 let b_ordering = selection_kind_ordering(b);
740 a_ordering.cmp(&b_ordering)
741 }
742 }
743 }
744 fn compare_directives(&self, a: &Directive<'s, T>, b: &Directive<'s, T>) -> Ordering {
745 a.name.cmp(&b.name)
746 }
747 fn compare_arguments(
748 &self,
749 a: &(T::Value, Value<'s, T>),
750 b: &(T::Value, Value<'s, T>),
751 ) -> Ordering {
752 a.0.cmp(&b.0)
753 }
754 fn compare_variable_definitions(
755 &self,
756 a: &VariableDefinition<'s, T>,
757 b: &VariableDefinition<'s, T>,
758 ) -> Ordering {
759 a.name.cmp(&b.name)
760 }
761}
762
763fn selection_kind_ordering<'s, T: Text<'s>>(selection: &Selection<'s, T>) -> u8 {
765 match selection {
766 Selection::FragmentSpread(_) => 1,
767 Selection::InlineFragment(_) => 2,
768 Selection::Field(_) => 3,
769 }
770}
771
772fn definition_kind_ordering<'a, T: Text<'a>>(definition: &Definition<'a, T>) -> u8 {
774 match definition {
775 Definition::Operation(_) => 1,
776 Definition::Fragment(_) => 2,
777 }
778}
779
780pub fn normalize_operation<'a>(operation_document: &Document<'a, String>) -> Document<'a, String> {
781 let mut strip_literals_transformer = StripLiteralsTransformer {};
782 let normalized = strip_literals_transformer
783 .transform_document(operation_document)
784 .replace_or_else(|| operation_document.clone());
785
786 SortSelectionsTransform::new()
787 .transform_document(&normalized)
788 .replace_or_else(|| normalized.clone())
789}
790
791#[derive(Clone)]
792pub struct ProcessedOperation {
793 pub operation: String,
794 pub hash: String,
795 pub coordinates: Vec<String>,
796}
797
798pub struct OperationProcessor {
799 cache: Cache<String, Option<ProcessedOperation>>,
800}
801
802impl Default for OperationProcessor {
803 fn default() -> Self {
804 Self::new()
805 }
806}
807
808impl OperationProcessor {
809 pub fn new() -> OperationProcessor {
810 OperationProcessor {
811 cache: Cache::new(1000),
812 }
813 }
814
815 pub fn process(
816 &self,
817 query: &str,
818 schema: &SchemaDocument<'static, String>,
819 ) -> Result<Option<ProcessedOperation>, String> {
820 if self.cache.contains_key(query) {
821 let entry = self
822 .cache
823 .get(query)
824 .expect("Unable to acquire Cache in OperationProcessor.process");
825 Ok(entry.clone())
826 } else {
827 let result = self.transform(query, schema)?;
828 self.cache.insert(query.to_string(), result.clone());
829 Ok(result)
830 }
831 }
832
833 fn transform(
834 &self,
835 operation: &str,
836 schema: &SchemaDocument<'static, String>,
837 ) -> Result<Option<ProcessedOperation>, String> {
838 let parsed = parse_query(operation)
839 .map_err(|e| e.to_string())?
840 .into_static();
841
842 let is_introspection = parsed.definitions.iter().find(|def| match def {
843 Definition::Operation(OperationDefinition::Query(query)) => query
844 .selection_set
845 .items
846 .iter()
847 .any(|selection| match selection {
848 Selection::Field(field) => field.name == "__schema" || field.name == "__type",
849 _ => false,
850 }),
851 _ => false,
852 });
853
854 if is_introspection.is_some() {
855 return Ok(None);
856 }
857
858 let schema_coordinates_result =
859 collect_schema_coordinates(&parsed, schema).map_err(|e| e.to_string())?;
860
861 let schema_coordinates: Vec<String> = Vec::from_iter(schema_coordinates_result);
862
863 let normalized = normalize_operation(&parsed);
864
865 let printed = minify_query_document(&normalized);
866 let hash = format!("{:x}", md5::compute(printed.clone()));
867
868 Ok(Some(ProcessedOperation {
869 operation: printed,
870 hash,
871 coordinates: schema_coordinates,
872 }))
873 }
874}
875
876#[cfg(test)]
877mod tests {
878 use std::collections::HashSet;
879
880 use graphql_tools::parser::parse_query;
881 use graphql_tools::parser::parse_schema;
882
883 use super::collect_schema_coordinates;
884
885 const SCHEMA_SDL: &str = "
886 type Query {
887 project(selector: ProjectSelectorInput!): Project
888 projectsByType(type: ProjectType!): [Project!]!
889 projectsByTypes(types: [ ProjectType!]!): [Project!]!
890 projects(filter: FilterInput, and: [FilterInput!]): [Project!]!
891 projectsByMetadata(metadata: JSON): [Project!]!
892 }
893
894 type Mutation {
895 deleteProject(selector: ProjectSelectorInput!): DeleteProjectPayload!
896 }
897
898 input ProjectSelectorInput {
899 organization: ID!
900 project: ID!
901 }
902
903 input FilterInput {
904 type: ProjectType
905 pagination: PaginationInput
906 order: [ProjectOrderByInput!]
907 metadata: JSON
908 }
909
910 input PaginationInput {
911 limit: Int
912 offset: Int
913 }
914
915 input ProjectOrderByInput {
916 field: String!
917 direction: OrderDirection
918 }
919
920 enum OrderDirection {
921 ASC
922 DESC
923 }
924
925 type ProjectSelector {
926 organization: ID!
927 project: ID!
928 }
929
930 type DeleteProjectPayload {
931 selector: ProjectSelector!
932 deletedProject: Project!
933 }
934
935 type Project {
936 id: ID!
937 cleanId: ID!
938 name: String!
939 type: ProjectType!
940 buildUrl: String
941 validationUrl: String
942 }
943
944 enum ProjectType {
945 FEDERATION
946 STITCHING
947 SINGLE
948 }
949
950 scalar JSON
951 ";
952
953 #[test]
954 fn basic_test() {
955 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
956
957 let document = parse_query::<String>(
958 "
959 mutation deleteProjectOperation($selector: ProjectSelectorInput!) {
960 deleteProject(selector: $selector) {
961 selector {
962 organization
963 project
964 }
965 deletedProject {
966 ...ProjectFields
967 }
968 }
969 }
970 fragment ProjectFields on Project {
971 id
972 cleanId
973 name
974 type
975 }
976 ",
977 )
978 .unwrap();
979
980 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
981
982 let expected = vec![
983 "Mutation.deleteProject",
984 "Mutation.deleteProject.selector",
985 "DeleteProjectPayload.selector",
986 "ProjectSelector.organization",
987 "ProjectSelector.project",
988 "DeleteProjectPayload.deletedProject",
989 "ID",
990 "Project.id",
991 "Project.cleanId",
992 "Project.name",
993 "Project.type",
994 "ProjectType.FEDERATION",
995 "ProjectType.STITCHING",
996 "ProjectType.SINGLE",
997 "ProjectSelectorInput.organization",
998 "ProjectSelectorInput.project",
999 ]
1000 .into_iter()
1001 .map(|s| s.to_string())
1002 .collect::<HashSet<String>>();
1003
1004 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1005 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1006
1007 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1008 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1009 }
1010
1011 #[test]
1012 fn entire_input() {
1013 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1014 let document = parse_query::<String>(
1015 "
1016 query projects($filter: FilterInput) {
1017 projects(filter: $filter) {
1018 name
1019 }
1020 }
1021 ",
1022 )
1023 .unwrap();
1024
1025 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1026
1027 let expected = vec![
1028 "Query.projects",
1029 "Query.projects.filter",
1030 "Project.name",
1031 "FilterInput.type",
1032 "ProjectType.FEDERATION",
1033 "ProjectType.STITCHING",
1034 "ProjectType.SINGLE",
1035 "FilterInput.pagination",
1036 "PaginationInput.limit",
1037 "Int",
1038 "PaginationInput.offset",
1039 "FilterInput.metadata",
1040 "FilterInput.order",
1041 "ProjectOrderByInput.field",
1042 "String",
1043 "ProjectOrderByInput.direction",
1044 "OrderDirection.ASC",
1045 "OrderDirection.DESC",
1046 "JSON",
1047 ]
1048 .into_iter()
1049 .map(|s| s.to_string())
1050 .collect::<HashSet<String>>();
1051
1052 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1053 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1054
1055 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1056 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1057 }
1058
1059 #[test]
1060 fn entire_input_list() {
1061 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1062 let document = parse_query::<String>(
1063 "
1064 query projects($filter: FilterInput) {
1065 projects(and: $filter) {
1066 name
1067 }
1068 }
1069 ",
1070 )
1071 .unwrap();
1072
1073 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1074
1075 let expected = vec![
1076 "Query.projects",
1077 "Query.projects.and",
1078 "Project.name",
1079 "FilterInput.type",
1080 "ProjectType.FEDERATION",
1081 "ProjectType.STITCHING",
1082 "ProjectType.SINGLE",
1083 "FilterInput.pagination",
1084 "FilterInput.metadata",
1085 "PaginationInput.limit",
1086 "Int",
1087 "PaginationInput.offset",
1088 "FilterInput.order",
1089 "ProjectOrderByInput.field",
1090 "String",
1091 "ProjectOrderByInput.direction",
1092 "OrderDirection.ASC",
1093 "OrderDirection.DESC",
1094 "JSON",
1095 ]
1096 .into_iter()
1097 .map(|s| s.to_string())
1098 .collect::<HashSet<String>>();
1099
1100 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1101 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1102
1103 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1104 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1105 }
1106
1107 #[test]
1108 fn entire_input_and_enum_value() {
1109 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1110 let document = parse_query::<String>(
1111 "
1112 query getProjects($pagination: PaginationInput) {
1113 projects(and: { pagination: $pagination, type: FEDERATION }) {
1114 name
1115 }
1116 }
1117 ",
1118 )
1119 .unwrap();
1120
1121 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1122
1123 let expected = vec![
1124 "Query.projects",
1125 "Query.projects.and",
1126 "Query.projects.and!",
1127 "Project.name",
1128 "PaginationInput.limit",
1129 "Int",
1130 "PaginationInput.offset",
1131 "FilterInput.pagination",
1132 "FilterInput.type",
1133 "FilterInput.type!",
1134 "ProjectType.FEDERATION",
1135 ]
1136 .into_iter()
1137 .map(|s| s.to_string())
1138 .collect::<HashSet<String>>();
1139
1140 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1141 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1142
1143 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1144 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1145 }
1146
1147 #[test]
1148 fn enum_value_list() {
1149 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1150 let document = parse_query::<String>(
1151 "
1152 query getProjects {
1153 projectsByTypes(types: [FEDERATION, STITCHING]) {
1154 name
1155 }
1156 }
1157 ",
1158 )
1159 .unwrap();
1160
1161 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1162
1163 let expected = vec![
1164 "Query.projectsByTypes",
1165 "Query.projectsByTypes.types",
1166 "Query.projectsByTypes.types!",
1167 "Project.name",
1168 "ProjectType.FEDERATION",
1169 "ProjectType.STITCHING",
1170 ]
1171 .into_iter()
1172 .map(|s| s.to_string())
1173 .collect::<HashSet<String>>();
1174
1175 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1176 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1177
1178 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1179 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1180 }
1181
1182 #[test]
1183 fn enums_and_scalars_input() {
1184 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1185 let document = parse_query::<String>(
1186 "
1187 query getProjects($limit: Int!, $type: ProjectType!) {
1188 projects(filter: { pagination: { limit: $limit }, type: $type }) {
1189 id
1190 }
1191 }
1192 ",
1193 )
1194 .unwrap();
1195
1196 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1197
1198 let expected = vec![
1199 "Query.projects",
1200 "Query.projects.filter",
1201 "Query.projects.filter!",
1202 "Project.id",
1203 "Int",
1204 "ProjectType.FEDERATION",
1205 "ProjectType.STITCHING",
1206 "ProjectType.SINGLE",
1207 "FilterInput.pagination",
1208 "FilterInput.pagination!",
1209 "FilterInput.type",
1210 "PaginationInput.limit",
1211 ]
1212 .into_iter()
1213 .map(|s| s.to_string())
1214 .collect::<HashSet<String>>();
1215
1216 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1217 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1218
1219 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1220 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1221 }
1222
1223 #[test]
1224 fn hard_coded_scalars_input() {
1225 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1226 let document = parse_query::<String>(
1227 "
1228 {
1229 projects(filter: { pagination: { limit: 20 } }) {
1230 id
1231 }
1232 }
1233 ",
1234 )
1235 .unwrap();
1236
1237 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1238
1239 let expected = vec![
1240 "Query.projects",
1241 "Query.projects.filter",
1242 "Query.projects.filter!",
1243 "Project.id",
1244 "FilterInput.pagination",
1245 "FilterInput.pagination!",
1246 "Int",
1247 "PaginationInput.limit",
1248 "PaginationInput.limit!",
1249 ]
1250 .into_iter()
1251 .map(|s| s.to_string())
1252 .collect::<HashSet<String>>();
1253
1254 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1255 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1256
1257 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1258 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1259 }
1260
1261 #[test]
1262 fn enum_values_object_field() {
1263 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1264 let document = parse_query::<String>(
1265 "
1266 query getProjects($limit: Int!) {
1267 projects(filter: { pagination: { limit: $limit }, type: FEDERATION }) {
1268 id
1269 }
1270 }
1271 ",
1272 )
1273 .unwrap();
1274
1275 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1276
1277 let expected = vec![
1278 "Query.projects",
1279 "Query.projects.filter",
1280 "Query.projects.filter!",
1281 "Project.id",
1282 "Int",
1283 "FilterInput.pagination",
1284 "FilterInput.pagination!",
1285 "FilterInput.type",
1286 "FilterInput.type!",
1287 "PaginationInput.limit",
1288 "ProjectType.FEDERATION",
1289 ]
1290 .into_iter()
1291 .map(|s| s.to_string())
1292 .collect::<HashSet<String>>();
1293
1294 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1295 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1296
1297 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1298 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1299 }
1300
1301 #[test]
1302 fn enum_list_inline() {
1303 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1304 let document = parse_query::<String>(
1305 "
1306 query getProjects {
1307 projectsByTypes(types: [FEDERATION]) {
1308 id
1309 }
1310 }
1311 ",
1312 )
1313 .unwrap();
1314
1315 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1316
1317 let expected = vec![
1318 "Query.projectsByTypes",
1319 "Query.projectsByTypes.types",
1320 "Query.projectsByTypes.types!",
1321 "Project.id",
1322 "ProjectType.FEDERATION",
1323 ]
1324 .into_iter()
1325 .map(|s| s.to_string())
1326 .collect::<HashSet<String>>();
1327
1328 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1329 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1330
1331 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1332 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1333 }
1334
1335 #[test]
1336 fn enum_list_variable() {
1337 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1338 let document_inline = parse_query::<String>(
1339 "
1340 query getProjects($types: [ProjectType!]!) {
1341 projectsByTypes(types: $types) {
1342 id
1343 }
1344 }
1345 ",
1346 )
1347 .unwrap();
1348
1349 let schema_coordinates = collect_schema_coordinates(&document_inline, &schema).unwrap();
1350
1351 let expected = vec![
1352 "Query.projectsByTypes",
1353 "Query.projectsByTypes.types",
1354 "Project.id",
1355 "ProjectType.FEDERATION",
1356 "ProjectType.STITCHING",
1357 "ProjectType.SINGLE",
1358 ]
1359 .into_iter()
1360 .map(|s| s.to_string())
1361 .collect::<HashSet<String>>();
1362
1363 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1364 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1365
1366 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1367 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1368 }
1369
1370 #[test]
1371 fn enum_values_argument() {
1372 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1373 let document = parse_query::<String>(
1374 "
1375 query getProjects {
1376 projectsByType(type: FEDERATION) {
1377 id
1378 }
1379 }
1380 ",
1381 )
1382 .unwrap();
1383
1384 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1385
1386 let expected = vec![
1387 "Query.projectsByType",
1388 "Query.projectsByType.type",
1389 "Query.projectsByType.type!",
1390 "Project.id",
1391 "ProjectType.FEDERATION",
1392 ]
1393 .into_iter()
1394 .map(|s| s.to_string())
1395 .collect::<HashSet<String>>();
1396
1397 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1398 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1399
1400 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1401 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1402 }
1403
1404 #[test]
1405 fn arguments() {
1406 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1407 let document = parse_query::<String>(
1408 "
1409 query getProjects($limit: Int!, $type: ProjectType!) {
1410 projects(filter: { pagination: { limit: $limit }, type: $type }) {
1411 id
1412 }
1413 }
1414 ",
1415 )
1416 .unwrap();
1417
1418 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1419
1420 let expected = vec![
1421 "Query.projects",
1422 "Query.projects.filter",
1423 "Query.projects.filter!",
1424 "Project.id",
1425 "Int",
1426 "ProjectType.FEDERATION",
1427 "ProjectType.STITCHING",
1428 "ProjectType.SINGLE",
1429 "FilterInput.pagination",
1430 "FilterInput.pagination!",
1431 "FilterInput.type",
1432 "PaginationInput.limit",
1433 ]
1434 .into_iter()
1435 .map(|s| s.to_string())
1436 .collect::<HashSet<String>>();
1437
1438 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1439 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1440
1441 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1442 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1443 }
1444
1445 #[test]
1446 fn skips_argument_directives() {
1447 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1448 let document = parse_query::<String>(
1449 "
1450 query getProjects($limit: Int!, $type: ProjectType!, $includeName: Boolean!) {
1451 projects(filter: { pagination: { limit: $limit }, type: $type }) {
1452 id
1453 ...NestedFragment
1454 }
1455 }
1456
1457 fragment NestedFragment on Project {
1458 ...IncludeNameFragment @include(if: $includeName)
1459 }
1460
1461 fragment IncludeNameFragment on Project {
1462 name
1463 }
1464 ",
1465 )
1466 .unwrap();
1467
1468 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1469
1470 let expected = vec![
1471 "Query.projects",
1472 "Query.projects.filter",
1473 "Query.projects.filter!",
1474 "Project.id",
1475 "Project.name",
1476 "Int",
1477 "ProjectType.FEDERATION",
1478 "ProjectType.STITCHING",
1479 "ProjectType.SINGLE",
1480 "Boolean",
1481 "FilterInput.pagination",
1482 "FilterInput.pagination!",
1483 "FilterInput.type",
1484 "PaginationInput.limit",
1485 ]
1486 .into_iter()
1487 .map(|s| s.to_string())
1488 .collect::<HashSet<String>>();
1489
1490 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1491 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1492
1493 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1494 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1495 }
1496
1497 #[test]
1498 fn used_only_input_fields() {
1499 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1500 let document = parse_query::<String>(
1501 "
1502 query getProjects($limit: Int!, $type: ProjectType!) {
1503 projects(filter: {
1504 pagination: { limit: $limit },
1505 type: $type
1506 }) {
1507 id
1508 }
1509 }
1510 ",
1511 )
1512 .unwrap();
1513
1514 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1515
1516 let expected = vec![
1517 "Query.projects",
1518 "Query.projects.filter",
1519 "Query.projects.filter!",
1520 "Project.id",
1521 "Int",
1522 "ProjectType.FEDERATION",
1523 "ProjectType.STITCHING",
1524 "ProjectType.SINGLE",
1525 "FilterInput.pagination",
1526 "FilterInput.pagination!",
1527 "FilterInput.type",
1528 "PaginationInput.limit",
1529 ]
1530 .into_iter()
1531 .map(|s| s.to_string())
1532 .collect::<HashSet<String>>();
1533
1534 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1535 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1536
1537 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1538 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1539 }
1540
1541 #[test]
1542 fn input_object_mixed() {
1543 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1544 let document = parse_query::<String>(
1545 "
1546 query getProjects($pagination: PaginationInput!, $type: ProjectType!) {
1547 projects(filter: { pagination: $pagination, type: $type }) {
1548 id
1549 }
1550 }
1551 ",
1552 )
1553 .unwrap();
1554
1555 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1556
1557 let expected = vec![
1558 "Query.projects",
1559 "Query.projects.filter",
1560 "Query.projects.filter!",
1561 "Project.id",
1562 "PaginationInput.limit",
1563 "Int",
1564 "PaginationInput.offset",
1565 "ProjectType.FEDERATION",
1566 "ProjectType.STITCHING",
1567 "ProjectType.SINGLE",
1568 "FilterInput.pagination",
1569 "FilterInput.type",
1570 ]
1571 .into_iter()
1572 .map(|s| s.to_string())
1573 .collect::<HashSet<String>>();
1574
1575 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1576 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1577
1578 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1579 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1580 }
1581
1582 #[test]
1583 fn custom_scalar_as_argument_inlined() {
1584 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1585 let document = parse_query::<String>(
1586 "
1587 query getProjects {
1588 projectsByMetadata(metadata: { key: { value: \"value\" } }) {
1589 name
1590 }
1591 }
1592 ",
1593 )
1594 .unwrap();
1595
1596 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1597
1598 let expected = vec![
1599 "Query.projectsByMetadata",
1600 "Query.projectsByMetadata.metadata",
1601 "Query.projectsByMetadata.metadata!",
1602 "Project.name",
1603 "JSON",
1604 ]
1605 .into_iter()
1606 .map(|s| s.to_string())
1607 .collect::<HashSet<String>>();
1608
1609 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1610 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1611
1612 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1613 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1614 }
1615
1616 #[test]
1617 fn custom_scalar_as_argument_variable() {
1618 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1619 let document = parse_query::<String>(
1620 "
1621 query getProjects($metadata: JSON) {
1622 projectsByMetadata(metadata: $metadata) {
1623 name
1624 }
1625 }
1626 ",
1627 )
1628 .unwrap();
1629
1630 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1631
1632 let expected = vec![
1633 "Query.projectsByMetadata",
1634 "Query.projectsByMetadata.metadata",
1635 "Project.name",
1636 "JSON",
1637 ]
1638 .into_iter()
1639 .map(|s| s.to_string())
1640 .collect::<HashSet<String>>();
1641
1642 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1643 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1644
1645 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1646 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1647 }
1648
1649 #[test]
1650 fn custom_scalar_as_argument_variable_with_default() {
1651 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1652 let document = parse_query::<String>(
1653 "
1654 query getProjects($metadata: JSON = { key: { value: \"value\" } }) {
1655 projectsByMetadata(metadata: $metadata) {
1656 name
1657 }
1658 }
1659 ",
1660 )
1661 .unwrap();
1662
1663 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1664
1665 let expected = vec![
1666 "Query.projectsByMetadata",
1667 "Query.projectsByMetadata.metadata",
1668 "Query.projectsByMetadata.metadata!",
1669 "Project.name",
1670 "JSON",
1671 ]
1672 .into_iter()
1673 .map(|s| s.to_string())
1674 .collect::<HashSet<String>>();
1675
1676 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1677 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1678
1679 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1680 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1681 }
1682
1683 #[test]
1684 fn custom_scalar_as_input_field_inlined() {
1685 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1686 let document = parse_query::<String>(
1687 "
1688 query getProjects {
1689 projects(filter: { metadata: { key: \"value\" } }) {
1690 name
1691 }
1692 }
1693 ",
1694 )
1695 .unwrap();
1696
1697 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1698
1699 let expected = vec![
1700 "Query.projects",
1701 "Query.projects.filter",
1702 "Query.projects.filter!",
1703 "FilterInput.metadata",
1704 "FilterInput.metadata!",
1705 "Project.name",
1706 "JSON",
1707 ]
1708 .into_iter()
1709 .map(|s| s.to_string())
1710 .collect::<HashSet<String>>();
1711
1712 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1713 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1714
1715 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1716 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1717 }
1718
1719 #[test]
1720 fn custom_scalar_as_input_field_variable() {
1721 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1722 let document = parse_query::<String>(
1723 "
1724 query getProjects($metadata: JSON) {
1725 projects(filter: { metadata: $metadata }) {
1726 name
1727 }
1728 }
1729 ",
1730 )
1731 .unwrap();
1732
1733 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1734
1735 let expected = vec![
1736 "Query.projects",
1737 "Query.projects.filter",
1738 "Query.projects.filter!",
1739 "FilterInput.metadata",
1740 "Project.name",
1741 "JSON",
1742 ]
1743 .into_iter()
1744 .map(|s| s.to_string())
1745 .collect::<HashSet<String>>();
1746
1747 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1748 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1749
1750 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1751 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1752 }
1753
1754 #[test]
1755 fn custom_scalar_as_input_field_variable_with_default() {
1756 let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
1757 let document = parse_query::<String>(
1758 "
1759 query getProjects($metadata: JSON = { key: { value: \"value\" } }) {
1760 projects(filter: { metadata: $metadata }) {
1761 name
1762 }
1763 }
1764 ",
1765 )
1766 .unwrap();
1767
1768 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1769
1770 let expected = vec![
1771 "Query.projects",
1772 "Query.projects.filter",
1773 "Query.projects.filter!",
1774 "FilterInput.metadata",
1775 "FilterInput.metadata!",
1776 "Project.name",
1777 "JSON",
1778 ]
1779 .into_iter()
1780 .map(|s| s.to_string())
1781 .collect::<HashSet<String>>();
1782
1783 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1784 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1785
1786 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1787 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1788 }
1789
1790 #[test]
1791 fn primitive_field_with_arg_schema_coor() {
1792 let schema = parse_schema::<String>(
1793 "type Query {
1794 hello(message: String): String
1795 }",
1796 )
1797 .unwrap();
1798 let document = parse_query::<String>(
1799 "
1800 query {
1801 hello(message: \"world\")
1802 }
1803 ",
1804 )
1805 .unwrap();
1806
1807 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1808 let expected = vec![
1809 "Query.hello",
1810 "Query.hello.message!",
1811 "Query.hello.message",
1812 "String",
1813 ]
1814 .into_iter()
1815 .map(|s| s.to_string())
1816 .collect::<HashSet<String>>();
1817
1818 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1819 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1820
1821 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1822 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1823 }
1824
1825 #[test]
1826 fn unused_variable_as_nullable_argument() {
1827 let schema = parse_schema::<String>(
1828 "
1829 type Query {
1830 random(a: String): String
1831 }
1832 ",
1833 )
1834 .unwrap();
1835 let document = parse_query::<String>(
1836 "
1837 query Foo($a: String) {
1838 random(a: $a)
1839 }
1840 ",
1841 )
1842 .unwrap();
1843
1844 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1845 let expected = vec!["Query.random", "Query.random.a", "String"]
1846 .into_iter()
1847 .map(|s| s.to_string())
1848 .collect::<HashSet<String>>();
1849
1850 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1851 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1852
1853 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1854 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1855 }
1856
1857 #[test]
1858 fn unused_nullable_input_field() {
1859 let schema = parse_schema::<String>(
1860 "
1861 type Query {
1862 random(a: A): String
1863 }
1864 input A {
1865 b: B
1866 }
1867 input B {
1868 c: C
1869 }
1870 input C {
1871 d: String
1872 }
1873 ",
1874 )
1875 .unwrap();
1876 let document = parse_query::<String>(
1877 "
1878 query Foo {
1879 random(a: { b: null })
1880 }
1881 ",
1882 )
1883 .unwrap();
1884
1885 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1886 let expected = vec![
1887 "Query.random",
1888 "Query.random.a",
1889 "Query.random.a!",
1890 "A.b",
1891 "B.c",
1892 "C.d",
1893 "String",
1894 ]
1895 .into_iter()
1896 .map(|s| s.to_string())
1897 .collect::<HashSet<String>>();
1898
1899 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1900 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1901
1902 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1903 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1904 }
1905
1906 #[test]
1907 fn required_variable_as_input_field() {
1908 let schema = parse_schema::<String>(
1909 "
1910 type Query {
1911 random(a: A): String
1912 }
1913 input A {
1914 b: String
1915 }
1916 ",
1917 )
1918 .unwrap();
1919 let document = parse_query::<String>(
1920 "
1921 query Foo($b:String! = \"b\") {
1922 random(a: { b: $b })
1923 }
1924 ",
1925 )
1926 .unwrap();
1927
1928 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1929 let expected = vec![
1930 "Query.random",
1931 "Query.random.a",
1932 "Query.random.a!",
1933 "A.b",
1934 "A.b!",
1935 "String",
1936 ]
1937 .into_iter()
1938 .map(|s| s.to_string())
1939 .collect::<HashSet<String>>();
1940
1941 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1942 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1943
1944 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1945 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1946 }
1947
1948 #[test]
1949 fn undefined_variable_as_input_field() {
1950 let schema = parse_schema::<String>(
1951 "
1952 type Query {
1953 random(a: A): String
1954 }
1955 input A {
1956 b: String
1957 }
1958 ",
1959 )
1960 .unwrap();
1961 let document = parse_query::<String>(
1962 "
1963 query Foo($b: String!) {
1964 random(a: { b: $b })
1965 }
1966 ",
1967 )
1968 .unwrap();
1969
1970 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
1971 let expected = vec![
1972 "Query.random",
1973 "Query.random.a",
1974 "Query.random.a!",
1975 "A.b",
1976 "String",
1977 ]
1978 .into_iter()
1979 .map(|s| s.to_string())
1980 .collect::<HashSet<String>>();
1981
1982 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
1983 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
1984
1985 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
1986 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
1987 }
1988
1989 #[test]
1990 fn deeply_nested_variables() {
1991 let schema = parse_schema::<String>(
1992 "
1993 type Query {
1994 random(a: A): String
1995 }
1996 input A {
1997 b: B
1998 }
1999 input B {
2000 c: C
2001 }
2002 input C {
2003 d: String
2004 }
2005 ",
2006 )
2007 .unwrap();
2008 let document = parse_query::<String>(
2009 "
2010 query Random($a: A = { b: { c: { d: \"D\" } } }) {
2011 random(a: $a)
2012 }
2013 ",
2014 )
2015 .unwrap();
2016
2017 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2018 let expected = vec![
2019 "Query.random",
2020 "Query.random.a",
2021 "Query.random.a!",
2022 "A.b",
2023 "A.b!",
2024 "B.c",
2025 "B.c!",
2026 "C.d",
2027 "C.d!",
2028 "String",
2029 ]
2030 .into_iter()
2031 .map(|s| s.to_string())
2032 .collect::<HashSet<String>>();
2033
2034 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2035 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2036
2037 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2038 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2039 }
2040
2041 #[test]
2042 fn aliased_field() {
2043 let schema = parse_schema::<String>(
2044 "
2045 type Query {
2046 random(a: String): String
2047 }
2048 input C {
2049 d: String
2050 }
2051 ",
2052 )
2053 .unwrap();
2054 let document = parse_query::<String>(
2055 "
2056 query Random($a: String= \"B\" ) {
2057 foo: random(a: $a )
2058 }
2059 ",
2060 )
2061 .unwrap();
2062
2063 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2064 let expected = vec![
2065 "Query.random",
2066 "Query.random.a",
2067 "Query.random.a!",
2068 "String",
2069 ]
2070 .into_iter()
2071 .map(|s| s.to_string())
2072 .collect::<HashSet<String>>();
2073
2074 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2075 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2076
2077 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2078 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2079 }
2080
2081 #[test]
2082 fn multiple_fields_with_mixed_nullability() {
2083 let schema = parse_schema::<String>(
2084 "
2085 type Query {
2086 random(a: String): String
2087 }
2088 input C {
2089 d: String
2090 }
2091 ",
2092 )
2093 .unwrap();
2094 let document = parse_query::<String>(
2095 "
2096 query Random($a: String = null) {
2097 nullable: random(a: $a)
2098 nonnullable: random(a: \"B\")
2099 }
2100 ",
2101 )
2102 .unwrap();
2103
2104 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2105 let expected = vec![
2106 "Query.random",
2107 "Query.random.a",
2108 "Query.random.a!",
2109 "String",
2110 ]
2111 .into_iter()
2112 .map(|s| s.to_string())
2113 .collect::<HashSet<String>>();
2114
2115 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2116 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2117
2118 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2119 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2120 }
2121
2122 #[test]
2123 fn nonnull_and_default_arguments() {
2124 let schema = parse_schema::<String>(
2125 "
2126 type Query {
2127 user(id: ID!, name: String): User
2128 }
2129
2130 type User {
2131 id: ID!
2132 name: String
2133 }
2134 ",
2135 )
2136 .unwrap();
2137 let document = parse_query::<String>(
2138 "
2139 query($id: ID! = \"123\") {
2140 user(id: $id) { name }
2141 }
2142 ",
2143 )
2144 .unwrap();
2145
2146 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2147 let expected = vec![
2148 "User.name",
2149 "Query.user",
2150 "ID",
2151 "Query.user.id!",
2152 "Query.user.id",
2153 ]
2154 .into_iter()
2155 .map(|s| s.to_string())
2156 .collect::<HashSet<String>>();
2157
2158 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2159 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2160
2161 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2162 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2163 }
2164
2165 #[test]
2166 fn default_nullable_arguments() {
2167 let schema = parse_schema::<String>(
2168 "
2169 type Query {
2170 user(id: ID!, name: String): User
2171 }
2172
2173 type User {
2174 id: ID!
2175 name: String
2176 }
2177 ",
2178 )
2179 .unwrap();
2180 let document = parse_query::<String>(
2181 "
2182 query($name: String = \"John\") {
2183 user(id: \"fixed\", name: $name) { id }
2184 }
2185 ",
2186 )
2187 .unwrap();
2188
2189 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2190 let expected = vec![
2191 "User.id",
2192 "Query.user",
2193 "ID",
2194 "Query.user.id!",
2195 "Query.user.id",
2196 "Query.user.name!",
2197 "Query.user.name",
2198 "String",
2199 ]
2200 .into_iter()
2201 .map(|s| s.to_string())
2202 .collect::<HashSet<String>>();
2203
2204 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2205 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2206
2207 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2208 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2209 }
2210
2211 #[test]
2212 fn non_null_no_default_arguments() {
2213 let schema = parse_schema::<String>(
2214 "
2215 type Query {
2216 user(id: ID!, name: String): User
2217 }
2218
2219 type User {
2220 id: ID!
2221 name: String
2222 }
2223 ",
2224 )
2225 .unwrap();
2226 let document = parse_query::<String>(
2227 "
2228 query($id: ID!) {
2229 user(id: $id) { name }
2230 }
2231 ",
2232 )
2233 .unwrap();
2234
2235 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2236 let expected = vec!["User.name", "Query.user", "ID", "Query.user.id"]
2237 .into_iter()
2238 .map(|s| s.to_string())
2239 .collect::<HashSet<String>>();
2240
2241 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2242 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2243
2244 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2245 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2246 }
2247
2248 #[test]
2249 fn fixed_arguments() {
2250 let schema = parse_schema::<String>(
2251 "
2252 type Query {
2253 user(id: ID!, name: String): User
2254 }
2255
2256 type User {
2257 id: ID!
2258 name: String
2259 }
2260 ",
2261 )
2262 .unwrap();
2263 let document = parse_query::<String>(
2264 "
2265 query($name: String) {
2266 user(id: \"fixed\", name: $name) { id }
2267 }
2268 ",
2269 )
2270 .unwrap();
2271
2272 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2273 let expected = vec![
2274 "User.id",
2275 "Query.user",
2276 "ID",
2277 "Query.user.id!",
2278 "Query.user.id",
2279 "Query.user.name",
2280 "String",
2281 ]
2282 .into_iter()
2283 .map(|s| s.to_string())
2284 .collect::<HashSet<String>>();
2285
2286 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2287 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2288
2289 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2290 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2291 }
2292
2293 #[test]
2294 fn recursive_fragments() {
2295 let schema = parse_schema::<String>(
2296 "
2297 type Query {
2298 user(id: ID!): User
2299 }
2300 type User {
2301 id: ID!
2302 friends: [User!]!
2303 }
2304 ",
2305 )
2306 .unwrap();
2307 let document = parse_query::<String>(
2308 "
2309 query UserQuery($id: ID!) {
2310 user(id: $id) {
2311 ...UserFragment
2312 }
2313 }
2314 fragment UserFragment on User {
2315 id
2316 friends {
2317 ...UserFragment
2318 }
2319 }
2320 ",
2321 )
2322 .unwrap();
2323
2324 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2325
2326 let expected = vec![
2327 "Query.user",
2328 "Query.user.id",
2329 "User.id",
2330 "User.friends",
2331 "ID",
2332 ]
2333 .into_iter()
2334 .map(|s| s.to_string())
2335 .collect::<HashSet<String>>();
2336
2337 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2338 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2339
2340 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2341 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2342 }
2343
2344 #[test]
2345 fn recursive_input_types() {
2346 let schema = parse_schema::<String>(
2347 "
2348 type Query {
2349 node(id: ID!): Node
2350 }
2351
2352 type Mutation {
2353 createNode(input: NodeInput!): Node
2354 }
2355 input NodeInput {
2356 name: String!
2357 parent: NodeInput
2358 }
2359 type Node {
2360 id: ID!
2361 name: String!
2362 parent: Node
2363 }
2364 ",
2365 )
2366 .unwrap();
2367 let document = parse_query::<String>(
2368 "
2369 mutation CreateNode($input: NodeInput!) {
2370 createNode(input: $input) {
2371 id
2372 name
2373 }
2374 }
2375 ",
2376 )
2377 .unwrap();
2378
2379 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2380 let expected = vec![
2381 "Mutation.createNode",
2382 "Mutation.createNode.input",
2383 "Node.id",
2384 "Node.name",
2385 "NodeInput.name",
2386 "NodeInput.parent",
2387 "String",
2388 ]
2389 .into_iter()
2390 .map(|s| s.to_string())
2391 .collect::<HashSet<String>>();
2392
2393 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2394 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2395
2396 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2397 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2398 }
2399
2400 #[test]
2401 fn recursive_null_input() {
2402 let schema = parse_schema::<String>(
2403 "
2404 type Query {
2405 someField(input: RecursiveInput!): SomeType
2406 }
2407 input RecursiveInput {
2408 field: String
2409 nested: RecursiveInput
2410 }
2411 type SomeType {
2412 id: ID!
2413 }
2414 ",
2415 )
2416 .unwrap();
2417 let document = parse_query::<String>(
2418 "
2419 query MyQuery {
2420 someField(input: { nested: null }) { id }
2421 }
2422 ",
2423 )
2424 .unwrap();
2425
2426 let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
2427 let expected = vec![
2428 "Query.someField",
2429 "Query.someField.input",
2430 "Query.someField.input!",
2431 "SomeType.id",
2432 "RecursiveInput.field",
2433 "RecursiveInput.nested",
2434 "String",
2435 ]
2436 .into_iter()
2437 .map(|s| s.to_string())
2438 .collect::<HashSet<String>>();
2439
2440 let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
2441 let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
2442
2443 assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
2444 assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
2445 }
2446}