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