spinne_core/graph/
component_graph.rs

1use petgraph::{graph::NodeIndex, Graph};
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, path::PathBuf};
4
5use spinne_logger::Logger;
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct Component {
9    pub name: String,
10    pub file_path: PathBuf,
11    pub prop_usage: HashMap<String, usize>,
12}
13
14/// ComponentGraph is a graph of components and their relationships.
15/// Components are nodes and relationships are edges.
16#[derive(Debug)]
17pub struct ComponentGraph {
18    pub graph: Graph<Component, ()>,
19}
20
21#[derive(Serialize, Deserialize, Debug)]
22pub struct SerializableComponentGraph {
23    pub nodes: Vec<Component>,
24    pub edges: Vec<(usize, usize)>,
25}
26
27/// Represents a graph of components and their relationships.
28impl ComponentGraph {
29    /// Creates a new empty ComponentGraph.
30    pub fn new() -> Self {
31        Self {
32            graph: Graph::new(),
33        }
34    }
35
36    /// Checks if a component exists in the graph.
37    /// A component is uniquely identified by its name and file path.
38    pub fn has_component(&self, key: &str, file_path: &PathBuf) -> bool {
39        self.graph
40            .node_indices()
41            .any(|i| self.graph[i].name == key && self.graph[i].file_path == *file_path)
42    }
43
44    /// Checks if an edge exists between two components.
45    pub fn has_edge(&self, from: NodeIndex, to: NodeIndex) -> bool {
46        self.graph.contains_edge(from, to)
47    }
48
49    /// Tries to find the index of a component in the graph.
50    pub fn get_component(&self, key: &str, file_path: &PathBuf) -> Option<NodeIndex> {
51        self.graph
52            .node_indices()
53            .find(|i| self.graph[*i].name == key && self.graph[*i].file_path == *file_path)
54    }
55
56    /// Adds a new component to the graph.
57    /// If the component already exists, it returns the existing component's index.
58    /// Otherwise, it adds a new component and returns the index of the new component.
59    pub fn add_component(&mut self, key: String, file_path: PathBuf) -> NodeIndex {
60        if !self.has_component(&key, &file_path) {
61            Logger::debug(
62                &format!("Adding new component: {:?}, {:?}", key, file_path),
63                2,
64            );
65            let node_index = self.graph.add_node(Component {
66                name: key,
67                file_path,
68                prop_usage: HashMap::new(),
69            });
70            node_index
71        } else {
72            Logger::debug(
73                &format!("Called add_component for existing component: {:?}", key),
74                2,
75            );
76            self.graph
77                .node_indices()
78                .find(|i| self.graph[*i].name == key && self.graph[*i].file_path == file_path)
79                .unwrap()
80        }
81    }
82
83    /// Adds a child edge between two components.
84    pub fn add_child(&mut self, parent: (&str, &PathBuf), child: (&str, &PathBuf)) {
85        let parent_index = self.get_or_add_component(parent.0, parent.1.clone());
86        let child_index = self.get_or_add_component(child.0, child.1.clone());
87
88        Logger::debug(
89            &format!("Adding child edge: {:?} -> {:?}", parent.0, child.0),
90            2,
91        );
92        self.graph.add_edge(parent_index, child_index, ());
93    }
94
95    /// Tries to find the index of a component in the graph.
96    /// If the component does not exist, it adds a new component and returns the index of the new component.
97    fn get_or_add_component(&mut self, name: &str, file_path: PathBuf) -> NodeIndex {
98        match self.get_component(name, &file_path) {
99            Some(index) => index,
100            None => self.add_component(name.to_string(), file_path),
101        }
102    }
103
104    /// Adds a prop usage to a component.
105    /// If the component does not exist, it does nothing.
106    pub fn add_prop_usage(&mut self, component: &str, file_path: &PathBuf, prop: String) {
107        if let Some(node_index) = self.get_component(component, file_path) {
108            let component = &mut self.graph[node_index];
109
110            Logger::debug(
111                &format!("Adding prop usage: {:?} -> {:?}", component, prop),
112                2,
113            );
114            *component.prop_usage.entry(prop).or_insert(0) += 1;
115        }
116    }
117
118    /// Prints the graph to the console.
119    pub fn print_graph(&self) {
120        println!("{:?}", self.graph);
121    }
122
123    /// Converts the graph to a serializable format.
124    /// This can be used to save the graph to a file or to send it over the network.
125    pub fn to_serializable(&self) -> SerializableComponentGraph {
126        let nodes: Vec<Component> = self
127            .graph
128            .node_indices()
129            .map(|i| Component {
130                name: self.graph[i].name.clone(),
131                file_path: self.graph[i].file_path.clone(),
132                prop_usage: self.graph[i].prop_usage.clone(),
133            })
134            .collect();
135
136        let edges: Vec<(usize, usize)> = self
137            .graph
138            .edge_indices()
139            .map(|e| {
140                let (a, b) = self.graph.edge_endpoints(e).unwrap();
141                (a.index(), b.index())
142            })
143            .collect();
144
145        SerializableComponentGraph { nodes, edges }
146    }
147
148    /// Converts a serializable graph back to a ComponentGraph.
149    /// This can be used to load a graph from a file or to receive it over the network.
150    pub fn from_serializable(serializable: SerializableComponentGraph) -> Self {
151        let mut graph = Graph::new();
152        let node_indices: Vec<NodeIndex> = serializable
153            .nodes
154            .into_iter()
155            .map(|component| graph.add_node(component))
156            .collect();
157
158        for (from, to) in serializable.edges {
159            graph.add_edge(node_indices[from], node_indices[to], ());
160        }
161
162        ComponentGraph { graph }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_new() {
172        let graph = ComponentGraph::new();
173        assert_eq!(graph.graph.node_count(), 0);
174        assert_eq!(graph.graph.edge_count(), 0);
175    }
176
177    #[test]
178    fn test_add_component() {
179        let mut graph = ComponentGraph::new();
180        let file_path = PathBuf::from("/path/to/Component.tsx");
181        let key = "MyComponent".to_string();
182
183        let node_index = graph.add_component(key.clone(), file_path.clone());
184
185        assert_eq!(graph.graph.node_count(), 1);
186        assert_eq!(graph.graph.edge_count(), 0);
187        assert!(graph.has_component(&key, &file_path));
188        assert_eq!(graph.graph[node_index].name, key);
189        assert_eq!(graph.graph[node_index].file_path, file_path);
190    }
191
192    #[test]
193    fn test_add_duplicate_component() {
194        let mut graph = ComponentGraph::new();
195        let file_path = PathBuf::from("/path/to/Component.tsx");
196        let key = "MyComponent".to_string();
197
198        let node_index = graph.add_component(key.clone(), file_path.clone());
199        let second_node_index = graph.add_component(key.clone(), file_path.clone());
200
201        assert_eq!(graph.graph.node_count(), 1);
202        assert_eq!(graph.graph.edge_count(), 0);
203        assert!(graph.has_component(&key, &file_path));
204        assert_eq!(second_node_index, node_index);
205    }
206
207    #[test]
208    fn test_add_child() {
209        let mut graph = ComponentGraph::new();
210        let parent_key = "ParentComponent".to_string();
211        let child_key = "ChildComponent".to_string();
212
213        let parent_node_index = graph.add_component(
214            parent_key.clone(),
215            PathBuf::from("/path/to/ParentComponent.tsx"),
216        );
217        let child_node_index = graph.add_component(
218            child_key.clone(),
219            PathBuf::from("/path/to/ChildComponent.tsx"),
220        );
221
222        graph.add_child(
223            (&parent_key, &PathBuf::from("/path/to/ParentComponent.tsx")),
224            (&child_key, &PathBuf::from("/path/to/ChildComponent.tsx")),
225        );
226
227        assert_eq!(graph.graph.node_count(), 2);
228        assert_eq!(graph.graph.edge_count(), 1);
229        assert!(graph.has_component(&parent_key, &PathBuf::from("/path/to/ParentComponent.tsx")));
230        assert!(graph.has_component(&child_key, &PathBuf::from("/path/to/ChildComponent.tsx")));
231        assert!(graph
232            .graph
233            .contains_edge(parent_node_index, child_node_index));
234    }
235
236    #[test]
237    fn test_complex_graph_structure() {
238        let mut graph = ComponentGraph::new();
239
240        // Add components
241        let app_path = PathBuf::from("/path/to/App.tsx");
242        let app = graph.add_component("App".to_string(), app_path.clone());
243        let header =
244            graph.add_component("Header".to_string(), PathBuf::from("/path/to/Header.tsx"));
245
246        let footer =
247            graph.add_component("Footer".to_string(), PathBuf::from("/path/to/Footer.tsx"));
248        let content =
249            graph.add_component("Content".to_string(), PathBuf::from("/path/to/Content.tsx"));
250
251        // Add relationships
252        graph.add_child(
253            ("App", &app_path),
254            ("Header", &PathBuf::from("/path/to/Header.tsx")),
255        );
256        graph.add_child(
257            ("App", &app_path),
258            ("Footer", &PathBuf::from("/path/to/Footer.tsx")),
259        );
260        graph.add_child(
261            ("App", &app_path),
262            ("Content", &PathBuf::from("/path/to/Content.tsx")),
263        );
264
265        // Assertions
266        assert_eq!(graph.graph.node_count(), 4);
267        assert_eq!(graph.graph.edge_count(), 3);
268
269        let app_component = &graph.graph[app];
270        assert_eq!(app_component.name, "App");
271        assert_eq!(app_component.file_path, app_path);
272
273        let header_component = &graph.graph[header];
274        assert_eq!(header_component.name, "Header");
275        assert_eq!(
276            header_component.file_path,
277            PathBuf::from("/path/to/Header.tsx")
278        );
279
280        let footer_component = &graph.graph[footer];
281        assert_eq!(footer_component.name, "Footer");
282        assert_eq!(
283            footer_component.file_path,
284            PathBuf::from("/path/to/Footer.tsx")
285        );
286
287        let content_component = &graph.graph[content];
288        assert_eq!(content_component.name, "Content");
289        assert_eq!(
290            content_component.file_path,
291            PathBuf::from("/path/to/Content.tsx")
292        );
293
294        assert!(graph.graph.contains_edge(app, header));
295        assert!(graph.graph.contains_edge(app, footer));
296        assert!(graph.graph.contains_edge(app, content));
297    }
298
299    #[test]
300    fn test_cyclic_dependency() {
301        let mut graph = ComponentGraph::new();
302
303        let component_a = graph.add_component(
304            "ComponentA".to_string(),
305            PathBuf::from("/path/to/ComponentA.tsx"),
306        );
307        let component_b = graph.add_component(
308            "ComponentB".to_string(),
309            PathBuf::from("/path/to/ComponentB.tsx"),
310        );
311
312        graph.add_child(
313            ("ComponentA", &PathBuf::from("/path/to/ComponentA.tsx")),
314            ("ComponentB", &PathBuf::from("/path/to/ComponentB.tsx")),
315        );
316        graph.add_child(
317            ("ComponentB", &PathBuf::from("/path/to/ComponentB.tsx")),
318            ("ComponentA", &PathBuf::from("/path/to/ComponentA.tsx")),
319        );
320
321        assert_eq!(graph.graph.node_count(), 2);
322
323        assert_eq!(graph.graph.edge_count(), 2);
324        assert!(graph.graph.contains_edge(component_a, component_b));
325        assert!(graph.graph.contains_edge(component_b, component_a));
326    }
327
328    #[test]
329    fn test_to_serializable() {
330        let mut graph = ComponentGraph::new();
331        graph.add_component(
332            "ComponentA".to_string(),
333            PathBuf::from("/path/to/ComponentA.tsx"),
334        );
335        graph.add_component(
336            "ComponentB".to_string(),
337            PathBuf::from("/path/to/ComponentB.tsx"),
338        );
339
340        graph.add_child(
341            ("ComponentA", &PathBuf::from("/path/to/ComponentA.tsx")),
342            ("ComponentB", &PathBuf::from("/path/to/ComponentB.tsx")),
343        );
344
345        let serializable = graph.to_serializable();
346        assert_eq!(serializable.nodes.len(), 2);
347        assert_eq!(serializable.edges.len(), 1);
348    }
349}