use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OfficialPackage {
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub repo: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub arch: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub version: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct OfficialIndex {
pub pkgs: Vec<OfficialPackage>,
#[serde(skip)]
pub name_to_idx: HashMap<String, usize>,
}
impl OfficialIndex {
pub fn rebuild_name_index(&mut self) {
self.name_to_idx.clear();
self.name_to_idx.reserve(self.pkgs.len());
for (i, pkg) in self.pkgs.iter().enumerate() {
self.name_to_idx.insert(pkg.name.to_lowercase(), i);
}
}
#[must_use]
pub fn find_package_by_name(&self, name: &str) -> Option<&OfficialPackage> {
let name_lower = name.to_lowercase();
if let Some(&idx) = self.name_to_idx.get(&name_lower) {
return self.pkgs.get(idx);
}
self.pkgs.iter().find(|p| p.name.eq_ignore_ascii_case(name))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IndexQueryResult {
pub package: OfficialPackage,
pub fuzzy_score: Option<i64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum InstalledPackagesMode {
LeafOnly,
AllExplicit,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MirrorInfo {
pub url: String,
pub active: bool,
pub protocols: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MirrorDiscoveryLimits {
pub max_response_bytes: usize,
pub max_mirrors: usize,
}
impl Default for MirrorDiscoveryLimits {
fn default() -> Self {
Self {
max_response_bytes: 512 * 1024,
max_mirrors: 128,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rebuild_name_index_populates_hashmap() {
let mut index = OfficialIndex {
pkgs: vec![
OfficialPackage {
name: "PackageA".to_string(),
repo: "core".to_string(),
arch: "x86_64".to_string(),
version: "1.0".to_string(),
description: "Desc A".to_string(),
},
OfficialPackage {
name: "PackageB".to_string(),
repo: "extra".to_string(),
arch: "any".to_string(),
version: "2.0".to_string(),
description: "Desc B".to_string(),
},
],
name_to_idx: HashMap::new(),
};
index.rebuild_name_index();
assert_eq!(index.name_to_idx.len(), 2);
assert_eq!(index.name_to_idx.get("packagea"), Some(&0));
assert_eq!(index.name_to_idx.get("packageb"), Some(&1));
assert_eq!(index.name_to_idx.get("PackageA"), None);
}
#[test]
fn find_package_by_name_uses_hashmap() {
let mut index = OfficialIndex {
pkgs: vec![
OfficialPackage {
name: "ripgrep".to_string(),
repo: "extra".to_string(),
arch: "x86_64".to_string(),
version: "14.0.0".to_string(),
description: "Fast grep".to_string(),
},
OfficialPackage {
name: "vim".to_string(),
repo: "extra".to_string(),
arch: "x86_64".to_string(),
version: "9.0".to_string(),
description: "Text editor".to_string(),
},
],
name_to_idx: HashMap::new(),
};
index.rebuild_name_index();
let result = index.find_package_by_name("ripgrep");
assert!(result.is_some());
assert_eq!(result.map(|p| p.name.as_str()), Some("ripgrep"));
let result_upper = index.find_package_by_name("RIPGREP");
assert!(result_upper.is_some());
assert_eq!(result_upper.map(|p| p.name.as_str()), Some("ripgrep"));
let not_found = index.find_package_by_name("nonexistent");
assert!(not_found.is_none());
}
#[test]
fn find_package_by_name_fallback_to_linear_scan() {
let index = OfficialIndex {
pkgs: vec![OfficialPackage {
name: "test-package".to_string(),
repo: "core".to_string(),
arch: "x86_64".to_string(),
version: "1.0".to_string(),
description: "Test".to_string(),
}],
name_to_idx: HashMap::new(),
};
let result = index.find_package_by_name("test-package");
assert!(result.is_some());
assert_eq!(result.map(|p| p.name.as_str()), Some("test-package"));
let result_upper = index.find_package_by_name("TEST-PACKAGE");
assert!(result_upper.is_some());
}
#[test]
fn serialization_deserialization() {
let mut index = OfficialIndex {
pkgs: vec![
OfficialPackage {
name: "package1".to_string(),
repo: "core".to_string(),
arch: "x86_64".to_string(),
version: "1.0".to_string(),
description: "Package 1".to_string(),
},
OfficialPackage {
name: "package2".to_string(),
repo: "extra".to_string(),
arch: "any".to_string(),
version: "2.0".to_string(),
description: "Package 2".to_string(),
},
],
name_to_idx: HashMap::new(),
};
index.rebuild_name_index();
let json = serde_json::to_string(&index).expect("Serialization should succeed");
assert!(!json.contains("name_to_idx"));
let mut deserialized: OfficialIndex =
serde_json::from_str(&json).expect("Deserialization should succeed");
assert_eq!(deserialized.pkgs.len(), 2);
assert!(deserialized.name_to_idx.is_empty());
deserialized.rebuild_name_index();
assert_eq!(deserialized.name_to_idx.len(), 2);
assert_eq!(deserialized.name_to_idx.get("package1"), Some(&0));
assert_eq!(deserialized.name_to_idx.get("package2"), Some(&1));
let found = deserialized.find_package_by_name("package1");
assert!(found.is_some());
assert_eq!(found.map(|p| p.name.as_str()), Some("package1"));
}
#[test]
fn index_query_result_creation() {
let package = OfficialPackage {
name: "test".to_string(),
repo: "core".to_string(),
arch: "x86_64".to_string(),
version: "1.0".to_string(),
description: "Test package".to_string(),
};
let result_with_score = IndexQueryResult {
package: package.clone(),
fuzzy_score: Some(100),
};
assert_eq!(result_with_score.fuzzy_score, Some(100));
let result_without_score = IndexQueryResult {
package,
fuzzy_score: None,
};
assert_eq!(result_without_score.fuzzy_score, None);
let json = serde_json::to_string(&result_with_score).expect("Should serialize");
let deserialized: IndexQueryResult =
serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(deserialized.fuzzy_score, Some(100));
assert_eq!(deserialized.package.name, "test");
}
}