use crate::config::enhanced_models::*;
use crate::metrics::definitions::{MetricCategory, ThresholdSet, METRIC_REGISTRY};
use std::collections::{HashMap, HashSet};
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Category weights validation failed: {0}")]
CategoryWeights(String),
#[error("Metric override validation failed: {0}")]
MetricOverride(String),
#[error("Threshold validation failed: {0}")]
Threshold(String),
#[error("Content expectation validation failed: {0}")]
ContentExpectation(String),
#[error("Penalty configuration validation failed: {0}")]
PenaltyConfig(String),
#[error("Bonus configuration validation failed: {0}")]
BonusConfig(String),
#[error("General validation error: {0}")]
General(String),
}
#[derive(Debug, Clone)]
pub struct ProfileValidator {
valid_metrics: HashSet<String>,
valid_categories: HashSet<String>,
}
impl ProfileValidator {
pub fn new() -> Self {
let valid_metrics = METRIC_REGISTRY
.all_metrics()
.iter()
.map(|m| m.field_name.to_string())
.collect();
let valid_categories = MetricCategory::all()
.iter()
.map(|c| format!("{:?}", c).to_lowercase())
.collect();
Self {
valid_metrics,
valid_categories,
}
}
pub fn validate_profile(
&self,
profile: &EnhancedScoringProfile,
) -> Result<ValidationReport, ValidationError> {
let mut report = ValidationReport::new();
self.validate_metadata(&profile.metadata, &mut report)?;
self.validate_category_weights(&profile.category_weights, &mut report)?;
self.validate_metric_overrides(&profile.metric_overrides, &mut report)?;
self.validate_content_expectations(&profile.content_expectations, &mut report)?;
self.validate_quality_bands(&profile.quality_bands, &mut report)?;
self.validate_penalties(&profile.penalties, &mut report)?;
self.validate_bonuses(&profile.bonuses, &mut report)?;
self.validate_consistency(profile, &mut report)?;
Ok(report)
}
pub fn normalize_profile(
&self,
profile: &mut EnhancedScoringProfile,
) -> Result<(), ValidationError> {
self.normalize_category_weights(&mut profile.category_weights)?;
self.apply_default_overrides(profile)?;
self.normalize_quality_bands(&mut profile.quality_bands)?;
Ok(())
}
fn validate_metadata(
&self,
metadata: &ProfileMetadata,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
if metadata.name.is_empty() {
report.add_error("Profile name cannot be empty".to_string());
}
if metadata.description.is_empty() {
report.add_warning("Profile description is empty".to_string());
}
if metadata.target_content_types.is_empty() {
report.add_warning("No target content types specified".to_string());
}
if !self.is_valid_version(&metadata.version) {
report.add_warning(format!(
"Version format may be invalid: {}",
metadata.version
));
}
Ok(())
}
fn validate_category_weights(
&self,
weights: &CategoryWeights,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
let weight_sum: f32 = weights.values().sum();
if (weight_sum - 1.0).abs() > 0.1 {
report.add_error(format!(
"Category weights sum to {:.2}, should sum to approximately 1.0",
weight_sum
));
} else if (weight_sum - 1.0).abs() > 0.01 {
report.add_warning(format!(
"Category weights sum to {:.3}, will be normalized to 1.0",
weight_sum
));
}
for (category, weight) in weights {
if *weight < 0.0 {
report.add_error(format!(
"Category '{}' has negative weight: {}",
category, weight
));
}
if *weight > 1.0 {
report.add_error(format!(
"Category '{}' has weight > 1.0: {}",
category, weight
));
}
if !self.valid_categories.contains(&category.to_lowercase()) {
report.add_warning(format!("Unknown category: '{}'", category));
}
}
if !weights.contains_key("content") {
report.add_suggestion("Consider adding 'content' category weight".to_string());
}
Ok(())
}
fn validate_metric_overrides(
&self,
overrides: &HashMap<String, MetricOverride>,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
for (metric_name, override_config) in overrides {
if !self.valid_metrics.contains(metric_name) {
report.add_error(format!("Unknown metric in override: '{}'", metric_name));
continue;
}
if override_config.weight < 0.0 {
report.add_error(format!(
"Metric '{}' has negative weight: {}",
metric_name, override_config.weight
));
}
if override_config.weight > 1.0 {
report.add_warning(format!(
"Metric '{}' has very high weight: {}",
metric_name, override_config.weight
));
}
if override_config.penalty_multiplier < 0.0 {
report.add_error(format!(
"Metric '{}' has negative penalty multiplier: {}",
metric_name, override_config.penalty_multiplier
));
}
if let Some(thresholds) = &override_config.thresholds {
self.validate_threshold_set(metric_name, thresholds, report)?;
}
for (i, bonus) in override_config.bonus_conditions.iter().enumerate() {
if bonus.bonus_points < 0.0 {
report.add_error(format!(
"Metric '{}' bonus condition {} has negative points: {}",
metric_name, i, bonus.bonus_points
));
}
if bonus.bonus_points > 50.0 {
report.add_warning(format!(
"Metric '{}' bonus condition {} has very high points: {}",
metric_name, i, bonus.bonus_points
));
}
}
}
Ok(())
}
fn validate_threshold_set(
&self,
metric_name: &str,
thresholds: &ThresholdSet,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
if thresholds.excellent < thresholds.good {
report.add_error(format!(
"Metric '{}': excellent threshold ({}) should be >= good threshold ({})",
metric_name, thresholds.excellent, thresholds.good
));
}
if thresholds.good < thresholds.fair {
report.add_error(format!(
"Metric '{}': good threshold ({}) should be >= fair threshold ({})",
metric_name, thresholds.good, thresholds.fair
));
}
if thresholds.fair < thresholds.poor {
report.add_error(format!(
"Metric '{}': fair threshold ({}) should be >= poor threshold ({})",
metric_name, thresholds.fair, thresholds.poor
));
}
if thresholds.poor < 0.0 {
report.add_warning(format!(
"Metric '{}' has negative poor threshold: {}",
metric_name, thresholds.poor
));
}
Ok(())
}
fn validate_content_expectations(
&self,
expectations: &ContentExpectations,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
if let Some(word_exp) = &expectations.word_count {
if word_exp.minimum > word_exp.optimal_range.0 {
report.add_error(format!(
"Word count minimum ({}) is greater than optimal range start ({})",
word_exp.minimum, word_exp.optimal_range.0
));
}
if word_exp.optimal_range.0 > word_exp.optimal_range.1 {
report.add_error(format!(
"Word count optimal range is invalid: {} to {}",
word_exp.optimal_range.0, word_exp.optimal_range.1
));
}
if let Some(max) = word_exp.maximum_useful {
if max < word_exp.optimal_range.1 {
report.add_warning(format!(
"Word count maximum useful ({}) is less than optimal range end ({})",
max, word_exp.optimal_range.1
));
}
}
}
if let Some(heading_exp) = &expectations.heading_structure {
if heading_exp.maximum_heading_depth > 6 {
report.add_warning("Maximum heading depth > 6 may not be meaningful".to_string());
}
if heading_exp.minimum_headings > 20 {
report.add_warning(format!(
"Minimum headings requirement ({}) seems very high",
heading_exp.minimum_headings
));
}
if let Some((min, max)) = heading_exp.heading_length_limits {
if min >= max {
report.add_error(format!(
"Heading length limits invalid: min ({}) >= max ({})",
min, max
));
}
}
}
if let Some(media_exp) = &expectations.media_requirements {
if media_exp.alt_text_coverage > 1.0 {
report.add_error(format!(
"Alt text coverage cannot exceed 100%: {}",
media_exp.alt_text_coverage
));
}
if media_exp.alt_text_coverage < 0.0 {
report.add_error(format!(
"Alt text coverage cannot be negative: {}",
media_exp.alt_text_coverage
));
}
if let Some((min, max)) = media_exp.image_to_text_ratio {
if min > max {
report.add_error(format!(
"Image to text ratio invalid: min ({}) > max ({})",
min, max
));
}
}
}
if let Some(seo_exp) = &expectations.seo_requirements {
if let Some((min, max)) = seo_exp.title_length_range {
if min >= max {
report.add_error(format!(
"Title length range invalid: min ({}) >= max ({})",
min, max
));
}
if max > 200 {
report.add_warning(format!("Title length max ({}) is very long", max));
}
}
if let Some((min, max)) = seo_exp.meta_description_length_range {
if min >= max {
report.add_error(format!(
"Meta description length range invalid: min ({}) >= max ({})",
min, max
));
}
if max > 300 {
report.add_warning(format!(
"Meta description max length ({}) is very long",
max
));
}
}
}
Ok(())
}
fn validate_quality_bands(
&self,
bands: &QualityBandConfig,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
if bands.excellent < bands.good {
report.add_error(format!(
"Excellent band ({}) should be >= good band ({})",
bands.excellent, bands.good
));
}
if bands.good < bands.fair {
report.add_error(format!(
"Good band ({}) should be >= fair band ({})",
bands.good, bands.fair
));
}
if bands.fair < bands.poor {
report.add_error(format!(
"Fair band ({}) should be >= poor band ({})",
bands.fair, bands.poor
));
}
if bands.excellent > 100.0 {
report.add_warning(format!("Excellent band ({}) is above 100", bands.excellent));
}
if bands.poor < 0.0 {
report.add_warning(format!("Poor band ({}) is below 0", bands.poor));
}
Ok(())
}
fn validate_penalties(
&self,
penalties: &PenaltyConfig,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
self.validate_penalty_group("severe", &penalties.severe_penalties, report)?;
self.validate_penalty_group("moderate", &penalties.moderate_penalties, report)?;
self.validate_penalty_group("light", &penalties.light_penalties, report)?;
Ok(())
}
fn validate_penalty_group(
&self,
_group_name: &str,
penalties: &HashMap<String, GlobalPenalty>,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
for (penalty_name, penalty) in penalties {
match &penalty.trigger_condition {
PenaltyTrigger::MetricBelow { metric, .. }
| PenaltyTrigger::MetricAbove { metric, .. }
| PenaltyTrigger::MetricEquals { metric, .. }
| PenaltyTrigger::MetricMissing { metric } => {
if !self.valid_metrics.contains(metric) {
report.add_error(format!(
"Penalty '{}' references unknown metric: '{}'",
penalty_name, metric
));
}
}
_ => {} }
match &penalty.penalty_type {
PenaltyType::FixedPoints { points } => {
if *points < 0.0 {
report.add_error(format!(
"Penalty '{}' has negative points: {}",
penalty_name, points
));
}
if *points > 100.0 {
report.add_warning(format!(
"Penalty '{}' has very high points: {}",
penalty_name, points
));
}
}
PenaltyType::Multiplier { factor } => {
if *factor < 0.0 {
report.add_error(format!(
"Penalty '{}' has negative multiplier: {}",
penalty_name, factor
));
}
if *factor > 1.0 {
report.add_warning(format!(
"Penalty '{}' multiplier > 1.0 increases score: {}",
penalty_name, factor
));
}
}
PenaltyType::CategoryPenalty {
category,
multiplier,
} => {
if !self.valid_categories.contains(&category.to_lowercase()) {
report.add_warning(format!(
"Penalty '{}' references unknown category: '{}'",
penalty_name, category
));
}
if *multiplier < 0.0 {
report.add_error(format!(
"Penalty '{}' has negative category multiplier: {}",
penalty_name, multiplier
));
}
}
}
}
Ok(())
}
fn validate_bonuses(
&self,
bonuses: &BonusConfig,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
self.validate_bonus_group("excellence", &bonuses.excellence_bonuses, report)?;
self.validate_bonus_group("achievement", &bonuses.achievement_bonuses, report)?;
self.validate_bonus_group("synergy", &bonuses.synergy_bonuses, report)?;
Ok(())
}
fn validate_bonus_group(
&self,
_group_name: &str,
bonuses: &HashMap<String, GlobalBonus>,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
for (bonus_name, bonus) in bonuses {
if bonus.bonus_points < 0.0 {
report.add_error(format!(
"Bonus '{}' has negative points: {}",
bonus_name, bonus.bonus_points
));
}
if bonus.bonus_points > 50.0 {
report.add_warning(format!(
"Bonus '{}' has very high points: {}",
bonus_name, bonus.bonus_points
));
}
match &bonus.trigger_condition {
BonusTrigger::MetricExcellence { metric, .. } => {
if !self.valid_metrics.contains(metric) {
report.add_error(format!(
"Bonus '{}' references unknown metric: '{}'",
bonus_name, metric
));
}
}
BonusTrigger::MultipleMetricsGood { metrics, .. } => {
for metric in metrics {
if !self.valid_metrics.contains(metric) {
report.add_error(format!(
"Bonus '{}' references unknown metric: '{}'",
bonus_name, metric
));
}
}
}
BonusTrigger::CategoryExcellence { category, .. } => {
if !self.valid_categories.contains(&category.to_lowercase()) {
report.add_warning(format!(
"Bonus '{}' references unknown category: '{}'",
bonus_name, category
));
}
}
_ => {} }
}
Ok(())
}
fn validate_consistency(
&self,
profile: &EnhancedScoringProfile,
report: &mut ValidationReport,
) -> Result<(), ValidationError> {
for (category, weight) in &profile.category_weights {
if *weight > 0.2 {
let category_metrics = self.get_metrics_for_category(category);
let has_overrides = category_metrics
.iter()
.any(|metric| profile.metric_overrides.contains_key(metric));
if !has_overrides {
report.add_suggestion(format!(
"Category '{}' has high weight ({:.1}%) but no metric overrides",
category,
weight * 100.0
));
}
}
}
if let Some(word_exp) = &profile.content_expectations.word_count {
if let Some(word_override) = profile.metric_overrides.get("word_count") {
if let Some(thresholds) = &word_override.thresholds {
if thresholds.excellent as usize > word_exp.optimal_range.1 {
report.add_warning("Word count metric threshold excellent is higher than content expectation optimal range".to_string());
}
}
}
}
Ok(())
}
fn normalize_category_weights(
&self,
weights: &mut CategoryWeights,
) -> Result<(), ValidationError> {
let weight_sum: f32 = weights.values().sum();
if weight_sum > 0.0 && (weight_sum - 1.0).abs() > 0.01 {
for (_, weight) in weights.iter_mut() {
*weight /= weight_sum;
}
}
Ok(())
}
fn apply_default_overrides(
&self,
_profile: &mut EnhancedScoringProfile,
) -> Result<(), ValidationError> {
Ok(())
}
fn normalize_quality_bands(
&self,
bands: &mut QualityBandConfig,
) -> Result<(), ValidationError> {
if bands.excellent < bands.good {
bands.excellent = bands.good + 5.0;
}
if bands.good < bands.fair {
bands.good = bands.fair + 5.0;
}
if bands.fair < bands.poor {
bands.fair = bands.poor + 5.0;
}
bands.excellent = bands.excellent.clamp(80.0, 100.0);
bands.good = bands.good.clamp(60.0, 95.0);
bands.fair = bands.fair.clamp(40.0, 85.0);
bands.poor = bands.poor.clamp(0.0, 75.0);
Ok(())
}
fn is_valid_version(&self, version: &str) -> bool {
let parts: Vec<&str> = version.split('.').collect();
parts.len() >= 2 && parts.len() <= 3 && parts.iter().all(|part| part.parse::<u32>().is_ok())
}
fn get_metrics_for_category(&self, category: &str) -> Vec<String> {
match category.to_lowercase().as_str() {
"content" => vec![
"word_count".to_string(),
"readability_fk".to_string(),
"main_text_ratio".to_string(),
],
"media" => vec!["images_count".to_string(), "alt_text_coverage".to_string()],
"seo" => vec!["title_length".to_string(), "meta_desc_len".to_string()],
_ => Vec::new(),
}
}
}
impl Default for ProfileValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profile_validator_creation() {
let validator = ProfileValidator::new();
assert!(!validator.valid_metrics.is_empty());
assert!(!validator.valid_categories.is_empty());
}
#[test]
fn test_validate_empty_profile() {
let validator = ProfileValidator::new();
let profile = EnhancedScoringProfile::default();
let result = validator.validate_profile(&profile);
assert!(result.is_ok());
let report = result.unwrap();
assert!(!report.is_valid); }
#[test]
fn test_normalize_category_weights() {
let validator = ProfileValidator::new();
let mut weights = HashMap::new();
weights.insert("content".to_string(), 0.4);
weights.insert("seo".to_string(), 0.4);
let result = validator.normalize_category_weights(&mut weights);
assert!(result.is_ok());
let sum: f32 = weights.values().sum();
assert!((sum - 1.0).abs() < 0.01);
}
#[test]
fn test_threshold_validation() {
let validator = ProfileValidator::new();
let mut report = ValidationReport::new();
let valid_thresholds = ThresholdSet {
excellent: 100.0,
good: 80.0,
fair: 60.0,
poor: 30.0,
};
let result =
validator.validate_threshold_set("test_metric", &valid_thresholds, &mut report);
assert!(result.is_ok());
assert!(report.is_valid);
let invalid_thresholds = ThresholdSet {
excellent: 50.0, good: 80.0,
fair: 60.0,
poor: 30.0,
};
let mut report2 = ValidationReport::new();
let result2 =
validator.validate_threshold_set("test_metric", &invalid_thresholds, &mut report2);
assert!(result2.is_ok());
assert!(!report2.is_valid); }
#[test]
fn test_version_validation() {
let validator = ProfileValidator::new();
assert!(validator.is_valid_version("1.0.0"));
assert!(validator.is_valid_version("2.1"));
assert!(!validator.is_valid_version("1.0.0.0"));
assert!(!validator.is_valid_version("invalid"));
assert!(!validator.is_valid_version("1.a.0"));
}
}