use std::collections::{HashMap, HashSet};
use serde_json::Value;
use super::error::{ValidationError, ValidationReport};
use super::integrity::{get_component_references, MAX_GLOBAL_DEPTH};
use super::ref_fields::RefFieldSpec;
pub fn analyze_topology(
components: &[Value],
spec: &RefFieldSpec,
root_id: &str,
allow_orphan_components: bool,
allow_missing_root: bool,
) -> (HashSet<String>, ValidationReport) {
let mut report = ValidationReport::new();
let mut adj: HashMap<String, Vec<String>> = HashMap::new();
let mut all_ids: HashSet<String> = HashSet::new();
for comp in components {
let Some(comp_id) = comp
.as_object()
.and_then(|o| o.get("id"))
.and_then(|v| v.as_str())
else {
continue;
};
all_ids.insert(comp_id.to_string());
adj.entry(comp_id.to_string()).or_default();
for (ref_id, field) in get_component_references(comp, spec) {
if ref_id == comp_id {
report.push(ValidationError::self_ref(comp_id, &field));
continue;
}
adj.entry(comp_id.to_string()).or_default().push(ref_id);
}
}
let mut visited: HashSet<String> = HashSet::new();
let mut on_stack: HashSet<String> = HashSet::new();
if allow_missing_root {
let mut start_nodes: Vec<String> = all_ids.iter().cloned().collect();
start_nodes.sort();
for node_id in start_nodes {
if !visited.contains(&node_id) {
dfs(&node_id, &adj, &mut visited, &mut on_stack, 0, &mut report);
}
}
} else {
if all_ids.contains(root_id) {
dfs(root_id, &adj, &mut visited, &mut on_stack, 0, &mut report);
}
if !allow_orphan_components {
let mut orphans: Vec<String> = all_ids
.iter()
.filter(|id| !visited.contains(*id))
.cloned()
.collect();
orphans.sort();
for orphan in orphans {
report.push(ValidationError::orphan(&orphan, root_id));
}
}
}
(visited, report)
}
fn dfs(
node_id: &str,
adj: &HashMap<String, Vec<String>>,
visited: &mut HashSet<String>,
on_stack: &mut HashSet<String>,
depth: u32,
report: &mut ValidationReport,
) {
if depth > MAX_GLOBAL_DEPTH {
report.push(ValidationError::global_depth(node_id));
return;
}
visited.insert(node_id.to_string());
on_stack.insert(node_id.to_string());
if let Some(neighbors) = adj.get(node_id) {
for nb in neighbors {
if !visited.contains(nb) {
dfs(nb, adj, visited, on_stack, depth + 1, report);
} else if on_stack.contains(nb) {
report.push(ValidationError::circular(nb));
}
}
}
on_stack.remove(node_id);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate::error::ValidationErrorCode;
use crate::validate::ref_fields::RefFieldSpec;
use serde_json::json;
fn spec() -> RefFieldSpec {
RefFieldSpec::DEFAULT
}
#[test]
fn valid_chain_visits_root_and_child() {
let components = vec![
json!({ "id": "root", "component": "Node", "child": "n1" }),
json!({ "id": "n1", "component": "Node" }),
];
let (visited, report) = analyze_topology(&components, &spec(), "root", false, false);
assert!(report.is_empty(), "unexpected errors: {report}");
let expected: HashSet<String> = ["root", "n1"].into_iter().map(String::from).collect();
assert_eq!(visited, expected);
}
#[test]
fn self_reference_detected() {
let components = vec![json!({ "id": "root", "component": "Node", "child": "root" })];
let (_visited, report) = analyze_topology(&components, &spec(), "root", false, false);
assert!(report.has_code(&ValidationErrorCode::SelfReference));
}
#[test]
fn circular_reference_detected() {
let components = vec![
json!({ "id": "root", "component": "Node", "child": "n1" }),
json!({ "id": "n1", "component": "Node", "child": "root" }),
];
let (_visited, report) = analyze_topology(&components, &spec(), "root", false, false);
assert!(report.has_code(&ValidationErrorCode::CircularReference));
}
#[test]
fn orphan_strict_reports_error() {
let components = vec![
json!({ "id": "root", "component": "Node" }),
json!({ "id": "orphan", "component": "Node" }),
];
let (_visited, report) = analyze_topology(&components, &spec(), "root", false, false);
assert!(report.has_code(&ValidationErrorCode::OrphanComponent));
}
#[test]
fn orphan_relaxed_allowed() {
let components = vec![
json!({ "id": "root", "component": "Node" }),
json!({ "id": "orphan", "component": "Node" }),
];
let (_visited, report) = analyze_topology(&components, &spec(), "root", true, false);
assert!(report.is_empty(), "orphan should be allowed under RELAXED, got: {report}");
}
#[test]
fn coverage_combined_orphan_cycle_self() {
let orphan_components = vec![
json!({ "id": "root", "component": "Node", "child": "A" }),
json!({ "id": "A", "component": "Node" }),
json!({ "id": "B", "component": "Node" }),
];
let (_, r) = analyze_topology(&orphan_components, &spec(), "root", false, false);
assert!(r.has_code(&ValidationErrorCode::OrphanComponent));
let cycle_components = vec![
json!({ "id": "root", "component": "Node", "child": "A" }),
json!({ "id": "A", "component": "Node", "child": "B" }),
json!({ "id": "B", "component": "Node", "child": "A" }),
];
let (_, r) = analyze_topology(&cycle_components, &spec(), "root", true, false);
assert!(r.has_code(&ValidationErrorCode::CircularReference));
let self_components = vec![json!({ "id": "root", "component": "Node", "child": "root" })];
let (_, r) = analyze_topology(&self_components, &spec(), "root", true, false);
assert!(r.has_code(&ValidationErrorCode::SelfReference));
}
}