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/// Describes one attribute key (on a node or an edge). Everything the UI needs
49/// to label, explain, format, compute and threshold the metric — so the viewer
50/// hardcodes no metric by name.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AttributeSpec {
53    pub value_type: ValueType,
54    /// Concise display label (table grouping, popup rows).
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub label: Option<String>,
57    /// Full name used as a tooltip title (falls back to `label`).
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub name: Option<String>,
60    /// Short label for narrow table headers (falls back to `label`).
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub short: Option<String>,
63    /// Long human description (tooltip body).
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub description: Option<String>,
66    /// Human-readable formula, e.g. `"sloc × (fan_in × fan_out)²"` (display only).
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub formula: Option<String>,
69    /// Evaluable JS expression over sibling attribute names + `Math`, e.g.
70    /// `"sloc * (fan_in * fan_out) ** 2"`. Lets the UI show the live derivation.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub calc: Option<String>,
73    /// `"higher_better"` / `"lower_better"` — drives delta colouring.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub direction: Option<String>,
76    /// Format large values with K/M suffixes.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub abbreviate: Option<bool>,
79    /// Optional group this attribute belongs to, by [`AttributeGroup`] key.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub group: Option<String>,
82    /// Optional two-tier thresholds (language-calibrated).
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub thresholds: Option<Thresholds>,
85}
86
87impl AttributeSpec {
88    /// A minimal spec with just a type + label (the common structural case).
89    pub fn new(value_type: ValueType, label: &str) -> Self {
90        Self {
91            value_type,
92            label: Some(label.to_string()),
93            name: None,
94            short: None,
95            description: None,
96            formula: None,
97            calc: None,
98            direction: None,
99            abbreviate: None,
100            group: None,
101            thresholds: None,
102        }
103    }
104}
105
106/// Visual + label semantics of one node kind (`"file"` / `"external"` / …).
107/// Keyed by kind in [`Level::node_kinds`].
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct NodeKindSpec {
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub label: Option<String>,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub plural: Option<String>,
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub fill: Option<String>,
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub stroke: Option<String>,
118    /// `true` marks a third-party node (a library); the UI derives "external
119    /// edge" from the endpoint kind, not from any edge flag.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub external: Option<bool>,
122}
123
124/// Label + description of one cycle kind (`"mutual"` / `"chain"`).
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct CycleKindSpec {
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub label: Option<String>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub description: Option<String>,
131}
132
133/// How the viewer should cluster nodes in the diagram. Exactly one of `key`
134/// (group by the value of a node attribute, e.g. `crate`) or `function` (a named
135/// grouper the viewer implements, e.g. `dir` — derive the folder from the path).
136/// Absent → the viewer falls back to its default `dir` grouper.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Grouping {
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub key: Option<String>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub function: Option<String>,
143}
144
145/// An analysis level the plugin can produce, with the semantics needed to score
146/// and draw it. The orchestrator merges in centrally-computed attribute specs
147/// and the computed `ui` block before writing the snapshot.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct Level {
150    pub name: String,
151    pub edge_kinds: BTreeMap<String, EdgeKindSpec>,
152    pub node_attributes: BTreeMap<String, AttributeSpec>,
153    pub edge_attributes: BTreeMap<String, AttributeSpec>,
154    pub attribute_groups: BTreeMap<String, AttributeGroup>,
155    /// Node-kind vocabulary (label/colour/external). Plugins seed it from
156    /// [`crate::default_node_kinds`] and may customize.
157    #[serde(default)]
158    pub node_kinds: BTreeMap<String, NodeKindSpec>,
159    /// Cycle-kind vocabulary. Plugins seed it from [`crate::default_cycle_kinds`].
160    #[serde(default)]
161    pub cycle_kinds: BTreeMap<String, CycleKindSpec>,
162    /// How the viewer should cluster nodes (defaults to `dir` when absent).
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub grouping: Option<Grouping>,
165}