Skip to main content

plugin_packager/
registry.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Registry integration for plugin-packager
5//!
6//! This module provides integration with the plugin registry system,
7//! allowing plugins to be registered, discovered, and managed through the registry.
8
9use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::Path;
13
14/// Plugin metadata for registry registration
15#[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/// Plugin dependency for registry
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct PluginDependency {
31    pub name: String,
32    pub version_requirement: String,
33}
34
35/// In-memory plugin registry for local installations
36pub struct LocalRegistry {
37    plugins: HashMap<String, PluginRegistryEntry>,
38}
39
40impl LocalRegistry {
41    /// Create a new local registry
42    pub fn new() -> Self {
43        Self {
44            plugins: HashMap::new(),
45        }
46    }
47
48    /// Register a plugin in the local registry
49    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    /// Find a plugin by name
56    pub fn find_by_name(&self, name: &str) -> Option<PluginRegistryEntry> {
57        self.plugins.values().find(|p| p.name == name).cloned()
58    }
59
60    /// Find a plugin by name and version
61    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    /// List all registered plugins
67    pub fn list_all(&self) -> Vec<PluginRegistryEntry> {
68        self.plugins.values().cloned().collect()
69    }
70
71    /// Search for plugins by keyword
72    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    /// Get the latest version of a plugin
92    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                // Simple version comparison (highest version first)
98                parse_version(&a.version).cmp(&parse_version(&b.version))
99            })
100            .cloned()
101    }
102
103    /// Remove a plugin from the registry
104    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    /// Check if a plugin is registered
111    pub fn exists(&self, name: &str, version: &str) -> bool {
112        let key = format!("{}:{}", name, version);
113        self.plugins.contains_key(&key)
114    }
115
116    /// Get plugin count
117    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
128/// Parse a version string into a sortable tuple
129/// Simple semantic version parsing for comparison
130fn 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/// Represents a version requirement (e.g., ">=1.0.0", "1.2.x", "2.0.0")
148#[derive(Debug, Clone)]
149pub struct VersionRequirement {
150    pub requirement: String,
151}
152
153impl VersionRequirement {
154    /// Create a new version requirement
155    pub fn new(requirement: String) -> Self {
156        Self { requirement }
157    }
158
159    /// Check if a version matches this requirement
160    pub fn matches(&self, version: &str) -> bool {
161        let req = self.requirement.trim();
162
163        // Check for wildcard first (before exact match check)
164        if req.ends_with('x') || req.ends_with('X') {
165            // 1.2.x wildcard match
166            let req_parts: Vec<&str> = req.split('.').collect();
167            let actual_parts: Vec<&str> = version.split('.').collect();
168
169            // Check all parts up to the wildcard
170            for (i, req_part) in req_parts.iter().enumerate() {
171                if *req_part == "x" || *req_part == "X" {
172                    // We've matched all non-wildcard parts
173                    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        // Exact version match (no operators)
186        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            // ~1.2.3 := >=1.2.3, <1.3.0
202            ("~", stripped.trim())
203        } else if let Some(stripped) = req.strip_prefix('^') {
204            // ^1.2.3 := >=1.2.3, <2.0.0 (caret allows minor/patch changes)
205            ("^", 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                // ~1.2.3 := >=1.2.3, <1.3.0
222                let (rmaj, rmin, rpatch) = req_ver;
223                let (amaj, amin, apatch) = act_ver;
224                amaj == rmaj && amin == rmin && apatch >= rpatch
225            }
226            "^" => {
227                // ^1.2.3 := >=1.2.3, <2.0.0
228                let (rmaj, _, _) = req_ver;
229                let (amaj, _, _) = act_ver;
230                amaj == rmaj && act_ver >= req_ver
231            }
232            _ => false,
233        }
234    }
235}
236
237/// Dependency resolver for plugin installation
238pub struct DependencyResolver {
239    registry: LocalRegistry,
240}
241
242/// Result of dependency resolution
243#[derive(Debug, Clone)]
244pub struct DependencyResolution {
245    /// Ordered list of plugins to install (from leaf to root)
246    pub install_order: Vec<String>,
247    /// Map of plugin name to specific version to install
248    pub version_map: HashMap<String, String>,
249    /// Any unmet dependencies
250    pub unmet_dependencies: Vec<UnmetDependency>,
251}
252
253/// Represents an unmet dependency
254#[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    /// Create a new dependency resolver
263    pub fn new(registry: LocalRegistry) -> Self {
264        Self { registry }
265    }
266
267    /// Resolve dependencies for a plugin
268    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; // Already processed this plugin@version
302        }
303        visited.insert(key);
304
305        // Find the plugin
306        if let Some(entry) = self.registry.find_by_version(plugin_name, plugin_version) {
307            // Process dependencies first (depth-first)
308            if let Some(deps) = &entry.dependencies {
309                for dep in deps {
310                    // Find matching version
311                    let req = VersionRequirement::new(dep.version_requirement.clone());
312                    if let Some(matching) = self.find_matching_version(&dep.name, &req) {
313                        // Recursively resolve
314                        self.resolve_recursive(
315                            &dep.name,
316                            &matching,
317                            install_order,
318                            version_map,
319                            unmet,
320                            visited,
321                        );
322                    } else {
323                        // Dependency not found in registry
324                        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            // Add this plugin to install order
334            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        // Return the latest matching version
353        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
360/// Registry persistence (for future: save/load to disk)
361pub struct RegistryPersistence;
362
363impl RegistryPersistence {
364    /// Save registry to file
365    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    /// Load registry from file
373    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    // Version requirement tests
479    #[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        // ~1.2.3 := >=1.2.3, <1.3.0
534        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        // ^1.2.3 := >=1.2.3, <2.0.0
544        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    // Dependency resolver tests
563    #[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        // Register plugin-b (dependency)
595        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        // Register plugin-a (depends on plugin-b)
609        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        // plugin-b should be installed first
629        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        // Register plugin-a without plugin-b in registry
638        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        // Should report unmet dependency
658        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        // Register multiple versions of plugin-b
668        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        // Register plugin-a depending on plugin-b >=1.0.0, <2.0.0
684        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(), // >=1.0.0, <2.0.0
696            }]),
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        // Should select latest matching version (1.5.0)
704        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}