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<'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 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}