use crate::config::enhanced_models::*;
use crate::config::profile_validator::ProfileValidator;
use crate::metrics::ThresholdSet;
use chrono::Utc;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum BuilderError {
UnknownMetric(String),
InvalidWeight(String),
InvalidThreshold(String),
ValidationFailed(String),
MissingRequiredField(String),
}
impl std::fmt::Display for BuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuilderError::UnknownMetric(msg) => write!(f, "Unknown metric: {}", msg),
BuilderError::InvalidWeight(msg) => write!(f, "Invalid weight: {}", msg),
BuilderError::InvalidThreshold(msg) => write!(f, "Invalid threshold: {}", msg),
BuilderError::ValidationFailed(msg) => write!(f, "Validation failed: {}", msg),
BuilderError::MissingRequiredField(msg) => write!(f, "Missing required field: {}", msg),
}
}
}
impl std::error::Error for BuilderError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentType {
LongFormArticle,
NewsArticle,
ProductPage,
LandingPage,
Portfolio,
Documentation,
AboutPage,
}
pub struct ProfileBuilder {
profile: EnhancedScoringProfile,
validator: ProfileValidator,
}
impl ProfileBuilder {
pub fn new(profile_name: &str) -> Self {
let mut profile = EnhancedScoringProfile::default_with_name(profile_name);
profile.category_weights.insert("content".to_string(), 0.40);
profile.category_weights.insert("seo".to_string(), 0.25);
profile
.category_weights
.insert("technical".to_string(), 0.15);
profile
.category_weights
.insert("structure".to_string(), 0.15);
profile
.category_weights
.insert("accessibility".to_string(), 0.05);
Self {
profile,
validator: ProfileValidator::new(),
}
}
pub fn for_content_type(mut self, content_type: ContentType) -> Self {
match content_type {
ContentType::LongFormArticle => {
self.profile
.category_weights
.insert("content".to_string(), 0.80);
self.profile
.category_weights
.insert("structure".to_string(), 0.08);
self.profile
.category_weights
.insert("seo".to_string(), 0.05);
self.profile
.category_weights
.insert("technical".to_string(), 0.04);
self.profile
.category_weights
.insert("accessibility".to_string(), 0.03);
self.profile.content_expectations.word_count = Some(WordCountExpectation {
minimum: 500,
optimal_range: (1000, 3000),
maximum_useful: Some(8000),
penalty_curve: PenaltyCurve::Exponential { base: 2.0 },
});
self.profile.metadata.target_content_types = vec![
"article".to_string(),
"blog_post".to_string(),
"editorial".to_string(),
];
}
ContentType::NewsArticle => {
self.profile
.category_weights
.insert("content".to_string(), 0.40);
self.profile
.category_weights
.insert("structure".to_string(), 0.15);
self.profile
.category_weights
.insert("seo".to_string(), 0.30);
self.profile
.category_weights
.insert("technical".to_string(), 0.10);
self.profile
.category_weights
.insert("accessibility".to_string(), 0.05);
self.profile.content_expectations.word_count = Some(WordCountExpectation {
minimum: 150,
optimal_range: (300, 800),
maximum_useful: Some(1500),
penalty_curve: PenaltyCurve::Linear,
});
self.profile.metadata.target_content_types =
vec!["news".to_string(), "press_release".to_string()];
}
ContentType::ProductPage => {
self.profile
.category_weights
.insert("content".to_string(), 0.25);
self.profile
.category_weights
.insert("structure".to_string(), 0.15);
self.profile
.category_weights
.insert("seo".to_string(), 0.35);
self.profile
.category_weights
.insert("technical".to_string(), 0.15);
self.profile
.category_weights
.insert("accessibility".to_string(), 0.10);
self.profile.content_expectations.word_count = Some(WordCountExpectation {
minimum: 100,
optimal_range: (200, 500),
maximum_useful: Some(1000),
penalty_curve: PenaltyCurve::Linear,
});
self.profile.metadata.target_content_types =
vec!["product".to_string(), "ecommerce".to_string()];
}
ContentType::LandingPage => {
self.profile
.category_weights
.insert("content".to_string(), 0.30);
self.profile
.category_weights
.insert("structure".to_string(), 0.20);
self.profile
.category_weights
.insert("seo".to_string(), 0.30);
self.profile
.category_weights
.insert("technical".to_string(), 0.15);
self.profile
.category_weights
.insert("accessibility".to_string(), 0.05);
self.profile.content_expectations.word_count = Some(WordCountExpectation {
minimum: 200,
optimal_range: (300, 800),
maximum_useful: Some(1500),
penalty_curve: PenaltyCurve::Linear,
});
}
ContentType::Portfolio => {
self.profile
.category_weights
.insert("content".to_string(), 0.20);
self.profile
.category_weights
.insert("structure".to_string(), 0.25);
self.profile
.category_weights
.insert("seo".to_string(), 0.15);
self.profile
.category_weights
.insert("technical".to_string(), 0.30);
self.profile
.category_weights
.insert("accessibility".to_string(), 0.10);
self.profile.content_expectations.word_count = Some(WordCountExpectation {
minimum: 100,
optimal_range: (200, 500),
maximum_useful: Some(1000),
penalty_curve: PenaltyCurve::Linear,
});
}
ContentType::Documentation => {
self.profile
.category_weights
.insert("content".to_string(), 0.50);
self.profile
.category_weights
.insert("structure".to_string(), 0.25);
self.profile
.category_weights
.insert("seo".to_string(), 0.10);
self.profile
.category_weights
.insert("technical".to_string(), 0.10);
self.profile
.category_weights
.insert("accessibility".to_string(), 0.05);
self.profile.content_expectations.word_count = Some(WordCountExpectation {
minimum: 300,
optimal_range: (500, 2000),
maximum_useful: Some(5000),
penalty_curve: PenaltyCurve::Linear,
});
}
ContentType::AboutPage => {
self.profile
.category_weights
.insert("content".to_string(), 0.45);
self.profile
.category_weights
.insert("structure".to_string(), 0.20);
self.profile
.category_weights
.insert("seo".to_string(), 0.20);
self.profile
.category_weights
.insert("technical".to_string(), 0.10);
self.profile
.category_weights
.insert("accessibility".to_string(), 0.05);
self.profile.content_expectations.word_count = Some(WordCountExpectation {
minimum: 200,
optimal_range: (400, 1000),
maximum_useful: Some(2000),
penalty_curve: PenaltyCurve::Linear,
});
}
}
self
}
pub fn with_metric_weight(
mut self,
metric_name: &str,
weight: f32,
) -> Result<Self, BuilderError> {
if !(0.0..=1.0).contains(&weight) {
return Err(BuilderError::InvalidWeight(format!(
"Weight must be between 0.0 and 1.0, got {}",
weight
)));
}
let metric_override = self
.profile
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverride::default);
metric_override.weight = weight;
Ok(self)
}
pub fn with_custom_threshold(
mut self,
metric_name: &str,
thresholds: ThresholdSet,
) -> Result<Self, BuilderError> {
let metric_override = self
.profile
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverride::default);
metric_override.thresholds = Some(thresholds);
Ok(self)
}
pub fn with_content_expectations(mut self, expectations: ContentExpectations) -> Self {
self.profile.content_expectations = expectations;
self
}
pub fn with_description(mut self, description: &str) -> Self {
self.profile.metadata.description = description.to_string();
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.profile.metadata.tags = tags;
self
}
pub fn with_category_weights(mut self, weights: HashMap<String, f32>) -> Self {
self.profile.category_weights = weights;
self
}
pub fn with_quality_bands(mut self, bands: QualityBandConfig) -> Self {
self.profile.quality_bands = bands;
self
}
pub fn with_metric_enabled(mut self, metric_name: &str, enabled: bool) -> Self {
let metric_override = self
.profile
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverride::default);
metric_override.enabled = enabled;
self
}
pub fn build(self) -> Result<EnhancedScoringProfile, BuilderError> {
let mut profile = self.profile;
profile.metadata.created_at = Utc::now().to_rfc3339();
let validation_result = self
.validator
.validate_profile(&profile)
.map_err(|e| BuilderError::ValidationFailed(e.to_string()))?;
if !validation_result.is_valid {
return Err(BuilderError::ValidationFailed(format!(
"Profile validation failed: {:?}",
validation_result.errors
)));
}
self.validator
.normalize_profile(&mut profile)
.map_err(|e| BuilderError::ValidationFailed(e.to_string()))?;
Ok(profile)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_basic() {
let profile = ProfileBuilder::new("test_profile")
.with_description("A test profile")
.build();
if let Err(e) = &profile {
eprintln!("Profile build failed: {:?}", e);
}
assert!(profile.is_ok());
let profile = profile.unwrap();
assert_eq!(profile.metadata.name, "test_profile");
}
#[test]
fn test_builder_with_content_type() {
let profile = ProfileBuilder::new("blog")
.for_content_type(ContentType::LongFormArticle)
.build();
assert!(profile.is_ok());
let profile = profile.unwrap();
assert!(profile.category_weights.get("content").unwrap() > &0.5);
assert!(profile.content_expectations.word_count.is_some());
}
#[test]
fn test_builder_with_custom_weights() {
let profile = ProfileBuilder::new("custom")
.for_content_type(ContentType::NewsArticle)
.with_metric_weight("word_count", 0.30)
.unwrap()
.build();
assert!(profile.is_ok());
let profile = profile.unwrap();
assert!(profile.metric_overrides.contains_key("word_count"));
let word_count_override = profile.metric_overrides.get("word_count").unwrap();
assert_eq!(word_count_override.weight, 0.30);
}
#[test]
fn test_builder_invalid_weight() {
let result = ProfileBuilder::new("test").with_metric_weight("word_count", 1.5);
assert!(result.is_err());
}
#[test]
fn test_builder_all_content_types() {
let content_types = vec![
ContentType::LongFormArticle,
ContentType::NewsArticle,
ContentType::ProductPage,
ContentType::LandingPage,
ContentType::Portfolio,
ContentType::Documentation,
ContentType::AboutPage,
];
for ct in content_types {
let profile = ProfileBuilder::new("test").for_content_type(ct).build();
assert!(
profile.is_ok(),
"Content type {:?} should build successfully",
ct
);
}
}
}