1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::confidence::Confidence;
6
7#[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#[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
53fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Hyperedge {
88 pub nodes: Vec<String>,
89 pub relation: String,
90 pub label: String,
91}
92
93#[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#[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#[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#[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 pub bridge_ratio: f64,
156 pub communities_touched: Vec<usize>,
157}
158
159#[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}