dot_agent_core/channel/
channel_registry.rs1use std::fs;
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{DotAgentError, Result};
11
12use super::types::{Channel, ChannelType};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ChannelRegistry {
17 pub channels: Vec<Channel>,
19}
20
21impl Default for ChannelRegistry {
22 fn default() -> Self {
23 Self::with_defaults()
24 }
25}
26
27impl ChannelRegistry {
28 const FILENAME: &'static str = "channels.toml";
29
30 pub fn with_defaults() -> Self {
32 Self {
33 channels: vec![Channel::github_global()],
34 }
35 }
36
37 pub fn load(base_dir: &Path) -> Result<Self> {
39 let path = base_dir.join(Self::FILENAME);
40 if !path.exists() {
41 return Ok(Self::with_defaults());
42 }
43
44 let content = fs::read_to_string(&path)?;
45 let mut registry: Self =
46 toml::from_str(&content).map_err(|e| DotAgentError::ConfigParseSimple {
47 message: e.to_string(),
48 })?;
49
50 if !registry
52 .channels
53 .iter()
54 .any(|c| c.builtin && c.name == "github")
55 {
56 registry.channels.insert(0, Channel::github_global());
57 }
58
59 Ok(registry)
60 }
61
62 pub fn save(&self, base_dir: &Path) -> Result<()> {
64 let path = base_dir.join(Self::FILENAME);
65 fs::create_dir_all(base_dir)?;
66 let content =
67 toml::to_string_pretty(self).map_err(|e| DotAgentError::ConfigParseSimple {
68 message: e.to_string(),
69 })?;
70 fs::write(path, content)?;
71 Ok(())
72 }
73
74 pub fn add(&mut self, channel: Channel) -> Result<()> {
76 if self.channels.iter().any(|c| c.name == channel.name) {
77 return Err(DotAgentError::ChannelAlreadyExists { name: channel.name });
78 }
79 self.channels.push(channel);
80 Ok(())
81 }
82
83 pub fn remove(&mut self, name: &str) -> Result<Channel> {
85 let idx = self
86 .channels
87 .iter()
88 .position(|c| c.name == name)
89 .ok_or_else(|| DotAgentError::ChannelNotFound {
90 name: name.to_string(),
91 })?;
92
93 if self.channels[idx].builtin {
94 return Err(DotAgentError::CannotRemoveBuiltinChannel {
95 name: name.to_string(),
96 });
97 }
98
99 Ok(self.channels.remove(idx))
100 }
101
102 pub fn get(&self, name: &str) -> Option<&Channel> {
104 self.channels.iter().find(|c| c.name == name)
105 }
106
107 pub fn get_mut(&mut self, name: &str) -> Option<&mut Channel> {
109 self.channels.iter_mut().find(|c| c.name == name)
110 }
111
112 pub fn list(&self) -> &[Channel] {
114 &self.channels
115 }
116
117 pub fn list_enabled(&self) -> Vec<&Channel> {
119 self.channels.iter().filter(|c| c.enabled).collect()
120 }
121
122 pub fn list_searchable(&self) -> Vec<&Channel> {
124 self.channels
125 .iter()
126 .filter(|c| c.enabled && c.is_searchable())
127 .collect()
128 }
129
130 pub fn list_by_type(&self, channel_type: ChannelType) -> Vec<&Channel> {
132 self.channels
133 .iter()
134 .filter(|c| c.channel_type == channel_type)
135 .collect()
136 }
137
138 pub fn enable(&mut self, name: &str) -> Result<()> {
140 let channel = self
141 .get_mut(name)
142 .ok_or_else(|| DotAgentError::ChannelNotFound {
143 name: name.to_string(),
144 })?;
145 channel.enabled = true;
146 Ok(())
147 }
148
149 pub fn disable(&mut self, name: &str) -> Result<()> {
151 let channel = self
152 .get_mut(name)
153 .ok_or_else(|| DotAgentError::ChannelNotFound {
154 name: name.to_string(),
155 })?;
156 channel.enabled = false;
157 Ok(())
158 }
159
160 pub fn cache_dir(base_dir: &Path, channel_name: &str) -> PathBuf {
162 base_dir.join("cache").join("channels").join(channel_name)
163 }
164
165 pub fn contains(&self, name: &str) -> bool {
167 self.channels.iter().any(|c| c.name == name)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use tempfile::TempDir;
175
176 #[test]
177 fn registry_with_defaults() {
178 let registry = ChannelRegistry::with_defaults();
179 assert_eq!(registry.channels.len(), 1);
180 assert_eq!(registry.channels[0].name, "github");
181 assert!(registry.channels[0].builtin);
182 }
183
184 #[test]
185 fn registry_add_remove() {
186 let mut registry = ChannelRegistry::with_defaults();
187
188 let channel = Channel::from_url("test", "https://github.com/test/awesome");
189 registry.add(channel).unwrap();
190 assert_eq!(registry.channels.len(), 2);
191
192 let channel2 = Channel::from_url("test", "https://github.com/other/awesome");
194 assert!(registry.add(channel2).is_err());
195
196 let removed = registry.remove("test").unwrap();
198 assert_eq!(removed.name, "test");
199 assert_eq!(registry.channels.len(), 1);
200
201 assert!(registry.remove("github").is_err());
203 }
204
205 #[test]
206 fn registry_enable_disable() {
207 let mut registry = ChannelRegistry::with_defaults();
208 registry
209 .add(Channel::from_url("test", "https://example.com"))
210 .unwrap();
211
212 assert!(registry.get("test").unwrap().enabled);
213
214 registry.disable("test").unwrap();
215 assert!(!registry.get("test").unwrap().enabled);
216
217 registry.enable("test").unwrap();
218 assert!(registry.get("test").unwrap().enabled);
219 }
220
221 #[test]
222 fn registry_save_load() {
223 let temp = TempDir::new().unwrap();
224 let base = temp.path();
225
226 let mut registry = ChannelRegistry::with_defaults();
227 registry
228 .add(Channel::from_url("test", "https://github.com/test/awesome"))
229 .unwrap();
230 registry.save(base).unwrap();
231
232 let loaded = ChannelRegistry::load(base).unwrap();
233 assert_eq!(loaded.channels.len(), 2);
234 assert!(loaded.get("github").is_some());
235 assert!(loaded.get("test").is_some());
236 }
237
238 #[test]
239 fn list_searchable() {
240 let mut registry = ChannelRegistry::with_defaults();
241 registry
242 .add(Channel::from_url(
243 "awesome1",
244 "https://github.com/test/awesome-list",
245 ))
246 .unwrap();
247 registry
248 .add(Channel::from_url(
249 "direct1",
250 "https://github.com/test/dotfiles",
251 ))
252 .unwrap();
253
254 let searchable = registry.list_searchable();
255 assert_eq!(searchable.len(), 2);
257 }
258
259 #[test]
260 fn list_by_type() {
261 let mut registry = ChannelRegistry::with_defaults();
262 registry
263 .add(Channel::from_url(
264 "awesome1",
265 "https://github.com/test/awesome-list",
266 ))
267 .unwrap();
268 registry
269 .add(Channel::from_hub("hub1", "official", "test"))
270 .unwrap();
271
272 let github = registry.list_by_type(ChannelType::GitHubGlobal);
273 assert_eq!(github.len(), 1);
274
275 let awesome = registry.list_by_type(ChannelType::AwesomeList);
276 assert_eq!(awesome.len(), 1);
277
278 let hub = registry.list_by_type(ChannelType::Hub);
279 assert_eq!(hub.len(), 1);
280 }
281}