Skip to main content

code_ranker_plugin_api/
attrs.rs

1//! Free-form attributes for nodes and edges — a string-keyed map of scalar
2//! values — plus the value-type tag used by the attribute dictionaries.
3//!
4//! This is what keeps the model **generic**: the plugin chooses the keys it
5//! knows (`"path"`, `"loc"`, `"visibility"`, `"version"`, or language-specific
6//! ones), the orchestrator adds computed keys (metrics), and consumers read the
7//! keys they understand via an [`AttributeSpec`](crate::AttributeSpec) dictionary
8//! (label/hint/type) carried in the snapshot.
9
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13/// Attribute bag. `BTreeMap` for deterministic (alphabetical) key order, so
14/// snapshots stay byte-stable.
15pub type Attributes = BTreeMap<String, AttrValue>;
16
17/// A scalar attribute value, serialized to its natural JSON form (no wrapper).
18///
19/// Numeric rounding (e.g. 3-significant-digit truncation for metrics) is applied
20/// by the producer *before* inserting a `Float`, not here.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[serde(untagged)]
23pub enum AttrValue {
24    // Order matters for untagged deserialization: bool before the numerics so
25    // `true`/`false` aren't misread, integers before floats, string last.
26    Bool(bool),
27    Int(i64),
28    Float(f64),
29    Str(String),
30}
31
32/// The kind of value an attribute holds — tells the UI what it can DO with the
33/// field (numbers: sum/average; strings: concatenate/count; bools: count). Used
34/// in [`AttributeSpec`](crate::AttributeSpec) to describe a key independently of
35/// whether any value is present.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum ValueType {
39    Bool,
40    Int,
41    Float,
42    Str,
43}
44
45impl AttrValue {
46    /// The [`ValueType`] tag for this value — single source of truth for the
47    /// value/type mapping.
48    pub fn value_type(&self) -> ValueType {
49        match self {
50            AttrValue::Bool(_) => ValueType::Bool,
51            AttrValue::Int(_) => ValueType::Int,
52            AttrValue::Float(_) => ValueType::Float,
53            AttrValue::Str(_) => ValueType::Str,
54        }
55    }
56}
57
58impl From<bool> for AttrValue {
59    fn from(v: bool) -> Self {
60        AttrValue::Bool(v)
61    }
62}
63impl From<i64> for AttrValue {
64    fn from(v: i64) -> Self {
65        AttrValue::Int(v)
66    }
67}
68impl From<u32> for AttrValue {
69    fn from(v: u32) -> Self {
70        AttrValue::Int(v as i64)
71    }
72}
73impl From<f64> for AttrValue {
74    fn from(v: f64) -> Self {
75        AttrValue::Float(v)
76    }
77}
78impl From<String> for AttrValue {
79    fn from(v: String) -> Self {
80        AttrValue::Str(v)
81    }
82}
83impl From<&str> for AttrValue {
84    fn from(v: &str) -> Self {
85        AttrValue::Str(v.to_string())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn value_type_maps_each_variant() {
95        assert_eq!(AttrValue::Bool(true).value_type(), ValueType::Bool);
96        assert_eq!(AttrValue::Int(1).value_type(), ValueType::Int);
97        assert_eq!(AttrValue::Float(1.5).value_type(), ValueType::Float);
98        assert_eq!(AttrValue::Str("x".into()).value_type(), ValueType::Str);
99    }
100
101    #[test]
102    fn from_impls_cover_each_scalar() {
103        assert_eq!(AttrValue::from(true), AttrValue::Bool(true));
104        assert_eq!(AttrValue::from(7_i64), AttrValue::Int(7));
105        assert_eq!(AttrValue::from(7_u32), AttrValue::Int(7));
106        assert_eq!(AttrValue::from(2.5_f64), AttrValue::Float(2.5));
107        assert_eq!(AttrValue::from("s".to_string()), AttrValue::Str("s".into()));
108        assert_eq!(AttrValue::from("s"), AttrValue::Str("s".into()));
109    }
110}