1use serde_json::Value;
2use std::{
3 collections::{HashMap, HashSet},
4 hash::{DefaultHasher, Hash, Hasher},
5 ops::Not,
6 path::PathBuf,
7};
8
9#[derive(Debug, Clone)]
11pub struct Component {
12 pub id: u64,
14 pub name: String,
16 pub path: PathBuf,
18 pub path_relative_to_root: PathBuf,
20 pub props: HashMap<String, usize>,
22 pub project: String,
24}
25
26impl Component {
27 pub fn new(
29 name: String,
30 path: PathBuf,
31 path_relative_to_root: PathBuf,
32 project: String,
33 ) -> Self {
34 let id = Self::compute_hash(&name, &path);
35 Component {
36 id,
37 name,
38 path,
39 path_relative_to_root,
40 props: HashMap::new(),
41 project,
42 }
43 }
44
45 fn compute_hash(name: &str, path: &PathBuf) -> u64 {
47 let mut hasher = DefaultHasher::new();
48 name.hash(&mut hasher);
49 path.hash(&mut hasher);
50 hasher.finish()
51 }
52}
53
54impl Not for Component {
55 type Output = bool;
56
57 fn not(self) -> Self::Output {
58 false }
60}
61
62impl Not for &Component {
63 type Output = bool;
64
65 fn not(self) -> Self::Output {
66 false }
68}
69
70#[derive(Debug, Clone)]
73pub struct ComponentGraph {
74 nodes: HashMap<u64, Component>,
76 edges: HashMap<u64, HashSet<u64>>,
78}
79
80impl ComponentGraph {
81 pub fn new() -> Self {
83 ComponentGraph {
84 nodes: HashMap::new(),
85 edges: HashMap::new(),
86 }
87 }
88
89 pub fn has_component(&self, id: u64) -> bool {
91 self.nodes.contains_key(&id)
92 }
93
94 pub fn add_component(&mut self, component: Component) -> &Component {
97 let id = component.id;
98 self.nodes.insert(id, component);
99 self.nodes.get(&id).unwrap()
100 }
101
102 pub fn add_edge(&mut self, from_id: u64, to_id: u64) -> bool {
105 if !self.has_component(from_id) || !self.has_component(to_id) {
106 return false;
107 }
108
109 self.edges.entry(from_id).or_default().insert(to_id)
110 }
111
112 pub fn add_component_with_deps(
115 &mut self,
116 component: Component,
117 deps: Vec<Component>,
118 ) -> &Component {
119 let component_id = component.id;
120 self.nodes.insert(component_id, component);
121
122 for dep in deps {
124 let dep_id = dep.id;
125 self.nodes.insert(dep_id, dep);
126 self.edges.entry(component_id).or_default().insert(dep_id);
127 }
128
129 self.nodes.get(&component_id).unwrap()
130 }
131
132 pub fn get_component(&self, id: u64) -> Option<&Component> {
134 self.nodes.get(&id)
135 }
136
137 pub fn find_component(&self, name: &str, path: &PathBuf) -> Option<&Component> {
140 let id = Component::compute_hash(name, path);
141 self.nodes.get(&id)
142 }
143
144 pub fn get_neighbors(&self, id: u64) -> Option<&HashSet<u64>> {
146 self.edges.get(&id)
147 }
148
149 pub fn has_edge(&self, from_id: u64, to_id: u64) -> bool {
151 self.edges
152 .get(&from_id)
153 .map_or(false, |neighbors| neighbors.contains(&to_id))
154 }
155
156 pub fn add_prop(&mut self, component_id: u64, prop: String) {
158 if let Some(component) = self.nodes.get_mut(&component_id) {
159 *component.props.entry(prop).or_insert(0) += 1;
160 }
161 }
162
163 pub fn node_count(&self) -> usize {
165 self.nodes.len()
166 }
167
168 pub fn edge_count(&self) -> usize {
170 self.edges.len()
171 }
172
173 pub fn to_serializable(&self) -> Value {
175 let mut components = Vec::new();
176 let mut edges = Vec::new();
177
178 for component in self.nodes.values() {
180 components.push(serde_json::json!({
181 "id": component.id,
182 "name": component.name,
183 "path": component.path_relative_to_root,
184 "props": component.props,
185 "project": component.project,
186 }));
187 }
188
189 for (from_id, to_ids) in &self.edges {
191 for to_id in to_ids {
192 edges.push(serde_json::json!({
193 "from": from_id,
194 "to": to_id,
195 }));
196 }
197 }
198
199 serde_json::json!({
200 "components": components,
201 "edges": edges,
202 })
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_add_component() {
212 let mut graph = ComponentGraph::new();
213 let comp1 = Component {
214 id: 1,
215 name: "Component A".to_string(),
216 path: PathBuf::from("file1.rs"),
217 path_relative_to_root: PathBuf::from("file1.rs"),
218 props: HashMap::new(),
219 project: "Project A".to_string(),
220 };
221
222 assert!(graph.add_component(comp1));
224
225 let comp1_dup = Component {
227 id: 1,
228 name: "Duplicate Component A".to_string(),
229 path: PathBuf::from("file1.rs"),
230 path_relative_to_root: PathBuf::from("file1.rs"),
231 props: HashMap::new(),
232 project: "Project A".to_string(),
233 };
234 assert!(graph.add_component(comp1_dup));
235
236 let retrieved = graph.get_component(1).unwrap();
238 assert_eq!(retrieved.name, "Duplicate Component A");
239 }
240
241 #[test]
242 fn test_add_edge() {
243 let mut graph = ComponentGraph::new();
244 let comp1 = Component {
245 id: 100,
246 name: "Component 1".to_string(),
247 path: PathBuf::from("file1.rs"),
248 path_relative_to_root: PathBuf::from("file1.rs"),
249 props: HashMap::new(),
250 project: "Project A".to_string(),
251 };
252 let comp2 = Component {
253 id: 200,
254 name: "Component 2".to_string(),
255 path: PathBuf::from("file2.rs"),
256 path_relative_to_root: PathBuf::from("file2.rs"),
257 props: HashMap::new(),
258 project: "Project A".to_string(),
259 };
260
261 assert!(graph.add_component(comp1));
263 assert!(graph.add_component(comp2));
264
265 assert!(graph.add_edge(100, 200));
267
268 let neighbors = graph.get_neighbors(100).unwrap();
270 assert!(neighbors.contains(&200));
271 }
272
273 #[test]
274 fn test_invalid_edges() {
275 let mut graph = ComponentGraph::new();
276 let comp1 = Component {
277 id: 50,
278 name: "Component 50".to_string(),
279 path: PathBuf::from("file1.rs"),
280 path_relative_to_root: PathBuf::from("file1.rs"),
281 props: HashMap::new(),
282 project: "Project A".to_string(),
283 };
284
285 assert!(graph.add_component(comp1));
287
288 assert!(!graph.add_edge(999, 50));
290
291 assert!(!graph.add_edge(50, 888));
293 }
294
295 #[test]
296 fn test_get_neighbors_non_existing() {
297 let graph = ComponentGraph::new();
298 assert!(graph.get_neighbors(10).is_none());
300 }
301
302 #[test]
303 fn test_add_prop() {
304 let mut graph = ComponentGraph::new();
305 let comp = Component {
306 id: 1,
307 name: "Test".to_string(),
308 path: PathBuf::from("test.rs"),
309 path_relative_to_root: PathBuf::from("test.rs"),
310 props: HashMap::new(),
311 project: "Test Project".to_string(),
312 };
313
314 graph.add_component(comp);
315
316 graph.add_prop(1, "test_prop".to_string());
318 graph.add_prop(1, "test_prop".to_string());
319
320 let component = graph.get_component(1).unwrap();
322 assert_eq!(*component.props.get("test_prop").unwrap(), 2);
323 }
324
325 #[test]
326 fn test_component_hash_consistency() {
327 let name = "TestComponent".to_string();
328 let path = PathBuf::from("src/components/test.rs");
329
330 let component1 = Component::new(
332 name.clone(),
333 path.clone(),
334 PathBuf::from("test.rs"),
335 "Project A".to_string(),
336 );
337 let component2 = Component::new(
338 name,
339 path,
340 PathBuf::from("test.rs"),
341 "Project A".to_string(),
342 );
343
344 assert_eq!(component1.id, component2.id);
346 }
347
348 #[test]
349 fn test_component_hash_uniqueness() {
350 let component1 = Component::new(
351 "Component1".to_string(),
352 PathBuf::from("src/comp1.rs"),
353 PathBuf::from("src/comp1.rs"),
354 "Project A".to_string(),
355 );
356 let component2 = Component::new(
357 "Component2".to_string(),
358 PathBuf::from("src/comp2.rs"),
359 PathBuf::from("src/comp2.rs"),
360 "Project A".to_string(),
361 );
362
363 assert_ne!(component1.id, component2.id);
365 }
366}