use std::collections::HashSet;
use crate::core::configs::op_config::{OpConfig, OpType};
use crate::core::exceptions::OperonError;
pub fn validate_graph(graph: &OpConfig) -> Result<(), OperonError> {
if !matches!(graph.kind, OpType::Graph) {
return Err(OperonError::Config(format!(
"validate_graph: expected type=\"graph\", got {:?}",
graph.kind
)));
}
if graph.entries.is_empty() {
return Err(OperonError::Config(format!(
"graph '{}': no entry ops declared",
graph.name
)));
}
let op_names: HashSet<&str> = graph.ops.keys().map(String::as_str).collect();
for entry in &graph.entries {
if !op_names.contains(entry.as_str()) {
return Err(OperonError::Config(format!(
"graph '{}': entry '{}' not in ops",
graph.name, entry
)));
}
}
for exit in &graph.exits {
if !op_names.contains(exit.as_str()) {
return Err(OperonError::Config(format!(
"graph '{}': exit '{}' not in ops",
graph.name, exit
)));
}
}
for (src, links) in &graph.compiled_adj {
if !op_names.contains(src.as_str()) {
return Err(OperonError::Config(format!(
"graph '{}': compiled_adj source '{}' not in ops",
graph.name, src
)));
}
for link in links {
if !op_names.contains(link.dst.as_str()) {
return Err(OperonError::Config(format!(
"graph '{}': edge {}→{} — destination not in ops",
graph.name, src, link.dst
)));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::configs::op_config::{CompiledLink, OpConfig, OpType};
use std::collections::BTreeMap;
fn graph_with_ops(entries: Vec<&str>, exits: Vec<&str>, ops: &[&str]) -> OpConfig {
let mut g = OpConfig {
kind: OpType::Graph,
name: "main".into(),
full_name: "main".into(),
entries: entries.iter().map(|s| s.to_string()).collect(),
exits: exits.iter().map(|s| s.to_string()).collect(),
..Default::default()
};
for op in ops {
g.ops.insert(
op.to_string(),
OpConfig {
kind: OpType::Code,
name: op.to_string(),
full_name: format!("main.{}", op),
..Default::default()
},
);
}
g
}
#[test]
fn rejects_non_graph_root() {
let mut cfg = OpConfig::default();
cfg.kind = OpType::Code;
assert!(validate_graph(&cfg).is_err());
}
#[test]
fn rejects_missing_entry() {
let g = graph_with_ops(vec![], vec![], &[]);
assert!(validate_graph(&g).is_err());
}
#[test]
fn rejects_unknown_entry() {
let g = graph_with_ops(vec!["ghost"], vec!["a"], &["a"]);
assert!(validate_graph(&g).is_err());
}
#[test]
fn rejects_unknown_edge_target() {
let mut g = graph_with_ops(vec!["a"], vec!["a"], &["a"]);
let mut adj = BTreeMap::new();
adj.insert(
"a".into(),
vec![CompiledLink {
dst: "b".into(),
soft: false,
}],
);
g.compiled_adj = adj;
assert!(validate_graph(&g).is_err());
}
#[test]
fn accepts_valid_graph() {
let g = graph_with_ops(vec!["a"], vec!["a"], &["a"]);
validate_graph(&g).unwrap();
}
}