hive_console_sdk/
graphql.rs

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