Skip to main content

oxirs_gql/
query_complexity.rs

1//! GraphQL query complexity analysis.
2//!
3//! Prevents expensive or abusive queries by calculating a complexity score
4//! and comparing it against configurable limits.  Each field has a base cost;
5//! list fields multiply the accumulated child cost by a configurable factor.
6
7use std::collections::HashMap;
8
9// ────────────────────────────────────────────────────────────────────────────
10// Configuration types
11// ────────────────────────────────────────────────────────────────────────────
12
13/// Per-field complexity override for a specific type.
14#[derive(Debug, Clone)]
15pub struct FieldComplexity {
16    pub field_name: String,
17    /// Base cost charged for selecting this field.
18    pub base_cost: usize,
19    /// Multiplier applied to this field's child cost subtree.
20    pub multiplier: usize,
21}
22
23impl FieldComplexity {
24    pub fn new(field_name: impl Into<String>, base_cost: usize, multiplier: usize) -> Self {
25        Self {
26            field_name: field_name.into(),
27            base_cost,
28            multiplier,
29        }
30    }
31}
32
33/// Complexity rules for a GraphQL object type.
34#[derive(Debug, Clone)]
35pub struct ComplexityRule {
36    pub type_name: String,
37    /// Per-field overrides keyed by field name.
38    pub fields: HashMap<String, FieldComplexity>,
39}
40
41impl ComplexityRule {
42    pub fn new(type_name: impl Into<String>) -> Self {
43        Self {
44            type_name: type_name.into(),
45            fields: HashMap::new(),
46        }
47    }
48
49    pub fn with_field(mut self, fc: FieldComplexity) -> Self {
50        self.fields.insert(fc.field_name.clone(), fc);
51        self
52    }
53}
54
55/// Global limits and defaults for complexity analysis.
56#[derive(Debug, Clone)]
57pub struct ComplexityConfig {
58    /// Maximum allowed complexity score (inclusive).
59    pub max_complexity: usize,
60    /// Maximum allowed query depth (inclusive).
61    pub max_depth: usize,
62    /// Default cost assigned to every field without an explicit rule.
63    pub default_cost: usize,
64    /// Multiplier applied to child cost when the field returns a list.
65    pub list_multiplier: usize,
66}
67
68impl ComplexityConfig {
69    pub fn new(
70        max_complexity: usize,
71        max_depth: usize,
72        default_cost: usize,
73        list_multiplier: usize,
74    ) -> Self {
75        Self {
76            max_complexity,
77            max_depth,
78            default_cost,
79            list_multiplier,
80        }
81    }
82}
83
84impl Default for ComplexityConfig {
85    fn default() -> Self {
86        Self::new(1_000, 10, 1, 10)
87    }
88}
89
90// ────────────────────────────────────────────────────────────────────────────
91// Query tree
92// ────────────────────────────────────────────────────────────────────────────
93
94/// A node in a simplified GraphQL query tree.
95#[derive(Debug, Clone)]
96pub struct QueryNode {
97    pub field: String,
98    pub type_name: String,
99    pub children: Vec<QueryNode>,
100    /// `true` if this field returns a list of items (e.g. `[User!]!`).
101    pub is_list: bool,
102    /// Resolved argument values (e.g. `"first" -> "10"`).
103    pub args: HashMap<String, String>,
104}
105
106impl QueryNode {
107    /// Create a leaf node (no children, not a list).
108    pub fn leaf(field: impl Into<String>, type_name: impl Into<String>) -> Self {
109        Self {
110            field: field.into(),
111            type_name: type_name.into(),
112            children: vec![],
113            is_list: false,
114            args: HashMap::new(),
115        }
116    }
117
118    /// Create a non-list node with children.
119    pub fn with_children(
120        field: impl Into<String>,
121        type_name: impl Into<String>,
122        children: Vec<QueryNode>,
123    ) -> Self {
124        Self {
125            field: field.into(),
126            type_name: type_name.into(),
127            children,
128            is_list: false,
129            args: HashMap::new(),
130        }
131    }
132
133    /// Create a list field node with children.
134    pub fn list_field(
135        field: impl Into<String>,
136        type_name: impl Into<String>,
137        children: Vec<QueryNode>,
138    ) -> Self {
139        Self {
140            field: field.into(),
141            type_name: type_name.into(),
142            children,
143            is_list: true,
144            args: HashMap::new(),
145        }
146    }
147
148    /// Depth of this node's subtree (leaf = 1).
149    pub fn depth(&self) -> usize {
150        if self.children.is_empty() {
151            1
152        } else {
153            1 + self.children.iter().map(|c| c.depth()).max().unwrap_or(0)
154        }
155    }
156
157    /// Total number of fields (nodes) in this subtree.
158    pub fn field_count(&self) -> usize {
159        1 + self.children.iter().map(|c| c.field_count()).sum::<usize>()
160    }
161}
162
163// ────────────────────────────────────────────────────────────────────────────
164// Analysis result
165// ────────────────────────────────────────────────────────────────────────────
166
167/// Output of a complexity analysis pass.
168#[derive(Debug, Clone)]
169pub struct ComplexityResult {
170    /// Computed total complexity score.
171    pub complexity: usize,
172    /// Maximum query depth observed.
173    pub depth: usize,
174    /// `true` if `complexity > max_complexity`.
175    pub exceeds_limit: bool,
176    /// `true` if `depth > max_depth`.
177    pub depth_exceeded: bool,
178    /// Per-field breakdown: `(path_string, cost)`.
179    pub breakdown: Vec<(String, usize)>,
180}
181
182// ────────────────────────────────────────────────────────────────────────────
183// Analyzer
184// ────────────────────────────────────────────────────────────────────────────
185
186/// Analyses GraphQL query trees for complexity and depth.
187pub struct QueryComplexityAnalyzer {
188    config: ComplexityConfig,
189    rules: HashMap<String, ComplexityRule>,
190}
191
192impl QueryComplexityAnalyzer {
193    pub fn new(config: ComplexityConfig) -> Self {
194        Self {
195            config,
196            rules: HashMap::new(),
197        }
198    }
199
200    /// Register per-type complexity rules.
201    pub fn add_rule(&mut self, rule: ComplexityRule) {
202        self.rules.insert(rule.type_name.clone(), rule);
203    }
204
205    /// Analyse the given query root and return a [`ComplexityResult`].
206    pub fn analyze(&self, root: &QueryNode) -> ComplexityResult {
207        let mut breakdown = Vec::new();
208        let complexity = self.traverse(root, &mut breakdown);
209        let depth = root.depth();
210        ComplexityResult {
211            complexity,
212            depth,
213            exceeds_limit: complexity > self.config.max_complexity,
214            depth_exceeded: depth > self.config.max_depth,
215            breakdown,
216        }
217    }
218
219    /// Returns the cost of `node` (depth is not used in cost calculation but kept for API compatibility).
220    pub fn node_cost(&self, node: &QueryNode, _depth: usize) -> usize {
221        // Base cost: check per-type rule first, fall back to default
222        let base = self
223            .rules
224            .get(&node.type_name)
225            .and_then(|r| r.fields.get(&node.field))
226            .map(|fc| fc.base_cost)
227            .unwrap_or(self.config.default_cost);
228
229        // Multiplier: rule-defined or list_multiplier if this is a list field
230        let multiplier = self
231            .rules
232            .get(&node.type_name)
233            .and_then(|r| r.fields.get(&node.field))
234            .map(|fc| fc.multiplier)
235            .unwrap_or(if node.is_list {
236                self.config.list_multiplier
237            } else {
238                1
239            });
240
241        let child_cost: usize = node.children.iter().map(|c| self.node_cost(c, 0)).sum();
242
243        base + multiplier * child_cost
244    }
245
246    fn traverse(&self, node: &QueryNode, breakdown: &mut Vec<(String, usize)>) -> usize {
247        let base = self
248            .rules
249            .get(&node.type_name)
250            .and_then(|r| r.fields.get(&node.field))
251            .map(|fc| fc.base_cost)
252            .unwrap_or(self.config.default_cost);
253
254        let multiplier = self
255            .rules
256            .get(&node.type_name)
257            .and_then(|r| r.fields.get(&node.field))
258            .map(|fc| fc.multiplier)
259            .unwrap_or(if node.is_list {
260                self.config.list_multiplier
261            } else {
262                1
263            });
264
265        let child_cost: usize = node
266            .children
267            .iter()
268            .map(|c| self.traverse(c, breakdown))
269            .sum();
270
271        let total = base + multiplier * child_cost;
272        breakdown.push((node.field.clone(), total));
273        total
274    }
275
276    /// Returns `true` if the result is within all configured limits.
277    pub fn is_allowed(&self, result: &ComplexityResult) -> bool {
278        !result.exceeds_limit && !result.depth_exceeded
279    }
280}
281
282// ────────────────────────────────────────────────────────────────────────────
283// Simple per-field cost API (lighter-weight, v1.7.0)
284// ────────────────────────────────────────────────────────────────────────────
285
286/// A simple flat complexity rule: cost per field name with optional multiplier argument.
287#[derive(Debug, Clone)]
288pub struct SimpleCostRule {
289    /// Field this rule applies to.
290    pub field_name: String,
291    /// Base cost for this field.
292    pub cost: usize,
293    /// If set, multiply `cost` by the value of this argument.
294    pub multiplier_arg: Option<String>,
295}
296
297impl SimpleCostRule {
298    /// Create a fixed-cost rule.
299    pub fn fixed(field_name: impl Into<String>, cost: usize) -> Self {
300        Self {
301            field_name: field_name.into(),
302            cost,
303            multiplier_arg: None,
304        }
305    }
306
307    /// Create a rule whose cost scales with a numeric argument.
308    pub fn multiplied(
309        field_name: impl Into<String>,
310        cost: usize,
311        multiplier_arg: impl Into<String>,
312    ) -> Self {
313        Self {
314            field_name: field_name.into(),
315            cost,
316            multiplier_arg: Some(multiplier_arg.into()),
317        }
318    }
319}
320
321/// A flat (non-tree) representation of a GraphQL field selection.
322#[derive(Debug, Clone)]
323pub struct QueryField {
324    /// Name of the selected field.
325    pub name: String,
326    /// Numeric arguments for multiplier evaluation.
327    pub args: HashMap<String, usize>,
328    /// Nested selections.
329    pub children: Vec<QueryField>,
330}
331
332impl QueryField {
333    /// Create a leaf field with no args and no children.
334    pub fn new(name: impl Into<String>) -> Self {
335        Self {
336            name: name.into(),
337            args: HashMap::new(),
338            children: Vec::new(),
339        }
340    }
341
342    /// Add a numeric argument (builder).
343    pub fn with_arg(mut self, key: impl Into<String>, val: usize) -> Self {
344        self.args.insert(key.into(), val);
345        self
346    }
347
348    /// Add a child field (builder).
349    pub fn with_child(mut self, child: QueryField) -> Self {
350        self.children.push(child);
351        self
352    }
353
354    /// Maximum nesting depth (leaf = 1).
355    pub fn depth(&self) -> usize {
356        if self.children.is_empty() {
357            1
358        } else {
359            1 + self.children.iter().map(|c| c.depth()).max().unwrap_or(0)
360        }
361    }
362}
363
364/// Lightweight complexity analyzer driven by [`SimpleCostRule`]s.
365pub struct SimpleComplexityAnalyzer {
366    rules: HashMap<String, SimpleCostRule>,
367    default_cost: usize,
368    limit: usize,
369}
370
371impl SimpleComplexityAnalyzer {
372    /// Create with the given cost limit (default field cost = 1).
373    pub fn new(limit: usize) -> Self {
374        Self {
375            rules: HashMap::new(),
376            default_cost: 1,
377            limit,
378        }
379    }
380
381    /// Override the default cost for fields without an explicit rule.
382    pub fn with_default_cost(mut self, cost: usize) -> Self {
383        self.default_cost = cost;
384        self
385    }
386
387    /// Register a field rule.
388    pub fn add_rule(&mut self, rule: SimpleCostRule) {
389        self.rules.insert(rule.field_name.clone(), rule);
390    }
391
392    /// Cost for a single field + its args.
393    pub fn cost_for_field(&self, name: &str, args: &HashMap<String, usize>) -> usize {
394        match self.rules.get(name) {
395            Some(rule) => match &rule.multiplier_arg {
396                Some(arg) => {
397                    let m = args.get(arg).copied().unwrap_or(1).max(1);
398                    rule.cost.saturating_mul(m)
399                }
400                None => rule.cost,
401            },
402            None => self.default_cost,
403        }
404    }
405
406    /// Analyse the given list of top-level fields and return total cost + per-field breakdown.
407    pub fn analyze_fields(&self, fields: &[QueryField]) -> (usize, Vec<(String, usize)>) {
408        let mut breakdown = Vec::new();
409        let total = self.sum_fields(fields, &mut breakdown);
410        (total, breakdown)
411    }
412
413    /// Returns `true` when query cost is within the limit.
414    pub fn is_within_limit(&self, fields: &[QueryField]) -> bool {
415        let (total, _) = self.analyze_fields(fields);
416        total <= self.limit
417    }
418
419    fn sum_fields(&self, fields: &[QueryField], out: &mut Vec<(String, usize)>) -> usize {
420        let mut total = 0_usize;
421        for f in fields {
422            let cost = self.cost_for_field(&f.name, &f.args);
423            out.push((f.name.clone(), cost));
424            total = total.saturating_add(cost);
425            total = total.saturating_add(self.sum_fields(&f.children, out));
426        }
427        total
428    }
429}
430
431// ────────────────────────────────────────────────────────────────────────────
432// Tests
433// ────────────────────────────────────────────────────────────────────────────
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    fn default_analyzer() -> QueryComplexityAnalyzer {
439        QueryComplexityAnalyzer::new(ComplexityConfig::new(100, 5, 1, 10))
440    }
441
442    // ── QueryNode helpers ─────────────────────────────────────────────────────
443
444    #[test]
445    fn test_leaf_depth_is_1() {
446        let n = QueryNode::leaf("id", "String");
447        assert_eq!(n.depth(), 1);
448    }
449
450    #[test]
451    fn test_leaf_field_count_is_1() {
452        let n = QueryNode::leaf("id", "String");
453        assert_eq!(n.field_count(), 1);
454    }
455
456    #[test]
457    fn test_with_children_depth() {
458        let n = QueryNode::with_children(
459            "user",
460            "User",
461            vec![
462                QueryNode::leaf("id", "ID"),
463                QueryNode::leaf("name", "String"),
464            ],
465        );
466        assert_eq!(n.depth(), 2);
467    }
468
469    #[test]
470    fn test_with_children_field_count() {
471        let n = QueryNode::with_children(
472            "user",
473            "User",
474            vec![
475                QueryNode::leaf("id", "ID"),
476                QueryNode::leaf("name", "String"),
477            ],
478        );
479        assert_eq!(n.field_count(), 3);
480    }
481
482    #[test]
483    fn test_list_field_is_list() {
484        let n = QueryNode::list_field("users", "User", vec![QueryNode::leaf("id", "ID")]);
485        assert!(n.is_list);
486    }
487
488    #[test]
489    fn test_list_field_depth() {
490        let n = QueryNode::list_field("users", "User", vec![QueryNode::leaf("id", "ID")]);
491        assert_eq!(n.depth(), 2);
492    }
493
494    #[test]
495    fn test_nested_depth() {
496        let inner =
497            QueryNode::with_children("posts", "Post", vec![QueryNode::leaf("title", "String")]);
498        let outer = QueryNode::with_children("user", "User", vec![inner]);
499        assert_eq!(outer.depth(), 3);
500    }
501
502    // ── single leaf cost ──────────────────────────────────────────────────────
503
504    #[test]
505    fn test_single_leaf_complexity() {
506        let a = default_analyzer();
507        let r = a.analyze(&QueryNode::leaf("id", "ID"));
508        assert_eq!(r.complexity, 1); // default_cost = 1
509    }
510
511    #[test]
512    fn test_two_leaves_cost_sum() {
513        let a = default_analyzer();
514        let n = QueryNode::with_children(
515            "root",
516            "Query",
517            vec![
518                QueryNode::leaf("a", "String"),
519                QueryNode::leaf("b", "String"),
520            ],
521        );
522        // root: 1 + 1*(1 + 1) = 3
523        let r = a.analyze(&n);
524        assert_eq!(r.complexity, 3);
525    }
526
527    // ── list multiplier ───────────────────────────────────────────────────────
528
529    #[test]
530    fn test_list_field_multiplier_applied() {
531        let a = default_analyzer(); // list_multiplier=10
532        let n = QueryNode::list_field("users", "User", vec![QueryNode::leaf("id", "ID")]);
533        // list: base=1, child_cost=1, multiplier=10 → 1 + 10*1 = 11
534        let r = a.analyze(&n);
535        assert_eq!(r.complexity, 11);
536    }
537
538    #[test]
539    fn test_non_list_no_extra_multiplier() {
540        let a = default_analyzer();
541        let n = QueryNode::with_children("user", "User", vec![QueryNode::leaf("id", "ID")]);
542        // base=1 + 1*(1) = 2
543        let r = a.analyze(&n);
544        assert_eq!(r.complexity, 2);
545    }
546
547    #[test]
548    fn test_list_field_two_children() {
549        let a = default_analyzer();
550        let n = QueryNode::list_field(
551            "users",
552            "User",
553            vec![
554                QueryNode::leaf("id", "ID"),
555                QueryNode::leaf("name", "String"),
556            ],
557        );
558        // base=1, child_cost=2, multiplier=10 → 1 + 20 = 21
559        let r = a.analyze(&n);
560        assert_eq!(r.complexity, 21);
561    }
562
563    // ── exceeds_limit / depth_exceeded ───────────────────────────────────────
564
565    #[test]
566    fn test_exceeds_limit_detected() {
567        let a = QueryComplexityAnalyzer::new(ComplexityConfig::new(5, 10, 1, 10));
568        let n = QueryNode::list_field(
569            "users",
570            "User",
571            vec![
572                QueryNode::leaf("id", "ID"),
573                QueryNode::leaf("name", "String"),
574                QueryNode::leaf("email", "String"),
575            ],
576        );
577        // 1 + 10*3 = 31 > 5
578        let r = a.analyze(&n);
579        assert!(r.exceeds_limit);
580    }
581
582    #[test]
583    fn test_does_not_exceed_limit() {
584        let a = QueryComplexityAnalyzer::new(ComplexityConfig::new(100, 10, 1, 10));
585        let r = a.analyze(&QueryNode::leaf("id", "ID"));
586        assert!(!r.exceeds_limit);
587    }
588
589    #[test]
590    fn test_depth_exceeded_detected() {
591        let a = QueryComplexityAnalyzer::new(ComplexityConfig::new(10_000, 2, 1, 1));
592        // depth 3 > max_depth 2
593        let deep = QueryNode::with_children(
594            "a",
595            "A",
596            vec![QueryNode::with_children(
597                "b",
598                "B",
599                vec![QueryNode::leaf("c", "C")],
600            )],
601        );
602        let r = a.analyze(&deep);
603        assert!(r.depth_exceeded);
604    }
605
606    #[test]
607    fn test_depth_not_exceeded() {
608        let a = QueryComplexityAnalyzer::new(ComplexityConfig::new(10_000, 5, 1, 1));
609        let n = QueryNode::with_children("a", "A", vec![QueryNode::leaf("b", "B")]);
610        let r = a.analyze(&n);
611        assert!(!r.depth_exceeded);
612    }
613
614    // ── custom rules ──────────────────────────────────────────────────────────
615
616    #[test]
617    fn test_custom_rule_overrides_default_cost() {
618        let mut a = QueryComplexityAnalyzer::new(ComplexityConfig::new(10_000, 10, 1, 10));
619        a.add_rule(
620            ComplexityRule::new("Query").with_field(FieldComplexity::new("expensiveField", 50, 1)),
621        );
622        let n = QueryNode::leaf("expensiveField", "Query"); // note: field="expensiveField", type_name="Query"
623                                                            // Wait - the rule is keyed on type_name; let's construct correctly:
624        let n2 = QueryNode {
625            field: "expensiveField".to_string(),
626            type_name: "Query".to_string(),
627            children: vec![],
628            is_list: false,
629            args: HashMap::new(),
630        };
631        let r = a.analyze(&n2);
632        assert_eq!(r.complexity, 50); // base_cost=50 overrides default 1
633        let _ = n; // suppress unused warning
634    }
635
636    #[test]
637    fn test_custom_rule_multiplier_override() {
638        let mut a = QueryComplexityAnalyzer::new(ComplexityConfig::new(10_000, 10, 1, 5));
639        // Override: multiplier = 100 for this list field
640        a.add_rule(
641            ComplexityRule::new("Query").with_field(FieldComplexity::new("heavyList", 1, 100)),
642        );
643        let n = QueryNode {
644            field: "heavyList".to_string(),
645            type_name: "Query".to_string(),
646            children: vec![QueryNode::leaf("id", "ID")],
647            is_list: true, // is_list=true but multiplier overridden by rule
648            args: HashMap::new(),
649        };
650        // base=1, child_cost=1, multiplier=100 → 1 + 100*1 = 101
651        let r = a.analyze(&n);
652        assert_eq!(r.complexity, 101);
653    }
654
655    // ── breakdown ─────────────────────────────────────────────────────────────
656
657    #[test]
658    fn test_breakdown_contains_all_fields() {
659        let a = default_analyzer();
660        let n = QueryNode::with_children(
661            "root",
662            "Query",
663            vec![
664                QueryNode::leaf("a", "String"),
665                QueryNode::leaf("b", "String"),
666            ],
667        );
668        let r = a.analyze(&n);
669        let fields: Vec<&str> = r.breakdown.iter().map(|(f, _)| f.as_str()).collect();
670        assert!(fields.contains(&"root"));
671        assert!(fields.contains(&"a"));
672        assert!(fields.contains(&"b"));
673    }
674
675    #[test]
676    fn test_breakdown_not_empty_for_single_leaf() {
677        let a = default_analyzer();
678        let r = a.analyze(&QueryNode::leaf("id", "ID"));
679        assert!(!r.breakdown.is_empty());
680    }
681
682    // ── is_allowed ────────────────────────────────────────────────────────────
683
684    #[test]
685    fn test_is_allowed_within_limits() {
686        let a = default_analyzer();
687        let r = a.analyze(&QueryNode::leaf("id", "ID"));
688        assert!(a.is_allowed(&r));
689    }
690
691    #[test]
692    fn test_is_allowed_complexity_exceeded() {
693        let a = QueryComplexityAnalyzer::new(ComplexityConfig::new(0, 10, 1, 1));
694        let r = a.analyze(&QueryNode::leaf("id", "ID")); // complexity=1 > max=0
695        assert!(!a.is_allowed(&r));
696    }
697
698    #[test]
699    fn test_is_allowed_depth_exceeded() {
700        let a = QueryComplexityAnalyzer::new(ComplexityConfig::new(10_000, 1, 1, 1));
701        let n = QueryNode::with_children("a", "A", vec![QueryNode::leaf("b", "B")]); // depth=2 > max=1
702        let r = a.analyze(&n);
703        assert!(!a.is_allowed(&r));
704    }
705
706    // ── deep nesting ──────────────────────────────────────────────────────────
707
708    #[test]
709    fn test_deep_nesting_cost_calculation() {
710        let a = QueryComplexityAnalyzer::new(ComplexityConfig::new(100_000, 20, 1, 1));
711        // depth 5: a→b→c→d→e(leaf)
712        let n = QueryNode::with_children(
713            "a",
714            "A",
715            vec![QueryNode::with_children(
716                "b",
717                "B",
718                vec![QueryNode::with_children(
719                    "c",
720                    "C",
721                    vec![QueryNode::with_children(
722                        "d",
723                        "D",
724                        vec![QueryNode::leaf("e", "E")],
725                    )],
726                )],
727            )],
728        );
729        let r = a.analyze(&n);
730        // Each node: base=1, multiplier=1, child propagates
731        // e=1, d=1+1=2, c=1+2=3, b=1+3=4, a=1+4=5
732        assert_eq!(r.complexity, 5);
733        assert_eq!(r.depth, 5);
734    }
735
736    #[test]
737    fn test_zero_child_leaf_node() {
738        let a = default_analyzer();
739        let n = QueryNode::leaf("scalar", "String");
740        let r = a.analyze(&n);
741        assert_eq!(r.complexity, 1);
742        assert_eq!(r.depth, 1);
743        assert_eq!(n.field_count(), 1);
744    }
745
746    #[test]
747    fn test_node_cost_matches_analyze() {
748        let a = default_analyzer();
749        let n = QueryNode::list_field("items", "Item", vec![QueryNode::leaf("id", "ID")]);
750        let r = a.analyze(&n);
751        let nc = a.node_cost(&n, 0);
752        assert_eq!(r.complexity, nc);
753    }
754
755    #[test]
756    fn test_multiple_rules_independent() {
757        let mut a = QueryComplexityAnalyzer::new(ComplexityConfig::new(10_000, 10, 1, 10));
758        a.add_rule(ComplexityRule::new("TypeA").with_field(FieldComplexity::new("fieldA", 5, 1)));
759        a.add_rule(ComplexityRule::new("TypeB").with_field(FieldComplexity::new("fieldB", 20, 1)));
760        let na = QueryNode {
761            field: "fieldA".to_string(),
762            type_name: "TypeA".to_string(),
763            children: vec![],
764            is_list: false,
765            args: HashMap::new(),
766        };
767        let nb = QueryNode {
768            field: "fieldB".to_string(),
769            type_name: "TypeB".to_string(),
770            children: vec![],
771            is_list: false,
772            args: HashMap::new(),
773        };
774        let ra = a.analyze(&na);
775        let rb = a.analyze(&nb);
776        assert_eq!(ra.complexity, 5);
777        assert_eq!(rb.complexity, 20);
778    }
779
780    // ── SimpleComplexityAnalyzer / QueryField ─────────────────────────────────
781
782    #[test]
783    fn test_query_field_new_defaults() {
784        let f = QueryField::new("users");
785        assert_eq!(f.name, "users");
786        assert!(f.args.is_empty());
787        assert!(f.children.is_empty());
788    }
789
790    #[test]
791    fn test_query_field_with_arg() {
792        let f = QueryField::new("list").with_arg("first", 10);
793        assert_eq!(f.args.get("first"), Some(&10));
794    }
795
796    #[test]
797    fn test_query_field_with_child() {
798        let f = QueryField::new("user").with_child(QueryField::new("id"));
799        assert_eq!(f.children.len(), 1);
800    }
801
802    #[test]
803    fn test_query_field_depth_leaf() {
804        let f = QueryField::new("id");
805        assert_eq!(f.depth(), 1);
806    }
807
808    #[test]
809    fn test_query_field_depth_two_levels() {
810        let f = QueryField::new("user").with_child(QueryField::new("id"));
811        assert_eq!(f.depth(), 2);
812    }
813
814    #[test]
815    fn test_query_field_depth_three_levels() {
816        let f =
817            QueryField::new("a").with_child(QueryField::new("b").with_child(QueryField::new("c")));
818        assert_eq!(f.depth(), 3);
819    }
820
821    #[test]
822    fn test_simple_analyzer_empty_fields_zero_cost() {
823        let a = SimpleComplexityAnalyzer::new(100);
824        assert!(a.is_within_limit(&[]));
825        let (total, _) = a.analyze_fields(&[]);
826        assert_eq!(total, 0);
827    }
828
829    #[test]
830    fn test_simple_analyzer_default_cost_one() {
831        let a = SimpleComplexityAnalyzer::new(100);
832        let fields = vec![QueryField::new("a"), QueryField::new("b")];
833        let (total, _) = a.analyze_fields(&fields);
834        assert_eq!(total, 2);
835    }
836
837    #[test]
838    fn test_simple_analyzer_custom_default_cost() {
839        let a = SimpleComplexityAnalyzer::new(100).with_default_cost(5);
840        let fields = vec![QueryField::new("a")];
841        let (total, _) = a.analyze_fields(&fields);
842        assert_eq!(total, 5);
843    }
844
845    #[test]
846    fn test_simple_analyzer_fixed_rule() {
847        let mut a = SimpleComplexityAnalyzer::new(100);
848        a.add_rule(SimpleCostRule::fixed("expensive", 50));
849        let fields = vec![QueryField::new("expensive")];
850        let (total, _) = a.analyze_fields(&fields);
851        assert_eq!(total, 50);
852    }
853
854    #[test]
855    fn test_simple_analyzer_multiplied_rule() {
856        let mut a = SimpleComplexityAnalyzer::new(10_000);
857        a.add_rule(SimpleCostRule::multiplied("list", 10, "first"));
858        let fields = vec![QueryField::new("list").with_arg("first", 5)];
859        let (total, _) = a.analyze_fields(&fields);
860        assert_eq!(total, 50); // 10 * 5
861    }
862
863    #[test]
864    fn test_simple_analyzer_multiplied_missing_arg_defaults_one() {
865        let mut a = SimpleComplexityAnalyzer::new(10_000);
866        a.add_rule(SimpleCostRule::multiplied("list", 10, "first"));
867        let fields = vec![QueryField::new("list")]; // no first arg
868        let (total, _) = a.analyze_fields(&fields);
869        assert_eq!(total, 10); // 10 * 1
870    }
871
872    #[test]
873    fn test_simple_analyzer_within_limit_true() {
874        let a = SimpleComplexityAnalyzer::new(100);
875        let fields = vec![QueryField::new("x")];
876        assert!(a.is_within_limit(&fields));
877    }
878
879    #[test]
880    fn test_simple_analyzer_exceeds_limit() {
881        let a = SimpleComplexityAnalyzer::new(0);
882        let fields = vec![QueryField::new("x")];
883        assert!(!a.is_within_limit(&fields));
884    }
885
886    #[test]
887    fn test_simple_analyzer_breakdown_has_entries() {
888        let a = SimpleComplexityAnalyzer::new(100);
889        let fields = vec![QueryField::new("a"), QueryField::new("b")];
890        let (_, breakdown) = a.analyze_fields(&fields);
891        assert_eq!(breakdown.len(), 2);
892    }
893
894    #[test]
895    fn test_simple_analyzer_children_included_in_total() {
896        let a = SimpleComplexityAnalyzer::new(100);
897        let f = QueryField::new("user")
898            .with_child(QueryField::new("name"))
899            .with_child(QueryField::new("email"));
900        let (total, _) = a.analyze_fields(&[f]);
901        assert_eq!(total, 3); // user + name + email
902    }
903
904    #[test]
905    fn test_simple_cost_rule_fixed() {
906        let rule = SimpleCostRule::fixed("field", 7);
907        assert_eq!(rule.cost, 7);
908        assert!(rule.multiplier_arg.is_none());
909    }
910
911    #[test]
912    fn test_simple_cost_rule_multiplied() {
913        let rule = SimpleCostRule::multiplied("field", 7, "count");
914        assert_eq!(rule.multiplier_arg, Some("count".to_string()));
915    }
916
917    #[test]
918    fn test_cost_for_field_unknown_uses_default() {
919        let a = SimpleComplexityAnalyzer::new(100).with_default_cost(3);
920        let cost = a.cost_for_field("unknown", &HashMap::new());
921        assert_eq!(cost, 3);
922    }
923
924    #[test]
925    fn test_cost_for_field_known_fixed() {
926        let mut a = SimpleComplexityAnalyzer::new(100);
927        a.add_rule(SimpleCostRule::fixed("known", 42));
928        let cost = a.cost_for_field("known", &HashMap::new());
929        assert_eq!(cost, 42);
930    }
931
932    #[test]
933    fn test_query_field_multiple_children() {
934        let f = QueryField::new("root")
935            .with_child(QueryField::new("a"))
936            .with_child(QueryField::new("b"))
937            .with_child(QueryField::new("c"));
938        assert_eq!(f.children.len(), 3);
939    }
940}