use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{DotAgentError, Result};
use super::types::Hub;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HubRegistry {
pub hubs: Vec<Hub>,
}
impl HubRegistry {
const FILENAME: &'static str = "hubs.toml";
pub fn load(base_dir: &Path) -> Result<Self> {
let path = base_dir.join(Self::FILENAME);
if !path.exists() {
return Ok(Self::with_official());
}
let content = fs::read_to_string(&path)?;
let mut registry: Self =
toml::from_str(&content).map_err(|e| DotAgentError::ConfigParseSimple {
message: e.to_string(),
})?;
if !registry.hubs.iter().any(|h| h.is_default) {
registry.hubs.insert(0, Hub::official());
}
Ok(registry)
}
pub fn with_official() -> Self {
Self {
hubs: vec![Hub::official()],
}
}
pub fn save(&self, base_dir: &Path) -> Result<()> {
let path = base_dir.join(Self::FILENAME);
fs::create_dir_all(base_dir)?;
let content =
toml::to_string_pretty(self).map_err(|e| DotAgentError::ConfigParseSimple {
message: e.to_string(),
})?;
fs::write(path, content)?;
Ok(())
}
pub fn add(&mut self, hub: Hub) -> Result<()> {
if self.hubs.iter().any(|h| h.name == hub.name) {
return Err(DotAgentError::HubAlreadyExists { name: hub.name });
}
self.hubs.push(hub);
Ok(())
}
pub fn remove(&mut self, name: &str) -> Result<Hub> {
let idx = self
.hubs
.iter()
.position(|h| h.name == name)
.ok_or_else(|| DotAgentError::HubNotFound {
name: name.to_string(),
})?;
if self.hubs[idx].is_default {
return Err(DotAgentError::CannotRemoveDefaultHub);
}
Ok(self.hubs.remove(idx))
}
pub fn get(&self, name: &str) -> Option<&Hub> {
self.hubs.iter().find(|h| h.name == name)
}
pub fn default_hub(&self) -> Option<&Hub> {
self.hubs.iter().find(|h| h.is_default)
}
pub fn list(&self) -> &[Hub] {
&self.hubs
}
pub fn cache_dir(base_dir: &Path, hub_name: &str) -> PathBuf {
base_dir.join("cache").join("hubs").join(hub_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn registry_with_official() {
let registry = HubRegistry::with_official();
assert_eq!(registry.hubs.len(), 1);
assert!(registry.hubs[0].is_default);
assert_eq!(registry.hubs[0].name, "official");
}
#[test]
fn registry_add_remove() {
let mut registry = HubRegistry::with_official();
let hub = Hub::new("company", "https://github.com/company/hub");
registry.add(hub).unwrap();
assert_eq!(registry.hubs.len(), 2);
let hub2 = Hub::new("company", "https://github.com/other/hub");
assert!(registry.add(hub2).is_err());
let removed = registry.remove("company").unwrap();
assert_eq!(removed.name, "company");
assert_eq!(registry.hubs.len(), 1);
assert!(registry.remove("official").is_err());
}
#[test]
fn registry_save_load() {
let temp = TempDir::new().unwrap();
let base = temp.path();
let mut registry = HubRegistry::with_official();
registry
.add(Hub::new("test", "https://github.com/test/hub"))
.unwrap();
registry.save(base).unwrap();
let loaded = HubRegistry::load(base).unwrap();
assert_eq!(loaded.hubs.len(), 2);
assert!(loaded.get("official").is_some());
assert!(loaded.get("test").is_some());
}
}