1use super::{StaticGraph, StaticNode};
7use std::collections::{HashMap, HashSet};
8
9const MAX_GRAPH_DEPTH: usize = 100;
11
12#[derive(Debug, Clone)]
14pub enum PruneError {
15 MultipleParents {
17 node: String,
18 parent1: String,
19 parent2: String,
20 },
21 NoParent { node: String },
23 CycleDetected,
25 NoRootNode,
27}
28
29impl std::fmt::Display for PruneError {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 PruneError::MultipleParents {
33 node,
34 parent1,
35 parent2,
36 } => {
37 write!(
38 f,
39 "Graph is malformed, node {} has multiple parents: {} and {}",
40 node, parent1, parent2
41 )
42 }
43 PruneError::NoParent { node } => {
44 write!(f, "Graph does not have a parent for {}", node)
45 }
46 PruneError::CycleDetected => {
47 write!(f, "Hit edge traversal limit when pruning, is the graph well-formed without cycles?")
48 }
49 PruneError::NoRootNode => {
50 write!(f, "Could not find root node in graph")
51 }
52 }
53 }
54}
55
56impl std::error::Error for PruneError {}
57
58pub fn find_root_node(graph: &StaticGraph) -> Option<&StaticNode> {
62 graph.nodes.iter().find(|node| {
63 node.nodegroup_id.is_none()
64 || node
65 .nodegroup_id
66 .as_ref()
67 .map(|s| s.is_empty())
68 .unwrap_or(true)
69 })
70}
71
72pub fn build_backedges(graph: &StaticGraph) -> Result<HashMap<String, String>, PruneError> {
76 let mut backedges: HashMap<String, String> = HashMap::new();
77
78 for edge in &graph.edges {
79 if let Some(existing_parent) = backedges.get(&edge.rangenode_id) {
80 return Err(PruneError::MultipleParents {
81 node: edge.rangenode_id.clone(),
82 parent1: existing_parent.clone(),
83 parent2: edge.domainnode_id.clone(),
84 });
85 }
86 backedges.insert(edge.rangenode_id.clone(), edge.domainnode_id.clone());
87 }
88
89 Ok(backedges)
90}
91
92pub fn prune_graph<F>(
105 graph: &StaticGraph,
106 is_nodegroup_permitted: F,
107 keep_functions: Option<&[String]>,
108) -> Result<StaticGraph, PruneError>
109where
110 F: Fn(&str) -> bool,
111{
112 let root_node = find_root_node(graph).ok_or(PruneError::NoRootNode)?;
114 let root = root_node.nodeid.clone();
115
116 let all_nodegroups: HashSet<String> = graph
118 .nodes
119 .iter()
120 .filter_map(|n| n.nodegroup_id.clone())
121 .collect();
122
123 let mut allowed_nodegroups: HashMap<String, bool> = all_nodegroups
126 .iter()
127 .filter(|ng_id| is_nodegroup_permitted(ng_id))
128 .map(|ng_id| {
129 let is_root = ng_id.is_empty() || *ng_id == root;
130 (ng_id.clone(), is_root)
131 })
132 .collect();
133
134 let backedges = build_backedges(graph)?;
136
137 allowed_nodegroups.insert(root.clone(), true);
139
140 let mut loops = 0;
142 while loops < MAX_GRAPH_DEPTH {
143 let unrooted: Vec<String> = allowed_nodegroups
144 .iter()
145 .filter(|(_, &rooted)| !rooted)
146 .map(|(ng, _)| ng.clone())
147 .collect();
148
149 if unrooted.is_empty() {
150 break;
151 }
152
153 for ng in unrooted {
154 if ng == root {
155 continue;
156 }
157
158 let next = backedges
159 .get(&ng)
160 .ok_or_else(|| PruneError::NoParent { node: ng.clone() })?;
161
162 allowed_nodegroups.insert(ng.clone(), true);
163 if !allowed_nodegroups.contains_key(next) {
164 allowed_nodegroups.insert(next.clone(), false);
165 }
166 }
167
168 loops += 1;
169 }
170
171 if loops >= MAX_GRAPH_DEPTH {
172 return Err(PruneError::CycleDetected);
173 }
174
175 let allowed_nodes: HashSet<String> = graph
177 .nodes
178 .iter()
179 .filter(|node| {
180 node.nodegroup_id
181 .as_ref()
182 .and_then(|ng_id| allowed_nodegroups.get(ng_id))
183 .copied()
184 .unwrap_or(false)
185 || node.nodeid == root
186 })
187 .map(|node| node.nodeid.clone())
188 .collect();
189
190 let mut pruned = graph.clone();
192
193 pruned.cards = pruned.cards.map(|cards| {
195 cards
196 .into_iter()
197 .filter(|card| {
198 allowed_nodegroups
199 .get(&card.nodegroup_id)
200 .copied()
201 .unwrap_or(false)
202 })
203 .collect()
204 });
205
206 pruned.cards_x_nodes_x_widgets = pruned.cards_x_nodes_x_widgets.map(|cxnxws| {
208 cxnxws
209 .into_iter()
210 .filter(|cxnxw| allowed_nodes.contains(&cxnxw.node_id))
211 .collect()
212 });
213
214 pruned.edges.retain(|edge| {
216 (edge.domainnode_id == root || allowed_nodes.contains(&edge.domainnode_id))
217 && allowed_nodes.contains(&edge.rangenode_id)
218 });
219
220 pruned
222 .nodegroups
223 .retain(|ng| allowed_nodegroups.contains_key(&ng.nodegroupid));
224
225 pruned
227 .nodes
228 .retain(|node| allowed_nodes.contains(&node.nodeid));
229
230 if let Some(keep_fns) = keep_functions {
232 pruned.functions_x_graphs = pruned.functions_x_graphs.map(|fxgs| {
233 fxgs.into_iter()
234 .filter(|fxg| keep_fns.contains(&fxg.function_id))
235 .collect()
236 });
237 } else {
238 pruned.functions_x_graphs = Some(Vec::new());
239 }
240
241 Ok(pruned)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::graph::{StaticEdge, StaticNodegroup};
248 use serde_json::json;
249
250 fn create_test_node(nodeid: &str, nodegroup_id: Option<&str>) -> StaticNode {
251 let node_json = json!({
252 "nodeid": nodeid,
253 "name": nodeid,
254 "datatype": if nodegroup_id.is_none() { "semantic" } else { "string" },
255 "nodegroup_id": nodegroup_id,
256 "alias": nodeid,
257 "graph_id": "test_graph",
258 "is_collector": false,
259 "isrequired": false,
260 "exportable": true,
261 "issearchable": false,
262 "istopnode": nodegroup_id.is_none(),
263 });
264 serde_json::from_value(node_json).expect("Failed to create test node")
265 }
266
267 fn create_test_edge(domain: &str, range: &str) -> StaticEdge {
268 StaticEdge {
269 edgeid: format!("{}->{}", domain, range),
270 domainnode_id: domain.to_string(),
271 rangenode_id: range.to_string(),
272 graph_id: "test_graph".to_string(),
273 name: None,
274 description: None,
275 ontologyproperty: None,
276 source_identifier_id: None,
277 }
278 }
279
280 fn create_test_nodegroup(nodegroupid: &str) -> StaticNodegroup {
281 StaticNodegroup {
282 nodegroupid: nodegroupid.to_string(),
283 cardinality: Some("n".to_string()),
284 parentnodegroup_id: None,
285 legacygroupid: None,
286 grouping_node_id: None,
287 }
288 }
289
290 #[test]
291 fn test_prune_graph_filters_unpermitted_nodegroups() {
292 let root = create_test_node("root", None);
294 let child1 = create_test_node("child1", Some("child1"));
295 let child2 = create_test_node("child2", Some("child2"));
296
297 let edge1 = create_test_edge("root", "child1");
298 let edge2 = create_test_edge("root", "child2");
299
300 let ng1 = create_test_nodegroup("child1");
301 let ng2 = create_test_nodegroup("child2");
302
303 let graph_json = json!({
304 "graphid": "test_graph",
305 "name": {"en": "Test Graph"},
306 "nodes": [root.clone(), child1, child2],
307 "edges": [edge1, edge2],
308 "nodegroups": [ng1, ng2],
309 "root": root,
310 "cards": [],
311 "cards_x_nodes_x_widgets": [],
312 "functions_x_graphs": [],
313 "config": {}
314 });
315
316 let graph: StaticGraph =
317 serde_json::from_value(graph_json).expect("Failed to create graph");
318
319 let permitted = |ng: &str| ng == "child1";
321
322 let pruned = prune_graph(&graph, permitted, None).expect("Prune failed");
323
324 assert!(
326 pruned.nodes.iter().any(|n| n.nodeid == "root"),
327 "Root should be included"
328 );
329 assert!(
330 pruned.nodes.iter().any(|n| n.nodeid == "child1"),
331 "child1 should be included"
332 );
333 assert!(
334 !pruned.nodes.iter().any(|n| n.nodeid == "child2"),
335 "child2 should NOT be included"
336 );
337
338 assert!(pruned
340 .nodegroups
341 .iter()
342 .any(|ng| ng.nodegroupid == "child1"));
343 assert!(!pruned
344 .nodegroups
345 .iter()
346 .any(|ng| ng.nodegroupid == "child2"));
347
348 assert!(pruned.edges.iter().any(|e| e.rangenode_id == "child1"));
350 assert!(!pruned.edges.iter().any(|e| e.rangenode_id == "child2"));
351 }
352
353 #[test]
354 fn test_prune_graph_includes_path_to_root() {
355 let root = create_test_node("root", None);
358 let middle = create_test_node("middle", Some("middle"));
359 let leaf = create_test_node("leaf", Some("leaf"));
360
361 let edge1 = create_test_edge("root", "middle");
362 let edge2 = create_test_edge("middle", "leaf");
363
364 let ng_middle = create_test_nodegroup("middle");
365 let ng_leaf = create_test_nodegroup("leaf");
366
367 let graph_json = json!({
368 "graphid": "test_graph",
369 "name": {"en": "Test Graph"},
370 "nodes": [root.clone(), middle, leaf],
371 "edges": [edge1, edge2],
372 "nodegroups": [ng_middle, ng_leaf],
373 "root": root,
374 "cards": [],
375 "cards_x_nodes_x_widgets": [],
376 "functions_x_graphs": [],
377 "config": {}
378 });
379
380 let graph: StaticGraph =
381 serde_json::from_value(graph_json).expect("Failed to create graph");
382
383 let permitted = |ng: &str| ng == "leaf";
385
386 let pruned = prune_graph(&graph, permitted, None).expect("Prune failed");
387
388 assert!(pruned.nodes.iter().any(|n| n.nodeid == "root"));
390 assert!(
391 pruned.nodes.iter().any(|n| n.nodeid == "middle"),
392 "middle should be included for path"
393 );
394 assert!(pruned.nodes.iter().any(|n| n.nodeid == "leaf"));
395 }
396
397 #[test]
398 fn test_prune_graph_detects_multiple_parents() {
399 let root = create_test_node("root", None);
400 let child = create_test_node("child", Some("child"));
401
402 let edge1 = create_test_edge("root", "child");
404 let mut edge2 = create_test_edge("root", "child");
405 edge2.domainnode_id = "other_parent".to_string();
406
407 let ng = create_test_nodegroup("child");
408
409 let graph_json = json!({
410 "graphid": "test_graph",
411 "name": {"en": "Test Graph"},
412 "nodes": [root.clone(), child],
413 "edges": [edge1, edge2],
414 "nodegroups": [ng],
415 "root": root,
416 "cards": [],
417 "cards_x_nodes_x_widgets": [],
418 "functions_x_graphs": [],
419 "config": {}
420 });
421
422 let graph: StaticGraph =
423 serde_json::from_value(graph_json).expect("Failed to create graph");
424
425 let result = prune_graph(&graph, |_| true, None);
426 assert!(matches!(result, Err(PruneError::MultipleParents { .. })));
427 }
428}