|
| 1 | +use std::collections::HashMap; |
| 2 | + |
| 3 | +use clap::Parser; |
| 4 | + |
| 5 | +use crate::{DepGraph, Edge, NodeInfo}; |
| 6 | + |
| 7 | +/// Cut edges between subgraphs, isolating each subgraph. |
| 8 | +#[derive(Clone, Debug, Default, Parser)] |
| 9 | +pub struct SliceArgs { |
| 10 | + /// Remove nodes that are not inside any subgraph. |
| 11 | + #[clap(long)] |
| 12 | + pub drop_orphans: bool, |
| 13 | + |
| 14 | + /// Also slice within nested subgraphs recursively. |
| 15 | + #[clap(short, long)] |
| 16 | + pub recursive: bool, |
| 17 | +} |
| 18 | + |
| 19 | +/// Cut edges that cross subgraph boundaries, isolating each subgraph into a |
| 20 | +/// disconnected component. |
| 21 | +/// |
| 22 | +/// In the default (top-level) mode, every node is assigned to the top-level |
| 23 | +/// subgraph it belongs to (even if nested deeper). Edges are kept only when |
| 24 | +/// both endpoints share the same top-level group. With `--drop-orphans`, |
| 25 | +/// root-level nodes (not inside any subgraph) and their edges are also removed. |
| 26 | +/// |
| 27 | +/// In `--recursive` mode, the same logic is applied independently at each level |
| 28 | +/// of the subgraph hierarchy: edges at a given level are kept only when both |
| 29 | +/// endpoints belong to the same immediate child subgraph (or are both |
| 30 | +/// root-level at that scope). |
| 31 | +pub fn slice(graph: &DepGraph, args: &SliceArgs) -> eyre::Result<DepGraph> { |
| 32 | + if args.recursive { |
| 33 | + Ok(slice_recursive(graph, args.drop_orphans)) |
| 34 | + } else { |
| 35 | + Ok(slice_toplevel(graph, args.drop_orphans)) |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +/// Recursively assign all nodes in `sg` (and its nested subgraphs) to `group`. |
| 40 | +fn assign_group(sg: &DepGraph, map: &mut HashMap<String, usize>, group: usize) { |
| 41 | + for id in sg.nodes.keys() { |
| 42 | + map.insert(id.clone(), group); |
| 43 | + } |
| 44 | + for child in &sg.subgraphs { |
| 45 | + assign_group(child, map, group); |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +/// Check if an edge should be kept based on the group map. |
| 50 | +/// |
| 51 | +/// An edge is kept when both endpoints belong to the same group. Endpoints not |
| 52 | +/// present in the map are considered root-level; two root-level endpoints are |
| 53 | +/// kept unless `drop_orphans` is true. |
| 54 | +fn edge_allowed(edge: &Edge, group_map: &HashMap<String, usize>, drop_orphans: bool) -> bool { |
| 55 | + match (group_map.get(&edge.from), group_map.get(&edge.to)) { |
| 56 | + (Some(f), Some(t)) => f == t, |
| 57 | + (None, None) => !drop_orphans, |
| 58 | + _ => false, |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +/// Filter nodes at one level: drop root-level nodes when `drop_orphans` is set. |
| 63 | +fn filter_nodes( |
| 64 | + graph: &DepGraph, |
| 65 | + group_map: &HashMap<String, usize>, |
| 66 | + drop_orphans: bool, |
| 67 | +) -> indexmap::IndexMap<String, NodeInfo> { |
| 68 | + if drop_orphans { |
| 69 | + graph |
| 70 | + .nodes |
| 71 | + .iter() |
| 72 | + .filter(|(id, _)| group_map.contains_key(id.as_str())) |
| 73 | + .map(|(id, info)| (id.clone(), info.clone())) |
| 74 | + .collect() |
| 75 | + } else { |
| 76 | + graph.nodes.clone() |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +fn slice_toplevel(graph: &DepGraph, drop_orphans: bool) -> DepGraph { |
| 81 | + let mut group_map = HashMap::new(); |
| 82 | + for (i, sg) in graph.subgraphs.iter().enumerate() { |
| 83 | + assign_group(sg, &mut group_map, i); |
| 84 | + } |
| 85 | + rebuild(graph, &group_map, drop_orphans) |
| 86 | +} |
| 87 | + |
| 88 | +/// Rebuild the graph tree, filtering edges using a global group map. |
| 89 | +fn rebuild(graph: &DepGraph, group_map: &HashMap<String, usize>, drop_orphans: bool) -> DepGraph { |
| 90 | + let nodes = filter_nodes(graph, group_map, drop_orphans); |
| 91 | + let edges: Vec<Edge> = graph |
| 92 | + .edges |
| 93 | + .iter() |
| 94 | + .filter(|e| edge_allowed(e, group_map, drop_orphans)) |
| 95 | + .cloned() |
| 96 | + .collect(); |
| 97 | + let subgraphs = graph |
| 98 | + .subgraphs |
| 99 | + .iter() |
| 100 | + .map(|sg| rebuild(sg, group_map, drop_orphans)) |
| 101 | + .collect(); |
| 102 | + |
| 103 | + DepGraph { |
| 104 | + id: graph.id.clone(), |
| 105 | + attrs: graph.attrs.clone(), |
| 106 | + nodes, |
| 107 | + edges, |
| 108 | + subgraphs, |
| 109 | + ..Default::default() |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +fn slice_recursive(graph: &DepGraph, drop_orphans: bool) -> DepGraph { |
| 114 | + // Nothing to slice at a leaf level (no subgraph boundaries). |
| 115 | + if graph.subgraphs.is_empty() { |
| 116 | + return DepGraph { |
| 117 | + id: graph.id.clone(), |
| 118 | + attrs: graph.attrs.clone(), |
| 119 | + nodes: graph.nodes.clone(), |
| 120 | + edges: graph.edges.clone(), |
| 121 | + subgraphs: vec![], |
| 122 | + ..Default::default() |
| 123 | + }; |
| 124 | + } |
| 125 | + |
| 126 | + let mut group_map = HashMap::new(); |
| 127 | + for (i, sg) in graph.subgraphs.iter().enumerate() { |
| 128 | + assign_group(sg, &mut group_map, i); |
| 129 | + } |
| 130 | + |
| 131 | + let nodes = filter_nodes(graph, &group_map, drop_orphans); |
| 132 | + let edges: Vec<Edge> = graph |
| 133 | + .edges |
| 134 | + .iter() |
| 135 | + .filter(|e| edge_allowed(e, &group_map, drop_orphans)) |
| 136 | + .cloned() |
| 137 | + .collect(); |
| 138 | + let subgraphs = graph |
| 139 | + .subgraphs |
| 140 | + .iter() |
| 141 | + .map(|sg| slice_recursive(sg, drop_orphans)) |
| 142 | + .collect(); |
| 143 | + |
| 144 | + DepGraph { |
| 145 | + id: graph.id.clone(), |
| 146 | + attrs: graph.attrs.clone(), |
| 147 | + nodes, |
| 148 | + edges, |
| 149 | + subgraphs, |
| 150 | + ..Default::default() |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +#[cfg(test)] |
| 155 | +mod tests { |
| 156 | + use super::*; |
| 157 | + |
| 158 | + fn make_graph(nodes: &[&str], edges: &[(&str, &str)], subgraphs: Vec<DepGraph>) -> DepGraph { |
| 159 | + DepGraph { |
| 160 | + nodes: nodes |
| 161 | + .iter() |
| 162 | + .map(|id| (id.to_string(), NodeInfo::new(*id))) |
| 163 | + .collect(), |
| 164 | + edges: edges |
| 165 | + .iter() |
| 166 | + .map(|(from, to)| Edge { |
| 167 | + from: from.to_string(), |
| 168 | + to: to.to_string(), |
| 169 | + ..Default::default() |
| 170 | + }) |
| 171 | + .collect(), |
| 172 | + subgraphs, |
| 173 | + ..Default::default() |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + fn node_ids(graph: &DepGraph) -> Vec<&str> { |
| 178 | + graph.nodes.keys().map(|s| s.as_str()).collect() |
| 179 | + } |
| 180 | + |
| 181 | + fn edge_pairs(graph: &DepGraph) -> Vec<(&str, &str)> { |
| 182 | + graph |
| 183 | + .edges |
| 184 | + .iter() |
| 185 | + .map(|e| (e.from.as_str(), e.to.as_str())) |
| 186 | + .collect() |
| 187 | + } |
| 188 | + |
| 189 | + #[test] |
| 190 | + fn empty_graph() { |
| 191 | + let g = DepGraph::default(); |
| 192 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 193 | + assert!(result.nodes.is_empty()); |
| 194 | + assert!(result.edges.is_empty()); |
| 195 | + assert!(result.subgraphs.is_empty()); |
| 196 | + } |
| 197 | + |
| 198 | + #[test] |
| 199 | + fn no_subgraphs_preserves_all() { |
| 200 | + let g = make_graph(&["a", "b"], &[("a", "b")], vec![]); |
| 201 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 202 | + assert_eq!(node_ids(&result), vec!["a", "b"]); |
| 203 | + assert_eq!(edge_pairs(&result), vec![("a", "b")]); |
| 204 | + } |
| 205 | + |
| 206 | + #[test] |
| 207 | + fn no_subgraphs_drop_orphans_removes_all() { |
| 208 | + let g = make_graph(&["a", "b"], &[("a", "b")], vec![]); |
| 209 | + let args = SliceArgs { |
| 210 | + drop_orphans: true, |
| 211 | + ..Default::default() |
| 212 | + }; |
| 213 | + let result = slice(&g, &args).unwrap(); |
| 214 | + assert!(result.nodes.is_empty()); |
| 215 | + assert!(result.edges.is_empty()); |
| 216 | + } |
| 217 | + |
| 218 | + #[test] |
| 219 | + fn cross_subgraph_edges_removed() { |
| 220 | + let sg0 = make_graph(&["a"], &[], vec![]); |
| 221 | + let sg1 = make_graph(&["b"], &[], vec![]); |
| 222 | + let g = make_graph(&[], &[("a", "b")], vec![sg0, sg1]); |
| 223 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 224 | + assert!(result.edges.is_empty()); |
| 225 | + } |
| 226 | + |
| 227 | + #[test] |
| 228 | + fn intra_subgraph_edges_preserved() { |
| 229 | + let sg = make_graph(&["a", "b"], &[], vec![]); |
| 230 | + let g = make_graph(&[], &[("a", "b")], vec![sg]); |
| 231 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 232 | + assert_eq!(edge_pairs(&result), vec![("a", "b")]); |
| 233 | + } |
| 234 | + |
| 235 | + #[test] |
| 236 | + fn root_to_subgraph_edges_removed() { |
| 237 | + let sg = make_graph(&["b"], &[], vec![]); |
| 238 | + let g = make_graph(&["a"], &[("a", "b")], vec![sg]); |
| 239 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 240 | + assert!(result.edges.is_empty()); |
| 241 | + assert_eq!(node_ids(&result), vec!["a"]); |
| 242 | + } |
| 243 | + |
| 244 | + #[test] |
| 245 | + fn root_nodes_preserved_by_default() { |
| 246 | + let sg = make_graph(&["b"], &[], vec![]); |
| 247 | + let g = make_graph(&["a"], &[], vec![sg]); |
| 248 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 249 | + assert_eq!(node_ids(&result), vec!["a"]); |
| 250 | + } |
| 251 | + |
| 252 | + #[test] |
| 253 | + fn drop_orphans_removes_root_nodes_and_edges() { |
| 254 | + let sg = make_graph(&["b", "c"], &[], vec![]); |
| 255 | + let g = make_graph(&["a"], &[("a", "b"), ("b", "c")], vec![sg]); |
| 256 | + let args = SliceArgs { |
| 257 | + drop_orphans: true, |
| 258 | + ..Default::default() |
| 259 | + }; |
| 260 | + let result = slice(&g, &args).unwrap(); |
| 261 | + assert!(result.nodes.is_empty()); |
| 262 | + // b->c kept (both in same group), a->b dropped (cross-boundary) |
| 263 | + assert_eq!(edge_pairs(&result), vec![("b", "c")]); |
| 264 | + } |
| 265 | + |
| 266 | + #[test] |
| 267 | + fn nested_subgraph_toplevel_groups_under_outermost() { |
| 268 | + let inner = make_graph(&["c"], &[], vec![]); |
| 269 | + let outer = make_graph(&["a"], &[], vec![inner]); |
| 270 | + let g = make_graph(&[], &[("a", "c")], vec![outer]); |
| 271 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 272 | + // Both a and c belong to the same top-level group |
| 273 | + assert_eq!(edge_pairs(&result), vec![("a", "c")]); |
| 274 | + } |
| 275 | + |
| 276 | + #[test] |
| 277 | + fn recursive_cuts_within_nested_subgraphs() { |
| 278 | + let inner = make_graph(&["b"], &[], vec![]); |
| 279 | + let outer = make_graph(&["a"], &[("a", "b")], vec![inner]); |
| 280 | + let g = make_graph(&[], &[], vec![outer]); |
| 281 | + |
| 282 | + // Top-level: a and b both in group 0, edge preserved inside the subgraph |
| 283 | + let result_toplevel = slice(&g, &SliceArgs::default()).unwrap(); |
| 284 | + assert_eq!(edge_pairs(&result_toplevel.subgraphs[0]), vec![("a", "b")]); |
| 285 | + |
| 286 | + // Recursive: at the outer subgraph level, a is root, b is in inner -> cut |
| 287 | + let args = SliceArgs { |
| 288 | + recursive: true, |
| 289 | + ..Default::default() |
| 290 | + }; |
| 291 | + let result_recursive = slice(&g, &args).unwrap(); |
| 292 | + assert!(result_recursive.subgraphs[0].edges.is_empty()); |
| 293 | + } |
| 294 | + |
| 295 | + #[test] |
| 296 | + fn recursive_drop_orphans_at_each_level() { |
| 297 | + let inner = make_graph(&["b"], &[], vec![]); |
| 298 | + let outer = make_graph(&["a"], &[("a", "b")], vec![inner]); |
| 299 | + let g = make_graph(&[], &[], vec![outer]); |
| 300 | + |
| 301 | + let args = SliceArgs { |
| 302 | + recursive: true, |
| 303 | + drop_orphans: true, |
| 304 | + }; |
| 305 | + let result = slice(&g, &args).unwrap(); |
| 306 | + // a is an orphan at the outer subgraph level -> dropped |
| 307 | + assert!(result.subgraphs[0].nodes.is_empty()); |
| 308 | + assert!(result.subgraphs[0].edges.is_empty()); |
| 309 | + // b is in the inner subgraph -> preserved |
| 310 | + assert_eq!(node_ids(&result.subgraphs[0].subgraphs[0]), vec!["b"]); |
| 311 | + } |
| 312 | + |
| 313 | + #[test] |
| 314 | + fn subgraph_attrs_and_id_preserved() { |
| 315 | + let mut sg = make_graph(&["a"], &[], vec![]); |
| 316 | + sg.id = Some("cluster_0".to_string()); |
| 317 | + sg.attrs.insert("color".to_string(), "blue".to_string()); |
| 318 | + let g = make_graph(&[], &[], vec![sg]); |
| 319 | + |
| 320 | + let result = slice(&g, &SliceArgs::default()).unwrap(); |
| 321 | + assert_eq!(result.subgraphs[0].id.as_deref(), Some("cluster_0")); |
| 322 | + assert_eq!( |
| 323 | + result.subgraphs[0].attrs.get("color").map(String::as_str), |
| 324 | + Some("blue") |
| 325 | + ); |
| 326 | + } |
| 327 | +} |
0 commit comments