use crate::utils::error::{Error, Result};
use std::collections::{HashMap, HashSet};
pub fn detect_cycles<S: std::hash::BuildHasher>(
graph: &HashMap<String, Vec<String>, S>,
) -> Vec<Vec<String>> {
let mut cycles = Vec::new();
let mut visited = HashSet::new();
let mut recursion_stack = HashSet::new();
let mut path = Vec::new();
for node in graph.keys() {
if !visited.contains(node) {
dfs(
node,
graph,
&mut visited,
&mut recursion_stack,
&mut path,
&mut cycles,
);
}
}
cycles
}
fn dfs<S: std::hash::BuildHasher>(
node: &str, graph: &HashMap<String, Vec<String>, S>, visited: &mut HashSet<String>,
recursion_stack: &mut HashSet<String>, path: &mut Vec<String>, cycles: &mut Vec<Vec<String>>,
) {
visited.insert(node.to_string());
recursion_stack.insert(node.to_string());
path.push(node.to_string());
if let Some(neighbors) = graph.get(node) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
dfs(neighbor, graph, visited, recursion_stack, path, cycles);
} else if recursion_stack.contains(neighbor) {
let cycle_start = path
.iter()
.position(|n| n == neighbor)
.unwrap_or(path.len());
let mut cycle = path[cycle_start..].to_vec();
cycle.push(neighbor.clone());
cycles.push(cycle);
}
}
}
recursion_stack.remove(node);
path.pop();
}
pub fn validate_acyclic<S: std::hash::BuildHasher>(
imports: &HashMap<String, Vec<String>, S>,
) -> Result<()> {
let cycles = detect_cycles(imports);
if cycles.is_empty() {
Ok(())
} else {
let cycle_strings: Vec<String> = cycles
.iter()
.map(|cycle| {
let cycle_str = cycle.join(" → ");
format!(" {}", cycle_str)
})
.collect();
Err(Error::new(&format!(
"Cyclic ontology dependencies detected:\n{}\n\nFix: Remove circular imports from ontology files",
cycle_strings.join("\n")
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_cycles() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["B".to_string(), "C".to_string()]);
graph.insert("B".to_string(), vec!["D".to_string()]);
graph.insert("C".to_string(), vec!["D".to_string()]);
graph.insert("D".to_string(), vec![]);
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 0);
}
#[test]
fn test_simple_cycle() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["B".to_string()]);
graph.insert("B".to_string(), vec!["C".to_string()]);
graph.insert("C".to_string(), vec!["A".to_string()]);
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].len(), 4); }
#[test]
fn test_self_loop() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["A".to_string()]);
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 1);
}
#[test]
fn test_multiple_cycles() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["B".to_string()]);
graph.insert("B".to_string(), vec!["C".to_string()]);
graph.insert("C".to_string(), vec!["A".to_string()]);
graph.insert("D".to_string(), vec!["E".to_string()]);
graph.insert("E".to_string(), vec!["D".to_string()]);
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 2);
}
#[test]
fn test_disconnected_components() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["B".to_string()]);
graph.insert("B".to_string(), vec![]);
graph.insert("C".to_string(), vec!["D".to_string()]);
graph.insert("D".to_string(), vec![]);
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 0);
}
#[test]
fn test_validate_acyclic_success() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["B".to_string()]);
graph.insert("B".to_string(), vec![]);
assert!(validate_acyclic(&graph).is_ok());
}
#[test]
fn test_validate_acyclic_failure() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["B".to_string()]);
graph.insert("B".to_string(), vec!["A".to_string()]);
let result = validate_acyclic(&graph);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Cyclic ontology dependencies detected"));
}
#[test]
fn test_empty_graph() {
let graph = HashMap::new();
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 0);
}
#[test]
fn test_complex_dag() {
let mut graph = HashMap::new();
graph.insert("A".to_string(), vec!["B".to_string(), "C".to_string()]);
graph.insert("B".to_string(), vec!["D".to_string()]);
graph.insert("C".to_string(), vec!["D".to_string()]);
graph.insert("D".to_string(), vec![]);
let cycles = detect_cycles(&graph);
assert_eq!(cycles.len(), 0);
}
}