use crate::error::EvalResult;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReferentialIntegrityEvaluation {
pub vendor_integrity: EntityIntegrity,
pub customer_integrity: EntityIntegrity,
pub material_integrity: EntityIntegrity,
pub employee_integrity: EntityIntegrity,
pub account_integrity: EntityIntegrity,
pub cost_center_integrity: EntityIntegrity,
pub overall_integrity_score: f64,
pub total_valid_references: usize,
pub total_invalid_references: usize,
pub total_orphaned_entities: usize,
pub passes: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityIntegrity {
pub entity_type: String,
pub total_entities: usize,
pub entities_referenced: usize,
pub valid_references: usize,
pub invalid_references: usize,
pub orphaned_entities: usize,
pub integrity_score: f64,
pub usage_rate: f64,
}
impl Default for EntityIntegrity {
fn default() -> Self {
Self {
entity_type: String::new(),
total_entities: 0,
entities_referenced: 0,
valid_references: 0,
invalid_references: 0,
orphaned_entities: 0,
integrity_score: 1.0,
usage_rate: 1.0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ReferentialData {
pub vendors: EntityReferenceData,
pub customers: EntityReferenceData,
pub materials: EntityReferenceData,
pub employees: EntityReferenceData,
pub accounts: EntityReferenceData,
pub cost_centers: EntityReferenceData,
}
#[derive(Debug, Clone, Default)]
pub struct EntityReferenceData {
pub valid_ids: std::collections::HashSet<String>,
pub references: Vec<String>,
}
impl EntityReferenceData {
pub fn new() -> Self {
Self::default()
}
pub fn add_entity(&mut self, id: String) {
self.valid_ids.insert(id);
}
pub fn add_reference(&mut self, id: String) {
self.references.push(id);
}
}
pub struct ReferentialIntegrityEvaluator {
min_integrity_score: f64,
min_usage_rate: f64,
}
impl ReferentialIntegrityEvaluator {
pub fn new(min_integrity_score: f64, min_usage_rate: f64) -> Self {
Self {
min_integrity_score,
min_usage_rate,
}
}
pub fn evaluate(&self, data: &ReferentialData) -> EvalResult<ReferentialIntegrityEvaluation> {
let vendor_integrity = self.evaluate_entity("Vendor", &data.vendors);
let customer_integrity = self.evaluate_entity("Customer", &data.customers);
let material_integrity = self.evaluate_entity("Material", &data.materials);
let employee_integrity = self.evaluate_entity("Employee", &data.employees);
let account_integrity = self.evaluate_entity("Account", &data.accounts);
let cost_center_integrity = self.evaluate_entity("CostCenter", &data.cost_centers);
let integrities = [
&vendor_integrity,
&customer_integrity,
&material_integrity,
&employee_integrity,
&account_integrity,
&cost_center_integrity,
];
let total_valid_references: usize = integrities.iter().map(|i| i.valid_references).sum();
let total_invalid_references: usize =
integrities.iter().map(|i| i.invalid_references).sum();
let total_orphaned_entities: usize = integrities.iter().map(|i| i.orphaned_entities).sum();
let total_refs = total_valid_references + total_invalid_references;
let overall_integrity_score = if total_refs > 0 {
total_valid_references as f64 / total_refs as f64
} else {
1.0
};
let usage_ok = integrities
.iter()
.all(|i| i.total_entities == 0 || i.usage_rate >= self.min_usage_rate);
let passes = overall_integrity_score >= self.min_integrity_score && usage_ok;
Ok(ReferentialIntegrityEvaluation {
vendor_integrity,
customer_integrity,
material_integrity,
employee_integrity,
account_integrity,
cost_center_integrity,
overall_integrity_score,
total_valid_references,
total_invalid_references,
total_orphaned_entities,
passes,
})
}
fn evaluate_entity(&self, entity_type: &str, data: &EntityReferenceData) -> EntityIntegrity {
let total_entities = data.valid_ids.len();
let mut valid_references = 0;
let mut invalid_references = 0;
let mut referenced_ids = std::collections::HashSet::new();
for reference in &data.references {
if data.valid_ids.contains(reference) {
valid_references += 1;
referenced_ids.insert(reference.clone());
} else {
invalid_references += 1;
}
}
let entities_referenced = referenced_ids.len();
let orphaned_entities = total_entities.saturating_sub(entities_referenced);
let total_refs = valid_references + invalid_references;
let integrity_score = if total_refs > 0 {
valid_references as f64 / total_refs as f64
} else {
1.0
};
let usage_rate = if total_entities > 0 {
entities_referenced as f64 / total_entities as f64
} else {
1.0
};
EntityIntegrity {
entity_type: entity_type.to_string(),
total_entities,
entities_referenced,
valid_references,
invalid_references,
orphaned_entities,
integrity_score,
usage_rate,
}
}
}
impl Default for ReferentialIntegrityEvaluator {
fn default() -> Self {
Self::new(0.99, 0.80) }
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_perfect_integrity() {
let mut data = ReferentialData::default();
data.vendors.add_entity("V001".to_string());
data.vendors.add_entity("V002".to_string());
data.vendors.add_reference("V001".to_string());
data.vendors.add_reference("V002".to_string());
data.vendors.add_reference("V001".to_string());
let evaluator = ReferentialIntegrityEvaluator::default();
let result = evaluator.evaluate(&data).unwrap();
assert_eq!(result.vendor_integrity.integrity_score, 1.0);
assert_eq!(result.vendor_integrity.valid_references, 3);
assert_eq!(result.vendor_integrity.invalid_references, 0);
assert_eq!(result.vendor_integrity.orphaned_entities, 0);
}
#[test]
fn test_invalid_references() {
let mut data = ReferentialData::default();
data.vendors.add_entity("V001".to_string());
data.vendors.add_reference("V001".to_string());
data.vendors.add_reference("V999".to_string());
let evaluator = ReferentialIntegrityEvaluator::default();
let result = evaluator.evaluate(&data).unwrap();
assert_eq!(result.vendor_integrity.valid_references, 1);
assert_eq!(result.vendor_integrity.invalid_references, 1);
assert_eq!(result.vendor_integrity.integrity_score, 0.5);
}
#[test]
fn test_orphaned_entities() {
let mut data = ReferentialData::default();
data.vendors.add_entity("V001".to_string());
data.vendors.add_entity("V002".to_string());
data.vendors.add_entity("V003".to_string());
data.vendors.add_reference("V001".to_string());
let evaluator = ReferentialIntegrityEvaluator::default();
let result = evaluator.evaluate(&data).unwrap();
assert_eq!(result.vendor_integrity.entities_referenced, 1);
assert_eq!(result.vendor_integrity.orphaned_entities, 2);
assert!(result.vendor_integrity.usage_rate < 0.5);
}
#[test]
fn test_empty_data() {
let data = ReferentialData::default();
let evaluator = ReferentialIntegrityEvaluator::default();
let result = evaluator.evaluate(&data).unwrap();
assert_eq!(result.overall_integrity_score, 1.0);
assert!(result.passes);
}
}