1use std::collections::{BTreeMap, BTreeSet};
30use std::fmt::Write;
31
32use crate::{DomainHierarchy, SymbolTable};
33
34#[derive(Debug, Clone)]
36pub struct HierarchyNode {
37 pub name: String,
39 pub children: Vec<HierarchyNode>,
41 pub depth: usize,
43 pub domain_size: usize,
45}
46
47#[derive(Debug, Clone, Default)]
49pub struct HierarchyStats {
50 pub root_count: usize,
52 pub total_domains: usize,
54 pub max_depth: usize,
56 pub max_breadth: usize,
58 pub leaf_count: usize,
60}
61
62impl HierarchyStats {
63 pub fn is_flat(&self) -> bool {
65 self.max_depth <= 1
66 }
67
68 pub fn summary(&self) -> String {
70 format!(
71 "{} domains, depth {}, {} roots, {} leaves",
72 self.total_domains, self.max_depth, self.root_count, self.leaf_count
73 )
74 }
75}
76
77pub fn build_hierarchy(table: &SymbolTable, hierarchy: &DomainHierarchy) -> Vec<HierarchyNode> {
83 let domain_names: BTreeSet<String> = table.domains.keys().cloned().collect();
85
86 if domain_names.is_empty() {
87 return Vec::new();
88 }
89
90 let mut children_map: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
92 let mut has_parent: BTreeSet<String> = BTreeSet::new();
93
94 for name in &domain_names {
95 if let Some(parent) = hierarchy.get_parent(name) {
96 if domain_names.contains(parent) {
98 children_map
99 .entry(parent.to_string())
100 .or_default()
101 .insert(name.clone());
102 has_parent.insert(name.clone());
103 }
104 }
105 }
106
107 let roots: Vec<String> = domain_names
109 .iter()
110 .filter(|name| !has_parent.contains(*name))
111 .cloned()
112 .collect();
113
114 roots
116 .into_iter()
117 .map(|name| build_node(&name, 0, table, &children_map))
118 .collect()
119}
120
121fn build_node(
123 name: &str,
124 depth: usize,
125 table: &SymbolTable,
126 children_map: &BTreeMap<String, BTreeSet<String>>,
127) -> HierarchyNode {
128 let domain_size = table.get_domain(name).map(|d| d.cardinality).unwrap_or(0);
129
130 let children: Vec<HierarchyNode> = children_map
131 .get(name)
132 .map(|kids| {
133 kids.iter()
134 .map(|child| build_node(child, depth + 1, table, children_map))
135 .collect()
136 })
137 .unwrap_or_default();
138
139 HierarchyNode {
140 name: name.to_string(),
141 children,
142 depth,
143 domain_size,
144 }
145}
146
147pub fn hierarchy_stats(table: &SymbolTable, hierarchy: &DomainHierarchy) -> HierarchyStats {
149 let roots = build_hierarchy(table, hierarchy);
150
151 if roots.is_empty() {
152 return HierarchyStats::default();
153 }
154
155 let mut stats = HierarchyStats {
156 root_count: roots.len(),
157 total_domains: 0,
158 max_depth: 0,
159 max_breadth: 0,
160 leaf_count: 0,
161 };
162
163 for root in &roots {
164 collect_stats(root, &mut stats);
165 }
166
167 stats
168}
169
170fn collect_stats(node: &HierarchyNode, stats: &mut HierarchyStats) {
172 stats.total_domains += 1;
173
174 let effective_depth = node.depth + 1; if effective_depth > stats.max_depth {
176 stats.max_depth = effective_depth;
177 }
178
179 let breadth = node.children.len();
180 if breadth > stats.max_breadth {
181 stats.max_breadth = breadth;
182 }
183
184 if node.children.is_empty() {
185 stats.leaf_count += 1;
186 }
187
188 for child in &node.children {
189 collect_stats(child, stats);
190 }
191}
192
193pub fn render_hierarchy_ascii(table: &SymbolTable, hierarchy: &DomainHierarchy) -> String {
206 let roots = build_hierarchy(table, hierarchy);
207 let mut out = String::new();
208
209 if roots.is_empty() {
210 out.push_str("(empty hierarchy)\n");
211 return out;
212 }
213
214 for (i, root) in roots.iter().enumerate() {
215 let is_last = i == roots.len() - 1;
216 render_node_ascii(&mut out, root, "", is_last, true);
217 }
218
219 out
220}
221
222fn render_node_ascii(
224 out: &mut String,
225 node: &HierarchyNode,
226 prefix: &str,
227 is_last: bool,
228 is_root: bool,
229) {
230 let connector = if is_root {
231 ""
232 } else if is_last {
233 "└── "
234 } else {
235 "├── "
236 };
237
238 let _ = writeln!(
239 out,
240 "{}{}{} ({})",
241 prefix, connector, node.name, node.domain_size
242 );
243
244 let child_prefix = if is_root {
245 String::new()
246 } else if is_last {
247 format!("{} ", prefix)
248 } else {
249 format!("{}│ ", prefix)
250 };
251
252 for (i, child) in node.children.iter().enumerate() {
253 let child_is_last = i == node.children.len() - 1;
254 render_node_ascii(out, child, &child_prefix, child_is_last, false);
255 }
256}
257
258pub fn render_hierarchy_dot(table: &SymbolTable, hierarchy: &DomainHierarchy) -> String {
262 let roots = build_hierarchy(table, hierarchy);
263 let mut dot = String::new();
264 let _ = writeln!(dot, "digraph DomainHierarchy {{");
265 let _ = writeln!(dot, " rankdir=TB;");
266 let _ = writeln!(dot, " node [shape=box];");
267
268 for root in &roots {
269 render_dot_node(&mut dot, root);
270 }
271
272 let _ = writeln!(dot, "}}");
273 dot
274}
275
276fn render_dot_node(dot: &mut String, node: &HierarchyNode) {
278 let _ = writeln!(
279 dot,
280 " \"{}\" [label=\"{}\\n(size={})\"];",
281 node.name, node.name, node.domain_size
282 );
283 for child in &node.children {
284 let _ = writeln!(dot, " \"{}\" -> \"{}\";", node.name, child.name);
285 render_dot_node(dot, child);
286 }
287}
288
289pub fn ancestors(hierarchy: &DomainHierarchy, domain: &str) -> Vec<String> {
294 hierarchy.get_ancestors(domain)
295}
296
297pub fn descendants(hierarchy: &DomainHierarchy, domain: &str) -> Vec<String> {
301 hierarchy.get_descendants(domain)
302}
303
304pub fn render_hierarchy_indented(table: &SymbolTable, hierarchy: &DomainHierarchy) -> String {
308 let roots = build_hierarchy(table, hierarchy);
309 let mut out = String::new();
310
311 if roots.is_empty() {
312 out.push_str("(empty hierarchy)\n");
313 return out;
314 }
315
316 for root in &roots {
317 render_indented_node(&mut out, root);
318 }
319
320 out
321}
322
323fn render_indented_node(out: &mut String, node: &HierarchyNode) {
325 let indent = " ".repeat(node.depth);
326 let _ = writeln!(out, "{}{} ({})", indent, node.name, node.domain_size);
327 for child in &node.children {
328 render_indented_node(out, child);
329 }
330}
331
332pub fn path_between(hierarchy: &DomainHierarchy, from: &str, to: &str) -> Option<Vec<String>> {
336 if from == to {
337 return Some(vec![from.to_string()]);
338 }
339
340 let from_ancestors = hierarchy.get_ancestors(from);
342 if let Some(pos) = from_ancestors.iter().position(|a| a == to) {
343 let mut path = vec![from.to_string()];
344 path.extend(from_ancestors[..=pos].to_vec());
345 return Some(path);
346 }
347
348 let to_ancestors = hierarchy.get_ancestors(to);
350 if let Some(pos) = to_ancestors.iter().position(|a| a == from) {
351 let mut path: Vec<String> = to_ancestors[..=pos].iter().rev().cloned().collect();
352 path.push(to.to_string());
353 let mut result = vec![from.to_string()];
357 for ancestor in to_ancestors[..pos].iter().rev() {
358 result.push(ancestor.clone());
359 }
360 result.push(to.to_string());
361 return Some(result);
362 }
363
364 None
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::DomainInfo;
371
372 fn make_table(domains: &[(&str, usize)]) -> SymbolTable {
374 let mut table = SymbolTable::new();
375 for &(name, card) in domains {
376 table
377 .add_domain(DomainInfo::new(name, card))
378 .expect("add_domain should succeed");
379 }
380 table
381 }
382
383 #[test]
384 fn test_hierarchy_empty_table() {
385 let table = SymbolTable::new();
386 let hierarchy = DomainHierarchy::new();
387 let ascii = render_hierarchy_ascii(&table, &hierarchy);
388 assert_eq!(ascii, "(empty hierarchy)\n");
389 }
390
391 #[test]
392 fn test_hierarchy_flat_domains() {
393 let table = make_table(&[("Alpha", 10), ("Beta", 20), ("Gamma", 30)]);
394 let hierarchy = DomainHierarchy::new();
395 let ascii = render_hierarchy_ascii(&table, &hierarchy);
396 assert!(ascii.contains("Alpha"));
398 assert!(ascii.contains("Beta"));
399 assert!(ascii.contains("Gamma"));
400 assert!(!ascii.contains("├"));
402 assert!(!ascii.contains("└"));
403 }
404
405 #[test]
406 fn test_hierarchy_stats_flat() {
407 let table = make_table(&[("A", 1), ("B", 2), ("C", 3)]);
408 let hierarchy = DomainHierarchy::new();
409 let stats = hierarchy_stats(&table, &hierarchy);
410 assert_eq!(stats.max_depth, 1);
411 assert!(stats.is_flat());
412 assert_eq!(stats.root_count, 3);
413 assert_eq!(stats.leaf_count, 3);
414 }
415
416 #[test]
417 fn test_hierarchy_stats_summary() {
418 let table = make_table(&[("A", 1), ("B", 2)]);
419 let hierarchy = DomainHierarchy::new();
420 let summary = hierarchy_stats(&table, &hierarchy).summary();
421 assert!(summary.contains("2 domains"));
422 assert!(summary.contains("2 roots"));
423 }
424
425 #[test]
426 fn test_hierarchy_node_depth() {
427 let table = make_table(&[("Entity", 500), ("Person", 100), ("Student", 50)]);
428 let mut hierarchy = DomainHierarchy::new();
429 hierarchy.add_subtype("Person", "Entity");
430 hierarchy.add_subtype("Student", "Person");
431
432 let roots = build_hierarchy(&table, &hierarchy);
433 assert_eq!(roots.len(), 1);
434 assert_eq!(roots[0].depth, 0);
435 assert_eq!(roots[0].name, "Entity");
436
437 assert_eq!(roots[0].children.len(), 1);
439 assert_eq!(roots[0].children[0].depth, 1);
440
441 assert_eq!(roots[0].children[0].children.len(), 1);
443 assert_eq!(roots[0].children[0].children[0].depth, 2);
444 }
445
446 #[test]
447 fn test_hierarchy_ascii_contains_names() {
448 let table = make_table(&[("Entity", 500), ("Person", 100)]);
449 let mut hierarchy = DomainHierarchy::new();
450 hierarchy.add_subtype("Person", "Entity");
451
452 let ascii = render_hierarchy_ascii(&table, &hierarchy);
453 assert!(ascii.contains("Entity"));
454 assert!(ascii.contains("Person"));
455 assert!(ascii.contains("500"));
456 assert!(ascii.contains("100"));
457 }
458
459 #[test]
460 fn test_hierarchy_ascii_tree_connectors() {
461 let table = make_table(&[("Entity", 500), ("Person", 100), ("Org", 200)]);
462 let mut hierarchy = DomainHierarchy::new();
463 hierarchy.add_subtype("Person", "Entity");
464 hierarchy.add_subtype("Org", "Entity");
465
466 let ascii = render_hierarchy_ascii(&table, &hierarchy);
467 let has_branch = ascii.contains('├') || ascii.contains('└');
469 assert!(has_branch, "Expected tree connectors in:\n{}", ascii);
470 }
471
472 #[test]
473 fn test_hierarchy_dot_contains_digraph() {
474 let table = make_table(&[("A", 1)]);
475 let hierarchy = DomainHierarchy::new();
476 let dot = render_hierarchy_dot(&table, &hierarchy);
477 assert!(dot.starts_with("digraph DomainHierarchy {"));
478 }
479
480 #[test]
481 fn test_hierarchy_dot_contains_edges() {
482 let table = make_table(&[("Parent", 10), ("Child", 5)]);
483 let mut hierarchy = DomainHierarchy::new();
484 hierarchy.add_subtype("Child", "Parent");
485
486 let dot = render_hierarchy_dot(&table, &hierarchy);
487 assert!(dot.contains("->"), "Expected edges in DOT output:\n{}", dot);
488 assert!(dot.contains("\"Parent\" -> \"Child\""));
489 }
490
491 #[test]
492 fn test_hierarchy_stats_default() {
493 let stats = HierarchyStats::default();
494 assert_eq!(stats.root_count, 0);
495 assert_eq!(stats.total_domains, 0);
496 assert_eq!(stats.max_depth, 0);
497 assert_eq!(stats.max_breadth, 0);
498 assert_eq!(stats.leaf_count, 0);
499 }
500
501 #[test]
502 fn test_hierarchy_stats_leaf_count() {
503 let table = make_table(&[
504 ("Entity", 500),
505 ("Person", 100),
506 ("Student", 50),
507 ("Teacher", 30),
508 ("Org", 200),
509 ]);
510 let mut hierarchy = DomainHierarchy::new();
511 hierarchy.add_subtype("Person", "Entity");
512 hierarchy.add_subtype("Student", "Person");
513 hierarchy.add_subtype("Teacher", "Person");
514 hierarchy.add_subtype("Org", "Entity");
515
516 let stats = hierarchy_stats(&table, &hierarchy);
517 assert_eq!(stats.leaf_count, 3);
519 }
520
521 #[test]
522 fn test_hierarchy_stats_root_count() {
523 let table = make_table(&[("A", 1), ("B", 2), ("C", 3), ("D", 4)]);
524 let mut hierarchy = DomainHierarchy::new();
525 hierarchy.add_subtype("B", "A");
526 let stats = hierarchy_stats(&table, &hierarchy);
528 assert_eq!(stats.root_count, 3); }
530
531 #[test]
532 fn test_ancestors_empty() {
533 let hierarchy = DomainHierarchy::new();
534 let result = ancestors(&hierarchy, "Root");
535 assert!(result.is_empty());
536 }
537
538 #[test]
539 fn test_descendants_leaf() {
540 let hierarchy = DomainHierarchy::new();
541 let result = descendants(&hierarchy, "Leaf");
543 assert!(result.is_empty());
544 }
545
546 #[test]
547 fn test_hierarchy_render_deterministic() {
548 let table = make_table(&[
549 ("Entity", 500),
550 ("Person", 100),
551 ("Student", 50),
552 ("Org", 200),
553 ]);
554 let mut hierarchy = DomainHierarchy::new();
555 hierarchy.add_subtype("Person", "Entity");
556 hierarchy.add_subtype("Student", "Person");
557 hierarchy.add_subtype("Org", "Entity");
558
559 let ascii1 = render_hierarchy_ascii(&table, &hierarchy);
560 let ascii2 = render_hierarchy_ascii(&table, &hierarchy);
561 assert_eq!(ascii1, ascii2, "Rendering should be deterministic");
562 }
563
564 #[test]
565 fn test_hierarchy_multiple_roots() {
566 let table = make_table(&[("TreeA", 10), ("TreeB", 20), ("ChildA", 5)]);
567 let mut hierarchy = DomainHierarchy::new();
568 hierarchy.add_subtype("ChildA", "TreeA");
569
570 let roots = build_hierarchy(&table, &hierarchy);
571 assert_eq!(roots.len(), 2); let names: Vec<&str> = roots.iter().map(|r| r.name.as_str()).collect();
573 assert!(names.contains(&"TreeA"));
574 assert!(names.contains(&"TreeB"));
575 }
576
577 #[test]
578 fn test_hierarchy_stats_max_breadth() {
579 let table = make_table(&[("Root", 100), ("A", 10), ("B", 20), ("C", 30)]);
580 let mut hierarchy = DomainHierarchy::new();
581 hierarchy.add_subtype("A", "Root");
582 hierarchy.add_subtype("B", "Root");
583 hierarchy.add_subtype("C", "Root");
584
585 let stats = hierarchy_stats(&table, &hierarchy);
586 assert_eq!(stats.max_breadth, 3);
587 }
588
589 #[test]
590 fn test_hierarchy_node_domain_size() {
591 let table = make_table(&[("Entity", 500), ("Person", 100)]);
592 let mut hierarchy = DomainHierarchy::new();
593 hierarchy.add_subtype("Person", "Entity");
594
595 let roots = build_hierarchy(&table, &hierarchy);
596 assert_eq!(roots[0].domain_size, 500);
597 assert_eq!(roots[0].children[0].domain_size, 100);
598 }
599}