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 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 let field_key = field.response_name();
91
92 let next_index = self.scopes_arena.next_id();
95
96 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 let parent_multiplier = parent_scope.multiplier_for_field(field);
108
109 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 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 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 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 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 possible_type_names
215 .map(|possible_type_name| {
216 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 let unique_field_keys = inner_selections
245 .iter()
246 .flat_map(|child_scope| child_scope.keys())
247 .unique();
248
249 unique_field_keys
251 .map(|field_key| {
252 let mut base_cost = 0;
253 let mut multiplier = 0;
254
255 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 = 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 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}