Skip to main content

code_ranker_plugin_api/
level.rs

1//! Level descriptors + the **semantics dictionaries** that let the core handle
2//! unknown kinds/keys without hardcoding their names, and let the UI render any
3//! language/metric set purely from data: edge kinds ([`EdgeKindSpec`]),
4//! node/edge attributes ([`AttributeSpec`], grouped via [`AttributeGroup`]),
5//! node kinds ([`NodeKindSpec`]) and cycle kinds ([`CycleKindSpec`]).
6//!
7//! The dictionaries are **maps** keyed by the kind/attribute/group name; the
8//! spec value holds only the remaining metadata.
9
10use crate::attrs::ValueType;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13
14/// Semantics of one edge kind. Keyed by the edge `kind` in
15/// [`Level::edge_kinds`]. `flow` is the single source of truth for "is this
16/// information flow": counted in coupling/cycles AND drawn when `true`;
17/// structural (e.g. `contains`) and excluded/hidden when `false`.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct EdgeKindSpec {
20    pub flow: bool,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub label: Option<String>,
23    /// Long human description (used as a UI tooltip).
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub description: Option<String>,
26}
27
28/// A named group of attributes (UI section). Keyed by group name in
29/// [`Level::attribute_groups`]; attributes reference it via
30/// [`AttributeSpec::group`]. Metadata only — storage stays flat.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AttributeGroup {
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub label: Option<String>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub description: Option<String>,
37}
38
39/// Two-tier per-metric thresholds (at/under `info` is fine; above `warning` is
40/// likely a problem). Carried on an [`AttributeSpec`]; produced by a plugin
41/// (language-calibrated), absent when a metric has no calibration.
42#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
43pub struct Thresholds {
44    pub info: f64,
45    pub warning: f64,
46}
47
48/// Whether a metric's delta is "good" when it moves up or down — drives the
49/// green/red colouring in the viewer. `Neutral` (the default) means the metric
50/// has no agreed-good direction (raw sizes, structural counts) and is left
51/// uncoloured. `Neutral` is skipped on the wire, so a neutral metric serializes
52/// exactly as the old `Option<String>` form did: the `direction` field absent.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum Direction {
56    #[default]
57    Neutral,
58    LowerBetter,
59    HigherBetter,
60}
61
62impl Direction {
63    /// `true` for the default (no opinion) — used by serde to omit the field.
64    pub fn is_neutral(&self) -> bool {
65        matches!(self, Direction::Neutral)
66    }
67}
68
69/// Describes one attribute key (on a node or an edge). Everything the UI needs
70/// to label, explain, format, compute and threshold the metric — so the viewer
71/// hardcodes no metric by name.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct AttributeSpec {
74    pub value_type: ValueType,
75    /// Concise display label (table grouping, popup rows).
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub label: Option<String>,
78    /// Full name used as a tooltip title (falls back to `label`).
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub name: Option<String>,
81    /// Short label for narrow table headers (falls back to `label`).
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub short: Option<String>,
84    /// Long human description (tooltip body).
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub description: Option<String>,
87    /// Human-readable formula, e.g. `"sloc × (fan_in × fan_out)²"` (display only).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub formula: Option<String>,
90    /// Evaluable JS expression over sibling attribute names + `Math`, e.g.
91    /// `"sloc * (fan_in * fan_out) ** 2"`. Lets the UI show the live derivation.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub calc: Option<String>,
94    /// Whether higher or lower is "better" — drives delta colouring. `Neutral`
95    /// (the default) is omitted from the wire.
96    #[serde(default, skip_serializing_if = "Direction::is_neutral")]
97    pub direction: Direction,
98    /// Format large values with K/M suffixes.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub abbreviate: Option<bool>,
101    /// Optional group this attribute belongs to, by [`AttributeGroup`] key.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub group: Option<String>,
104    /// Optional two-tier thresholds (language-calibrated).
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub thresholds: Option<Thresholds>,
107}
108
109impl AttributeSpec {
110    /// A minimal spec with just a type + label (the common structural case).
111    pub fn new(value_type: ValueType, label: &str) -> Self {
112        Self {
113            value_type,
114            label: Some(label.to_string()),
115            name: None,
116            short: None,
117            description: None,
118            formula: None,
119            calc: None,
120            direction: Direction::Neutral,
121            abbreviate: None,
122            group: None,
123            thresholds: None,
124        }
125    }
126}
127
128/// One row of a declarative attribute table: a flat, named-field description of
129/// an [`AttributeSpec`]. Empty `&str` fields become `None`; `direction` defaults
130/// to [`Direction::Neutral`]. Build a whole dictionary with [`attr_dict`].
131///
132/// This replaces both the per-field `spec.x = Some(...)` boilerplate and the
133/// positional metric-row tuples that the metric/coupling crates used to carry
134/// separately, so every centrally-computed spec is declared the same way.
135#[derive(Clone)]
136pub struct SpecRow {
137    pub group: &'static str,
138    pub value_type: ValueType,
139    pub label: &'static str,
140    pub name: &'static str,
141    pub short: &'static str,
142    pub description: &'static str,
143    pub formula: &'static str,
144    pub calc: &'static str,
145    pub direction: Direction,
146    pub abbreviate: bool,
147}
148
149impl Default for SpecRow {
150    fn default() -> Self {
151        SpecRow {
152            group: "",
153            value_type: ValueType::Int,
154            label: "",
155            name: "",
156            short: "",
157            description: "",
158            formula: "",
159            calc: "",
160            direction: Direction::Neutral,
161            abbreviate: false,
162        }
163    }
164}
165
166impl SpecRow {
167    fn into_spec(self) -> AttributeSpec {
168        let opt = |s: &str| (!s.is_empty()).then(|| s.to_string());
169        AttributeSpec {
170            value_type: self.value_type,
171            label: opt(self.label),
172            name: opt(self.name),
173            short: opt(self.short),
174            description: opt(self.description),
175            formula: opt(self.formula),
176            calc: opt(self.calc),
177            direction: self.direction,
178            abbreviate: self.abbreviate.then_some(true),
179            group: opt(self.group),
180            thresholds: None,
181        }
182    }
183}
184
185/// Assemble a `key → AttributeSpec` dictionary from a declarative table.
186pub fn attr_dict(rows: Vec<(&'static str, SpecRow)>) -> BTreeMap<String, AttributeSpec> {
187    rows.into_iter()
188        .map(|(k, r)| (k.to_string(), r.into_spec()))
189        .collect()
190}
191
192/// Build an [`AttributeGroup`] from a label + description.
193pub fn group(label: &str, description: &str) -> AttributeGroup {
194    AttributeGroup {
195        label: Some(label.to_string()),
196        description: Some(description.to_string()),
197    }
198}
199
200/// Visual + label semantics of one node kind (`"file"` / `"external"` / …).
201/// Keyed by kind in [`Level::node_kinds`].
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct NodeKindSpec {
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub label: Option<String>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub plural: Option<String>,
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub fill: Option<String>,
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub stroke: Option<String>,
212    /// `true` marks a third-party node (a library); the UI derives "external
213    /// edge" from the endpoint kind, not from any edge flag.
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub external: Option<bool>,
216}
217
218/// Label + description of one cycle kind (`"mutual"` / `"chain"`).
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct CycleKindSpec {
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub label: Option<String>,
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub description: Option<String>,
225}
226
227/// How the viewer should cluster nodes in the diagram. Exactly one of `key`
228/// (group by the value of a node attribute, e.g. `crate`) or `function` (a named
229/// grouper the viewer implements, e.g. `dir` — derive the folder from the path).
230/// Absent → the viewer falls back to its default `dir` grouper.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct Grouping {
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub key: Option<String>,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub function: Option<String>,
237}
238
239/// An analysis level the plugin can produce, with the semantics needed to score
240/// and draw it. The orchestrator merges in centrally-computed attribute specs
241/// and the computed `ui` block before writing the snapshot.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct Level {
244    pub name: String,
245    pub edge_kinds: BTreeMap<String, EdgeKindSpec>,
246    pub node_attributes: BTreeMap<String, AttributeSpec>,
247    pub edge_attributes: BTreeMap<String, AttributeSpec>,
248    pub attribute_groups: BTreeMap<String, AttributeGroup>,
249    /// Node-kind vocabulary (label/colour/external). Plugins seed it from
250    /// [`crate::default_node_kinds`] and may customize.
251    #[serde(default)]
252    pub node_kinds: BTreeMap<String, NodeKindSpec>,
253    /// Cycle-kind vocabulary. Plugins seed it from [`crate::default_cycle_kinds`].
254    #[serde(default)]
255    pub cycle_kinds: BTreeMap<String, CycleKindSpec>,
256    /// How the viewer should cluster nodes (defaults to `dir` when absent).
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub grouping: Option<Grouping>,
259}