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<'a, T: TypeDefinition, F: Default> Default for ComplexityScope<'a, 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<'a, T: TypeDefinition, F> ComplexityScope<'a, 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).expect("Schema had parse errors");
458        let schema_definition =
459            SchemaDefinition::try_from(&definition_document).expect("Schema had errors");
460        let executable_document = ExecutableDocument::parse(source)
461            .unwrap_or_else(|_| panic!("Document had parse errors"));
462        let cache = Cache::new(&executable_document, &schema_definition);
463        let variables = variables.as_object().expect("Variables must be an object");
464        let complexity = ComplexityAnalyzer::analyze(
465            &executable_document,
466            &schema_definition,
467            operation_name,
468            variables,
469            &cache,
470            (),
471        )
472        .unwrap();
473
474        assert_eq!(complexity, expected_complexity);
475    }
476
477    fn check_complexity_with_operation_name(
478        source: &str,
479        operation_name: Option<&str>,
480        expected_complexity: usize,
481    ) {
482        check_complexity_with_operation_name_and_variables(
483            source,
484            operation_name,
485            &serde_json::json!({}),
486            expected_complexity,
487        )
488    }
489
490    fn check_complexity_with_variables(
491        source: &str,
492        variables: JsonValue,
493        expected_complexity: usize,
494    ) {
495        check_complexity_with_operation_name_and_variables(
496            source,
497            None,
498            &variables,
499            expected_complexity,
500        )
501    }
502
503    fn check_complexity(source: &str, expected_complexity: usize) {
504        check_complexity_with_operation_name_and_variables(
505            source,
506            None,
507            &serde_json::json!({}),
508            expected_complexity,
509        )
510    }
511
512    #[test]
513    fn basic_cost_metrics() {
514        check_complexity(r#"{ zeroScalar }"#, 0);
515        check_complexity(r#"{ zeroEnum }"#, 0);
516        check_complexity(r#"{ oneObject { zeroScalar } }"#, 1);
517        check_complexity(r#"{ oneInterface { zeroScalar } }"#, 1);
518        check_complexity(r#"{ oneUnion { ...on BasicObject { zeroScalar } } }"#, 1);
519    }
520
521    #[test]
522    fn basic_list_cost_metrics() {
523        check_complexity(r#"{ zeroScalarList }"#, 0);
524        check_complexity(r#"{ zeroEnumList }"#, 0);
525        check_complexity(r#"{ oneObjectList { zeroScalar } }"#, 1);
526        check_complexity(r#"{ fiveObjectList { zeroScalar } }"#, 5);
527    }
528
529    #[test]
530    fn basic_cost_metrics_nested() {
531        check_complexity(
532            r#"{
533          oneObject { # 1 + 4 = 5
534            oneObject { oneObject { zeroEnum twoScalar } } # 1 + 1 + 0 + 2 = 4
535          }
536        }"#,
537            5,
538        );
539    }
540
541    #[test]
542    fn field_cost_metrics() {
543        check_complexity(r#"{ fiveScalar }"#, 5);
544        check_complexity(r#"{ fiveEnum }"#, 5);
545        check_complexity(r#"{ fiveBasicObject { zeroScalar } }"#, 5);
546    }
547
548    #[test]
549    fn field_cost_metrics_nested() {
550        check_complexity(
551            r#"query { # 5 + 5 + 9 = 19
552          fiveScalar # 5
553          fiveEnum # 5
554          fiveBasicObject { # 5 + 4 = 9
555            twoObject { # 2 + 2 = 4
556              twoScalar
557            }
558          }
559        }"#,
560            19,
561        );
562    }
563
564    #[test]
565    fn active_operation_name() {
566        check_complexity_with_operation_name(
567            r#"
568          query { fiveScalar }
569        "#,
570            None,
571            5,
572        );
573
574        check_complexity_with_operation_name(
575            r#"
576          query Test { fiveScalar }
577        "#,
578            None,
579            5,
580        );
581
582        check_complexity_with_operation_name(
583            r#"
584            query Test1 { twoScalar }
585            query Test2 { fiveScalar }
586        "#,
587            Some("Test1"),
588            2,
589        );
590    }
591
592    #[test]
593    fn gracefully_handles_invalid_fields_and_fragment_types() {
594        // final reported result is None based on invalid query status
595        check_complexity(
596            r#"{
597            bogusField
598            ...on BogusType { bogusField }
599            ...BogusSpread
600            fiveScalar
601        }"#,
602            5,
603        );
604    }
605
606    #[test]
607    fn fragment_definitions() {
608        check_complexity(
609            r#"
610          query { # 3 + 7 = 10
611            oneObject { ...Attrs } # 1 + 2 = 3
612            fiveBasicObject { ...Attrs } # 5 + 2 = 7
613          }
614          fragment Attrs on BasicObject {
615            twoObject { zeroScalar } # 2 + 0 = 2
616          }
617        "#,
618            10,
619        );
620    }
621
622    #[test]
623    fn skip_and_include_fields_with_bool_literals() {
624        check_complexity(
625            r#"query { # 1 + 7 = 8
626            oneObject { zeroScalar } # 1 + 0 = 1
627            fiveBasicObject @skip(if: false) { twoScalar } # (5 + 2) * 1 = 7
628        }"#,
629            8,
630        );
631
632        check_complexity(
633            r#"query { # 1 + 0 = 1
634            oneObject { zeroScalar } # 1 + 0 = 1
635            fiveBasicObject @skip(if: true) { twoScalar } # (5 + 0) * 0 = 0
636        }"#,
637            1,
638        );
639
640        check_complexity(
641            r#"query {
642            oneObject { zeroScalar }
643            fiveBasicObject @include(if: false) { twoScalar }
644        }"#,
645            1,
646        );
647
648        check_complexity(
649            r#"query {
650            oneObject { zeroScalar }
651            fiveBasicObject @include(if: true) { twoScalar }
652        }"#,
653            8,
654        );
655    }
656
657    #[test]
658    fn skip_and_include_fields_with_variables() {
659        check_complexity_with_variables(
660            r#"query($enabled: Boolean) {
661            oneObject { zeroScalar }
662            fiveBasicObject @skip(if: $enabled) { twoScalar }
663        }"#,
664            serde_json::json!({ "enabled": false }),
665            8,
666        );
667
668        check_complexity_with_variables(
669            r#"query($enabled: Boolean) {
670            oneObject { zeroScalar }
671            fiveBasicObject @skip(if: $enabled) { twoScalar }
672        }"#,
673            serde_json::json!({ "enabled": true }),
674            1,
675        );
676
677        check_complexity_with_variables(
678            r#"query($enabled: Boolean) {
679            oneObject { zeroScalar }
680            fiveBasicObject @include(if: $enabled) { twoScalar }
681        }"#,
682            serde_json::json!({ "enabled": false }),
683            1,
684        );
685
686        check_complexity_with_variables(
687            r#"query($enabled: Boolean) {
688            oneObject { zeroScalar }
689            fiveBasicObject @include(if: $enabled) { twoScalar }
690        }"#,
691            serde_json::json!({ "enabled": true }),
692            8,
693        );
694    }
695
696    #[test]
697    fn skip_and_include_fields_with_default_variables() {
698        check_complexity(
699            r#"query($enabled: Boolean = false) {
700            oneObject { zeroScalar }
701            fiveBasicObject @skip(if: $enabled) { twoScalar }
702        }"#,
703            8,
704        );
705
706        check_complexity(
707            r#"query($enabled: Boolean = true) {
708            oneObject { zeroScalar }
709            fiveBasicObject @skip(if: $enabled) { twoScalar }
710        }"#,
711            1,
712        );
713    }
714
715    #[test]
716    fn skip_and_include_inline_fragments_with_bool_literals() {
717        check_complexity(
718            r#"query {
719            oneObject { zeroScalar }
720            ... @skip(if: false) { fiveBasicObject { twoScalar } }
721        }"#,
722            8,
723        );
724
725        check_complexity(
726            r#"query {
727            oneObject { zeroScalar }
728            ... @skip(if: true) { fiveBasicObject { twoScalar } }
729        }"#,
730            1,
731        );
732
733        check_complexity(
734            r#"query {
735            oneObject { zeroScalar }
736            ... @include(if: false) { fiveBasicObject { twoScalar } }
737        }"#,
738            1,
739        );
740
741        check_complexity(
742            r#"query {
743            oneObject { zeroScalar }
744            ... @include(if: true) { fiveBasicObject { twoScalar } }
745        }"#,
746            8,
747        );
748    }
749
750    #[test]
751    fn skip_and_include_fragment_spreads_with_bool_literals() {
752        check_complexity(
753            r#"
754            fragment Stuff on Query { fiveBasicObject { twoScalar } }
755            query {
756                oneObject { zeroScalar }
757                ... Stuff @skip(if: false)
758            }
759        "#,
760            8,
761        );
762
763        check_complexity(
764            r#"
765            fragment Stuff on Query { fiveBasicObject { twoScalar } }
766            query {
767                oneObject { zeroScalar }
768                ... Stuff @skip(if: true)
769            }
770        "#,
771            1,
772        );
773
774        check_complexity(
775            r#"
776            fragment Stuff on Query { fiveBasicObject { twoScalar } }
777            query {
778                oneObject { zeroScalar }
779                ... Stuff @include(if: false)
780            }
781        "#,
782            1,
783        );
784
785        check_complexity(
786            r#"
787            fragment Stuff on Query { fiveBasicObject { twoScalar } }
788            query {
789                oneObject { zeroScalar }
790                ... Stuff @include(if: true)
791            }
792        "#,
793            8,
794        );
795    }
796
797    #[test]
798    fn skip_and_include_fragments_with_variables() {
799        check_complexity_with_variables(
800            r#"query($enabled: Boolean) {
801            oneObject { zeroScalar }
802            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
803        }"#,
804            serde_json::json!({ "enabled": false }),
805            8,
806        );
807
808        check_complexity_with_variables(
809            r#"query($enabled: Boolean) {
810            oneObject { zeroScalar }
811            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
812        }"#,
813            serde_json::json!({ "enabled": true }),
814            1,
815        );
816
817        check_complexity_with_variables(
818            r#"
819            fragment Stuff on Query { fiveBasicObject { twoScalar } }
820            query($enabled: Boolean) {
821                oneObject { zeroScalar }
822                ... Stuff @include(if: $enabled)
823            }
824        "#,
825            serde_json::json!({ "enabled": false }),
826            1,
827        );
828
829        check_complexity_with_variables(
830            r#"
831            fragment Stuff on Query { fiveBasicObject { twoScalar } }
832            query($enabled: Boolean) {
833                oneObject { zeroScalar }
834                ... Stuff @include(if: $enabled)
835            }
836        "#,
837            serde_json::json!({ "enabled": true }),
838            8,
839        );
840    }
841
842    #[test]
843    fn skip_and_include_fragments_with_default_variables() {
844        check_complexity(
845            r#"query($enabled: Boolean = false) {
846            oneObject { zeroScalar }
847            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
848        }"#,
849            8,
850        );
851
852        check_complexity(
853            r#"query($enabled: Boolean = true) {
854            oneObject { zeroScalar }
855            ... @skip(if: $enabled) { fiveBasicObject { twoScalar } }
856        }"#,
857            1,
858        );
859    }
860
861    #[test]
862    fn skipped_paths_still_cost_when_revisited() {
863        check_complexity(
864            r#"{
865            oneObjectConnection(first: 7) { # 1 + 0 = 1
866              edges @skip(if: true) { node { twoScalar } } # skip = 0
867            }
868            oneObjectConnection(first: 7) { # 0 + (3 * floor(2 * log(7))) = 9
869              edges { node { twoScalar } } # (0 + 1 + 2) = 3
870            }
871        }"#,
872            10,
873        );
874    }
875
876    #[test]
877    fn connection_with_slicing_arguments_and_sized_fields() {
878        check_complexity(
879            r#"{
880            oneObjectConnection(first: 7) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
881              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
882              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
883              pageInfo { hasNextPage } # 0
884            }
885        }"#,
886            19,
887        );
888    }
889
890    #[test]
891    fn connection_with_slicing_arguments_using_variables() {
892        check_complexity_with_variables(
893            r#"query($first: Int) {
894            oneObjectConnection(first: $first) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
895              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
896              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
897              pageInfo { hasNextPage } # 0
898            }
899        }"#,
900            serde_json::json!({ "first": 7 }),
901            19,
902        );
903    }
904
905    #[test]
906    fn connection_with_slicing_arguments_using_default_variables() {
907        check_complexity(
908            r#"query($first: Int = 7) {
909            oneObjectConnection(first: $first) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
910              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
911              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
912              pageInfo { hasNextPage } # 0
913            }
914        }"#,
915            19,
916        );
917    }
918
919    #[test]
920    fn connection_with_multiple_slicing_arguments_uses_max() {
921        check_complexity(
922            r#"query($last: Int = 0, $first: Int = 7) {
923            oneObjectConnection(last: $last, first: $first) { # 1 + 3 * floor(2 * log(7))) = 10
924              edges { node { twoScalar } } # (0 + 1 + 2) = 3
925            }
926        }"#,
927            10,
928        );
929
930        check_complexity(
931            r#"query($first: Int = 7, $last: Int) {
932            oneObjectConnection(first: $first, last: $last) { # 1 + 3 * floor(2 * log(7))) = 10
933              edges { node { twoScalar } } # (0 + 1 + 2) = 3
934            }
935        }"#,
936            10,
937        );
938
939        check_complexity(
940            r#"query($first: Int = 7) {
941            oneObjectConnection(first: $first, last: null) { # 1 + 3 * floor(2 * log(7))) = 10
942              edges { node { twoScalar } } # (0 + 1 + 2) = 3
943            }
944        }"#,
945            10,
946        );
947    }
948
949    #[test]
950    fn connection_with_slicing_arguments_and_sized_fields_via_inline_fragment() {
951        check_complexity(
952            r#"
953            query {
954                oneObjectConnection(first: 7) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
955                  ...on BasicObjectConnection {
956                    edges { node { zeroScalar twoScalar } } # (1 + 0 + 0 + 2) = 3
957                    ...on BasicObjectConnection {
958                        nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
959                    }
960                  }
961                  pageInfo { hasNextPage } # 0
962                }
963            }
964        "#,
965            19,
966        );
967    }
968
969    #[test]
970    fn connection_with_slicing_arguments_and_sized_fields_via_fragment_spread() {
971        check_complexity(
972            r#"
973            query { # 19 + 13 = 32
974                seven: oneObjectConnection(first: 7) { # 1 + (3 + 3) * floor(2 * log(7))) = 19
975                  ...ConnectionAttrs
976                  pageInfo { hasNextPage } # 0
977                }
978                three: oneObjectConnection(first: 3) { # 1 + (3 + 3) * floor(2 * log(3))) = 13
979                  ...ConnectionAttrs
980                  pageInfo { hasNextPage } # 0
981                }
982            }
983            fragment ConnectionAttrs on BasicObjectConnection {
984                edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
985                ...ConnectionNodeAttrs
986            }
987            fragment ConnectionNodeAttrs on BasicObjectConnection {
988                nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
989            }
990        "#,
991            32,
992        );
993    }
994
995    #[test]
996    fn zero_and_negative_multipliers_are_zero() {
997        check_complexity(
998            r#"{
999            oneObjectConnection(first: 0) { # 1 + (3 * 0) = 1
1000              edges { node { twoScalar } } # (0 + 1 + 2) = 3
1001            }
1002        }"#,
1003            1,
1004        );
1005
1006        check_complexity(
1007            r#"{
1008            oneObjectConnection(first: -7) { # 1 + (3 * 0) = 1
1009              edges { node { twoScalar } } # (0 + 1 + 2) = 3
1010            }
1011        }"#,
1012            1,
1013        );
1014    }
1015
1016    #[test]
1017    fn connection_with_base_cost() {
1018        check_complexity(
1019            r#"{
1020            twoObjectConnection(first: 7) { # 2 + 3 * floor(2 * log(7))) = 11
1021              edges { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) = 3
1022            }
1023        }"#,
1024            11,
1025        );
1026    }
1027
1028    #[test]
1029    fn connection_with_skipped_sized_fields() {
1030        check_complexity(
1031            r#"{
1032            oneObjectConnection(first: 7) { # 1 + 3 * floor(2 * log(7))) = 10
1033              edges @skip(if: true) { node { zeroScalar twoScalar } } # (0 + 1 + 0 + 2) * 0 = 0
1034              nodes { zeroScalar twoScalar } # (1 + 0 + 2) = 3
1035              pageInfo { hasNextPage } # 0
1036            }
1037        }"#,
1038            10,
1039        );
1040    }
1041
1042    #[test]
1043    fn basic_overlapping_field_paths_only_cost_once() {
1044        check_complexity(
1045            r#"{ # 3 + 2 = 5
1046            oneObject { twoScalar } # 1 + 2 = 3
1047            oneObject { twoEnum } # 0 + 2 = 2
1048        }"#,
1049            5,
1050        );
1051    }
1052
1053    #[test]
1054    fn overlapping_field_paths_with_multipliers_only_cost_once() {
1055        check_complexity(
1056            r#"{
1057            twoObjectConnection(first: 7) { # 2 + (3 + 2 + 3) * floor(2 * log(7))) = 26
1058              ...EdgesOnly
1059              ...EdgesAndNodes
1060            }
1061        }
1062        fragment EdgesOnly on BasicObjectConnection {
1063          edges { node { twoScalar } } # 0 + 1 + 2 = 3
1064        }
1065        fragment EdgesAndNodes on BasicObjectConnection {
1066          edges { node { twoScalar twoEnum } } # X + X + X + 2 = 2
1067          nodes { twoScalar } # 1 + 2 = 3
1068        }"#,
1069            26,
1070        );
1071    }
1072
1073    #[test]
1074    fn performs_inline_traversal_of_fragment_spreads() {
1075        check_complexity(
1076            r#"{
1077            node(id: "1") { # 1 + max(1, 3) = 4
1078                ...OnAbstract
1079                ...OnConcrete
1080            }
1081        }
1082        fragment OnAbstract on BasicInterface { # 1
1083          oneObject { zeroScalar } # 1
1084        }
1085        fragment OnConcrete on BasicObject { # 2 + 1 from BasicInterface = 3
1086          twoObject { zeroScalar } # 2
1087        }"#,
1088            4,
1089        );
1090    }
1091
1092    #[test]
1093    fn abstract_scope_uses_max_fragment_cost() {
1094        check_complexity(
1095            r#"{
1096          node(id: "r2d2c3p0") { # 1 + max(1, 4, 2, 1) = 5
1097            id
1098            ...on Node { # 1
1099              one { zeroScalar }
1100            }
1101            ...on Product { # 2 + HasComments = 3
1102              featuredImage: one { zeroScalar }
1103              featuredMedia: one { zeroScalar }
1104            }
1105            ...on User { # 1 + HasComments = 2
1106              companyContactProfiles: one { zeroScalar }
1107            }
1108            ...on HasComments { # 1 = 1
1109              comments { body }
1110            }
1111          }
1112        }"#,
1113            5,
1114        );
1115    }
1116
1117    #[test]
1118    fn nested_abstract_scopes_merge_possible_costs() {
1119        check_complexity(
1120            r#"{
1121          node(id: "r2d2c3p0") { # 1 + max(3, 3) = 4
1122            ... {
1123              ...on Product {
1124                product1: one { zeroScalar }
1125                product2: one { zeroScalar }
1126              }
1127              ...on User {
1128                user1: one { zeroScalar }
1129              }
1130            }
1131            ...on Product {
1132              product3: one { zeroScalar }
1133            }
1134            ...on User {
1135              user2: one { zeroScalar }
1136              user3: one { zeroScalar }
1137            }
1138          }
1139        }"#,
1140            4,
1141        );
1142    }
1143
1144    #[test]
1145    fn overlapping_abstract_scopes_merge_possible_costs() {
1146        check_complexity(
1147            r#"{
1148          node(id: "r2d2c3p0") { # 1 + max(3, 2, 2) = 4
1149            ...on Product { # 1 + 1 + 1 = 3
1150              product1: one { zeroScalar } # 1
1151              product2: one { zeroScalar } # 1
1152            }
1153            ...on User { # 1 + 1 = 2
1154              user2: one { zeroScalar } # 1
1155              user3: one { zeroScalar } # 1
1156            }
1157          }
1158          node(id: "r2d2c3p0") { # overlapping scope
1159            ...on Product { # overlapping scope
1160              product2: one { zeroScalar } # 0
1161              product3: one { zeroScalar } # 1
1162            }
1163            ...on BasicObject { # 1 + 1 = 2
1164              basic1: one { zeroScalar } # 1
1165              basic2: one { zeroScalar } # 1
1166            }
1167          }
1168        }"#,
1169            4,
1170        );
1171    }
1172
1173    #[test]
1174    fn does_not_traverse_recursive_fragment_cycles() {
1175        check_complexity(
1176            r#"
1177          query {
1178            node(id: "r2d2c3p0") { ...Alpha }
1179          }
1180          fragment Alpha on Product {
1181            a: one { zeroScalar }
1182            ...Bravo
1183          }
1184          fragment Bravo on Product {
1185            b: one { zeroScalar }
1186            ...Alpha
1187          }
1188        "#,
1189            3,
1190        );
1191    }
1192
1193    #[test]
1194    fn skips_valid_typed_selections_under_invalid_paths() {
1195        check_complexity(
1196            r#"
1197          query {
1198            validScope: oneObject {
1199              invalidScope {
1200                ...on Product {
1201                  validScope: one { id }
1202                }
1203              }
1204            }
1205          }
1206        "#,
1207            1,
1208        );
1209    }
1210}