Skip to main content

hive_console_sdk/agent/
utils.rs

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