Skip to main content

boundary_core/
graph.rs

1use std::collections::HashMap;
2
3use petgraph::graph::{DiGraph, NodeIndex};
4use petgraph::visit::EdgeRef;
5use serde::{Deserialize, Serialize};
6
7use crate::metrics_report::LayerCouplingMatrix;
8use crate::types::{
9    ArchLayer, ArchitectureMode, Component, ComponentId, Dependency, DependencyKind, SourceLocation,
10};
11
12/// Node in the dependency graph
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GraphNode {
15    pub id: ComponentId,
16    pub name: String,
17    pub layer: Option<ArchLayer>,
18    pub is_cross_cutting: bool,
19    #[serde(default)]
20    pub architecture_mode: ArchitectureMode,
21    #[serde(default)]
22    pub location: SourceLocation,
23}
24
25/// Edge in the dependency graph
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct GraphEdge {
28    pub kind: DependencyKind,
29    pub location: SourceLocation,
30    pub import_path: Option<String>,
31}
32
33/// Directed dependency graph of architectural components.
34pub struct DependencyGraph {
35    graph: DiGraph<GraphNode, GraphEdge>,
36    index: HashMap<ComponentId, NodeIndex>,
37}
38
39impl DependencyGraph {
40    pub fn new() -> Self {
41        Self {
42            graph: DiGraph::new(),
43            index: HashMap::new(),
44        }
45    }
46
47    /// Add a component as a node. Returns the node index.
48    pub fn add_component(&mut self, component: &Component) -> NodeIndex {
49        if let Some(&idx) = self.index.get(&component.id) {
50            return idx;
51        }
52        let node = GraphNode {
53            id: component.id.clone(),
54            name: component.name.clone(),
55            layer: component.layer,
56            is_cross_cutting: component.is_cross_cutting,
57            architecture_mode: component.architecture_mode,
58            location: component.location.clone(),
59        };
60        let idx = self.graph.add_node(node);
61        self.index.insert(component.id.clone(), idx);
62        idx
63    }
64
65    /// Ensure a component ID exists as a node (create a minimal node if needed).
66    pub fn ensure_node(
67        &mut self,
68        id: &ComponentId,
69        layer: Option<ArchLayer>,
70        is_cross_cutting: bool,
71    ) -> NodeIndex {
72        self.ensure_node_with_mode(id, layer, is_cross_cutting, ArchitectureMode::Ddd)
73    }
74
75    /// Ensure a component ID exists as a node with a specific architecture mode.
76    pub fn ensure_node_with_mode(
77        &mut self,
78        id: &ComponentId,
79        layer: Option<ArchLayer>,
80        is_cross_cutting: bool,
81        architecture_mode: ArchitectureMode,
82    ) -> NodeIndex {
83        if let Some(&idx) = self.index.get(id) {
84            return idx;
85        }
86        let node = GraphNode {
87            id: id.clone(),
88            name: id.0.clone(),
89            layer,
90            is_cross_cutting,
91            architecture_mode,
92            location: SourceLocation::default(),
93        };
94        let idx = self.graph.add_node(node);
95        self.index.insert(id.clone(), idx);
96        idx
97    }
98
99    /// Add a dependency as an edge.
100    pub fn add_dependency(&mut self, dep: &Dependency) {
101        let from_idx = self.ensure_node(&dep.from, None, false);
102        let to_idx = self.ensure_node(&dep.to, None, false);
103        let edge = GraphEdge {
104            kind: dep.kind.clone(),
105            location: dep.location.clone(),
106            import_path: dep.import_path.clone(),
107        };
108        self.graph.add_edge(from_idx, to_idx, edge);
109    }
110
111    /// Iterate over all edges with their source and target nodes.
112    pub fn edges_with_nodes(&self) -> Vec<(&GraphNode, &GraphNode, &GraphEdge)> {
113        self.graph
114            .edge_references()
115            .map(|e| {
116                let src = &self.graph[e.source()];
117                let tgt = &self.graph[e.target()];
118                (src, tgt, e.weight())
119            })
120            .collect()
121    }
122
123    /// Find cycles using DFS. Returns groups of component IDs that form cycles.
124    pub fn find_cycles(&self) -> Vec<Vec<ComponentId>> {
125        let sccs = petgraph::algo::kosaraju_scc(&self.graph);
126        sccs.into_iter()
127            .filter(|scc| scc.len() > 1)
128            .map(|scc| scc.iter().map(|&idx| self.graph[idx].id.clone()).collect())
129            .collect()
130    }
131
132    /// Count ports and adapters in the graph.
133    pub fn node_count(&self) -> usize {
134        self.graph.node_count()
135    }
136
137    /// Get all nodes
138    pub fn nodes(&self) -> Vec<&GraphNode> {
139        self.graph.node_weights().collect()
140    }
141
142    /// Count nodes grouped by layer.
143    pub fn nodes_by_layer(&self) -> HashMap<String, usize> {
144        let mut counts: HashMap<String, usize> = HashMap::new();
145        for node in self.graph.node_weights() {
146            if node.is_cross_cutting {
147                *counts.entry("cross_cutting".to_string()).or_insert(0) += 1;
148            } else {
149                let key = match node.layer {
150                    Some(layer) => layer.to_string(),
151                    None => "unclassified".to_string(),
152                };
153                *counts.entry(key).or_insert(0) += 1;
154            }
155        }
156        counts
157    }
158
159    /// Build a layer coupling matrix from edge data.
160    pub fn layer_coupling_matrix(&self) -> LayerCouplingMatrix {
161        let mut matrix = LayerCouplingMatrix::new();
162        for edge in self.graph.edge_references() {
163            let src = &self.graph[edge.source()];
164            let tgt = &self.graph[edge.target()];
165            if let (Some(from_layer), Some(to_layer)) = (src.layer, tgt.layer) {
166                matrix.increment(&from_layer, &to_layer);
167            }
168        }
169        matrix
170    }
171
172    /// Calculate max dependency depth using BFS from each root node.
173    pub fn max_dependency_depth(&self) -> usize {
174        use petgraph::visit::Bfs;
175        let mut max_depth = 0;
176        // Find root nodes (no incoming edges)
177        for idx in self.graph.node_indices() {
178            let has_incoming = self
179                .graph
180                .neighbors_directed(idx, petgraph::Direction::Incoming)
181                .next()
182                .is_some();
183            if !has_incoming {
184                let mut bfs = Bfs::new(&self.graph, idx);
185                let mut depth = 0;
186                let mut current_level_end = idx;
187                let mut next_level_end = idx;
188                while let Some(node) = bfs.next(&self.graph) {
189                    if node == current_level_end || node == idx {
190                        // Check neighbors to find end of next level
191                        for neighbor in self.graph.neighbors(node) {
192                            next_level_end = neighbor;
193                        }
194                    }
195                    for neighbor in self.graph.neighbors(node) {
196                        next_level_end = neighbor;
197                    }
198                    if node == current_level_end && node != idx {
199                        depth += 1;
200                        current_level_end = next_level_end;
201                    }
202                }
203                // Simple approach: count edges in longest path from root
204                max_depth = max_depth.max(depth);
205            }
206        }
207        // Fallback: use edge count as rough depth estimate
208        if max_depth == 0 && self.graph.edge_count() > 0 {
209            max_depth = 1;
210        }
211        max_depth
212    }
213}
214
215impl Default for DependencyGraph {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::types::*;
225    use std::path::PathBuf;
226
227    fn make_component(id: &str, name: &str, layer: Option<ArchLayer>) -> Component {
228        Component {
229            id: ComponentId(id.to_string()),
230            name: name.to_string(),
231            kind: ComponentKind::Entity(EntityInfo {
232                name: name.to_string(),
233                fields: vec![],
234                methods: vec![],
235                is_active_record: false,
236            }),
237            layer,
238            location: SourceLocation {
239                file: PathBuf::from("test.go"),
240                line: 1,
241                column: 1,
242            },
243            is_cross_cutting: false,
244            architecture_mode: ArchitectureMode::Ddd,
245        }
246    }
247
248    fn make_dep(from: &str, to: &str) -> Dependency {
249        Dependency {
250            from: ComponentId(from.to_string()),
251            to: ComponentId(to.to_string()),
252            kind: DependencyKind::Import,
253            location: SourceLocation {
254                file: PathBuf::from("test.go"),
255                line: 1,
256                column: 1,
257            },
258            import_path: None,
259        }
260    }
261
262    #[test]
263    fn test_add_component_and_dependency() {
264        let mut graph = DependencyGraph::new();
265        let c1 = make_component("a", "A", Some(ArchLayer::Domain));
266        let c2 = make_component("b", "B", Some(ArchLayer::Infrastructure));
267
268        graph.add_component(&c1);
269        graph.add_component(&c2);
270        assert_eq!(graph.node_count(), 2);
271
272        graph.add_dependency(&make_dep("a", "b"));
273        let edges = graph.edges_with_nodes();
274        assert_eq!(edges.len(), 1);
275    }
276
277    #[test]
278    fn test_find_cycles() {
279        let mut graph = DependencyGraph::new();
280        let c1 = make_component("a", "A", None);
281        let c2 = make_component("b", "B", None);
282
283        graph.add_component(&c1);
284        graph.add_component(&c2);
285        graph.add_dependency(&make_dep("a", "b"));
286        graph.add_dependency(&make_dep("b", "a"));
287
288        let cycles = graph.find_cycles();
289        assert!(!cycles.is_empty(), "should detect cycle");
290    }
291
292    #[test]
293    fn test_no_duplicate_nodes() {
294        let mut graph = DependencyGraph::new();
295        let c1 = make_component("a", "A", None);
296
297        graph.add_component(&c1);
298        graph.add_component(&c1); // duplicate
299        assert_eq!(graph.node_count(), 1);
300    }
301}