#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GraphIssue {
CycleDetected {
description: String,
},
DisconnectedInput {
node_label: String,
},
DisconnectedOutput {
node_label: String,
},
IncompatibleFormat {
from_label: String,
to_label: String,
},
MissingResource {
node_label: String,
resource: String,
},
Warning {
message: String,
},
}
impl GraphIssue {
#[must_use]
pub fn is_fatal(&self) -> bool {
!matches!(self, Self::Warning { .. })
}
#[must_use]
pub fn category(&self) -> &'static str {
match self {
Self::CycleDetected { .. } => "cycle",
Self::DisconnectedInput { .. } => "disconnected_input",
Self::DisconnectedOutput { .. } => "disconnected_output",
Self::IncompatibleFormat { .. } => "format_mismatch",
Self::MissingResource { .. } => "missing_resource",
Self::Warning { .. } => "warning",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct GraphValidationResult {
issues: Vec<GraphIssue>,
}
impl GraphValidationResult {
#[must_use]
pub fn new() -> Self {
Self { issues: Vec::new() }
}
pub fn push(&mut self, issue: GraphIssue) {
self.issues.push(issue);
}
#[must_use]
pub fn has_fatal(&self) -> bool {
self.issues.iter().any(|i| i.is_fatal())
}
#[must_use]
pub fn is_clean(&self) -> bool {
self.issues.is_empty()
}
#[must_use]
pub fn issues(&self) -> &[GraphIssue] {
&self.issues
}
#[must_use]
pub fn fatal_count(&self) -> usize {
self.issues.iter().filter(|i| i.is_fatal()).count()
}
#[must_use]
pub fn warning_count(&self) -> usize {
self.issues.iter().filter(|i| !i.is_fatal()).count()
}
}
#[derive(Debug, Clone)]
pub struct GraphDescriptor {
pub edges: Vec<(usize, usize)>,
pub node_labels: Vec<String>,
pub requires_input: Vec<bool>,
pub requires_output: Vec<bool>,
}
impl GraphDescriptor {
#[must_use]
pub fn new(n: usize) -> Self {
Self {
edges: Vec::new(),
node_labels: (0..n).map(|i| format!("node_{i}")).collect(),
requires_input: vec![true; n],
requires_output: vec![true; n],
}
}
pub fn add_edge(&mut self, from: usize, to: usize) {
self.edges.push((from, to));
}
}
pub struct GraphValidator;
impl GraphValidator {
#[must_use]
pub fn validate(desc: &GraphDescriptor) -> GraphValidationResult {
let mut result = GraphValidationResult::new();
Self::check_connectivity(desc, &mut result);
Self::check_cycles(desc, &mut result);
result
}
#[must_use]
pub fn cycle_count(desc: &GraphDescriptor) -> usize {
let n = desc.node_labels.len();
if n == 0 {
return 0;
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for &(f, t) in &desc.edges {
if f < n && t < n {
adj[f].push(t);
}
}
let mut color = vec![0u8; n];
let mut back_edges = 0usize;
fn dfs(node: usize, adj: &[Vec<usize>], color: &mut Vec<u8>, back_edges: &mut usize) {
color[node] = 1;
for &nb in &adj[node] {
if color[nb] == 1 {
*back_edges += 1;
} else if color[nb] == 0 {
dfs(nb, adj, color, back_edges);
}
}
color[node] = 2;
}
for i in 0..n {
if color[i] == 0 {
dfs(i, &adj, &mut color, &mut back_edges);
}
}
back_edges
}
fn check_connectivity(desc: &GraphDescriptor, result: &mut GraphValidationResult) {
let n = desc.node_labels.len();
let mut has_incoming = vec![false; n];
let mut has_outgoing = vec![false; n];
for &(f, t) in &desc.edges {
if f < n {
has_outgoing[f] = true;
}
if t < n {
has_incoming[t] = true;
}
}
for i in 0..n {
if desc.requires_input[i] && !has_incoming[i] {
result.push(GraphIssue::DisconnectedInput {
node_label: desc.node_labels[i].clone(),
});
}
if desc.requires_output[i] && !has_outgoing[i] {
result.push(GraphIssue::DisconnectedOutput {
node_label: desc.node_labels[i].clone(),
});
}
}
}
fn check_cycles(desc: &GraphDescriptor, result: &mut GraphValidationResult) {
if Self::cycle_count(desc) > 0 {
result.push(GraphIssue::CycleDetected {
description: "Directed cycle detected in graph".to_string(),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linear_desc() -> GraphDescriptor {
let mut d = GraphDescriptor::new(3);
d.requires_input[0] = false;
d.requires_output[2] = false;
d.add_edge(0, 1);
d.add_edge(1, 2);
d
}
#[test]
fn test_cycle_is_fatal() {
let issue = GraphIssue::CycleDetected {
description: "cycle".to_string(),
};
assert!(issue.is_fatal());
}
#[test]
fn test_warning_not_fatal() {
let issue = GraphIssue::Warning {
message: "note".to_string(),
};
assert!(!issue.is_fatal());
}
#[test]
fn test_disconnected_input_is_fatal() {
let issue = GraphIssue::DisconnectedInput {
node_label: "n".to_string(),
};
assert!(issue.is_fatal());
}
#[test]
fn test_category_cycle() {
let issue = GraphIssue::CycleDetected {
description: String::new(),
};
assert_eq!(issue.category(), "cycle");
}
#[test]
fn test_category_warning() {
let issue = GraphIssue::Warning {
message: String::new(),
};
assert_eq!(issue.category(), "warning");
}
#[test]
fn test_empty_result_is_clean() {
let r = GraphValidationResult::new();
assert!(r.is_clean());
assert!(!r.has_fatal());
}
#[test]
fn test_has_fatal_after_push_fatal() {
let mut r = GraphValidationResult::new();
r.push(GraphIssue::CycleDetected {
description: String::new(),
});
assert!(r.has_fatal());
}
#[test]
fn test_fatal_count() {
let mut r = GraphValidationResult::new();
r.push(GraphIssue::CycleDetected {
description: String::new(),
});
r.push(GraphIssue::Warning {
message: String::new(),
});
assert_eq!(r.fatal_count(), 1);
}
#[test]
fn test_warning_count() {
let mut r = GraphValidationResult::new();
r.push(GraphIssue::Warning {
message: "w".to_string(),
});
r.push(GraphIssue::Warning {
message: "w2".to_string(),
});
assert_eq!(r.warning_count(), 2);
}
#[test]
fn test_validate_linear_is_clean() {
let d = linear_desc();
let r = GraphValidator::validate(&d);
assert!(r.is_clean());
}
#[test]
fn test_validate_cycle_produces_fatal() {
let mut d = GraphDescriptor::new(2);
d.requires_input[0] = false;
d.requires_output[1] = false;
d.add_edge(0, 1);
d.add_edge(1, 0); let r = GraphValidator::validate(&d);
assert!(r.has_fatal());
}
#[test]
fn test_validate_disconnected_input_detected() {
let mut d = GraphDescriptor::new(2);
d.requires_input[0] = false;
d.requires_input[1] = true;
d.requires_output[0] = false;
d.requires_output[1] = false;
let r = GraphValidator::validate(&d);
assert!(r.has_fatal());
}
#[test]
fn test_cycle_count_no_cycle() {
let d = linear_desc();
assert_eq!(GraphValidator::cycle_count(&d), 0);
}
#[test]
fn test_cycle_count_single_cycle() {
let mut d = GraphDescriptor::new(3);
d.add_edge(0, 1);
d.add_edge(1, 2);
d.add_edge(2, 0);
assert_eq!(GraphValidator::cycle_count(&d), 1);
}
#[test]
fn test_cycle_count_empty_graph() {
let d = GraphDescriptor::new(0);
assert_eq!(GraphValidator::cycle_count(&d), 0);
}
}