use std::collections::BTreeMap;
pub type RuleId = &'static str;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Info,
Warning,
Error,
}
#[non_exhaustive]
pub struct GraphSnapshot {
pub entity_count: usize,
pub edge_count: usize,
}
#[non_exhaustive]
pub struct ValidationContext<'a> {
pub snapshot: &'a GraphSnapshot,
pub config: &'a BTreeMap<&'static str, serde_json::Value>,
}
#[non_exhaustive]
pub struct Violation {
pub rule_id: &'static str,
pub severity: Severity,
pub message: String,
pub fixable: bool,
pub entity_id: Option<String>,
pub edge_id: Option<String>,
}
impl Violation {
pub fn new(rule_id: &'static str, severity: Severity, message: impl Into<String>) -> Self {
Self {
rule_id,
severity,
message: message.into(),
fixable: false,
entity_id: None,
edge_id: None,
}
}
pub fn with_entity(mut self, id: impl Into<String>) -> Self {
self.entity_id = Some(id.into());
self
}
}
pub type RuleFn = fn(&ValidationContext<'_>) -> Vec<Violation>;
pub type FixFn = fn(&ValidationContext<'_>, &[Violation]) -> Option<GraphPatch>;
#[non_exhaustive]
pub struct GraphPatch;
pub struct ValidationRule {
pub id: RuleId,
pub severity: Severity,
pub description: &'static str,
pub check: RuleFn,
pub fix: Option<FixFn>,
}
#[derive(Default)]
pub struct ValidationReport {
pub violations_by_rule: BTreeMap<String, Vec<Violation>>,
}
impl ValidationReport {
pub fn add(&mut self, rule_id: &str, violations: Vec<Violation>) {
self.violations_by_rule
.entry(rule_id.to_string())
.or_default()
.extend(violations);
}
pub fn error_count(&self) -> usize {
self.violations_by_rule
.values()
.flat_map(|vs| vs.iter())
.filter(|v| v.severity == Severity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.violations_by_rule
.values()
.flat_map(|vs| vs.iter())
.filter(|v| v.severity == Severity::Warning)
.count()
}
pub fn passed(&self) -> bool {
self.error_count() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn violation_builder() {
let v = Violation::new("test/rule", Severity::Warning, "something is off")
.with_entity("abc123");
assert_eq!(v.rule_id, "test/rule");
assert_eq!(v.severity, Severity::Warning);
assert!(!v.fixable);
assert_eq!(v.entity_id.as_deref(), Some("abc123"));
}
#[test]
fn report_error_count() {
let mut report = ValidationReport::default();
report.add(
"test/rule",
vec![
Violation::new("test/rule", Severity::Error, "bad"),
Violation::new("test/rule", Severity::Warning, "meh"),
],
);
assert_eq!(report.error_count(), 1);
assert_eq!(report.warning_count(), 1);
assert!(!report.passed());
}
#[test]
fn report_passed_when_no_errors() {
let mut report = ValidationReport::default();
report.add(
"test/rule",
vec![Violation::new("test/rule", Severity::Warning, "meh")],
);
assert!(report.passed());
}
#[test]
fn graph_patch_is_constructible() {
let _patch = GraphPatch;
}
#[test]
fn validation_rule_fields() {
fn dummy_check(_ctx: &ValidationContext<'_>) -> Vec<Violation> {
vec![]
}
let rule = ValidationRule {
id: "bio/taxa",
severity: Severity::Warning,
description: "taxa must exist",
check: dummy_check,
fix: None,
};
assert_eq!(rule.id, "bio/taxa");
assert!(rule.fix.is_none());
}
}