use crate::metrics::definitions::{
MetricCategory, MetricDefinition, ProfileMetricConfig, METRIC_REGISTRY,
};
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
#[error("Metric not found: {0}")]
MetricNotFound(String),
#[error("Profile configuration not found for metric '{metric}' in profile '{profile}'")]
ProfileConfigNotFound { metric: String, profile: String },
#[error("Invalid metric category: {0}")]
InvalidCategory(String),
#[error("Registry validation failed: {0}")]
ValidationFailed(String),
}
#[derive(Debug, Clone)]
pub struct RegistryStats {
pub total_metrics: usize,
pub metrics_by_category: HashMap<MetricCategory, usize>,
pub configurable_metrics: usize,
pub html_only_metrics: usize,
pub network_required_metrics: usize,
pub profiles_with_overrides: HashMap<String, usize>,
}
pub struct MetricRegistryOps;
impl MetricRegistryOps {
pub fn get_metric_definition(
field_name: &str,
) -> Result<&'static MetricDefinition, RegistryError> {
METRIC_REGISTRY
.get_metric_definition(field_name)
.ok_or_else(|| RegistryError::MetricNotFound(field_name.to_string()))
}
pub fn get_profile_config(
field_name: &str,
profile: &str,
) -> Result<ProfileMetricConfig, RegistryError> {
let metric_def = Self::get_metric_definition(field_name)?;
if let Some(config) = METRIC_REGISTRY.get_profile_config(field_name, profile) {
Ok(config.clone())
} else {
Ok(Self::create_default_config(metric_def))
}
}
pub fn get_profile_metrics(
profile: &str,
) -> HashMap<String, (MetricDefinition, ProfileMetricConfig)> {
let mut profile_metrics = HashMap::new();
for metric_def in METRIC_REGISTRY.all_metrics() {
let config = match Self::get_profile_config(metric_def.field_name, profile) {
Ok(config) => config,
Err(_) => Self::create_default_config(metric_def),
};
if config.enabled {
profile_metrics.insert(
metric_def.field_name.to_string(),
(metric_def.clone(), config),
);
}
}
profile_metrics
}
pub fn get_category_metrics(
category: MetricCategory,
profile: &str,
) -> Vec<(MetricDefinition, ProfileMetricConfig)> {
METRIC_REGISTRY
.list_metrics_by_category(category)
.into_iter()
.filter_map(|metric_def| {
Self::get_profile_config(metric_def.field_name, profile)
.ok()
.filter(|config| config.enabled)
.map(|config| (metric_def.clone(), config))
})
.collect()
}
pub fn validate_profile(
_profile_name: &str,
metric_overrides: &HashMap<String, ProfileMetricConfig>,
) -> Result<(), RegistryError> {
let mut errors = Vec::new();
for metric_name in metric_overrides.keys() {
if METRIC_REGISTRY.get_metric_definition(metric_name).is_none() {
errors.push(format!("Unknown metric: {}", metric_name));
}
}
let mut category_weights: HashMap<MetricCategory, f32> = HashMap::new();
for (metric_name, config) in metric_overrides {
if let Some(metric_def) = METRIC_REGISTRY.get_metric_definition(metric_name) {
let current_weight = category_weights
.entry(metric_def.category.clone())
.or_insert(0.0);
*current_weight += config.weight;
}
}
for (category, total_weight) in category_weights {
if total_weight > 5.0 {
errors.push(format!(
"Category {:?} has excessive total weight: {:.2}",
category, total_weight
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(RegistryError::ValidationFailed(errors.join("; ")))
}
}
pub fn get_registry_stats() -> RegistryStats {
let all_metrics = METRIC_REGISTRY.all_metrics();
let total_metrics = all_metrics.len();
let mut metrics_by_category = HashMap::new();
let mut configurable_metrics = 0;
let mut html_only_metrics = 0;
let mut network_required_metrics = 0;
let mut profiles_with_overrides = HashMap::new();
for metric in &all_metrics {
*metrics_by_category
.entry(metric.category.clone())
.or_insert(0) += 1;
if metric.default_enabled {
configurable_metrics += 1;
}
if metric.html_only_available {
html_only_metrics += 1;
}
if metric.requires_network {
network_required_metrics += 1;
}
for profile_name in metric.profile_configurations.keys() {
*profiles_with_overrides
.entry(profile_name.clone())
.or_insert(0) += 1;
}
}
RegistryStats {
total_metrics,
metrics_by_category,
configurable_metrics,
html_only_metrics,
network_required_metrics,
profiles_with_overrides,
}
}
pub fn list_available_profiles() -> Vec<String> {
let mut profiles = std::collections::HashSet::new();
for metric in METRIC_REGISTRY.all_metrics() {
for profile_name in metric.profile_configurations.keys() {
profiles.insert(profile_name.clone());
}
}
let mut profile_list: Vec<String> = profiles.into_iter().collect();
profile_list.sort();
profile_list
}
pub fn find_missing_profile_configs(profile: &str) -> Vec<String> {
let mut missing = Vec::new();
for metric in METRIC_REGISTRY.all_metrics() {
if metric.default_enabled && !metric.profile_configurations.contains_key(profile) {
missing.push(metric.field_name.to_string());
}
}
missing
}
fn create_default_config(metric_def: &MetricDefinition) -> ProfileMetricConfig {
use crate::metrics::definitions::{ProfileMetricConfig, ThresholdSet};
ProfileMetricConfig {
weight: metric_def.default_weight,
enabled: metric_def.default_enabled,
thresholds: ThresholdSet::default(),
penalty_multiplier: 1.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
}
}
pub fn get_recommended_metrics(content_type: &str) -> Vec<String> {
match content_type.to_lowercase().as_str() {
"article" | "blog" | "content" => vec![
"word_count".to_string(),
"readability_fk".to_string(),
"headings_count".to_string(),
"main_text_ratio".to_string(),
"sentence_count".to_string(),
"title_len".to_string(),
],
"news" => vec![
"word_count".to_string(),
"title_len".to_string(),
"readability_fk".to_string(),
"meta_desc_len".to_string(),
"publish_date".to_string(),
],
"product" | "ecommerce" => vec![
"images_count".to_string(),
"title_len".to_string(),
"meta_desc_len".to_string(),
"image_alt_coverage".to_string(),
"structured_data_score".to_string(),
],
"portfolio" => vec![
"images_count".to_string(),
"image_alt_coverage".to_string(),
"mobile_friendly_score".to_string(),
"design_system_compliance".to_string(),
],
_ => vec![
"word_count".to_string(),
"title_len".to_string(),
"headings_count".to_string(),
"images_count".to_string(),
],
}
}
}
pub mod registry_utils {
use super::*;
pub fn metric_exists(field_name: &str) -> bool {
METRIC_REGISTRY.get_metric_definition(field_name).is_some()
}
pub fn get_metric_category(field_name: &str) -> Option<MetricCategory> {
METRIC_REGISTRY
.get_metric_definition(field_name)
.map(|def| def.category.clone())
}
pub fn requires_network(field_name: &str) -> bool {
METRIC_REGISTRY
.get_metric_definition(field_name)
.map(|def| def.requires_network)
.unwrap_or(true) }
pub fn get_default_weight(field_name: &str) -> f32 {
METRIC_REGISTRY
.get_metric_definition(field_name)
.map(|def| def.default_weight)
.unwrap_or(1.0)
}
pub fn print_registry_summary() {
let stats = MetricRegistryOps::get_registry_stats();
println!("=== Metric Registry Summary ===");
println!("Total metrics: {}", stats.total_metrics);
println!("Configurable metrics: {}", stats.configurable_metrics);
println!("HTML-only metrics: {}", stats.html_only_metrics);
println!(
"Network-required metrics: {}",
stats.network_required_metrics
);
println!("\nMetrics by category:");
for (category, count) in &stats.metrics_by_category {
println!(" {:?}: {}", category, count);
}
println!("\nProfiles with overrides:");
for (profile, count) in &stats.profiles_with_overrides {
println!(" {}: {} metrics", profile, count);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_registry_access() {
assert!(MetricRegistryOps::get_metric_definition("word_count").is_ok());
assert!(MetricRegistryOps::get_metric_definition("nonexistent_metric").is_err());
}
#[test]
fn test_profile_config_retrieval() {
let config = MetricRegistryOps::get_profile_config("word_count", "content_article");
assert!(config.is_ok());
let config = config.unwrap();
assert!(config.enabled);
assert!(config.weight > 0.0);
}
#[test]
fn test_category_metrics() {
let content_metrics =
MetricRegistryOps::get_category_metrics(MetricCategory::Content, "content_article");
assert!(!content_metrics.is_empty());
for (metric_def, _) in &content_metrics {
assert_eq!(metric_def.category, MetricCategory::Content);
}
}
#[test]
fn test_registry_stats() {
let stats = MetricRegistryOps::get_registry_stats();
assert!(stats.total_metrics > 0);
assert!(stats
.metrics_by_category
.contains_key(&MetricCategory::Content));
assert!(stats.configurable_metrics > 0);
}
#[test]
fn test_available_profiles() {
let profiles = MetricRegistryOps::list_available_profiles();
println!("Available profiles: {:?}", profiles);
assert!(profiles.contains(&"content_article".to_string()));
assert!(profiles.contains(&"news".to_string()));
assert!(profiles.contains(&"product".to_string()));
}
}