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