Skip to main content

code_ranker_plugin_api/
report.rs

1//! Per-language overrides of the global report view/stat lists.
2//!
3//! The report's column order, card-featured metrics, and JSON `stats` keys come
4//! from the global metric catalog (`code-ranker-graph/metrics/builtin.toml`). A
5//! language may *patch* those inherited lists from its `<lang>.toml` `[report]`
6//! section — add a language-specific metric (e.g. Rust `unsafe`), drop some, swap
7//! one in place, or replace the list wholesale — without restating the whole
8//! catalog. [`ListPatch`] is the patch primitive; the parsing of the TOML
9//! `[report]` section into these types lives in `code-ranker-plugins`.
10
11/// A patch over an inherited ordered string list. Either a wholesale
12/// [`replace_all`](Self::replace_all) (a plain TOML array) or a set of in-place
13/// edits applied to the inherited base, in this order: `clear` → `remove` →
14/// `replace` → `after` / `before` → `prepend` → `add`. The result is
15/// de-duplicated, keeping the first occurrence (order-stable).
16#[derive(Debug, Clone, Default, PartialEq)]
17pub struct ListPatch {
18    /// Plain-array form: replace the inherited list outright (then dedup).
19    pub replace_all: Option<Vec<String>>,
20    /// Start from an empty list instead of the inherited base.
21    pub clear: bool,
22    /// Drop every element equal to one of these.
23    pub remove: Vec<String>,
24    /// Swap an element in place, preserving its position: `(old, new)`.
25    pub replace: Vec<(String, String)>,
26    /// Insert items immediately **after** an anchor element: `(anchor, items)`.
27    /// No-op if the anchor is absent.
28    pub after: Vec<(String, Vec<String>)>,
29    /// Insert items immediately **before** an anchor element: `(anchor, items)`.
30    pub before: Vec<(String, Vec<String>)>,
31    /// Insert at the front (before the inherited elements).
32    pub prepend: Vec<String>,
33    /// Append at the end.
34    pub add: Vec<String>,
35}
36
37impl ListPatch {
38    /// Apply the patch to `base`, returning the resulting order-stable, de-duped list.
39    pub fn apply(&self, base: &[String]) -> Vec<String> {
40        if let Some(all) = &self.replace_all {
41            return dedup(all.clone());
42        }
43        let mut out: Vec<String> = if self.clear {
44            Vec::new()
45        } else {
46            base.to_vec()
47        };
48        if !self.remove.is_empty() {
49            out.retain(|x| !self.remove.iter().any(|r| r == x));
50        }
51        for (old, new) in &self.replace {
52            if let Some(pos) = out.iter().position(|x| x == old) {
53                out[pos] = new.clone();
54            }
55        }
56        for (anchor, items) in &self.after {
57            if let Some(pos) = out.iter().position(|x| x == anchor) {
58                out.splice(pos + 1..pos + 1, items.iter().cloned());
59            }
60        }
61        for (anchor, items) in &self.before {
62            if let Some(pos) = out.iter().position(|x| x == anchor) {
63                out.splice(pos..pos, items.iter().cloned());
64            }
65        }
66        if !self.prepend.is_empty() {
67            let mut front = self.prepend.clone();
68            front.extend(out);
69            out = front;
70        }
71        out.extend(self.add.iter().cloned());
72        dedup(out)
73    }
74
75    /// True when the patch makes no change (no override declared).
76    pub fn is_noop(&self) -> bool {
77        self.replace_all.is_none()
78            && !self.clear
79            && self.remove.is_empty()
80            && self.replace.is_empty()
81            && self.after.is_empty()
82            && self.before.is_empty()
83            && self.prepend.is_empty()
84            && self.add.is_empty()
85    }
86}
87
88/// De-duplicate a list, keeping the first occurrence of each element (order-stable).
89fn dedup(list: Vec<String>) -> Vec<String> {
90    let mut seen = std::collections::HashSet::new();
91    list.into_iter()
92        .filter(|x| seen.insert(x.clone()))
93        .collect()
94}
95
96/// A language's overrides of the global report lists. Each field patches the
97/// inherited list from the metric catalog; an empty (no-op) patch leaves the
98/// global default untouched.
99#[derive(Debug, Clone, Default)]
100pub struct ReportOverride {
101    /// The node-table column order (`[report].columns`).
102    pub columns: ListPatch,
103    /// The card-featured metrics (`[report].card`).
104    pub card: ListPatch,
105    /// The JSON report's aggregate `stats` keys.
106    pub stats: ListPatch,
107    /// Metrics the SVG map offers as circle-size modes (`ui.size`), on top
108    /// of the built-in `loc` / `hk` modes.
109    pub size: ListPatch,
110    /// Metrics the SVG map offers as on/off node filters (`ui.filter`) —
111    /// keep only nodes where the metric has signal — alongside the built-in `cycle`.
112    pub filter: ListPatch,
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn v(xs: &[&str]) -> Vec<String> {
120        xs.iter().map(|s| s.to_string()).collect()
121    }
122
123    #[test]
124    fn apply_covers_every_op() {
125        let base = v(&["kind", "sloc", "hk", "volume", "effort"]);
126
127        // remove (one or many) + add (appended, de-duped against the base)
128        let p = ListPatch {
129            remove: v(&["volume", "effort"]),
130            add: v(&["unsafe", "hk"]), // hk already present → not duplicated
131            ..Default::default()
132        };
133        assert_eq!(p.apply(&base), v(&["kind", "sloc", "hk", "unsafe"]));
134
135        // replace in place (position preserved)
136        let p = ListPatch {
137            replace: vec![("sloc".into(), "lloc".into())],
138            ..Default::default()
139        };
140        assert_eq!(
141            p.apply(&base),
142            v(&["kind", "lloc", "hk", "volume", "effort"])
143        );
144
145        // clear + add = a fresh list
146        let p = ListPatch {
147            clear: true,
148            add: v(&["kind", "hk"]),
149            ..Default::default()
150        };
151        assert_eq!(p.apply(&base), v(&["kind", "hk"]));
152
153        // after / before insert relative to an anchor (position preserved)
154        let p = ListPatch {
155            after: vec![("hk".into(), v(&["tsr"]))],
156            ..Default::default()
157        };
158        assert_eq!(
159            p.apply(&base),
160            v(&["kind", "sloc", "hk", "tsr", "volume", "effort"])
161        );
162        let p = ListPatch {
163            before: vec![("hk".into(), v(&["tsr"]))],
164            ..Default::default()
165        };
166        assert_eq!(
167            p.apply(&base),
168            v(&["kind", "sloc", "tsr", "hk", "volume", "effort"])
169        );
170
171        // prepend goes to the front
172        let p = ListPatch {
173            prepend: v(&["unsafe"]),
174            ..Default::default()
175        };
176        assert_eq!(
177            p.apply(&base),
178            v(&["unsafe", "kind", "sloc", "hk", "volume", "effort"])
179        );
180
181        // replace_all wins outright (and de-dups)
182        let p = ListPatch {
183            replace_all: Some(v(&["a", "b", "a"])),
184            ..Default::default()
185        };
186        assert_eq!(p.apply(&base), v(&["a", "b"]));
187
188        // a no-op patch returns the base unchanged
189        assert!(ListPatch::default().is_noop());
190        assert_eq!(ListPatch::default().apply(&base), base);
191    }
192}