1use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::Path;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PluginRegistryEntry {
17 pub plugin_id: String,
18 pub name: String,
19 pub version: String,
20 pub abi_version: String,
21 pub description: Option<String>,
22 pub author: Option<String>,
23 pub license: Option<String>,
24 pub keywords: Option<Vec<String>>,
25 pub dependencies: Option<Vec<PluginDependency>>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct PluginDependency {
31 pub name: String,
32 pub version_requirement: String,
33}
34
35pub struct LocalRegistry {
37 plugins: HashMap<String, PluginRegistryEntry>,
38}
39
40impl LocalRegistry {
41 pub fn new() -> Self {
43 Self {
44 plugins: HashMap::new(),
45 }
46 }
47
48 pub fn register(&mut self, entry: PluginRegistryEntry) -> Result<()> {
50 let key = format!("{}:{}", entry.name, entry.version);
51 self.plugins.insert(key, entry);
52 Ok(())
53 }
54
55 pub fn find_by_name(&self, name: &str) -> Option<PluginRegistryEntry> {
57 self.plugins.values().find(|p| p.name == name).cloned()
58 }
59
60 pub fn find_by_version(&self, name: &str, version: &str) -> Option<PluginRegistryEntry> {
62 let key = format!("{}:{}", name, version);
63 self.plugins.get(&key).cloned()
64 }
65
66 pub fn list_all(&self) -> Vec<PluginRegistryEntry> {
68 self.plugins.values().cloned().collect()
69 }
70
71 pub fn search(&self, query: &str) -> Vec<PluginRegistryEntry> {
73 let q = query.to_lowercase();
74 self.plugins
75 .values()
76 .filter(|p| {
77 p.name.to_lowercase().contains(&q)
78 || p.description
79 .as_ref()
80 .map(|d| d.to_lowercase().contains(&q))
81 .unwrap_or(false)
82 || p.keywords
83 .as_ref()
84 .map(|k| k.iter().any(|kw| kw.to_lowercase().contains(&q)))
85 .unwrap_or(false)
86 })
87 .cloned()
88 .collect()
89 }
90
91 pub fn get_latest(&self, name: &str) -> Option<PluginRegistryEntry> {
93 self.plugins
94 .values()
95 .filter(|p| p.name == name)
96 .max_by(|a, b| {
97 parse_version(&a.version).cmp(&parse_version(&b.version))
99 })
100 .cloned()
101 }
102
103 pub fn remove(&mut self, name: &str, version: &str) -> Result<()> {
105 let key = format!("{}:{}", name, version);
106 self.plugins.remove(&key);
107 Ok(())
108 }
109
110 pub fn exists(&self, name: &str, version: &str) -> bool {
112 let key = format!("{}:{}", name, version);
113 self.plugins.contains_key(&key)
114 }
115
116 pub fn count(&self) -> usize {
118 self.plugins.len()
119 }
120}
121
122impl Default for LocalRegistry {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128fn parse_version(version: &str) -> (u32, u32, u32) {
131 let parts: Vec<&str> = version.split('.').collect();
132 let major = parts
133 .first()
134 .and_then(|p| p.parse::<u32>().ok())
135 .unwrap_or(0);
136 let minor = parts
137 .get(1)
138 .and_then(|p| p.parse::<u32>().ok())
139 .unwrap_or(0);
140 let patch = parts
141 .get(2)
142 .and_then(|p| p.parse::<u32>().ok())
143 .unwrap_or(0);
144 (major, minor, patch)
145}
146
147#[derive(Debug, Clone)]
149pub struct VersionRequirement {
150 pub requirement: String,
151}
152
153impl VersionRequirement {
154 pub fn new(requirement: String) -> Self {
156 Self { requirement }
157 }
158
159 pub fn matches(&self, version: &str) -> bool {
161 let req = self.requirement.trim();
162
163 if req.ends_with('x') || req.ends_with('X') {
165 let req_parts: Vec<&str> = req.split('.').collect();
167 let actual_parts: Vec<&str> = version.split('.').collect();
168
169 for (i, req_part) in req_parts.iter().enumerate() {
171 if *req_part == "x" || *req_part == "X" {
172 return true;
174 }
175 if i >= actual_parts.len() {
176 return false;
177 }
178 if actual_parts[i] != *req_part {
179 return false;
180 }
181 }
182 return true;
183 }
184
185 if !req.starts_with(&['>', '<', '=', '!', '~', '^'][..]) {
187 return version == req;
188 }
189
190 let (op, ver) = if let Some(stripped) = req.strip_prefix(">=") {
191 (">=", stripped.trim())
192 } else if let Some(stripped) = req.strip_prefix("<=") {
193 ("<=", stripped.trim())
194 } else if let Some(stripped) = req.strip_prefix("!=") {
195 ("!=", stripped.trim())
196 } else if let Some(stripped) = req.strip_prefix('>') {
197 (">", stripped.trim())
198 } else if let Some(stripped) = req.strip_prefix('<') {
199 ("<", stripped.trim())
200 } else if let Some(stripped) = req.strip_prefix('~') {
201 ("~", stripped.trim())
203 } else if let Some(stripped) = req.strip_prefix('^') {
204 ("^", stripped.trim())
206 } else {
207 ("=", req)
208 };
209
210 let req_ver = parse_version(ver);
211 let act_ver = parse_version(version);
212
213 match op {
214 "=" => req_ver == act_ver,
215 ">" => act_ver > req_ver,
216 ">=" => act_ver >= req_ver,
217 "<" => act_ver < req_ver,
218 "<=" => act_ver <= req_ver,
219 "!=" => req_ver != act_ver,
220 "~" => {
221 let (rmaj, rmin, rpatch) = req_ver;
223 let (amaj, amin, apatch) = act_ver;
224 amaj == rmaj && amin == rmin && apatch >= rpatch
225 }
226 "^" => {
227 let (rmaj, _, _) = req_ver;
229 let (amaj, _, _) = act_ver;
230 amaj == rmaj && act_ver >= req_ver
231 }
232 _ => false,
233 }
234 }
235}
236
237pub struct DependencyResolver {
239 registry: LocalRegistry,
240}
241
242#[derive(Debug, Clone)]
244pub struct DependencyResolution {
245 pub install_order: Vec<String>,
247 pub version_map: HashMap<String, String>,
249 pub unmet_dependencies: Vec<UnmetDependency>,
251}
252
253#[derive(Debug, Clone)]
255pub struct UnmetDependency {
256 pub plugin_name: String,
257 pub required_by: String,
258 pub version_requirement: String,
259}
260
261impl DependencyResolver {
262 pub fn new(registry: LocalRegistry) -> Self {
264 Self { registry }
265 }
266
267 pub fn resolve(&self, plugin_name: &str, plugin_version: &str) -> Result<DependencyResolution> {
269 let mut install_order = Vec::new();
270 let mut version_map = HashMap::new();
271 let mut unmet = Vec::new();
272 let mut visited = std::collections::HashSet::new();
273
274 self.resolve_recursive(
275 plugin_name,
276 plugin_version,
277 &mut install_order,
278 &mut version_map,
279 &mut unmet,
280 &mut visited,
281 );
282
283 Ok(DependencyResolution {
284 install_order,
285 version_map,
286 unmet_dependencies: unmet,
287 })
288 }
289
290 fn resolve_recursive(
291 &self,
292 plugin_name: &str,
293 plugin_version: &str,
294 install_order: &mut Vec<String>,
295 version_map: &mut HashMap<String, String>,
296 unmet: &mut Vec<UnmetDependency>,
297 visited: &mut std::collections::HashSet<String>,
298 ) {
299 let key = format!("{}:{}", plugin_name, plugin_version);
300 if visited.contains(&key) {
301 return; }
303 visited.insert(key);
304
305 if let Some(entry) = self.registry.find_by_version(plugin_name, plugin_version) {
307 if let Some(deps) = &entry.dependencies {
309 for dep in deps {
310 let req = VersionRequirement::new(dep.version_requirement.clone());
312 if let Some(matching) = self.find_matching_version(&dep.name, &req) {
313 self.resolve_recursive(
315 &dep.name,
316 &matching,
317 install_order,
318 version_map,
319 unmet,
320 visited,
321 );
322 } else {
323 unmet.push(UnmetDependency {
325 plugin_name: dep.name.clone(),
326 required_by: plugin_name.to_string(),
327 version_requirement: dep.version_requirement.clone(),
328 });
329 }
330 }
331 }
332
333 if !install_order.contains(&plugin_name.to_string()) {
335 install_order.push(plugin_name.to_string());
336 version_map.insert(plugin_name.to_string(), plugin_version.to_string());
337 }
338 }
339 }
340
341 fn find_matching_version(
342 &self,
343 plugin_name: &str,
344 requirement: &VersionRequirement,
345 ) -> Option<String> {
346 let all = self.registry.list_all();
347 let matching: Vec<_> = all
348 .iter()
349 .filter(|p| p.name == plugin_name && requirement.matches(&p.version))
350 .collect();
351
352 matching
354 .iter()
355 .max_by(|a, b| parse_version(&a.version).cmp(&parse_version(&b.version)))
356 .map(|p| p.version.clone())
357 }
358}
359
360pub struct RegistryPersistence;
362
363impl RegistryPersistence {
364 pub fn save(registry: &LocalRegistry, path: &Path) -> Result<()> {
366 let entries = registry.list_all();
367 let json = serde_json::to_string_pretty(&entries).context("serializing registry")?;
368 std::fs::write(path, json).context("writing registry file")?;
369 Ok(())
370 }
371
372 pub fn load(path: &Path) -> Result<LocalRegistry> {
374 let content = std::fs::read_to_string(path).context("reading registry file")?;
375 let entries: Vec<PluginRegistryEntry> =
376 serde_json::from_str(&content).context("deserializing registry")?;
377
378 let mut registry = LocalRegistry::new();
379 for entry in entries {
380 registry.register(entry)?;
381 }
382 Ok(registry)
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_registry_register() {
392 let mut registry = LocalRegistry::new();
393 let entry = PluginRegistryEntry {
394 plugin_id: "test-plugin".to_string(),
395 name: "test-plugin".to_string(),
396 version: "1.0.0".to_string(),
397 abi_version: "2.0".to_string(),
398 description: Some("A test plugin".to_string()),
399 author: None,
400 license: None,
401 keywords: None,
402 dependencies: None,
403 };
404
405 registry.register(entry).unwrap();
406 assert_eq!(registry.count(), 1);
407 }
408
409 #[test]
410 fn test_registry_find() {
411 let mut registry = LocalRegistry::new();
412 let entry = PluginRegistryEntry {
413 plugin_id: "my-plugin".to_string(),
414 name: "my-plugin".to_string(),
415 version: "1.0.0".to_string(),
416 abi_version: "2.0".to_string(),
417 description: Some("My plugin".to_string()),
418 author: None,
419 license: None,
420 keywords: None,
421 dependencies: None,
422 };
423
424 registry.register(entry).unwrap();
425
426 let found = registry.find_by_name("my-plugin");
427 assert!(found.is_some());
428 assert_eq!(found.unwrap().version, "1.0.0");
429 }
430
431 #[test]
432 fn test_registry_search() {
433 let mut registry = LocalRegistry::new();
434
435 for i in 0..3 {
436 let entry = PluginRegistryEntry {
437 plugin_id: format!("plugin-{}", i),
438 name: format!("plugin-{}", i),
439 version: "1.0.0".to_string(),
440 abi_version: "2.0".to_string(),
441 description: Some(format!("Test plugin number {}", i)),
442 author: None,
443 license: None,
444 keywords: Some(vec!["test".to_string()]),
445 dependencies: None,
446 };
447 registry.register(entry).unwrap();
448 }
449
450 let results = registry.search("test");
451 assert_eq!(results.len(), 3);
452 }
453
454 #[test]
455 fn test_registry_latest_version() {
456 let mut registry = LocalRegistry::new();
457
458 for version in &["0.1.0", "1.0.0", "2.0.0"] {
459 let entry = PluginRegistryEntry {
460 plugin_id: "versioned-plugin".to_string(),
461 name: "versioned-plugin".to_string(),
462 version: version.to_string(),
463 abi_version: "2.0".to_string(),
464 description: None,
465 author: None,
466 license: None,
467 keywords: None,
468 dependencies: None,
469 };
470 registry.register(entry).unwrap();
471 }
472
473 let latest = registry.get_latest("versioned-plugin");
474 assert!(latest.is_some());
475 assert_eq!(latest.unwrap().version, "2.0.0");
476 }
477
478 #[test]
480 fn test_version_requirement_exact() {
481 let req = VersionRequirement::new("1.0.0".to_string());
482 assert!(req.matches("1.0.0"));
483 assert!(!req.matches("1.0.1"));
484 assert!(!req.matches("1.1.0"));
485 }
486
487 #[test]
488 fn test_version_requirement_greater_than() {
489 let req = VersionRequirement::new(">1.0.0".to_string());
490 assert!(!req.matches("1.0.0"));
491 assert!(req.matches("1.0.1"));
492 assert!(req.matches("1.1.0"));
493 assert!(req.matches("2.0.0"));
494 }
495
496 #[test]
497 fn test_version_requirement_greater_than_or_equal() {
498 let req = VersionRequirement::new(">=1.0.0".to_string());
499 assert!(req.matches("1.0.0"));
500 assert!(req.matches("1.0.1"));
501 assert!(req.matches("2.0.0"));
502 assert!(!req.matches("0.9.9"));
503 }
504
505 #[test]
506 fn test_version_requirement_less_than() {
507 let req = VersionRequirement::new("<2.0.0".to_string());
508 assert!(req.matches("1.9.9"));
509 assert!(req.matches("1.0.0"));
510 assert!(!req.matches("2.0.0"));
511 assert!(!req.matches("3.0.0"));
512 }
513
514 #[test]
515 fn test_version_requirement_less_than_or_equal() {
516 let req = VersionRequirement::new("<=2.0.0".to_string());
517 assert!(req.matches("1.0.0"));
518 assert!(req.matches("2.0.0"));
519 assert!(!req.matches("2.0.1"));
520 assert!(!req.matches("3.0.0"));
521 }
522
523 #[test]
524 fn test_version_requirement_not_equal() {
525 let req = VersionRequirement::new("!=1.0.0".to_string());
526 assert!(!req.matches("1.0.0"));
527 assert!(req.matches("1.0.1"));
528 assert!(req.matches("2.0.0"));
529 }
530
531 #[test]
532 fn test_version_requirement_tilde() {
533 let req = VersionRequirement::new("~1.2.3".to_string());
535 assert!(!req.matches("1.2.2"));
536 assert!(req.matches("1.2.3"));
537 assert!(req.matches("1.2.10"));
538 assert!(!req.matches("1.3.0"));
539 }
540
541 #[test]
542 fn test_version_requirement_caret() {
543 let req = VersionRequirement::new("^1.2.3".to_string());
545 assert!(!req.matches("1.2.2"));
546 assert!(req.matches("1.2.3"));
547 assert!(req.matches("1.9.0"));
548 assert!(req.matches("1.100.100"));
549 assert!(!req.matches("2.0.0"));
550 }
551
552 #[test]
553 fn test_version_requirement_wildcard() {
554 let req = VersionRequirement::new("1.2.x".to_string());
555 assert!(req.matches("1.2.0"));
556 assert!(req.matches("1.2.1"));
557 assert!(req.matches("1.2.100"));
558 assert!(!req.matches("1.3.0"));
559 assert!(!req.matches("2.2.0"));
560 }
561
562 #[test]
564 fn test_dependency_resolver_no_dependencies() {
565 let mut registry = LocalRegistry::new();
566 let entry = PluginRegistryEntry {
567 plugin_id: "plugin-a".to_string(),
568 name: "plugin-a".to_string(),
569 version: "1.0.0".to_string(),
570 abi_version: "2.0".to_string(),
571 description: None,
572 author: None,
573 license: None,
574 keywords: None,
575 dependencies: None,
576 };
577 registry.register(entry).unwrap();
578
579 let resolver = DependencyResolver::new(registry);
580 let result = resolver.resolve("plugin-a", "1.0.0").unwrap();
581
582 assert_eq!(result.install_order, vec!["plugin-a"]);
583 assert_eq!(
584 result.version_map.get("plugin-a"),
585 Some(&"1.0.0".to_string())
586 );
587 assert_eq!(result.unmet_dependencies.len(), 0);
588 }
589
590 #[test]
591 fn test_dependency_resolver_with_single_dependency() {
592 let mut registry = LocalRegistry::new();
593
594 let entry_b = PluginRegistryEntry {
596 plugin_id: "plugin-b".to_string(),
597 name: "plugin-b".to_string(),
598 version: "1.0.0".to_string(),
599 abi_version: "2.0".to_string(),
600 description: None,
601 author: None,
602 license: None,
603 keywords: None,
604 dependencies: None,
605 };
606 registry.register(entry_b).unwrap();
607
608 let entry_a = PluginRegistryEntry {
610 plugin_id: "plugin-a".to_string(),
611 name: "plugin-a".to_string(),
612 version: "1.0.0".to_string(),
613 abi_version: "2.0".to_string(),
614 description: None,
615 author: None,
616 license: None,
617 keywords: None,
618 dependencies: Some(vec![PluginDependency {
619 name: "plugin-b".to_string(),
620 version_requirement: "1.0.0".to_string(),
621 }]),
622 };
623 registry.register(entry_a).unwrap();
624
625 let resolver = DependencyResolver::new(registry);
626 let result = resolver.resolve("plugin-a", "1.0.0").unwrap();
627
628 assert_eq!(result.install_order, vec!["plugin-b", "plugin-a"]);
630 assert_eq!(result.unmet_dependencies.len(), 0);
631 }
632
633 #[test]
634 fn test_dependency_resolver_with_missing_dependency() {
635 let mut registry = LocalRegistry::new();
636
637 let entry_a = PluginRegistryEntry {
639 plugin_id: "plugin-a".to_string(),
640 name: "plugin-a".to_string(),
641 version: "1.0.0".to_string(),
642 abi_version: "2.0".to_string(),
643 description: None,
644 author: None,
645 license: None,
646 keywords: None,
647 dependencies: Some(vec![PluginDependency {
648 name: "plugin-b".to_string(),
649 version_requirement: ">=1.0.0".to_string(),
650 }]),
651 };
652 registry.register(entry_a).unwrap();
653
654 let resolver = DependencyResolver::new(registry);
655 let result = resolver.resolve("plugin-a", "1.0.0").unwrap();
656
657 assert_eq!(result.unmet_dependencies.len(), 1);
659 assert_eq!(result.unmet_dependencies[0].plugin_name, "plugin-b");
660 assert_eq!(result.unmet_dependencies[0].required_by, "plugin-a");
661 }
662
663 #[test]
664 fn test_dependency_resolver_version_matching() {
665 let mut registry = LocalRegistry::new();
666
667 for version in &["0.9.0", "1.0.0", "1.5.0", "2.0.0"] {
669 let entry = PluginRegistryEntry {
670 plugin_id: "plugin-b".to_string(),
671 name: "plugin-b".to_string(),
672 version: version.to_string(),
673 abi_version: "2.0".to_string(),
674 description: None,
675 author: None,
676 license: None,
677 keywords: None,
678 dependencies: None,
679 };
680 registry.register(entry).unwrap();
681 }
682
683 let entry_a = PluginRegistryEntry {
685 plugin_id: "plugin-a".to_string(),
686 name: "plugin-a".to_string(),
687 version: "1.0.0".to_string(),
688 abi_version: "2.0".to_string(),
689 description: None,
690 author: None,
691 license: None,
692 keywords: None,
693 dependencies: Some(vec![PluginDependency {
694 name: "plugin-b".to_string(),
695 version_requirement: "^1.0.0".to_string(), }]),
697 };
698 registry.register(entry_a).unwrap();
699
700 let resolver = DependencyResolver::new(registry);
701 let result = resolver.resolve("plugin-a", "1.0.0").unwrap();
702
703 assert_eq!(
705 result.version_map.get("plugin-b"),
706 Some(&"1.5.0".to_string())
707 );
708 assert_eq!(result.unmet_dependencies.len(), 0);
709 }
710}