use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::time::Duration;
use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Mirror {
pub url: Url,
pub country: Option<String>,
pub speed: Option<f64>,
pub latency: Option<Duration>,
pub score: Option<f64>,
#[serde(with = "chrono::serde::ts_seconds_option")]
pub last_tested: Option<chrono::DateTime<chrono::Utc>>,
pub is_static: bool,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub mirror: Mirror,
pub success: bool,
pub speed: Option<f64>,
pub latency: Option<Duration>,
pub score: Option<f64>,
pub error: Option<String>,
#[serde(with = "chrono::serde::ts_seconds")]
pub tested_at: chrono::DateTime<chrono::Utc>,
}
impl Mirror {
pub fn new(url: Url) -> Self {
Self {
url,
country: None,
speed: None,
latency: None,
score: None,
last_tested: None,
is_static: false,
metadata: HashMap::new(),
}
}
pub fn new_static(url: Url) -> Self {
Self {
url,
country: None,
speed: None,
latency: None,
score: None,
last_tested: None,
is_static: true,
metadata: HashMap::new(),
}
}
pub fn calculate_score(&mut self, speed_weight: f64, latency_weight: f64) {
if let (Some(speed), Some(latency)) = (self.speed, self.latency) {
let speed_score = (speed / 100.0).min(1.0);
let latency_ms = latency.as_millis() as f64;
let latency_score = (1.0 - (latency_ms / 1000.0)).max(0.0);
self.score = Some(speed_score * speed_weight + latency_score * latency_weight);
}
}
pub fn update_from_test(&mut self, speed: f64, latency: Duration, speed_weight: f64, latency_weight: f64) {
self.speed = Some(speed);
self.latency = Some(latency);
self.last_tested = Some(chrono::Utc::now());
self.calculate_score(speed_weight, latency_weight);
}
pub fn needs_retest(&self, threshold_secs: u64) -> bool {
match self.last_tested {
None => true,
Some(last_test) => {
let now = chrono::Utc::now();
let elapsed = now.signed_duration_since(last_test);
elapsed.num_seconds() as u64 > threshold_secs
}
}
}
pub fn hostname(&self) -> String {
self.url
.host_str()
.unwrap_or("unknown")
.to_string()
}
pub fn domain(&self) -> String {
let hostname = self.hostname();
let parts: Vec<&str> = hostname.split('.').collect();
if parts.len() >= 2 {
format!("{}.{}", parts[parts.len() - 2], parts[parts.len() - 1])
} else {
hostname
}
}
pub fn format_speed(&self) -> String {
match self.speed {
Some(speed) => {
if speed < 1.0 {
format!("{:.2} KB/s", speed * 1024.0)
} else {
format!("{:.2} MB/s", speed)
}
}
None => "N/A".to_string(),
}
}
pub fn format_latency(&self) -> String {
match self.latency {
Some(latency) => format!("{} ms", latency.as_millis()),
None => "N/A".to_string(),
}
}
pub fn format_score(&self) -> String {
match self.score {
Some(score) => format!("{:.1}%", score * 100.0),
None => "N/A".to_string(),
}
}
pub fn url_string(&self) -> String {
self.url.to_string()
}
}
impl TestResult {
pub fn success(
mirror: Mirror,
speed: f64,
latency: Duration,
score: f64,
) -> Self {
Self {
mirror,
success: true,
speed: Some(speed),
latency: Some(latency),
score: Some(score),
error: None,
tested_at: chrono::Utc::now(),
}
}
pub fn failure(mirror: Mirror, error: String) -> Self {
Self {
mirror,
success: false,
speed: None,
latency: None,
score: None,
error: Some(error),
tested_at: chrono::Utc::now(),
}
}
pub fn into_mirror(self) -> Mirror {
self.mirror
}
}
impl PartialEq for Mirror {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
}
}
impl Eq for Mirror {}
impl PartialOrd for Mirror {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Mirror {
fn cmp(&self, other: &Self) -> Ordering {
match (self.score, other.score) {
(Some(a), Some(b)) => {
b.partial_cmp(&a).unwrap_or(Ordering::Equal)
}
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => {
self.url.as_str().cmp(other.url.as_str())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mirror_creation() {
let url = Url::parse("https://mirror.example.com/repo").unwrap();
let mirror = Mirror::new(url.clone());
assert_eq!(mirror.url, url);
assert_eq!(mirror.is_static, false);
assert!(mirror.score.is_none());
}
#[test]
fn test_static_mirror_creation() {
let url = Url::parse("https://mirror.example.com/repo").unwrap();
let mirror = Mirror::new_static(url);
assert_eq!(mirror.is_static, true);
}
#[test]
fn test_score_calculation() {
let url = Url::parse("https://mirror.example.com/repo").unwrap();
let mut mirror = Mirror::new(url);
mirror.speed = Some(50.0);
mirror.latency = Some(Duration::from_millis(100));
mirror.calculate_score(0.7, 0.3);
assert!(mirror.score.is_some());
let score = mirror.score.unwrap();
assert!((score - 0.62).abs() < 0.01);
}
#[test]
fn test_mirror_ordering() {
let url1 = Url::parse("https://mirror1.example.com/repo").unwrap();
let url2 = Url::parse("https://mirror2.example.com/repo").unwrap();
let mut mirror1 = Mirror::new(url1);
let mut mirror2 = Mirror::new(url2);
mirror1.score = Some(0.8);
mirror2.score = Some(0.6);
assert!(mirror1 < mirror2);
}
#[test]
fn test_hostname_extraction() {
let url = Url::parse("https://mirrors.kernel.org/debian/").unwrap();
let mirror = Mirror::new(url);
assert_eq!(mirror.hostname(), "mirrors.kernel.org");
assert_eq!(mirror.domain(), "kernel.org");
}
#[test]
fn test_needs_retest() {
let url = Url::parse("https://mirror.example.com/repo").unwrap();
let mut mirror = Mirror::new(url);
assert!(mirror.needs_retest(3600));
mirror.last_tested = Some(chrono::Utc::now());
assert!(!mirror.needs_retest(3600));
}
}