use std::time::Instant;
use crate::config::ValidationConfig;
use crate::graph::KnowledgeGraph;
use crate::validation::report::{ValidationReport, ValidationReportBuilder};
use crate::validation::rules;
pub struct Validator {
config: ValidationConfig,
}
impl Validator {
pub fn new(config: ValidationConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(ValidationConfig::default())
}
pub fn validate(&self, graph: &KnowledgeGraph) -> ValidationReport {
let start = Instant::now();
let mut builder = ValidationReportBuilder::new()
.items_checked(graph.item_count())
.relationships_checked(graph.relationship_count());
let broken_refs = rules::check_broken_references(graph);
builder = builder.errors(broken_refs);
let orphans = rules::check_orphans(graph, self.config.strict_orphans);
if self.config.strict_orphans {
builder = builder.errors(orphans);
} else {
builder = builder.warnings(orphans);
}
let duplicates = rules::check_duplicates(graph);
builder = builder.errors(duplicates);
let cycles = rules::check_cycles(graph);
builder = builder.errors(cycles);
let metadata_errors = rules::check_metadata(graph, &self.config.allowed_custom_fields);
builder = builder.errors(metadata_errors);
let relationship_errors = rules::check_relationships(graph);
builder = builder.errors(relationship_errors);
let redundant = rules::check_redundant_relationships(graph);
builder = builder.warnings(redundant);
builder.duration(start.elapsed()).build()
}
}
impl Default for Validator {
fn default() -> Self {
Self::with_defaults()
}
}
pub fn validate(graph: &KnowledgeGraph) -> ValidationReport {
Validator::with_defaults().validate(graph)
}
pub fn validate_strict(graph: &KnowledgeGraph) -> ValidationReport {
let config = ValidationConfig {
strict_orphans: true,
..Default::default()
};
Validator::new(config).validate(graph)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use crate::model::{
ItemBuilder, ItemId, ItemType, RelationshipType, SourceLocation, UpstreamRefs,
};
use std::path::PathBuf;
fn create_item(
id: &str,
item_type: ItemType,
upstream: Option<UpstreamRefs>,
) -> crate::model::Item {
let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
let mut builder = ItemBuilder::new()
.id(ItemId::new_unchecked(id))
.item_type(item_type)
.name(format!("Test {}", id))
.source(source);
if let Some(up) = upstream {
builder = builder.upstream(up);
}
if item_type.requires_specification() {
builder = builder.specification("Test spec");
}
builder.build().unwrap()
}
#[test]
fn test_valid_graph() {
let graph = GraphBuilder::new()
.add_item(create_item("SOL-001", ItemType::Solution, None))
.add_item(create_item(
"UC-001",
ItemType::UseCase,
Some(UpstreamRefs {
refines: vec![ItemId::new_unchecked("SOL-001")],
..Default::default()
}),
))
.build()
.unwrap();
let report = validate(&graph);
assert!(report.is_valid(), "Valid graph should pass validation");
assert_eq!(report.error_count(), 0);
}
#[test]
fn test_broken_reference() {
let graph = GraphBuilder::new()
.add_item(create_item(
"UC-001",
ItemType::UseCase,
Some(UpstreamRefs {
refines: vec![ItemId::new_unchecked("SOL-MISSING")],
..Default::default()
}),
))
.build()
.unwrap();
let report = validate(&graph);
assert!(!report.is_valid());
assert!(report.error_count() > 0);
}
#[test]
fn test_orphan_warning() {
let graph = GraphBuilder::new()
.add_item(create_item("UC-001", ItemType::UseCase, None))
.build()
.unwrap();
let report = validate(&graph);
assert!(
report.is_valid(),
"Orphan should be warning in non-strict mode"
);
assert_eq!(report.warning_count(), 1);
}
#[test]
fn test_orphan_error_strict() {
let graph = GraphBuilder::new()
.add_item(create_item("UC-001", ItemType::UseCase, None))
.build()
.unwrap();
let report = validate_strict(&graph);
assert!(!report.is_valid(), "Orphan should be error in strict mode");
assert_eq!(report.error_count(), 1);
}
#[test]
fn test_cycle_detection() {
let mut graph = KnowledgeGraph::new(false);
let scen1 = create_item(
"SCEN-001",
ItemType::Scenario,
Some(UpstreamRefs {
refines: vec![ItemId::new_unchecked("SCEN-002")],
..Default::default()
}),
);
let scen2 = create_item(
"SCEN-002",
ItemType::Scenario,
Some(UpstreamRefs {
refines: vec![ItemId::new_unchecked("SCEN-001")],
..Default::default()
}),
);
graph.add_item(scen1);
graph.add_item(scen2);
graph.add_relationship(
&ItemId::new_unchecked("SCEN-001"),
&ItemId::new_unchecked("SCEN-002"),
RelationshipType::Refines,
);
graph.add_relationship(
&ItemId::new_unchecked("SCEN-002"),
&ItemId::new_unchecked("SCEN-001"),
RelationshipType::Refines,
);
let report = validate(&graph);
assert!(!report.is_valid(), "Cycle should be detected");
}
#[test]
fn test_invalid_relationship() {
let mut graph = KnowledgeGraph::new(false);
graph.add_item(create_item("SOL-001", ItemType::Solution, None));
graph.add_item(create_item(
"SCEN-001",
ItemType::Scenario,
Some(UpstreamRefs {
refines: vec![ItemId::new_unchecked("SOL-001")],
..Default::default()
}),
));
let report = validate(&graph);
assert!(
!report.is_valid(),
"Invalid relationship should be detected"
);
}
}