use crate::error::{PluginError, Result, ValidationError};
use crate::plugin::{list_plugins, PluginMetadata};
#[derive(Debug, Clone, Copy)]
pub struct ScoringWeights {
pub throughput: f64,
pub compression_ratio: f64,
}
impl ScoringWeights {
pub fn new(throughput: f64, compression_ratio: f64) -> Result<Self> {
if throughput < 0.0 || compression_ratio < 0.0 {
return Err(
ValidationError::InvalidWeights("Weights cannot be negative".to_string()).into(),
);
}
let sum = throughput + compression_ratio;
if (sum - 1.0).abs() > 1e-6 {
return Err(ValidationError::InvalidWeights(format!(
"Weights must sum to 1.0, got {sum}"
))
.into());
}
Ok(Self {
throughput,
compression_ratio,
})
}
}
impl Default for ScoringWeights {
fn default() -> Self {
Self {
throughput: 0.7,
compression_ratio: 0.3,
}
}
}
pub fn calculate_plugin_score(
plugin: &PluginMetadata,
all_plugins: &[PluginMetadata],
weights: &ScoringWeights,
) -> f64 {
if all_plugins.is_empty() {
return 0.0;
}
if all_plugins.len() == 1 {
return 1.0;
}
let log_throughputs: Vec<f64> = all_plugins.iter().map(|p| p.throughput.ln()).collect();
let plugin_log_throughput = plugin.throughput.ln();
let min_log_throughput = log_throughputs
.iter()
.copied()
.fold(f64::INFINITY, f64::min);
let max_log_throughput = log_throughputs
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
let compression_ratios: Vec<f64> = all_plugins.iter().map(|p| p.compression_ratio).collect();
let min_ratio = compression_ratios
.iter()
.copied()
.fold(f64::INFINITY, f64::min);
let max_ratio = compression_ratios
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
let norm_throughput = if (max_log_throughput - min_log_throughput).abs() < 1e-9 {
1.0 } else {
(plugin_log_throughput - min_log_throughput) / (max_log_throughput - min_log_throughput)
};
let norm_ratio = if (max_ratio - min_ratio).abs() < 1e-9 {
1.0 } else {
(max_ratio - plugin.compression_ratio) / (max_ratio - min_ratio)
};
weights.throughput * norm_throughput + weights.compression_ratio * norm_ratio
}
pub struct PluginSelector {
weights: ScoringWeights,
}
impl PluginSelector {
#[must_use]
pub fn new(weights: ScoringWeights) -> Self {
Self { weights }
}
pub fn select(&self) -> Result<PluginMetadata> {
let plugins = list_plugins();
if plugins.is_empty() {
return Err(PluginError::NotFound(
"No plugins available. Call init_plugins() first.".to_string(),
)
.into());
}
let mut scored_plugins: Vec<(f64, &PluginMetadata)> = plugins
.iter()
.map(|plugin| {
let score = calculate_plugin_score(plugin, &plugins, &self.weights);
(score, plugin)
})
.collect();
scored_plugins.sort_by(|a, b| {
b.0.partial_cmp(&a.0)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.1.name.cmp(b.1.name))
});
Ok(*scored_plugins[0].1)
}
pub fn select_by_name(&self, name: &str) -> Result<PluginMetadata> {
let plugins = list_plugins();
plugins.into_iter().find(|p| p.name == name).ok_or_else(|| {
PluginError::NotFound(format!(
"Plugin '{}' not found. Available plugins: {}",
name,
list_plugins()
.iter()
.map(|p| p.name)
.collect::<Vec<_>>()
.join(", ")
))
.into()
})
}
}
impl Default for PluginSelector {
fn default() -> Self {
Self::new(ScoringWeights::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scoring_weights_validation() {
assert!(ScoringWeights::new(0.7, 0.3).is_ok());
assert!(ScoringWeights::new(0.5, 0.5).is_ok());
assert!(ScoringWeights::new(1.0, 0.0).is_ok());
assert!(ScoringWeights::new(0.6, 0.6).is_err());
assert!(ScoringWeights::new(0.3, 0.3).is_err());
assert!(ScoringWeights::new(-0.1, 1.1).is_err());
}
#[test]
fn test_default_weights() {
let weights = ScoringWeights::default();
assert!((weights.throughput - 0.7).abs() < 1e-6);
assert!((weights.compression_ratio - 0.3).abs() < 1e-6);
}
#[test]
fn test_calculate_score_single_plugin() {
let plugin = PluginMetadata {
name: "test",
version: "1.0.0",
magic_number: [0x43, 0x52, 0x01, 0x00],
throughput: 500.0,
compression_ratio: 0.35,
description: "Test",
};
let plugins = vec![plugin];
let weights = ScoringWeights::default();
let score = calculate_plugin_score(&plugin, &plugins, &weights);
assert!((score - 1.0).abs() < 1e-6);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_calculate_score_multiple_plugins() {
let fast = PluginMetadata {
name: "fast",
version: "1.0.0",
magic_number: [0x43, 0x52, 0x01, 0x10],
throughput: 1000.0,
compression_ratio: 0.8,
description: "Fast but poor compression",
};
let slow = PluginMetadata {
name: "slow",
version: "1.0.0",
magic_number: [0x43, 0x52, 0x01, 0x11],
throughput: 100.0,
compression_ratio: 0.3,
description: "Slow but good compression",
};
let plugins = vec![fast, slow];
let weights = ScoringWeights::default();
let fast_score = calculate_plugin_score(&fast, &plugins, &weights);
let slow_score = calculate_plugin_score(&slow, &plugins, &weights);
assert!(fast_score > slow_score);
let balanced = ScoringWeights::new(0.5, 0.5).unwrap();
let fast_balanced = calculate_plugin_score(&fast, &plugins, &balanced);
let slow_balanced = calculate_plugin_score(&slow, &plugins, &balanced);
assert!((fast_balanced - fast_score).abs() > 1e-6);
assert!((slow_balanced - slow_score).abs() > 1e-6);
}
}