Skip to main content

graphify_core/
model.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::confidence::Confidence;
6
7// ---------------------------------------------------------------------------
8// NodeType
9// ---------------------------------------------------------------------------
10
11/// The kind of entity a graph node represents.
12///
13/// Serialized as lowercase strings (e.g. `"class"`, `"function"`).
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum NodeType {
17    Class,
18    Function,
19    Module,
20    Concept,
21    Paper,
22    Image,
23    File,
24    Method,
25    Interface,
26    Enum,
27    Struct,
28    Trait,
29    Constant,
30    Variable,
31    Package,
32    Namespace,
33}
34
35// ---------------------------------------------------------------------------
36// GraphNode
37// ---------------------------------------------------------------------------
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct GraphNode {
41    pub id: String,
42    pub label: String,
43    pub source_file: String,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub source_location: Option<String>,
46    pub node_type: NodeType,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub community: Option<usize>,
49    #[serde(flatten)]
50    pub extra: HashMap<String, serde_json::Value>,
51}
52
53// ---------------------------------------------------------------------------
54// GraphEdge
55// ---------------------------------------------------------------------------
56
57fn default_confidence_score() -> f64 {
58    1.0
59}
60
61fn default_weight() -> f64 {
62    1.0
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct GraphEdge {
67    pub source: String,
68    pub target: String,
69    pub relation: String,
70    pub confidence: Confidence,
71    #[serde(default = "default_confidence_score")]
72    pub confidence_score: f64,
73    pub source_file: String,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub source_location: Option<String>,
76    #[serde(default = "default_weight")]
77    pub weight: f64,
78    #[serde(flatten)]
79    pub extra: HashMap<String, serde_json::Value>,
80}
81
82// ---------------------------------------------------------------------------
83// Hyperedge
84// ---------------------------------------------------------------------------
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Hyperedge {
88    pub nodes: Vec<String>,
89    pub relation: String,
90    pub label: String,
91}
92
93// ---------------------------------------------------------------------------
94// ExtractionResult
95// ---------------------------------------------------------------------------
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct ExtractionResult {
99    pub nodes: Vec<GraphNode>,
100    pub edges: Vec<GraphEdge>,
101    #[serde(default)]
102    pub hyperedges: Vec<Hyperedge>,
103}
104
105// ---------------------------------------------------------------------------
106// CommunityInfo
107// ---------------------------------------------------------------------------
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct CommunityInfo {
111    pub id: usize,
112    pub nodes: Vec<String>,
113    pub cohesion: f64,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub label: Option<String>,
116}
117
118// ---------------------------------------------------------------------------
119// AnalysisResult & helpers
120// ---------------------------------------------------------------------------
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct GodNode {
124    pub id: String,
125    pub label: String,
126    pub degree: usize,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub community: Option<usize>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Surprise {
133    pub source: String,
134    pub target: String,
135    pub source_community: usize,
136    pub target_community: usize,
137    pub relation: String,
138}
139
140#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141pub struct AnalysisResult {
142    pub god_nodes: Vec<GodNode>,
143    pub surprises: Vec<Surprise>,
144    pub questions: Vec<String>,
145}
146
147/// A node that bridges multiple communities.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct BridgeNode {
150    pub id: String,
151    pub label: String,
152    pub total_edges: usize,
153    pub cross_community_edges: usize,
154    /// Ratio of cross-community edges to total edges (0.0–1.0).
155    pub bridge_ratio: f64,
156    pub communities_touched: Vec<usize>,
157}
158
159// ---------------------------------------------------------------------------
160// Tests
161// ---------------------------------------------------------------------------
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn sample_node() -> GraphNode {
168        GraphNode {
169            id: "my_class".into(),
170            label: "MyClass".into(),
171            source_file: "src/main.rs".into(),
172            source_location: Some("L42".into()),
173            node_type: NodeType::Class,
174            community: None,
175            extra: HashMap::new(),
176        }
177    }
178
179    fn sample_edge() -> GraphEdge {
180        GraphEdge {
181            source: "a".into(),
182            target: "b".into(),
183            relation: "calls".into(),
184            confidence: Confidence::Extracted,
185            confidence_score: 1.0,
186            source_file: "src/main.rs".into(),
187            source_location: None,
188            weight: 1.0,
189            extra: HashMap::new(),
190        }
191    }
192
193    #[test]
194    fn node_type_serializes_lowercase() {
195        assert_eq!(
196            serde_json::to_string(&NodeType::Class).unwrap(),
197            r#""class""#
198        );
199        assert_eq!(
200            serde_json::to_string(&NodeType::Function).unwrap(),
201            r#""function""#
202        );
203        assert_eq!(
204            serde_json::to_string(&NodeType::Namespace).unwrap(),
205            r#""namespace""#
206        );
207    }
208
209    #[test]
210    fn node_roundtrip() {
211        let node = sample_node();
212        let json = serde_json::to_string(&node).unwrap();
213        let back: GraphNode = serde_json::from_str(&json).unwrap();
214        assert_eq!(back.id, "my_class");
215        assert_eq!(back.node_type, NodeType::Class);
216    }
217
218    #[test]
219    fn node_skip_none_fields() {
220        let mut node = sample_node();
221        node.source_location = None;
222        node.community = None;
223        let json = serde_json::to_string(&node).unwrap();
224        assert!(!json.contains("source_location"));
225        assert!(!json.contains("community"));
226    }
227
228    #[test]
229    fn edge_defaults() {
230        let json = r#"{
231            "source": "a",
232            "target": "b",
233            "relation": "calls",
234            "confidence": "EXTRACTED",
235            "source_file": "x.rs"
236        }"#;
237        let edge: GraphEdge = serde_json::from_str(json).unwrap();
238        assert!((edge.confidence_score - 1.0).abs() < f64::EPSILON);
239        assert!((edge.weight - 1.0).abs() < f64::EPSILON);
240    }
241
242    #[test]
243    fn edge_roundtrip() {
244        let edge = sample_edge();
245        let json = serde_json::to_string(&edge).unwrap();
246        let back: GraphEdge = serde_json::from_str(&json).unwrap();
247        assert_eq!(back.relation, "calls");
248        assert_eq!(back.confidence, Confidence::Extracted);
249    }
250
251    #[test]
252    fn extraction_result_default() {
253        let r = ExtractionResult::default();
254        assert!(r.nodes.is_empty());
255        assert!(r.edges.is_empty());
256        assert!(r.hyperedges.is_empty());
257    }
258
259    #[test]
260    fn extra_fields_flatten() {
261        let mut node = sample_node();
262        node.extra
263            .insert("custom".into(), serde_json::Value::Bool(true));
264        let json = serde_json::to_string(&node).unwrap();
265        assert!(json.contains(r#""custom":true"#));
266    }
267
268    #[test]
269    fn community_info_roundtrip() {
270        let ci = CommunityInfo {
271            id: 0,
272            nodes: vec!["a".into(), "b".into()],
273            cohesion: 0.85,
274            label: Some("cluster-0".into()),
275        };
276        let json = serde_json::to_string(&ci).unwrap();
277        let back: CommunityInfo = serde_json::from_str(&json).unwrap();
278        assert_eq!(back.id, 0);
279        assert_eq!(back.nodes.len(), 2);
280    }
281
282    #[test]
283    fn analysis_result_default() {
284        let ar = AnalysisResult::default();
285        assert!(ar.god_nodes.is_empty());
286        assert!(ar.surprises.is_empty());
287        assert!(ar.questions.is_empty());
288    }
289}