Skip to content

Commit b066f23

Browse files
committed
Add depfilter slice subcommand
1 parent 0283c84 commit b066f23

4 files changed

Lines changed: 518 additions & 0 deletions

File tree

crates/csvizmo-depgraph/src/algorithm/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod reverse;
99
pub mod select;
1010
pub mod shorten;
1111
pub mod simplify;
12+
pub mod slice;
1213
pub mod sub;
1314

1415
use globset::{Glob, GlobSet, GlobSetBuilder};
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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+
}

crates/csvizmo-depgraph/src/bin/depfilter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use csvizmo_depgraph::algorithm;
66
use csvizmo_depgraph::algorithm::between::BetweenArgs;
77
use csvizmo_depgraph::algorithm::cycles::CyclesArgs;
88
use csvizmo_depgraph::algorithm::select::SelectArgs;
9+
use csvizmo_depgraph::algorithm::slice::SliceArgs;
910
use csvizmo_depgraph::emit::OutputFormat;
1011
use csvizmo_depgraph::parse::InputFormat;
1112
use csvizmo_utils::stdio::{get_input_reader, get_output_writer};
@@ -49,6 +50,8 @@ enum Command {
4950
Between(BetweenArgs),
5051
/// Detect cycles (strongly connected components) and output each as a subgraph
5152
Cycles(CyclesArgs),
53+
/// Cut edges between subgraphs, isolating each subgraph
54+
Slice(SliceArgs),
5255
}
5356

5457
fn main() -> eyre::Result<()> {
@@ -98,6 +101,7 @@ fn main() -> eyre::Result<()> {
98101
Command::Select(select_args) => algorithm::select::select(&graph, select_args)?,
99102
Command::Between(between_args) => algorithm::between::between(&graph, between_args)?,
100103
Command::Cycles(cycles_args) => algorithm::cycles::cycles(&graph, cycles_args)?,
104+
Command::Slice(slice_args) => algorithm::slice::slice(&graph, slice_args)?,
101105
};
102106

103107
let mut output = get_output_writer(&output_path)?;

0 commit comments

Comments
 (0)