Skip to main content

bluejay_validator/executable/operation/analyzers/
complexity_cost.rs

1use crate::executable::{
2    operation::{Analyzer, VariableValues, Visitor},
3    Cache,
4};
5use bluejay_core::definition::{
6    FieldDefinition, ObjectTypeDefinition, OutputType, SchemaDefinition, TypeDefinition,
7    TypeDefinitionReference, UnionMemberType, UnionTypeDefinition,
8};
9use bluejay_core::executable::{ExecutableDocument, Field};
10use bluejay_core::AsIter;
11use itertools::{Either, Itertools};
12use std::cmp::max;
13use std::collections::HashMap;
14
15mod arena;
16use arena::{Arena, NodeId};
17
18mod cost_computer;
19pub use cost_computer::{CostComputer, DefaultCostComputer, FieldMultipliers};
20
21mod relay_cost_computer;
22pub use relay_cost_computer::RelayCostComputer;
23
24pub struct ComplexityCost<
25    'a,
26    E: ExecutableDocument,
27    S: SchemaDefinition,
28    V: VariableValues,
29    C: CostComputer<'a, E, S, V> = DefaultCostComputer,
30> {
31    schema_definition: &'a S,
32    cost_computer: C,
33    scopes_arena: Arena<ComplexityScope<'a, S::TypeDefinition, C::FieldMultipliers>>,
34    scopes_stack: Vec<Option<NodeId>>,
35}
36
37impl<
38        'a,
39        E: ExecutableDocument,
40        S: SchemaDefinition,
41        V: VariableValues,
42        C: CostComputer<'a, E, S, V>,
43    > Visitor<'a, E, S, V> for ComplexityCost<'a, E, S, V, C>
44{
45    type ExtraInfo = ();
46    fn new(
47        operation_definition: &'a E::OperationDefinition,
48        schema_definition: &'a S,
49        variable_values: &'a V,
50        _: &'a Cache<'a, E, S>,
51        _: Self::ExtraInfo,
52    ) -> Self {
53        let mut scopes_arena = Arena::new();
54        let scopes_stack = vec![Some(scopes_arena.add(ComplexityScope::default()))];
55        Self {
56            schema_definition,
57            cost_computer: C::new(operation_definition, schema_definition, variable_values),
58            scopes_arena,
59            scopes_stack,
60        }
61    }
62
63    fn visit_field(
64        &mut self,
65        field: &'a <E as ExecutableDocument>::Field,
66        field_definition: &'a S::FieldDefinition,
67        scoped_type: TypeDefinitionReference<'a, S::TypeDefinition>,
68        included: bool,
69    ) {
70        if !included {
71            return;
72        }
73        let cost = self
74            .cost_computer
75            .cost_for_field_definition(field_definition);
76
77        // Don't grow the costing tree for leaf fields without cost,
78        // just hold their position in the traversal stack with a None scope
79        if cost == 0
80            && !field_definition
81                .r#type()
82                .base(self.schema_definition)
83                .is_composite()
84        {
85            self.scopes_stack.push(None);
86            return;
87        }
88
89        // Get the field key (custom alias or base field name)
90        let field_key = field.response_name();
91
92        // Get next insertion point in the Vec of mutable scopes, using arena tree pattern:
93        // https://docs.rs/indextree/latest/indextree/#arena-based-tree-data-structure
94        let next_index = self.scopes_arena.next_id();
95
96        // Get a mutable reference to the parent scope in the traversal stack
97        let parent_scope = self
98            .scopes_stack
99            .last()
100            .copied()
101            .flatten()
102            .and_then(|index| self.scopes_arena.get_mut(index))
103            .expect("expected a parent complexity scope");
104
105        // Collect any multipliers that the parent scope specifies for this field
106        // ie: connection > edges/nodes
107        let parent_multiplier = parent_scope.multiplier_for_field(field);
108
109        // find or create a reference to this field's scope in the parent tree of typed selections
110        // ie: parent_scope.typed_selections = { Type => { "field_key" => scopes_db_index, ... } }
111        let scope_index = *parent_scope
112            .typed_selections
113            .entry(scoped_type.name())
114            .or_insert_with(|| TypedSelection {
115                type_definition: scoped_type,
116                inner_selection: HashMap::new(),
117            })
118            .inner_selection
119            .entry(field_key)
120            .or_insert(next_index);
121
122        // This should really be part of a `or_insert_with` instead of the `or_insert` above,
123        // but the borrow checker doesn't like that.
124        if scope_index == next_index {
125            let field_multipliers = self
126                .cost_computer
127                .field_multipliers(field_definition, field);
128
129            self.scopes_arena.add(ComplexityScope {
130                field_multipliers,
131                ..Default::default()
132            });
133        }
134
135        // Push the current scope reference onto the traversal stack,
136        // and then get a mutable reference to the scope itself
137        self.scopes_stack.push(Some(scope_index));
138        let scope = self
139            .scopes_arena
140            .get_mut(scope_index)
141            .expect("invalid complexity scope tree reference");
142
143        // repeated scopes have a consistent argument multiplier in valid documents
144        scope.multiplier = parent_multiplier;
145        scope.cost = scope.cost.max(cost);
146    }
147
148    fn leave_field(
149        &mut self,
150        _field: &'a <E as ExecutableDocument>::Field,
151        _field_definition: &'a S::FieldDefinition,
152        _scoped_type: TypeDefinitionReference<'a, S::TypeDefinition>,
153        included: bool,
154    ) {
155        if included {
156            self.scopes_stack.pop().unwrap();
157        }
158    }
159}
160
161impl<
162        'a,
163        E: ExecutableDocument,
164        S: SchemaDefinition,
165        V: VariableValues,
166        C: CostComputer<'a, E, S, V>,
167    > Analyzer<'a, E, S, V> for ComplexityCost<'a, E, S, V, C>
168{
169    type Output = usize;
170
171    fn into_output(mut self) -> Self::Output {
172        self.result()
173    }
174}
175
176impl<
177        'a,
178        E: ExecutableDocument,
179        S: SchemaDefinition,
180        V: VariableValues,
181        C: CostComputer<'a, E, S, V>,
182    > ComplexityCost<'a, E, S, V, C>
183{
184    fn result(&mut self) -> usize {
185        let root_scope = self
186            .scopes_stack
187            .first()
188            .copied()
189            .flatten()
190            .and_then(|index| self.scopes_arena.get(index))
191            .unwrap();
192        self.merged_max_complexity_for_scopes(&[root_scope])
193    }
194
195    fn merged_max_complexity_for_scopes(
196        &self,
197        scopes: &[&ComplexityScope<'a, S::TypeDefinition, C::FieldMultipliers>],
198    ) -> usize {
199        // build a set of all unique possible type definitions
200        // with abstract types expanded to encompass all of their possible types
201        let possible_type_names = scopes
202            .iter()
203            .flat_map(|scope| {
204                scope
205                    .typed_selections
206                    .values()
207                    .map(|typed_selection| typed_selection.type_definition)
208            })
209            .unique_by(|ty| ty.name())
210            .flat_map(|ty| self.possible_type_names(&ty))
211            .unique();
212
213        // calculate a maximum possible cost among possible types
214        possible_type_names
215            .map(|possible_type_name| {
216                // collect inner selections from all scopes that intersect with this possible type
217                let inner_selections = scopes
218                    .iter()
219                    .flat_map(|scope| {
220                        scope
221                            .typed_selections
222                            .values()
223                            .filter_map(|typed_selection| {
224                                self.possible_type_names(&typed_selection.type_definition)
225                                    .any(|name| name == possible_type_name)
226                                    .then_some(&typed_selection.inner_selection)
227                            })
228                    })
229                    .collect::<Vec<_>>();
230
231                self.merged_max_complexity_for_selections(inner_selections)
232            })
233            .max()
234            .unwrap_or(0)
235    }
236
237    fn merged_max_complexity_for_selections(
238        &self,
239        inner_selections: Vec<&InnerSelection<'a>>,
240    ) -> usize {
241        // build a unique set of field keys from across inner selections.
242        // the same field keys may appear in selections on different types,
243        // ex: a "metafield" key may be selected on both Product and HasMetafield types.
244        let unique_field_keys = inner_selections
245            .iter()
246            .flat_map(|child_scope| child_scope.keys())
247            .unique();
248
249        // calculate a maximum possible cost for each unique field key
250        unique_field_keys
251            .map(|field_key| {
252                let mut base_cost = 0;
253                let mut multiplier = 0;
254
255                // collect child scopes from across composite selections
256                // leaf selections report their costs directly
257                let composite_scopes = inner_selections
258                    .iter()
259                    .filter_map(|inner_selection| {
260                        inner_selection
261                            .get(*field_key)
262                            .and_then(|scope_index| self.scopes_arena.get(*scope_index))
263                            .and_then(|child_scope| {
264                                // base_cost and multiplier select their maximums from across merged scopes
265                                // in case a field name has different costs in different scope types.
266                                base_cost = max(base_cost, child_scope.cost);
267                                multiplier = max(multiplier, child_scope.multiplier);
268
269                                if !child_scope.typed_selections.is_empty() {
270                                    Some(child_scope)
271                                } else {
272                                    None
273                                }
274                            })
275                    })
276                    .collect::<Vec<&ComplexityScope<'a, S::TypeDefinition, C::FieldMultipliers>>>();
277
278                let children_cost = self.merged_max_complexity_for_scopes(&composite_scopes);
279
280                (base_cost + children_cost) * multiplier
281            })
282            .sum()
283    }
284
285    fn possible_type_names(
286        &self,
287        ty: &TypeDefinitionReference<'a, S::TypeDefinition>,
288    ) -> impl Iterator<Item = &'a str> {
289        match ty {
290            TypeDefinitionReference::Object(_) => Either::Left(Some(ty.name()).into_iter()),
291            TypeDefinitionReference::Interface(itd) => Either::Right(Either::Left(
292                self.schema_definition
293                    .get_interface_implementors(itd)
294                    .map(ObjectTypeDefinition::name),
295            )),
296            TypeDefinitionReference::Union(utd) => Either::Right(Either::Right(
297                utd.union_member_types()
298                    .iter()
299                    .map(|union_member| union_member.name()),
300            )),
301            _ => Either::Left(None.into_iter()),
302        }
303    }
304}
305
306type InnerSelection<'a> = HashMap<&'a str, NodeId>;
307
308struct TypedSelection<'a, T: TypeDefinition> {
309    type_definition: TypeDefinitionReference<'a, T>,
310    inner_selection: InnerSelection<'a>,
311}
312
313struct ComplexityScope<'a, T: TypeDefinition, F> {
314    cost: usize,
315    multiplier: usize,
316    typed_selections: HashMap<&'a str, TypedSelection<'a, T>>,
317    field_multipliers: F,
318}
319
320impl<T: TypeDefinition, F: Default> Default for ComplexityScope<'_, T, F> {
321    fn default() -> Self {
322        Self {
323            cost: 0,
324            multiplier: 1,
325            typed_selections: HashMap::new(),
326            field_multipliers: F::default(),
327        }
328    }
329}
330
331impl<T: TypeDefinition, F> ComplexityScope<'_, T, F> {
332    fn multiplier_for_field<E: ExecutableDocument>(&self, field: &E::Field) -> usize
333    where
334        F: FieldMultipliers<E>,
335    {
336        self.field_multipliers.multiplier_for_field(field)
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::executable::{operation::Orchestrator, Cache};
344    use bluejay_parser::ast::{
345        definition::{DefaultContext, DefinitionDocument, SchemaDefinition},
346        executable::ExecutableDocument,
347        Parse,
348    };
349    use serde_json::Value as JsonValue;
350
351    type ComplexityAnalyzer<'a, E, S, V> =
352        Orchestrator<'a, E, S, V, ComplexityCost<'a, E, S, V, RelayCostComputer<'a, E, S, V>>>;
353
354    const TEST_SCHEMA: &str = r#"
355        directive @cost(weight: String!, kind: String) on FIELD_DEFINITION
356
357        enum BasicEnum {
358          YES
359          NO
360        }
361
362        interface Node {
363          id: ID!
364          one: BasicObject!
365        }
366
367        interface BasicInterface {
368          zeroScalar: String!
369          oneObject: BasicObject!
370        }
371
372        type BasicObject implements Node & BasicInterface {
373          id: ID!
374          one: BasicObject!
375          zeroScalar: String!
376          zeroEnum: BasicEnum!
377          oneObject: BasicObject!
378          twoScalar: String! @cost(weight: "2.0")
379          twoEnum: BasicEnum! @cost(weight: "2.0")
380          twoObject: BasicObject! @cost(weight: "2.0")
381        }
382
383        union BasicUnion = BasicObject
384
385        type Query {
386          zeroScalar: String!
387          zeroEnum: BasicEnum!
388          oneObject: BasicObject!
389          oneInterface: BasicInterface!
390          oneUnion: BasicUnion!
391          twoScalar: String! @cost(weight: "2.0")
392          twoEnum: BasicEnum! @cost(weight: "2.0")
393          fiveScalar: String! @cost(weight: "5.0")
394          fiveEnum: BasicEnum! @cost(weight: "5.0")
395          fiveBasicObject: BasicObject! @cost(weight: "5.0")
396
397          node(id: ID!): Node
398
399          zeroScalarList: [String!]!
400          zeroEnumList: [BasicEnum!]!
401          oneObjectList: [BasicObject!]!
402          fiveObjectList: [BasicObject!]! @cost(weight: "5.0")
403
404          oneObjectConnection(first: Int!, last: Int!): BasicObjectConnection @cost(weight: "1.0", kind: "connection")
405          twoObjectConnection(first: Int!, last: Int!): BasicObjectConnection @cost(weight: "2.0", kind: "connection")
406        }
407
408        type PageInfo {
409            hasNextPage: Boolean!
410            hasPreviousPage: Boolean!
411        }
412
413        type BasicObjectEdge {
414            cursor: String!
415            node: BasicObject!
416        }
417
418        type BasicObjectConnection {
419            edges: [BasicObjectEdge!]! @cost(weight: "0.0")
420            nodes: [BasicObject!]!
421            pageInfo: PageInfo! @cost(weight: "0.0")
422        }
423
424        type Comment {
425            body: String!
426        }
427
428        interface HasComments {
429          comments: [Comment]!
430        }
431
432        type Product implements Node & HasComments {
433            id: ID!
434            one: BasicObject!
435            comments: [Comment]!
436        }
437
438        type User implements Node & HasComments {
439            id: ID!
440            one: BasicObject!
441            comments: [Comment]!
442            oneObject: BasicObject!
443        }
444
445        schema {
446          query: Query
447        }
448    "#;
449
450    fn check_complexity_with_operation_name_and_variables(
451        source: &str,
452        operation_name: Option<&str>,
453        variables: &JsonValue,
454        expected_complexity: usize,
455    ) {
456        let definition_document: DefinitionDocument<'_, DefaultContext> =
457            DefinitionDocument::parse(TEST_SCHEMA)
458                .result
459                .expect("Schema had parse errors");
460        let schema_definition =
461            SchemaDefinition::try_from(&definition_document).expect("Schema had errors");
462        let executable_document = ExecutableDocument::parse(source)
463            .result
464            .unwrap_or_else(|_| panic!("Document had parse errors"));
465        let cache = Cache::new(&executable_document, &schema_definition);
466        let variables = variables.as_object().expect("Variables must be an object");
467        let complexity = ComplexityAnalyzer::analyze(
468            &executable_document,
469            &schema_definition,
470            operation_name,
471            variables,
472            &cache,
473            (),
474        )
475        .unwrap();
476
477        assert_eq!(complexity, expected_complexity);
478    }
479
480    fn check_complexity_with_operation_name(
481        source: &str,
482        operation_name: Option<&str>,
483        expected_complexity: usize,
484    ) {
485        check_complexity_with_operation_name_and_variables(
486            source,
487            operation_name,
488            &serde_json::json!({}),
489            expected_complexity,
490        )
491    }
492
493    fn check_complexity_with_variables(
494        source: &str,
495        variables: JsonValue,
496        expected_complexity: usize,
497    ) {
498        check_complexity_with_operation_name_and_variables(
499            source,
500            None,
501            &variables,
502            expected_complexity,
503        )
504    }
505
506    fn check_complexity(source: &str, expected_complexity: usize) {
507        check_complexity_with_operation_name_and_variables(
508            source,
509            None,
510            &serde_json::json!({}),
511            expected_complexity,
512        )
513    }
514
515    #[test]
516    fn basic_cost_metrics() {
517        check_complexity(r#"{ zeroScalar }"#, 0);
518        check_complexity(r#"{ zeroEnum }"#, 0);
519        check_complexity(r#"{ oneObject { zeroScalar } }"#, 1);
520        check_complexity(r#"{ oneInterface { zeroScalar } }"#, 1);
521        check_complexity(r#"{ oneUnion { ...on BasicObject { zeroScalar } } }"#, 1);
522    }
523
524    #[test]
525    fn basic_list_cost_metrics() {
526        check_complexity(r#"{ zeroScalarList }"#, 0);
527        check_complexity(r#"{ zeroEnumList }"#, 0);
528        check_complexity(r#"{ oneObjectList { zeroScalar } }"#, 1);
529        check_complexity(r#"{ fiveObjectList { zeroScalar } }"#, 5);
530    }
531
532    #[test]
533    fn basic_cost_metrics_nested() {
534        check_complexity(
535            r#"{
536          oneObject { # 1 + 4 = 5
537            oneObject { oneObject { zeroEnum twoScalar } } # 1 + 1 + 0 + 2 = 4
538          }
539        }"#,
540            5,
541        );
542    }
543
544    #[test]
545    fn field_cost_metrics() {
546        check_complexity(r#"{ fiveScalar }"#, 5);
547        check_complexity(r#"{ fiveEnum }"#, 5);
548        check_complexity(r#"{ fiveBasicObject { zeroScalar } }"#, 5);
549    }
550
551    #[test]
552    fn field_cost_metrics_nested() {
553        check_complexity(
554            r#"query { # 5 + 5 + 9 = 19
555          fiveScalar # 5
556          fiveEnum # 5
557          fiveBasicObject { # 5 + 4 = 9
558            twoObject { # 2 + 2 = 4
559              twoScalar
560            }
561          }
562        }"#,
563            19,
564        );
565    }
566
567    #[test]
568    fn active_operation_name() {
569        check_complexity_with_operation_name(
570            r#"
571          query { fiveScalar }
572        "#,
573            None,
574            5,
575        );
576
577        check_complexity_with_operation_name(
578            r#"
579          query Test { fiveScalar }
580        "#,
581            None,
582            5,
583        );
584
585        check_complexity_with_operation_name(
586            r#"
587            query Test1 { twoScalar }
588            query Test2 { fiveScalar }
589        "#,
590            Some("Test1"),
591            2,
592        );
593    }
594
595    #[test]
596    fn gracefully_handles_invalid_fields_and_fragment_types() {
597        // final reported result is None based on invalid query status
598        check_complexity(
599            r#"{
600            bogusField
601            ...on BogusType { bogusField }
602            ...BogusSpread
603            fiveScalar
604        }"#,
605            5,
606        );
607    }
608
609    #[test]
610    fn fragment_definitions() {
611        check_complexity(
612            r#"
613          query { # 3 + 7 = 10
614            oneObject { ...Attrs } # 1 + 2 = 3
615            fiveBasicObject { ...Attrs } # 5 + 2 = 7
616          }
617          fragment Attrs on BasicObject {
618            twoObject { zeroScalar } # 2 + 0 = 2
619          }
620        "#,
621            10,
622        );
623    }
624
625    #[test]
626    fn skip_and_include_fields_with_bool_literals() {
627        check_complexity(
628            r#"query { # 1 + 7 = 8
629            oneObject { zeroScalar } # 1 + 0 = 1
630            fiveBasicObject @skip(if: false) { twoScalar } # (5 + 2) * 1 = 7
631        }"#,
632            8,
633        );
634
635        check_complexity(
636            r#"query { # 1 + 0 = 1
637            oneObject { zeroScalar } # 1 + 0 = 1
638            fiveBasicObject @skip(if: true) { twoScalar } # (5 + 0) * 0 = 0
639        }"#,
640            1,
641        );
642
643        check_complexity(
644            r#"query {
645            oneObject { zeroScalar }
646            fiveBasicObject @include(if: false) { twoScalar }
647        }"#,
648            1,
649        );
650
651        check_complexity(
652            r#"query {
653            oneObject { zeroScalar }
654            fiveBasicObject @include(if: true) { twoScalar }
655        }"#,
656            8,
657        );
658    }
659
660    #[test]
661    fn skip_and_include_fields_with_variables() {
662        check_complexity_with_variables(
663            r#"query($enabled: Boolean) {
664            oneObject { zeroScalar }
665            fiveBasicObject @skip(if: $enabled) { twoScalar }
666        }"#,
667            serde_json::json!({ "enabled": false }),
668            8,
669        );
670
671        check_complexity_with_variables(
672            r#"query($enabled: Boolean) {
673            oneObject { zeroScalar }
674            fiveBasicObject @skip(if: $enabled) { twoScalar }
675        }"#,
676            serde_json::json!({ "enabled": true }),
677            1,
678        );
679
680        check_complexity_with_variables(
681            r#"query($enabled: Boolean) {
682            oneObject { zeroScalar }
683            fiveBasicObject @include(if: $enabled) { twoScalar }
684        }"#,
685            serde_json::json!({ "enabled": false }),
686            1,
687        );
688
689        check_complexity_with_variables(
690            r#"query($enabled: Boolean) {
691            oneObject { zeroScalar }
692            fiveBasicObject @include(if: $enabled) { twoScalar }
693        }"#,
694            serde_json::json!({ "enabled": true }),
695            8,
696        );
697    }
698
699    #[test]
700    fn skip_and_include_fields_with_default_variables() {
701        check_complexity(
702            r#"query($enabled: Boolean = false) {
703            oneObject { zeroScalar }
704            fiveBasicObject @skip(if: $enabled) { twoScalar }
705        }"#,
706            8,
707        );
708
709        check_complexity(
710            r#"query($enabled: Boolean = true) {
711            oneObject { zeroScalar }
712            fiveBasicObject @skip(if: $enabled) { twoScalar }
713        }"#,
714            1,
715        );
716    }
717
718    #[test]
719    fn skip_and_include_inline_fragments_with_bool_literals() {
720        check_complexity(
721            r#"query {
722            oneObject { zeroScalar }
723            ... @skip(if: false) { fiveBasicObject { twoScalar } }
724        }"#,
725            8,
726        );
727
728        check_complexity(
729            r#"query {
730            oneObject { zeroScalar }
731            ... @skip(if: true) { fiveBasicObject { twoScalar } }
732        }"#,
733            1,
734        );
735
736        check_complexity(
737            r#"query {
738            oneObject { zeroScalar }
739            ... @include(if: false) { fiveBasicObject { twoScalar } }
740        }"#,
741            1,
742        );
743
744        check_complexity(
745            r#"query {
746            oneObject { zeroScalar }
747            ... @include(if: true) { fiveBasicObject { twoScalar } }
748        }"#,
749            8,
750        );
751    }
752
753    #[test]
754    fn skip_and_include_fragment_spreads_with_bool_literals() {
755        check_complexity(
756            r#"
757            fragment Stuff on Query { fiveBasicObject { twoScalar } }
758            query {
759                oneObject { zeroScalar }
760                ... Stuff @skip(if: false)
761            }
762        "#,
763            8,
764        );
765
766        check_complexity(
767            r#"
768            fragment Stuff on Query { fiveBasicObject { twoScalar } }
769            query {
770                oneObject { zeroScalar }
771                ... Stuff @skip(if: true)
772            }
773        "#,
774            1,
775        );
776
777        check_complexity(
778            r#"
779            fragment Stuff on Query { fiveBasicObject { twoScalar } }
780            query {
781                oneObject { zeroScalar }
782                ... Stuff @include(if: false)
783            }
784        "#,
785            1,
786        );
787
788        check_complexity(
789            r#"
790            fragment Stuff on Query { fiveBasicObject { twoScalar } }
791            query {
792                oneObject { zeroScalar }
793                ... Stuff @include(if: true)
794            }
795        "#,
796            8,
797        );
798    }
799
800    #[test]
801    fn skip_and_include_fragments_with_variables() {
802        check_complexity_with_variables(
803            r#"query($enabled: Boolean) {
804            oneObject { zeroScalar }
805            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
806        }"#,
807            serde_json::json!({ "enabled": false }),
808            8,
809        );
810
811        check_complexity_with_variables(
812            r#"query($enabled: Boolean) {
813            oneObject { zeroScalar }
814            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
815        }"#,
816            serde_json::json!({ "enabled": true }),
817            1,
818        );
819
820        check_complexity_with_variables(
821            r#"
822            fragment Stuff on Query { fiveBasicObject { twoScalar } }
823            query($enabled: Boolean) {
824                oneObject { zeroScalar }
825                ... Stuff @include(if: $enabled)
826            }
827        "#,
828            serde_json::json!({ "enabled": false }),
829            1,
830        );
831
832        check_complexity_with_variables(
833            r#"
834            fragment Stuff on Query { fiveBasicObject { twoScalar } }
835            query($enabled: Boolean) {
836                oneObject { zeroScalar }
837                ... Stuff @include(if: $enabled)
838            }
839        "#,
840            serde_json::json!({ "enabled": true }),
841            8,
842        );
843    }
844
845    #[test]
846    fn skip_and_include_fragments_with_default_variables() {
847        check_complexity(
848            r#"query($enabled: Boolean = false) {
849            oneObject { zeroScalar }
850            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
851        }"#,
852            8,
853        );
854
855        check_complexity(
856            r#"query($enabled: Boolean = true) {
857            oneObject { zeroScalar }
858            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
859        }"#,
860            1,
861        );
862    }
863
864    #[test]
865    fn skipped_paths_still_cost_when_revisited() {
866        check_complexity(
867            r#"{
868            oneObjectConnection(first: 7) { # 1 + 0 = 1
869              edges @skip(if: true) { node { twoScalar } } # skip = 0
870            }
871            oneObjectConnection(first: 7) { # 0 + (3 * floor(2 * log(7))) = 9
872              edges { node { twoScalar } } # (0 + 1 + 2) = 3
873            }
874        }"#,
875            10,
876        );
877    }
878
879    #[test]
880    fn connection_with_slicing_arguments_and_sized_fields() {
881        check_complexity(
882            r#"{
883            oneObjectConnection(first: 7) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
884              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
885              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
886              pageInfo { hasNextPage } # 0
887            }
888        }"#,
889            19,
890        );
891    }
892
893    #[test]
894    fn connection_with_slicing_arguments_using_variables() {
895        check_complexity_with_variables(
896            r#"query($first: Int) {
897            oneObjectConnection(first: $first) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
898              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
899              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
900              pageInfo { hasNextPage } # 0
901            }
902        }"#,
903            serde_json::json!({ "first": 7 }),
904            19,
905        );
906    }
907
908    #[test]
909    fn connection_with_slicing_arguments_using_default_variables() {
910        check_complexity(
911            r#"query($first: Int = 7) {
912            oneObjectConnection(first: $first) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
913              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
914              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
915              pageInfo { hasNextPage } # 0
916            }
917        }"#,
918            19,
919        );
920    }
921
922    #[test]
923    fn connection_with_multiple_slicing_arguments_uses_max() {
924        check_complexity(
925            r#"query($last: Int = 0, $first: Int = 7) {
926            oneObjectConnection(last: $last, first: $first) { # 1 + 3 * floor(2 * log(7))) = 10
927              edges { node { twoScalar } } # (0 + 1 + 2) = 3
928            }
929        }"#,
930            10,
931        );
932
933        check_complexity(
934            r#"query($first: Int = 7, $last: Int) {
935            oneObjectConnection(first: $first, last: $last) { # 1 + 3 * floor(2 * log(7))) = 10
936              edges { node { twoScalar } } # (0 + 1 + 2) = 3
937            }
938        }"#,
939            10,
940        );
941
942        check_complexity(
943            r#"query($first: Int = 7) {
944            oneObjectConnection(first: $first, last: null) { # 1 + 3 * floor(2 * log(7))) = 10
945              edges { node { twoScalar } } # (0 + 1 + 2) = 3
946            }
947        }"#,
948            10,
949        );
950    }
951
952    #[test]
953    fn connection_with_slicing_arguments_and_sized_fields_via_inline_fragment() {
954        check_complexity(
955            r#"
956            query {
957                oneObjectConnection(first: 7) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
958                  ...on BasicObjectConnection {
959                    edges { node { zeroScalar twoScalar } } # (1 + 0 + 0 + 2) = 3
960                    ...on BasicObjectConnection {
961                        nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
962                    }
963                  }
964                  pageInfo { hasNextPage } # 0
965                }
966            }
967        "#,
968            19,
969        );
970    }
971
972    #[test]
973    fn connection_with_slicing_arguments_and_sized_fields_via_fragment_spread() {
974        check_complexity(
975            r#"
976            query { # 19 + 13 = 32
977                seven: oneObjectConnection(first: 7) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
978                  ...ConnectionAttrs
979                  pageInfo { hasNextPage } # 0
980                }
981                three: oneObjectConnection(first: 3) { # 1 + (3 + 3) * floor(2 * log(3))) = 13
982                  ...ConnectionAttrs
983                  pageInfo { hasNextPage } # 0
984                }
985            }
986            fragment ConnectionAttrs on BasicObjectConnection {
987                edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
988                ...ConnectionNodeAttrs
989            }
990            fragment ConnectionNodeAttrs on BasicObjectConnection {
991                nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
992            }
993        "#,
994            32,
995        );
996    }
997
998    #[test]
999    fn zero_and_negative_multipliers_are_zero() {
1000        check_complexity(
1001            r#"{
1002            oneObjectConnection(first: 0) { # 1 + (3 * 0) = 1
1003              edges { node { twoScalar } } # (0 + 1 + 2) = 3
1004            }
1005        }"#,
1006            1,
1007        );
1008
1009        check_complexity(
1010            r#"{
1011            oneObjectConnection(first: -7) { # 1 + (3 * 0) = 1
1012              edges { node { twoScalar } } # (0 + 1 + 2) = 3
1013            }
1014        }"#,
1015            1,
1016        );
1017    }
1018
1019    #[test]
1020    fn connection_with_base_cost() {
1021        check_complexity(
1022            r#"{
1023            twoObjectConnection(first: 7) { # 2 + 3 * floor(2 * log(7))) = 11
1024              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
1025            }
1026        }"#,
1027            11,
1028        );
1029    }
1030
1031    #[test]
1032    fn connection_with_skipped_sized_fields() {
1033        check_complexity(
1034            r#"{
1035            oneObjectConnection(first: 7) { # 1 + 3 * floor(2 * log(7))) = 10
1036              edges @skip(if: true) { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) * 0 = 0
1037              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
1038              pageInfo { hasNextPage } # 0
1039            }
1040        }"#,
1041            10,
1042        );
1043    }
1044
1045    #[test]
1046    fn basic_overlapping_field_paths_only_cost_once() {
1047        check_complexity(
1048            r#"{ # 3 + 2 = 5
1049            oneObject { twoScalar } # 1 + 2 = 3
1050            oneObject { twoEnum } # 0 + 2 = 2
1051        }"#,
1052            5,
1053        );
1054    }
1055
1056    #[test]
1057    fn overlapping_field_paths_with_multipliers_only_cost_once() {
1058        check_complexity(
1059            r#"{
1060            twoObjectConnection(first: 7) { # 2 + (3 + 2 + 3) * floor(2 * log(7))) = 26
1061              ...EdgesOnly
1062              ...EdgesAndNodes
1063            }
1064        }
1065        fragment EdgesOnly on BasicObjectConnection {
1066          edges { node { twoScalar } } # 0 + 1 + 2 = 3
1067        }
1068        fragment EdgesAndNodes on BasicObjectConnection {
1069          edges { node { twoScalar twoEnum } } # X + X + X + 2 = 2
1070          nodes { twoScalar } # 1 + 2 = 3
1071        }"#,
1072            26,
1073        );
1074    }
1075
1076    #[test]
1077    fn performs_inline_traversal_of_fragment_spreads() {
1078        check_complexity(
1079            r#"{
1080            node(id: "1") { # 1 + max(1, 3) = 4
1081                ...OnAbstract
1082                ...OnConcrete
1083            }
1084        }
1085        fragment OnAbstract on BasicInterface { # 1
1086          oneObject { zeroScalar } # 1
1087        }
1088        fragment OnConcrete on BasicObject { # 2 + 1 from BasicInterface = 3
1089          twoObject { zeroScalar } # 2
1090        }"#,
1091            4,
1092        );
1093    }
1094
1095    #[test]
1096    fn abstract_scope_uses_max_fragment_cost() {
1097        check_complexity(
1098            r#"{
1099          node(id: "r2d2c3p0") { # 1 + max(1, 4, 2, 1) = 5
1100            id
1101            ...on Node { # 1
1102              one { zeroScalar }
1103            }
1104            ...on Product { # 2 + HasComments = 3
1105              featuredImage: one { zeroScalar }
1106              featuredMedia: one { zeroScalar }
1107            }
1108            ...on User { # 1 + HasComments = 2
1109              companyContactProfiles: one { zeroScalar }
1110            }
1111            ...on HasComments { # 1 = 1
1112              comments { body }
1113            }
1114          }
1115        }"#,
1116            5,
1117        );
1118    }
1119
1120    #[test]
1121    fn nested_abstract_scopes_merge_possible_costs() {
1122        check_complexity(
1123            r#"{
1124          node(id: "r2d2c3p0") { # 1 + max(3, 3) = 4
1125            ... {
1126              ...on Product {
1127                product1: one { zeroScalar }
1128                product2: one { zeroScalar }
1129              }
1130              ...on User {
1131                user1: one { zeroScalar }
1132              }
1133            }
1134            ...on Product {
1135              product3: one { zeroScalar }
1136            }
1137            ...on User {
1138              user2: one { zeroScalar }
1139              user3: one { zeroScalar }
1140            }
1141          }
1142        }"#,
1143            4,
1144        );
1145    }
1146
1147    #[test]
1148    fn overlapping_abstract_scopes_merge_possible_costs() {
1149        check_complexity(
1150            r#"{
1151          node(id: "r2d2c3p0") { # 1 + max(3, 2, 2) = 4
1152            ...on Product { # 1 + 1 + 1 = 3
1153              product1: one { zeroScalar } # 1
1154              product2: one { zeroScalar } # 1
1155            }
1156            ...on User { # 1 + 1 = 2
1157              user2: one { zeroScalar } # 1
1158              user3: one { zeroScalar } # 1
1159            }
1160          }
1161          node(id: "r2d2c3p0") { # overlapping scope
1162            ...on Product { # overlapping scope
1163              product2: one { zeroScalar } # 0
1164              product3: one { zeroScalar } # 1
1165            }
1166            ...on BasicObject { # 1 + 1 = 2
1167              basic1: one { zeroScalar } # 1
1168              basic2: one { zeroScalar } # 1
1169            }
1170          }
1171        }"#,
1172            4,
1173        );
1174    }
1175
1176    #[test]
1177    fn does_not_traverse_recursive_fragment_cycles() {
1178        check_complexity(
1179            r#"
1180          query {
1181            node(id: "r2d2c3p0") { ...Alpha }
1182          }
1183          fragment Alpha on Product {
1184            a: one { zeroScalar }
1185            ...Bravo
1186          }
1187          fragment Bravo on Product {
1188            b: one { zeroScalar }
1189            ...Alpha
1190          }
1191        "#,
1192            3,
1193        );
1194    }
1195
1196    #[test]
1197    fn skips_valid_typed_selections_under_invalid_paths() {
1198        check_complexity(
1199            r#"
1200          query {
1201            validScope: oneObject {
1202              invalidScope {
1203                ...on Product {
1204                  validScope: one { id }
1205                }
1206              }
1207            }
1208          }
1209        "#,
1210            1,
1211        );
1212    }
1213}