spinne_core/graph/
component_graph.rs

1use serde_json::Value;
2use std::{
3    collections::{HashMap, HashSet},
4    hash::{DefaultHasher, Hash, Hasher},
5    ops::Not,
6    path::PathBuf,
7};
8
9/// Represents a component with a unique identifier.
10#[derive(Debug, Clone)]
11pub struct Component {
12    /// The unique identifier for the component.
13    pub id: u64,
14    /// The name of the component.
15    pub name: String,
16    /// The path to the component's file.
17    pub path: PathBuf,
18    /// The path to the component's file relative to the project root.
19    pub path_relative_to_root: PathBuf,
20    /// The properties of the component.
21    pub props: HashMap<String, usize>,
22    /// The project the component belongs to.
23    pub project: String,
24}
25
26impl Component {
27    /// Creates a new component with a hash derived from its name and path
28    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    /// Computes a hash from a component's name and path
46    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 // or true, depending on your use case
59    }
60}
61
62impl Not for &Component {
63    type Output = bool;
64
65    fn not(self) -> Self::Output {
66        false // or true, depending on your use case
67    }
68}
69
70/// A simple graph where nodes are components identified by a unique value,
71/// and edges represent the relationship "uses."
72#[derive(Debug, Clone)]
73pub struct ComponentGraph {
74    /// Maps component IDs (hashes) to the actual components.
75    nodes: HashMap<u64, Component>,
76    /// Represents edges: key is a component ID, and value is a set of component IDs it uses.
77    edges: HashMap<u64, HashSet<u64>>,
78}
79
80impl ComponentGraph {
81    /// Creates a new, empty Graph.
82    pub fn new() -> Self {
83        ComponentGraph {
84            nodes: HashMap::new(),
85            edges: HashMap::new(),
86        }
87    }
88
89    /// Checks if a component exists in the graph.
90    pub fn has_component(&self, id: u64) -> bool {
91        self.nodes.contains_key(&id)
92    }
93
94    /// Adds a component to the graph.
95    /// If the component already exists, it updates the component.
96    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    /// Adds a directional edge from `from_id` to `to_id`.
103    /// Returns `false` if either component does not exist.
104    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    /// Adds a component and its dependencies to the graph in a single operation.
113    /// Returns a reference to the added/updated component.
114    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        // Add all dependencies and create edges
123        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    /// Retrieves a component by its unique identifier.
133    pub fn get_component(&self, id: u64) -> Option<&Component> {
134        self.nodes.get(&id)
135    }
136
137    /// Retrieves a component by its name and path.
138    /// This is a convenience method for finding a component by its name and path.
139    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    /// Gets the neighbors (i.e., components used by the specified component).
145    pub fn get_neighbors(&self, id: u64) -> Option<&HashSet<u64>> {
146        self.edges.get(&id)
147    }
148
149    /// Checks if there is an edge from `from_id` to `to_id`.
150    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    /// Adds a property to a component.
157    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    /// Returns the number of components in the graph.
164    pub fn node_count(&self) -> usize {
165        self.nodes.len()
166    }
167
168    /// Returns the number of edges in the graph.
169    pub fn edge_count(&self) -> usize {
170        self.edges.len()
171    }
172
173    /// Converts the component graph into a serializable format for JSON output
174    pub fn to_serializable(&self) -> Value {
175        let mut components = Vec::new();
176        let mut edges = Vec::new();
177
178        // Convert nodes to serializable format
179        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        // Convert edges to serializable format
190        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        // Adding a new component should succeed.
223        assert!(graph.add_component(comp1));
224
225        // Attempting to add a duplicate component (same id) should update the component.
226        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        // Check that the component is stored correctly.
237        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        // Add components first.
262        assert!(graph.add_component(comp1));
263        assert!(graph.add_component(comp2));
264
265        // Add a valid edge.
266        assert!(graph.add_edge(100, 200));
267
268        // Ensure that the neighbor is correctly registered.
269        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        // Add only one component.
286        assert!(graph.add_component(comp1));
287
288        // Trying to add an edge where the source node doesn't exist.
289        assert!(!graph.add_edge(999, 50));
290
291        // Trying to add an edge where the target node doesn't exist.
292        assert!(!graph.add_edge(50, 888));
293    }
294
295    #[test]
296    fn test_get_neighbors_non_existing() {
297        let graph = ComponentGraph::new();
298        // Querying neighbors for a non-existent node should return None.
299        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        // Add the same prop twice
317        graph.add_prop(1, "test_prop".to_string());
318        graph.add_prop(1, "test_prop".to_string());
319
320        // Verify the prop count is 2
321        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        // Create two components with the same data
331        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        // They should have the same hash
345        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        // Different components should have different hashes
364        assert_ne!(component1.id, component2.id);
365    }
366}