use crate::reflection::schema_graph::{ForeignKeyMapping, Relationship, SchemaGraph};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationConfig {
pub enabled: bool,
pub strict_mode: bool,
pub max_validation_depth: usize,
pub custom_rules: Vec<CustomValidationRule>,
pub cache_results: bool,
}
impl Default for ValidationConfig {
fn default() -> Self {
Self {
enabled: true,
strict_mode: false,
max_validation_depth: 3,
custom_rules: vec![],
cache_results: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomValidationRule {
pub name: String,
pub applies_to_entities: Vec<String>,
pub validates_fields: Vec<String>,
pub rule_type: ValidationRuleType,
pub parameters: HashMap<String, String>,
pub error_message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationRuleType {
ForeignKeyExists,
FieldFormat,
Range,
Unique,
BusinessLogic,
Custom,
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
pub validated_entities: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub error_type: ValidationErrorType,
pub entity_name: String,
pub field_name: String,
pub message: String,
pub invalid_value: String,
pub suggested_fix: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationWarning {
pub warning_type: ValidationWarningType,
pub entity_name: String,
pub field_name: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationErrorType {
ForeignKeyNotFound,
InvalidFormat,
OutOfRange,
DuplicateValue,
BusinessRuleViolation,
CircularReference,
}
#[derive(Debug, Clone)]
pub enum ValidationWarningType {
DataInconsistency,
PerformanceConcern,
BestPracticeViolation,
}
#[derive(Debug, Default)]
pub struct ValidationDataStore {
entities: HashMap<String, Vec<GeneratedEntity>>,
foreign_key_index: HashMap<String, HashMap<String, Vec<usize>>>,
}
#[derive(Debug, Clone)]
pub struct GeneratedEntity {
pub entity_type: String,
pub primary_key: Option<String>,
pub field_values: HashMap<String, String>,
pub endpoint: String,
pub generated_at: std::time::SystemTime,
}
pub struct ValidationFramework {
config: ValidationConfig,
schema_graph: Option<SchemaGraph>,
data_store: ValidationDataStore,
validation_cache: HashMap<String, ValidationResult>,
}
impl ValidationFramework {
pub fn new(config: ValidationConfig) -> Self {
Self {
config,
schema_graph: None,
data_store: ValidationDataStore::default(),
validation_cache: HashMap::new(),
}
}
pub fn set_schema_graph(&mut self, schema_graph: SchemaGraph) {
info!(
"Setting schema graph with {} entities for validation",
schema_graph.entities.len()
);
self.schema_graph = Some(schema_graph);
}
pub fn register_entity(
&mut self,
entity: GeneratedEntity,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
debug!("Registering entity {} for endpoint {}", entity.entity_type, entity.endpoint);
let entity_type = entity.entity_type.clone();
let primary_key = entity.primary_key.clone();
let entities_list = self.data_store.entities.entry(entity_type.clone()).or_default();
let entity_index = entities_list.len();
entities_list.push(entity);
if let Some(pk) = primary_key {
let type_index = self.data_store.foreign_key_index.entry(entity_type).or_default();
let pk_index = type_index.entry(pk).or_default();
pk_index.push(entity_index);
}
Ok(())
}
pub fn validate_all_entities(&mut self) -> ValidationResult {
if !self.config.enabled {
return ValidationResult {
is_valid: true,
errors: vec![],
warnings: vec![],
validated_entities: vec![],
};
}
info!(
"Starting cross-endpoint validation of {} entity types",
self.data_store.entities.len()
);
let mut result = ValidationResult {
is_valid: true,
errors: vec![],
warnings: vec![],
validated_entities: vec![],
};
self.validate_foreign_key_relationships(&mut result);
self.validate_custom_rules(&mut result);
self.validate_referential_integrity(&mut result);
self.check_data_consistency(&mut result);
result.is_valid = result.errors.is_empty() || !self.config.strict_mode;
info!(
"Validation completed: {} errors, {} warnings",
result.errors.len(),
result.warnings.len()
);
result
}
fn validate_foreign_key_relationships(&self, result: &mut ValidationResult) {
if let Some(schema_graph) = &self.schema_graph {
for (entity_type, entities) in &self.data_store.entities {
result.validated_entities.push(entity_type.clone());
if let Some(fk_mappings) = schema_graph.foreign_keys.get(entity_type) {
for entity in entities {
self.validate_entity_foreign_keys(entity, fk_mappings, result);
}
}
}
}
}
fn validate_entity_foreign_keys(
&self,
entity: &GeneratedEntity,
fk_mappings: &[ForeignKeyMapping],
result: &mut ValidationResult,
) {
for mapping in fk_mappings {
if let Some(fk_value) = entity.field_values.get(&mapping.field_name) {
if !self.foreign_key_exists(&mapping.target_entity, fk_value) {
result.errors.push(ValidationError {
error_type: ValidationErrorType::ForeignKeyNotFound,
entity_name: entity.entity_type.clone(),
field_name: mapping.field_name.clone(),
message: format!(
"Foreign key '{}' references non-existent {} with value '{}'",
mapping.field_name, mapping.target_entity, fk_value
),
invalid_value: fk_value.clone(),
suggested_fix: Some(format!(
"Create a {} entity with primary key '{}'",
mapping.target_entity, fk_value
)),
});
}
}
}
}
fn foreign_key_exists(&self, target_entity: &str, key_value: &str) -> bool {
if let Some(type_index) = self.data_store.foreign_key_index.get(target_entity) {
type_index.contains_key(key_value)
} else {
false
}
}
fn validate_custom_rules(&self, result: &mut ValidationResult) {
for rule in &self.config.custom_rules {
for entity_type in &rule.applies_to_entities {
if let Some(entities) = self.data_store.entities.get(entity_type) {
for entity in entities {
self.validate_entity_against_rule(entity, rule, result);
}
}
}
}
}
fn validate_entity_against_rule(
&self,
entity: &GeneratedEntity,
rule: &CustomValidationRule,
result: &mut ValidationResult,
) {
match &rule.rule_type {
ValidationRuleType::ForeignKeyExists => {
self.validate_foreign_key_rule(entity, rule, result);
}
ValidationRuleType::FieldFormat => {
self.validate_field_format(entity, rule, result);
}
ValidationRuleType::Range => {
self.validate_field_range(entity, rule, result);
}
ValidationRuleType::Unique => {
self.validate_field_uniqueness(entity, rule, result);
}
ValidationRuleType::BusinessLogic | ValidationRuleType::Custom => {
self.validate_business_logic_rule(entity, rule, result);
}
}
}
fn validate_foreign_key_rule(
&self,
entity: &GeneratedEntity,
rule: &CustomValidationRule,
result: &mut ValidationResult,
) {
let Some(target_entity) = rule.parameters.get("target_entity") else {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::BestPracticeViolation,
entity_name: entity.entity_type.clone(),
field_name: None,
message: format!(
"Rule '{}' is missing required parameter 'target_entity'",
rule.name
),
});
return;
};
for field_name in &rule.validates_fields {
if let Some(fk_value) = entity.field_values.get(field_name) {
if !self.foreign_key_exists(target_entity, fk_value) {
result.errors.push(ValidationError {
error_type: ValidationErrorType::ForeignKeyNotFound,
entity_name: entity.entity_type.clone(),
field_name: field_name.clone(),
message: rule.error_message.clone(),
invalid_value: fk_value.clone(),
suggested_fix: Some(format!(
"Create a {} entity with primary key '{}'",
target_entity, fk_value
)),
});
}
}
}
}
fn validate_business_logic_rule(
&self,
entity: &GeneratedEntity,
rule: &CustomValidationRule,
result: &mut ValidationResult,
) {
let Some(field) = rule.parameters.get("field") else {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::BestPracticeViolation,
entity_name: entity.entity_type.clone(),
field_name: None,
message: format!(
"Rule '{}' skipped: missing 'field' parameter for {:?}",
rule.name, rule.rule_type
),
});
return;
};
let Some(operator) = rule.parameters.get("operator") else {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::BestPracticeViolation,
entity_name: entity.entity_type.clone(),
field_name: Some(field.clone()),
message: format!(
"Rule '{}' skipped: missing 'operator' parameter for {:?}",
rule.name, rule.rule_type
),
});
return;
};
let Some(expected) = rule.parameters.get("value") else {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::BestPracticeViolation,
entity_name: entity.entity_type.clone(),
field_name: Some(field.clone()),
message: format!(
"Rule '{}' skipped: missing 'value' parameter for {:?}",
rule.name, rule.rule_type
),
});
return;
};
if let Some(actual) = entity.field_values.get(field) {
let passed = match operator.as_str() {
"eq" => actual == expected,
"ne" => actual != expected,
"contains" => actual.contains(expected),
"starts_with" => actual.starts_with(expected),
"ends_with" => actual.ends_with(expected),
_ => {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::BestPracticeViolation,
entity_name: entity.entity_type.clone(),
field_name: Some(field.clone()),
message: format!(
"Rule '{}' uses unsupported operator '{}'",
rule.name, operator
),
});
return;
}
};
if !passed {
result.errors.push(ValidationError {
error_type: ValidationErrorType::BusinessRuleViolation,
entity_name: entity.entity_type.clone(),
field_name: field.clone(),
message: rule.error_message.clone(),
invalid_value: actual.clone(),
suggested_fix: Some(format!(
"Expected '{}' {} '{}'",
field, operator, expected
)),
});
}
}
}
fn validate_field_format(
&self,
entity: &GeneratedEntity,
rule: &CustomValidationRule,
result: &mut ValidationResult,
) {
for field_name in &rule.validates_fields {
if let Some(field_value) = entity.field_values.get(field_name) {
if let Some(pattern) = rule.parameters.get("pattern") {
if let Ok(regex) = regex::Regex::new(pattern) {
if !regex.is_match(field_value) {
result.errors.push(ValidationError {
error_type: ValidationErrorType::InvalidFormat,
entity_name: entity.entity_type.clone(),
field_name: field_name.clone(),
message: rule.error_message.clone(),
invalid_value: field_value.clone(),
suggested_fix: Some(format!(
"Value should match pattern: {}",
pattern
)),
});
}
}
}
}
}
}
fn validate_field_range(
&self,
entity: &GeneratedEntity,
rule: &CustomValidationRule,
result: &mut ValidationResult,
) {
for field_name in &rule.validates_fields {
if let Some(field_value) = entity.field_values.get(field_name) {
if let Ok(value) = field_value.parse::<f64>() {
let min = rule.parameters.get("min").and_then(|s| s.parse::<f64>().ok());
let max = rule.parameters.get("max").and_then(|s| s.parse::<f64>().ok());
let out_of_range = (min.is_some() && value < min.unwrap())
|| (max.is_some() && value > max.unwrap());
if out_of_range {
result.errors.push(ValidationError {
error_type: ValidationErrorType::OutOfRange,
entity_name: entity.entity_type.clone(),
field_name: field_name.clone(),
message: rule.error_message.clone(),
invalid_value: field_value.clone(),
suggested_fix: Some(format!(
"Value should be between {} and {}",
min.map_or("any".to_string(), |v| v.to_string()),
max.map_or("any".to_string(), |v| v.to_string())
)),
});
}
}
}
}
}
fn validate_field_uniqueness(
&self,
entity: &GeneratedEntity,
rule: &CustomValidationRule,
result: &mut ValidationResult,
) {
for field_name in &rule.validates_fields {
if let Some(field_value) = entity.field_values.get(field_name) {
let mut duplicate_count = 0;
if let Some(entities) = self.data_store.entities.get(&entity.entity_type) {
for other_entity in entities {
if let Some(other_value) = other_entity.field_values.get(field_name) {
if other_value == field_value {
duplicate_count += 1;
}
}
}
}
if duplicate_count > 1 {
result.errors.push(ValidationError {
error_type: ValidationErrorType::DuplicateValue,
entity_name: entity.entity_type.clone(),
field_name: field_name.clone(),
message: rule.error_message.clone(),
invalid_value: field_value.clone(),
suggested_fix: Some("Generate unique values for this field".to_string()),
});
}
}
}
}
fn validate_referential_integrity(&self, result: &mut ValidationResult) {
if let Some(schema_graph) = &self.schema_graph {
for relationship in &schema_graph.relationships {
self.validate_relationship_integrity(relationship, result);
}
}
}
fn validate_relationship_integrity(
&self,
relationship: &Relationship,
result: &mut ValidationResult,
) {
if let (Some(from_entities), Some(to_entities)) = (
self.data_store.entities.get(&relationship.from_entity),
self.data_store.entities.get(&relationship.to_entity),
) {
for from_entity in from_entities {
if let Some(ref_value) = from_entity.field_values.get(&relationship.field_name) {
let target_exists = to_entities
.iter()
.any(|to_entity| to_entity.primary_key.as_ref() == Some(ref_value));
if !target_exists && relationship.is_required {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::DataInconsistency,
entity_name: from_entity.entity_type.clone(),
field_name: Some(relationship.field_name.clone()),
message: format!(
"Required relationship from {} to {} not satisfied - referenced {} '{}' does not exist",
relationship.from_entity, relationship.to_entity,
relationship.to_entity, ref_value
),
});
}
}
}
}
}
fn check_data_consistency(&self, result: &mut ValidationResult) {
self.check_orphaned_entities(result);
self.check_performance_concerns(result);
}
fn check_orphaned_entities(&self, result: &mut ValidationResult) {
if let Some(schema_graph) = &self.schema_graph {
for (entity_type, entity_node) in &schema_graph.entities {
if entity_node.referenced_by.is_empty() && !entity_node.is_root {
if let Some(entities) = self.data_store.entities.get(entity_type) {
if !entities.is_empty() {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::DataInconsistency,
entity_name: entity_type.clone(),
field_name: None,
message: format!(
"Entity type {} is not referenced by any other entities but {} instances were generated",
entity_type, entities.len()
),
});
}
}
}
}
}
}
fn check_performance_concerns(&self, result: &mut ValidationResult) {
for (entity_type, entities) in &self.data_store.entities {
if entities.len() > 10000 {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::PerformanceConcern,
entity_name: entity_type.clone(),
field_name: None,
message: format!(
"Large number of {} entities ({}) may impact performance",
entity_type,
entities.len()
),
});
}
}
}
pub fn clear(&mut self) {
self.data_store.entities.clear();
self.data_store.foreign_key_index.clear();
self.validation_cache.clear();
info!("Validation framework data cleared");
}
pub fn get_statistics(&self) -> ValidationStatistics {
let total_entities: usize = self.data_store.entities.values().map(|v| v.len()).sum();
let entity_type_count = self.data_store.entities.len();
let indexed_keys: usize = self
.data_store
.foreign_key_index
.values()
.map(|type_index| type_index.len())
.sum();
ValidationStatistics {
total_entities,
entity_type_count,
indexed_foreign_keys: indexed_keys,
cache_size: self.validation_cache.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationStatistics {
pub total_entities: usize,
pub entity_type_count: usize,
pub indexed_foreign_keys: usize,
pub cache_size: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::SystemTime;
#[test]
fn test_validation_framework_creation() {
let config = ValidationConfig::default();
let framework = ValidationFramework::new(config);
assert!(framework.config.enabled);
assert!(!framework.config.strict_mode);
}
#[test]
fn test_entity_registration() {
let config = ValidationConfig::default();
let mut framework = ValidationFramework::new(config);
let entity = GeneratedEntity {
entity_type: "User".to_string(),
primary_key: Some("user_123".to_string()),
field_values: HashMap::from([
("id".to_string(), "user_123".to_string()),
("name".to_string(), "John Doe".to_string()),
]),
endpoint: "/users".to_string(),
generated_at: SystemTime::now(),
};
framework.register_entity(entity).expect("Should register entity successfully");
let stats = framework.get_statistics();
assert_eq!(stats.total_entities, 1);
assert_eq!(stats.entity_type_count, 1);
}
#[test]
fn test_validation_with_no_schema() {
let config = ValidationConfig::default();
let mut framework = ValidationFramework::new(config);
let entity1 = GeneratedEntity {
entity_type: "User".to_string(),
primary_key: Some("1".to_string()),
field_values: HashMap::from([("id".to_string(), "1".to_string())]),
endpoint: "/users".to_string(),
generated_at: SystemTime::now(),
};
framework.register_entity(entity1).unwrap();
let result = framework.validate_all_entities();
assert!(result.is_valid);
assert!(result.errors.is_empty());
}
#[test]
fn test_custom_validation_rule() {
let mut config = ValidationConfig::default();
config.custom_rules.push(CustomValidationRule {
name: "email_format".to_string(),
applies_to_entities: vec!["User".to_string()],
validates_fields: vec!["email".to_string()],
rule_type: ValidationRuleType::FieldFormat,
parameters: HashMap::from([(
"pattern".to_string(),
r"^[^@]+@[^@]+\.[^@]+$".to_string(),
)]),
error_message: "Invalid email format".to_string(),
});
let mut framework = ValidationFramework::new(config);
let entity = GeneratedEntity {
entity_type: "User".to_string(),
primary_key: Some("1".to_string()),
field_values: HashMap::from([
("id".to_string(), "1".to_string()),
("email".to_string(), "invalid-email".to_string()),
]),
endpoint: "/users".to_string(),
generated_at: SystemTime::now(),
};
framework.register_entity(entity).unwrap();
let result = framework.validate_all_entities();
assert!(!result.errors.is_empty());
assert_eq!(result.errors[0].error_type, ValidationErrorType::InvalidFormat);
}
}