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