dot_agent_core/channel/
hub_registry.rs1use std::fs;
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{DotAgentError, Result};
11
12use super::types::Hub;
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct HubRegistry {
17 pub hubs: Vec<Hub>,
19}
20
21impl HubRegistry {
22 const FILENAME: &'static str = "hubs.toml";
23
24 pub fn load(base_dir: &Path) -> Result<Self> {
26 let path = base_dir.join(Self::FILENAME);
27 if !path.exists() {
28 return Ok(Self::with_official());
30 }
31
32 let content = fs::read_to_string(&path)?;
33 let mut registry: Self =
34 toml::from_str(&content).map_err(|e| DotAgentError::ConfigParseSimple {
35 message: e.to_string(),
36 })?;
37
38 if !registry.hubs.iter().any(|h| h.is_default) {
40 registry.hubs.insert(0, Hub::official());
41 }
42
43 Ok(registry)
44 }
45
46 pub fn with_official() -> Self {
48 Self {
49 hubs: vec![Hub::official()],
50 }
51 }
52
53 pub fn save(&self, base_dir: &Path) -> Result<()> {
55 let path = base_dir.join(Self::FILENAME);
56 fs::create_dir_all(base_dir)?;
57 let content =
58 toml::to_string_pretty(self).map_err(|e| DotAgentError::ConfigParseSimple {
59 message: e.to_string(),
60 })?;
61 fs::write(path, content)?;
62 Ok(())
63 }
64
65 pub fn add(&mut self, hub: Hub) -> Result<()> {
67 if self.hubs.iter().any(|h| h.name == hub.name) {
68 return Err(DotAgentError::HubAlreadyExists { name: hub.name });
69 }
70 self.hubs.push(hub);
71 Ok(())
72 }
73
74 pub fn remove(&mut self, name: &str) -> Result<Hub> {
76 let idx = self
77 .hubs
78 .iter()
79 .position(|h| h.name == name)
80 .ok_or_else(|| DotAgentError::HubNotFound {
81 name: name.to_string(),
82 })?;
83
84 if self.hubs[idx].is_default {
85 return Err(DotAgentError::CannotRemoveDefaultHub);
86 }
87
88 Ok(self.hubs.remove(idx))
89 }
90
91 pub fn get(&self, name: &str) -> Option<&Hub> {
93 self.hubs.iter().find(|h| h.name == name)
94 }
95
96 pub fn default_hub(&self) -> Option<&Hub> {
98 self.hubs.iter().find(|h| h.is_default)
99 }
100
101 pub fn list(&self) -> &[Hub] {
103 &self.hubs
104 }
105
106 pub fn cache_dir(base_dir: &Path, hub_name: &str) -> PathBuf {
108 base_dir.join("cache").join("hubs").join(hub_name)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use tempfile::TempDir;
116
117 #[test]
118 fn registry_with_official() {
119 let registry = HubRegistry::with_official();
120 assert_eq!(registry.hubs.len(), 1);
121 assert!(registry.hubs[0].is_default);
122 assert_eq!(registry.hubs[0].name, "official");
123 }
124
125 #[test]
126 fn registry_add_remove() {
127 let mut registry = HubRegistry::with_official();
128
129 let hub = Hub::new("company", "https://github.com/company/hub");
130 registry.add(hub).unwrap();
131 assert_eq!(registry.hubs.len(), 2);
132
133 let hub2 = Hub::new("company", "https://github.com/other/hub");
135 assert!(registry.add(hub2).is_err());
136
137 let removed = registry.remove("company").unwrap();
139 assert_eq!(removed.name, "company");
140 assert_eq!(registry.hubs.len(), 1);
141
142 assert!(registry.remove("official").is_err());
144 }
145
146 #[test]
147 fn registry_save_load() {
148 let temp = TempDir::new().unwrap();
149 let base = temp.path();
150
151 let mut registry = HubRegistry::with_official();
152 registry
153 .add(Hub::new("test", "https://github.com/test/hub"))
154 .unwrap();
155 registry.save(base).unwrap();
156
157 let loaded = HubRegistry::load(base).unwrap();
158 assert_eq!(loaded.hubs.len(), 2);
159 assert!(loaded.get("official").is_some());
160 assert!(loaded.get("test").is_some());
161 }
162}