use crate::metrics::definitions::{
BonusCondition, BonusConditionType, LinearScorer, MetricScorer, MetricValue,
ProfileMetricConfig, ScoringFunction, ValidationRule,
};
#[derive(Debug, thiserror::Error)]
pub enum ScoringError {
#[error("Invalid metric value: {0}")]
InvalidValue(String),
#[error("Validation failed: {0}")]
ValidationError(String),
#[error("Unknown scoring function: {0}")]
UnknownFunction(String),
}
pub struct ScoringContext {
pub profile_name: String,
pub content_type: String,
pub page_metrics: Option<crate::models::models::PageMetrics>,
}
pub trait EnhancedMetricScorer: MetricScorer {
fn score_with_context(
&self,
value: MetricValue,
config: &ProfileMetricConfig,
_context: &ScoringContext,
) -> f32 {
self.score_metric(value, config)
}
}
pub struct WordCountScorer;
impl MetricScorer for WordCountScorer {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32 {
let word_count = value.as_f32();
let thresholds = &config.thresholds;
let base_score = if word_count == 0.0 {
0.0
} else if word_count >= thresholds.excellent {
let excellence_multiplier = (word_count / thresholds.excellent).min(2.0);
(100.0 + (excellence_multiplier - 1.0) * 5.0).min(105.0)
} else if word_count >= thresholds.good {
let range = thresholds.excellent - thresholds.good;
let position = (word_count - thresholds.good) / range;
80.0 + (position * 20.0)
} else if word_count >= thresholds.fair {
let range = thresholds.good - thresholds.fair;
let position = (word_count - thresholds.fair) / range;
60.0 + (position * 20.0)
} else if word_count >= thresholds.poor {
let range = thresholds.fair - thresholds.poor;
let position = (word_count - thresholds.poor) / range;
30.0 + (position * 30.0)
} else {
(word_count / thresholds.poor) * 30.0
};
if word_count < thresholds.poor * 0.5 {
let penalty_factor = 1.0 / config.penalty_multiplier.max(1.0);
(base_score * penalty_factor).clamp(0.0, 30.0)
} else {
base_score.clamp(0.0, 105.0) }
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
let mut final_score = base_score;
let numeric_value = value.as_f32();
for condition in conditions {
let bonus_applies = match &condition.condition_type {
BonusConditionType::ValueBetween { min, max } => {
numeric_value >= *min && numeric_value <= *max
}
BonusConditionType::ValueAbove { threshold } => numeric_value > *threshold,
BonusConditionType::ValueBelow { threshold } => numeric_value < *threshold,
BonusConditionType::ValueEquals { target } => (numeric_value - target).abs() < 0.01,
};
if bonus_applies {
final_score += condition.bonus_points;
}
}
final_score.clamp(0.0, 110.0)
}
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String> {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 5000.0,
reverse_scoring: false,
};
linear_scorer.validate_metric_value(value, rules)
}
}
impl EnhancedMetricScorer for WordCountScorer {
fn score_with_context(
&self,
value: MetricValue,
config: &ProfileMetricConfig,
context: &ScoringContext,
) -> f32 {
let word_count = value.as_f32();
let mut base_score = self.score_metric(value.clone(), config);
match context.profile_name.as_str() {
"content_article" => {
if word_count > 2000.0 {
base_score += 5.0;
}
}
"news" => {
if word_count > 1200.0 {
base_score *= 0.9; }
}
"product" => {
let word_count = value.as_f32();
if word_count > 500.0 {
base_score *= 0.85; }
}
_ => {}
}
base_score.clamp(0.0, 100.0)
}
}
pub struct ReadabilityScorer;
impl MetricScorer for ReadabilityScorer {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32 {
let fk_score = match value {
MetricValue::OptionF32(Some(score)) => score,
MetricValue::F32(score) => score,
_ => return 50.0, };
let thresholds = &config.thresholds;
let base_score = if fk_score <= thresholds.excellent {
100.0
} else if fk_score <= thresholds.good {
let range = thresholds.good - thresholds.excellent;
let position = (fk_score - thresholds.excellent) / range;
100.0 - (position * 20.0)
} else if fk_score <= thresholds.fair {
let range = thresholds.fair - thresholds.good;
let position = (fk_score - thresholds.good) / range;
80.0 - (position * 20.0)
} else if fk_score <= thresholds.poor {
let range = thresholds.poor - thresholds.fair;
let position = (fk_score - thresholds.fair) / range;
60.0 - (position * 30.0)
} else {
30.0 - ((fk_score - thresholds.poor) * 2.0).min(25.0)
};
base_score.clamp(0.0, 100.0)
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
let fk_score = value.as_f32();
let mut final_score = base_score;
for condition in conditions {
let bonus_applies = match &condition.condition_type {
BonusConditionType::ValueBetween { min, max } => {
fk_score >= *min && fk_score <= *max
}
BonusConditionType::ValueBelow { threshold } => fk_score < *threshold,
_ => false,
};
if bonus_applies {
final_score += condition.bonus_points;
}
}
final_score.clamp(0.0, 100.0)
}
fn validate_metric_value(
&self,
value: &MetricValue,
_rules: &[ValidationRule],
) -> Result<(), String> {
match value {
MetricValue::OptionF32(Some(score)) | MetricValue::F32(score) => {
if score.is_nan() || score.is_infinite() {
return Err("Readability score must be a valid number".to_string());
}
if *score < 0.0 || *score > 30.0 {
return Err("Readability score should be between 0 and 30".to_string());
}
}
MetricValue::OptionF32(None) => {} _ => return Err("Invalid readability value type".to_string()),
}
Ok(())
}
}
impl EnhancedMetricScorer for ReadabilityScorer {
fn score_with_context(
&self,
value: MetricValue,
config: &ProfileMetricConfig,
context: &ScoringContext,
) -> f32 {
let fk_score = value.as_f32();
let mut base_score = self.score_metric(value.clone(), config);
match context.profile_name.as_str() {
"content_article" => {
if fk_score > 15.0 {
base_score += 5.0; }
}
"news" => {
if fk_score > 12.0 {
base_score -= 10.0; }
}
"general" => {
if fk_score > 14.0 {
base_score -= 5.0;
}
}
_ => {}
}
base_score.clamp(0.0, 100.0)
}
}
pub struct ImageCountScorer;
impl MetricScorer for ImageCountScorer {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32 {
let image_count = value.as_f32();
let thresholds = &config.thresholds;
let base_score = if image_count >= thresholds.excellent {
100.0
} else if image_count >= thresholds.good {
let range = (thresholds.excellent - thresholds.good).max(0.001);
let position = ((image_count - thresholds.good) / range).clamp(0.0, 1.0);
80.0 + (position * 20.0)
} else if image_count >= thresholds.fair {
let range = (thresholds.good - thresholds.fair).max(0.001);
let position = ((image_count - thresholds.fair) / range).clamp(0.0, 1.0);
60.0 + (position * 20.0)
} else if image_count >= thresholds.poor {
let range = (thresholds.fair - thresholds.poor).max(0.001);
let position = ((image_count - thresholds.poor) / range).clamp(0.0, 1.0);
30.0 + (position * 30.0)
} else if image_count == 0.0 {
if thresholds.poor == 0.0 {
70.0
} else {
0.0
}
} else {
(image_count / thresholds.poor) * 30.0
};
base_score.clamp(0.0, 100.0)
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 50.0,
reverse_scoring: false,
};
linear_scorer.apply_bonuses(base_score, value, conditions)
}
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String> {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 50.0,
reverse_scoring: false,
};
linear_scorer.validate_metric_value(value, rules)
}
}
impl EnhancedMetricScorer for ImageCountScorer {
fn score_with_context(
&self,
value: MetricValue,
config: &ProfileMetricConfig,
context: &ScoringContext,
) -> f32 {
let image_count = value.as_f32();
let mut base_score = self.score_metric(value.clone(), config);
match context.profile_name.as_str() {
"product" => {
if image_count >= 5.0 {
base_score += 10.0; }
}
"portfolio" => {
if image_count >= 8.0 {
base_score += 8.0;
}
}
"content_article" => {
if image_count > 10.0 {
base_score *= 0.9; }
}
_ => {}
}
base_score.clamp(0.0, 100.0)
}
}
pub fn create_enhanced_scorer(
scoring_function: &ScoringFunction,
metric_name: &str,
) -> Box<dyn EnhancedMetricScorer> {
match metric_name {
"word_count" => Box::new(WordCountScorer),
"readability_fk" => Box::new(ReadabilityScorer),
"images_count" => Box::new(ImageCountScorer),
_ => {
let standard_scorer =
crate::metrics::definitions::create_metric_scorer(scoring_function);
Box::new(StandardScorerWrapper {
scorer: standard_scorer,
})
}
}
}
struct StandardScorerWrapper {
scorer: Box<dyn MetricScorer>,
}
impl MetricScorer for StandardScorerWrapper {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32 {
self.scorer.score_metric(value, config)
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
self.scorer.apply_bonuses(base_score, value, conditions)
}
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String> {
self.scorer.validate_metric_value(value, rules)
}
}
impl EnhancedMetricScorer for StandardScorerWrapper {
fn score_with_context(
&self,
value: MetricValue,
config: &ProfileMetricConfig,
_context: &ScoringContext,
) -> f32 {
self.score_metric(value, config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metrics::definitions::{ProfileMetricConfig, ThresholdSet};
#[test]
fn test_word_count_scorer() {
let scorer = WordCountScorer;
let config = ProfileMetricConfig {
weight: 0.25,
enabled: true,
thresholds: ThresholdSet {
excellent: 1500.0,
good: 800.0,
fair: 400.0,
poor: 100.0,
},
penalty_multiplier: 2.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
};
let score = scorer.score_metric(MetricValue::Usize(1600), &config);
assert!(score >= 95.0);
let score = scorer.score_metric(MetricValue::Usize(50), &config); assert!(score <= 25.0);
let score = scorer.score_metric(MetricValue::Usize(0), &config);
assert_eq!(score, 0.0);
}
#[test]
fn test_readability_scorer() {
let scorer = ReadabilityScorer;
let config = ProfileMetricConfig {
weight: 0.20,
enabled: true,
thresholds: ThresholdSet {
excellent: 8.0,
good: 12.0,
fair: 16.0,
poor: 20.0,
},
penalty_multiplier: 1.5,
bonus_conditions: Vec::new(),
scoring_function_override: None,
};
let score = scorer.score_metric(MetricValue::OptionF32(Some(6.0)), &config);
assert!(score >= 95.0);
let score = scorer.score_metric(MetricValue::OptionF32(Some(25.0)), &config);
assert!(score <= 30.0);
let score = scorer.score_metric(MetricValue::OptionF32(None), &config);
assert_eq!(score, 50.0);
}
}