1use anyhow::Result;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::time::{Duration, SystemTime};
14
15use crate::registry::{LocalRegistry, PluginRegistryEntry};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct RemoteRegistryConfig {
20 pub url: String,
22
23 pub token: Option<String>,
25
26 #[serde(default = "RemoteRegistryConfig::default_timeout")]
28 pub timeout_secs: u64,
29
30 #[serde(default = "RemoteRegistryConfig::default_cache_ttl")]
32 pub cache_ttl_secs: u64,
33
34 #[serde(default = "RemoteRegistryConfig::default_verify_ssl")]
36 pub verify_ssl: bool,
37}
38
39impl RemoteRegistryConfig {
40 fn default_timeout() -> u64 {
41 30
42 }
43
44 fn default_cache_ttl() -> u64 {
45 3600 }
47
48 fn default_verify_ssl() -> bool {
49 true
50 }
51
52 pub fn new(url: String) -> Self {
54 Self {
55 url,
56 token: None,
57 timeout_secs: Self::default_timeout(),
58 cache_ttl_secs: Self::default_cache_ttl(),
59 verify_ssl: Self::default_verify_ssl(),
60 }
61 }
62
63 pub fn with_token(mut self, token: String) -> Self {
65 self.token = Some(token);
66 self
67 }
68
69 pub fn with_timeout(mut self, secs: u64) -> Self {
71 self.timeout_secs = secs;
72 self
73 }
74
75 pub fn with_cache_ttl(mut self, secs: u64) -> Self {
77 self.cache_ttl_secs = secs;
78 self
79 }
80}
81
82pub struct RemoteRegistry {
84 config: RemoteRegistryConfig,
85 cache: HashMap<String, CachedEntry>,
86}
87
88#[derive(Clone, Debug)]
90struct CachedEntry {
91 entry: PluginRegistryEntry,
93 cached_at: SystemTime,
95}
96
97impl CachedEntry {
98 fn is_expired(&self, ttl: Duration) -> bool {
100 self.cached_at.elapsed().unwrap_or(ttl) > ttl
101 }
102}
103
104impl RemoteRegistry {
105 pub fn new(config: RemoteRegistryConfig) -> Self {
107 Self {
108 config,
109 cache: HashMap::new(),
110 }
111 }
112
113 pub fn search(&mut self, _query: &str, _limit: usize) -> Result<Vec<PluginRegistryEntry>> {
115 Ok(Vec::new())
127 }
128
129 pub fn get_plugin(&mut self, name: &str, version: Option<&str>) -> Result<PluginRegistryEntry> {
131 let ttl = Duration::from_secs(self.config.cache_ttl_secs);
133 let cache_key = if let Some(v) = version {
134 format!("{}:{}", name, v)
135 } else {
136 name.to_string()
137 };
138
139 if let Some(cached) = self.cache.get(&cache_key) {
140 if !cached.is_expired(ttl) {
141 return Ok(cached.entry.clone());
142 }
143 }
144
145 Err(anyhow::anyhow!(
150 "Plugin {}:{} not found in remote registry",
151 name,
152 version.unwrap_or("latest")
153 ))
154 }
155
156 pub fn get_latest(&mut self, name: &str) -> Result<PluginRegistryEntry> {
158 self.get_plugin(name, None)
159 }
160
161 pub fn clear_cache(&mut self) {
163 self.cache.clear();
164 }
165
166 pub fn cache_size(&self) -> usize {
168 self.cache.len()
169 }
170
171 pub fn prune_cache(&mut self) {
173 let ttl = Duration::from_secs(self.config.cache_ttl_secs);
174 self.cache.retain(|_, entry| !entry.is_expired(ttl));
175 }
176}
177
178pub struct HybridRegistry {
180 local: LocalRegistry,
181 remote: Option<RemoteRegistry>,
182 local_first: bool,
184}
185
186impl HybridRegistry {
187 pub fn local_only(local: LocalRegistry) -> Self {
189 Self {
190 local,
191 remote: None,
192 local_first: true,
193 }
194 }
195
196 pub fn with_remote(local: LocalRegistry, remote: RemoteRegistry) -> Self {
198 Self {
199 local,
200 remote: Some(remote),
201 local_first: true,
202 }
203 }
204
205 pub fn set_local_first(mut self, local_first: bool) -> Self {
207 self.local_first = local_first;
208 self
209 }
210
211 pub fn search(&mut self, query: &str, limit: usize) -> Result<Vec<PluginRegistryEntry>> {
213 if self.local_first {
214 let local_results: Vec<_> = self.local.search(query).into_iter().take(limit).collect();
216
217 if !local_results.is_empty() {
218 return Ok(local_results);
219 }
220
221 if let Some(remote) = &mut self.remote {
223 return remote.search(query, limit);
224 }
225
226 Ok(Vec::new())
227 } else {
228 if let Some(remote) = &mut self.remote {
230 let remote_results = remote.search(query, limit)?;
231 if !remote_results.is_empty() {
232 return Ok(remote_results);
233 }
234 }
235
236 let local_results: Vec<_> = self.local.search(query).into_iter().take(limit).collect();
238 Ok(local_results)
239 }
240 }
241
242 pub fn get(&mut self, name: &str, version: Option<&str>) -> Result<PluginRegistryEntry> {
244 if self.local_first {
245 if let Some(v) = version {
247 if let Some(entry) = self.local.find_by_version(name, v) {
248 return Ok(entry);
249 }
250 } else if let Some(entry) = self.local.find_by_name(name) {
251 return Ok(entry);
252 }
253
254 if let Some(remote) = &mut self.remote {
256 return remote.get_plugin(name, version);
257 }
258
259 Err(anyhow::anyhow!("Plugin {} not found", name))
260 } else {
261 if let Some(remote) = &mut self.remote {
263 if let Ok(entry) = remote.get_plugin(name, version) {
264 return Ok(entry);
265 }
266 }
267
268 if let Some(v) = version {
270 if let Some(entry) = self.local.find_by_version(name, v) {
271 return Ok(entry);
272 }
273 } else if let Some(entry) = self.local.find_by_name(name) {
274 return Ok(entry);
275 }
276
277 Err(anyhow::anyhow!("Plugin {} not found", name))
278 }
279 }
280
281 pub fn register_local(&mut self, entry: PluginRegistryEntry) -> Result<()> {
283 self.local.register(entry)
284 }
285
286 pub fn local_registry(&self) -> &LocalRegistry {
288 &self.local
289 }
290
291 pub fn sync_from_remote(&mut self, plugins: Vec<&str>) -> Result<usize> {
293 let mut synced = 0;
294
295 if let Some(remote) = &mut self.remote {
296 for plugin_name in plugins {
297 if let Ok(entry) = remote.get_plugin(plugin_name, None) {
298 let _ = self.local.register(entry);
299 synced += 1;
300 }
301 }
302 }
303
304 Ok(synced)
305 }
306
307 pub fn prune_remote_cache(&mut self) {
309 if let Some(remote) = &mut self.remote {
310 remote.prune_cache();
311 }
312 }
313
314 pub fn cache_stats(&self) -> CacheStats {
316 let remote_cache_size = self.remote.as_ref().map(|r| r.cache_size()).unwrap_or(0);
317
318 CacheStats {
319 local_plugins: self.local.count(),
320 remote_cache_size,
321 local_first: self.local_first,
322 }
323 }
324}
325
326#[derive(Debug, Clone)]
328pub struct CacheStats {
329 pub local_plugins: usize,
330 pub remote_cache_size: usize,
331 pub local_first: bool,
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_remote_registry_config() {
340 let config = RemoteRegistryConfig::new("https://registry.example.com".to_string());
341 assert_eq!(config.url, "https://registry.example.com");
342 assert_eq!(config.timeout_secs, 30);
343 assert_eq!(config.cache_ttl_secs, 3600);
344 assert!(config.verify_ssl);
345 }
346
347 #[test]
348 fn test_remote_registry_config_builder() {
349 let config = RemoteRegistryConfig::new("https://api.example.com".to_string())
350 .with_token("secret-token".to_string())
351 .with_timeout(60)
352 .with_cache_ttl(7200);
353
354 assert_eq!(config.url, "https://api.example.com");
355 assert_eq!(config.token, Some("secret-token".to_string()));
356 assert_eq!(config.timeout_secs, 60);
357 assert_eq!(config.cache_ttl_secs, 7200);
358 }
359
360 #[test]
361 fn test_hybrid_registry_local_only() {
362 let local = LocalRegistry::new();
363 let hybrid = HybridRegistry::local_only(local);
364
365 assert!(hybrid.remote.is_none());
366 assert!(hybrid.local_first);
367 }
368
369 #[test]
370 fn test_hybrid_registry_with_remote() {
371 let local = LocalRegistry::new();
372 let config = RemoteRegistryConfig::new("https://example.com".to_string());
373 let remote = RemoteRegistry::new(config);
374 let hybrid = HybridRegistry::with_remote(local, remote);
375
376 assert!(hybrid.remote.is_some());
377 assert!(hybrid.local_first);
378 }
379
380 #[test]
381 fn test_hybrid_registry_priority() {
382 let local = LocalRegistry::new();
383 let config = RemoteRegistryConfig::new("https://example.com".to_string());
384 let remote = RemoteRegistry::new(config);
385
386 let hybrid = HybridRegistry::with_remote(local, remote).set_local_first(false);
387 assert!(!hybrid.local_first);
388 }
389
390 #[test]
391 fn test_cached_entry_expiration() {
392 let entry = PluginRegistryEntry {
393 plugin_id: "test".to_string(),
394 name: "test".to_string(),
395 version: "1.0.0".to_string(),
396 abi_version: "2.0".to_string(),
397 description: None,
398 author: None,
399 license: None,
400 keywords: None,
401 dependencies: None,
402 };
403
404 let cached = CachedEntry {
405 entry,
406 cached_at: SystemTime::now(),
407 };
408
409 let generous_ttl = Duration::from_secs(3600);
411 assert!(!cached.is_expired(generous_ttl));
412
413 let short_ttl = Duration::from_secs(0);
415 assert!(cached.is_expired(short_ttl));
416 }
417
418 #[test]
419 fn test_hybrid_registry_search_local() -> Result<()> {
420 let mut local = LocalRegistry::new();
421 let entry = PluginRegistryEntry {
422 plugin_id: "test".to_string(),
423 name: "test".to_string(),
424 version: "1.0.0".to_string(),
425 abi_version: "2.0".to_string(),
426 description: Some("Test plugin".to_string()),
427 author: None,
428 license: None,
429 keywords: None,
430 dependencies: None,
431 };
432 local.register(entry)?;
433
434 let mut hybrid = HybridRegistry::local_only(local);
435 let results = hybrid.search("test", 10)?;
436
437 assert_eq!(results.len(), 1);
438 assert_eq!(results[0].name, "test");
439
440 Ok(())
441 }
442
443 #[test]
444 fn test_cache_stats() {
445 let local = LocalRegistry::new();
446 let config = RemoteRegistryConfig::new("https://example.com".to_string());
447 let remote = RemoteRegistry::new(config);
448 let hybrid = HybridRegistry::with_remote(local, remote);
449
450 let stats = hybrid.cache_stats();
451 assert_eq!(stats.local_plugins, 0);
452 assert_eq!(stats.remote_cache_size, 0);
453 assert!(stats.local_first);
454 }
455}