Skip to main content

deps_core/
ecosystem_registry.rs

1use dashmap::DashMap;
2use std::sync::Arc;
3use tower_lsp_server::ls_types::Uri;
4
5use crate::Ecosystem;
6
7/// Registry for all available ecosystems.
8///
9/// This registry manages ecosystem implementations and provides fast lookup
10/// by ecosystem ID or manifest filename. It's designed for thread-safe
11/// concurrent access using DashMap.
12///
13/// # Examples
14///
15/// ```no_run
16/// use deps_core::EcosystemRegistry;
17/// use std::sync::Arc;
18///
19/// let registry = EcosystemRegistry::new();
20///
21/// // Register ecosystems (would be actual implementations)
22/// // registry.register(Arc::new(CargoEcosystem::new(cache.clone())));
23/// // registry.register(Arc::new(NpmEcosystem::new(cache.clone())));
24///
25/// // Look up by filename
26/// if let Some(ecosystem) = registry.get_for_filename("Cargo.toml") {
27///     println!("Found ecosystem: {}", ecosystem.display_name());
28/// }
29///
30/// // List all registered ecosystems
31/// for id in registry.ecosystem_ids() {
32///     println!("Registered: {}", id);
33/// }
34/// ```
35pub struct EcosystemRegistry {
36    /// Map from ecosystem ID to implementation
37    ecosystems: DashMap<&'static str, Arc<dyn Ecosystem>>,
38    /// Map from filename to ecosystem ID (for fast lookup)
39    filename_map: DashMap<&'static str, &'static str>,
40}
41
42impl EcosystemRegistry {
43    /// Create a new empty registry
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use deps_core::EcosystemRegistry;
49    ///
50    /// let registry = EcosystemRegistry::new();
51    /// assert_eq!(registry.ecosystem_ids().len(), 0);
52    /// ```
53    pub fn new() -> Self {
54        Self {
55            ecosystems: DashMap::new(),
56            filename_map: DashMap::new(),
57        }
58    }
59
60    /// Register an ecosystem implementation
61    ///
62    /// This method registers the ecosystem and creates filename mappings
63    /// for all manifest filenames declared by the ecosystem.
64    ///
65    /// # Arguments
66    ///
67    /// * `ecosystem` - Arc-wrapped ecosystem implementation
68    ///
69    /// # Examples
70    ///
71    /// ```no_run
72    /// use deps_core::EcosystemRegistry;
73    /// use std::sync::Arc;
74    ///
75    /// let registry = EcosystemRegistry::new();
76    /// // registry.register(Arc::new(CargoEcosystem::new(cache)));
77    /// ```
78    pub fn register(&self, ecosystem: Arc<dyn Ecosystem>) {
79        let id = ecosystem.id();
80
81        // Register filename mappings
82        for filename in ecosystem.manifest_filenames() {
83            self.filename_map.insert(*filename, id);
84        }
85
86        // Register ecosystem
87        self.ecosystems.insert(id, ecosystem);
88    }
89
90    /// Get ecosystem by ID
91    ///
92    /// # Arguments
93    ///
94    /// * `id` - Ecosystem identifier (e.g., "cargo", "npm", "pypi")
95    ///
96    /// # Returns
97    ///
98    /// * `Some(Arc<dyn Ecosystem>)` - Registered ecosystem
99    /// * `None` - No ecosystem registered with this ID
100    ///
101    /// # Examples
102    ///
103    /// ```no_run
104    /// use deps_core::EcosystemRegistry;
105    ///
106    /// let registry = EcosystemRegistry::new();
107    /// if let Some(ecosystem) = registry.get("cargo") {
108    ///     println!("Found: {}", ecosystem.display_name());
109    /// }
110    /// ```
111    pub fn get(&self, id: &str) -> Option<Arc<dyn Ecosystem>> {
112        self.ecosystems.get(id).map(|e| Arc::clone(&e))
113    }
114
115    /// Get ecosystem for a filename
116    ///
117    /// # Arguments
118    ///
119    /// * `filename` - Manifest filename (e.g., "Cargo.toml", "package.json")
120    ///
121    /// # Returns
122    ///
123    /// * `Some(Arc<dyn Ecosystem>)` - Ecosystem handling this filename
124    /// * `None` - No ecosystem handles this filename
125    ///
126    /// # Examples
127    ///
128    /// ```no_run
129    /// use deps_core::EcosystemRegistry;
130    ///
131    /// let registry = EcosystemRegistry::new();
132    /// if let Some(ecosystem) = registry.get_for_filename("Cargo.toml") {
133    ///     println!("Cargo.toml handled by: {}", ecosystem.display_name());
134    /// }
135    /// ```
136    pub fn get_for_filename(&self, filename: &str) -> Option<Arc<dyn Ecosystem>> {
137        let id = self.filename_map.get(filename)?;
138        self.get(*id)
139    }
140
141    /// Get ecosystem from URI
142    ///
143    /// Extracts the filename from the URI path and looks up the ecosystem.
144    ///
145    /// # Arguments
146    ///
147    /// * `uri` - Document URI (file:///path/to/Cargo.toml)
148    ///
149    /// # Returns
150    ///
151    /// * `Some(Arc<dyn Ecosystem>)` - Ecosystem handling this file
152    /// * `None` - No ecosystem handles this file type or URI parsing failed
153    ///
154    /// # Examples
155    ///
156    /// ```no_run
157    /// use deps_core::EcosystemRegistry;
158    /// use tower_lsp_server::ls_types::Uri;
159    ///
160    /// let registry = EcosystemRegistry::new();
161    /// let uri = Uri::from_file_path("/home/user/project/Cargo.toml").unwrap();
162    ///
163    /// if let Some(ecosystem) = registry.get_for_uri(&uri) {
164    ///     println!("File handled by: {}", ecosystem.display_name());
165    /// }
166    /// ```
167    pub fn get_for_uri(&self, uri: &Uri) -> Option<Arc<dyn Ecosystem>> {
168        let path = uri.path().as_str();
169        let filename = path.rsplit('/').next()?;
170        self.get_for_filename(filename)
171    }
172
173    /// Get all registered ecosystem IDs
174    ///
175    /// Returns a vector of all ecosystem IDs currently registered.
176    /// This is useful for debugging and listing available ecosystems.
177    ///
178    /// # Returns
179    ///
180    /// Vector of ecosystem ID strings
181    ///
182    /// # Examples
183    ///
184    /// ```no_run
185    /// use deps_core::EcosystemRegistry;
186    ///
187    /// let registry = EcosystemRegistry::new();
188    /// // registry.register(cargo_ecosystem);
189    /// // registry.register(npm_ecosystem);
190    ///
191    /// for id in registry.ecosystem_ids() {
192    ///     println!("Registered ecosystem: {}", id);
193    /// }
194    /// ```
195    pub fn ecosystem_ids(&self) -> Vec<&'static str> {
196        self.ecosystems.iter().map(|e| *e.key()).collect()
197    }
198
199    /// Get ecosystem for a lock file name
200    ///
201    /// # Arguments
202    ///
203    /// * `filename` - Lock file name (e.g., "Cargo.lock", "package-lock.json")
204    ///
205    /// # Returns
206    ///
207    /// * `Some(Arc<dyn Ecosystem>)` - Ecosystem using this lock file
208    /// * `None` - No ecosystem uses this lock file
209    ///
210    /// # Examples
211    ///
212    /// ```no_run
213    /// use deps_core::EcosystemRegistry;
214    ///
215    /// let registry = EcosystemRegistry::new();
216    /// // registry.register(cargo_ecosystem);
217    ///
218    /// if let Some(ecosystem) = registry.get_for_lockfile("Cargo.lock") {
219    ///     println!("Cargo.lock handled by: {}", ecosystem.display_name());
220    /// }
221    /// ```
222    pub fn get_for_lockfile(&self, filename: &str) -> Option<Arc<dyn Ecosystem>> {
223        for entry in self.ecosystems.iter() {
224            let ecosystem = entry.value();
225            if ecosystem.lockfile_filenames().contains(&filename) {
226                return Some(Arc::clone(ecosystem));
227            }
228        }
229        None
230    }
231
232    /// Get all lock file patterns for file watching
233    ///
234    /// Returns glob patterns (e.g., "**/Cargo.lock") for all registered ecosystems.
235    ///
236    /// # Examples
237    ///
238    /// ```no_run
239    /// use deps_core::EcosystemRegistry;
240    ///
241    /// let registry = EcosystemRegistry::new();
242    /// // registry.register(cargo_ecosystem);
243    /// // registry.register(npm_ecosystem);
244    ///
245    /// let patterns = registry.all_lockfile_patterns();
246    /// for pattern in patterns {
247    ///     println!("Watching pattern: {}", pattern);
248    /// }
249    /// ```
250    pub fn all_lockfile_patterns(&self) -> Vec<String> {
251        let mut patterns = Vec::new();
252        for entry in self.ecosystems.iter() {
253            let ecosystem = entry.value();
254            for filename in ecosystem.lockfile_filenames() {
255                patterns.push(format!("**/{}", filename));
256            }
257        }
258        patterns
259    }
260}
261
262impl Default for EcosystemRegistry {
263    fn default() -> Self {
264        Self::new()
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use std::any::Any;
272    use tower_lsp_server::ls_types::{CompletionItem, Position};
273
274    use crate::{ParseResult, Registry, lsp_helpers::EcosystemFormatter};
275
276    struct MockFormatter;
277    impl EcosystemFormatter for MockFormatter {
278        fn format_version_for_text_edit(&self, version: &str) -> String {
279            version.to_string()
280        }
281        fn package_url(&self, name: &str) -> String {
282            format!("https://example.com/{name}")
283        }
284    }
285
286    // Mock ecosystem for testing
287    struct MockEcosystem {
288        id: &'static str,
289        display_name: &'static str,
290        filenames: &'static [&'static str],
291        lockfiles: &'static [&'static str],
292    }
293
294    impl crate::ecosystem::private::Sealed for MockEcosystem {}
295
296    impl Ecosystem for MockEcosystem {
297        fn id(&self) -> &'static str {
298            self.id
299        }
300
301        fn display_name(&self) -> &'static str {
302            self.display_name
303        }
304
305        fn manifest_filenames(&self) -> &[&'static str] {
306            self.filenames
307        }
308
309        fn lockfile_filenames(&self) -> &[&'static str] {
310            self.lockfiles
311        }
312
313        fn parse_manifest<'a>(
314            &'a self,
315            _content: &'a str,
316            _uri: &'a Uri,
317        ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>> {
318            Box::pin(async move { unimplemented!() })
319        }
320
321        fn registry(&self) -> Arc<dyn Registry> {
322            unimplemented!()
323        }
324
325        fn formatter(&self) -> &dyn EcosystemFormatter {
326            &MockFormatter
327        }
328
329        fn generate_completions<'a>(
330            &'a self,
331            _parse_result: &'a dyn ParseResult,
332            _position: Position,
333            _content: &'a str,
334        ) -> crate::ecosystem::BoxFuture<'a, Vec<CompletionItem>> {
335            Box::pin(async move { vec![] })
336        }
337
338        fn as_any(&self) -> &dyn Any {
339            self
340        }
341    }
342
343    #[test]
344    fn test_new_registry_is_empty() {
345        let registry = EcosystemRegistry::new();
346        assert_eq!(registry.ecosystem_ids().len(), 0);
347    }
348
349    #[test]
350    fn test_register_ecosystem() {
351        let registry = EcosystemRegistry::new();
352        let ecosystem = Arc::new(MockEcosystem {
353            id: "test",
354            display_name: "Test Ecosystem",
355            filenames: &["test.toml"],
356            lockfiles: &[],
357        });
358
359        registry.register(ecosystem);
360
361        assert_eq!(registry.ecosystem_ids().len(), 1);
362        assert!(registry.get("test").is_some());
363    }
364
365    #[test]
366    fn test_get_by_id() {
367        let registry = EcosystemRegistry::new();
368        let ecosystem = Arc::new(MockEcosystem {
369            id: "test",
370            display_name: "Test Ecosystem",
371            filenames: &["test.toml"],
372            lockfiles: &[],
373        });
374
375        registry.register(ecosystem);
376
377        let retrieved = registry.get("test").unwrap();
378        assert_eq!(retrieved.id(), "test");
379        assert_eq!(retrieved.display_name(), "Test Ecosystem");
380    }
381
382    #[test]
383    fn test_get_by_filename() {
384        let registry = EcosystemRegistry::new();
385        let ecosystem = Arc::new(MockEcosystem {
386            id: "test",
387            display_name: "Test Ecosystem",
388            filenames: &["test.toml", "test.json"],
389            lockfiles: &[],
390        });
391
392        registry.register(ecosystem);
393
394        let retrieved1 = registry.get_for_filename("test.toml").unwrap();
395        assert_eq!(retrieved1.id(), "test");
396
397        let retrieved2 = registry.get_for_filename("test.json").unwrap();
398        assert_eq!(retrieved2.id(), "test");
399
400        assert!(registry.get_for_filename("unknown.toml").is_none());
401    }
402
403    #[test]
404    fn test_get_by_uri() {
405        let registry = EcosystemRegistry::new();
406        let ecosystem = Arc::new(MockEcosystem {
407            id: "test",
408            display_name: "Test Ecosystem",
409            filenames: &["test.toml"],
410            lockfiles: &[],
411        });
412
413        registry.register(ecosystem);
414
415        let uri = Uri::from_file_path("/home/user/project/test.toml").unwrap();
416        let retrieved = registry.get_for_uri(&uri).unwrap();
417        assert_eq!(retrieved.id(), "test");
418
419        let unknown_uri = Uri::from_file_path("/home/user/project/unknown.toml").unwrap();
420        assert!(registry.get_for_uri(&unknown_uri).is_none());
421    }
422
423    #[test]
424    fn test_multiple_ecosystems() {
425        let registry = EcosystemRegistry::new();
426
427        let eco1 = Arc::new(MockEcosystem {
428            id: "cargo",
429            display_name: "Cargo",
430            filenames: &["Cargo.toml"],
431            lockfiles: &["Cargo.lock"],
432        });
433
434        let eco2 = Arc::new(MockEcosystem {
435            id: "npm",
436            display_name: "npm",
437            filenames: &["package.json"],
438            lockfiles: &["package-lock.json"],
439        });
440
441        registry.register(eco1);
442        registry.register(eco2);
443
444        assert_eq!(registry.ecosystem_ids().len(), 2);
445
446        assert_eq!(
447            registry.get_for_filename("Cargo.toml").unwrap().id(),
448            "cargo"
449        );
450        assert_eq!(
451            registry.get_for_filename("package.json").unwrap().id(),
452            "npm"
453        );
454    }
455
456    #[test]
457    fn test_get_for_lockfile() {
458        let registry = EcosystemRegistry::new();
459        let ecosystem = Arc::new(MockEcosystem {
460            id: "cargo",
461            display_name: "Cargo",
462            filenames: &["Cargo.toml"],
463            lockfiles: &["Cargo.lock"],
464        });
465
466        registry.register(ecosystem);
467
468        let retrieved = registry.get_for_lockfile("Cargo.lock").unwrap();
469        assert_eq!(retrieved.id(), "cargo");
470        assert_eq!(retrieved.display_name(), "Cargo");
471
472        // Unknown lockfile should return None
473        assert!(registry.get_for_lockfile("unknown.lock").is_none());
474    }
475
476    #[test]
477    fn test_get_for_lockfile_multiple_lockfiles() {
478        let registry = EcosystemRegistry::new();
479        let ecosystem = Arc::new(MockEcosystem {
480            id: "pypi",
481            display_name: "PyPI",
482            filenames: &["pyproject.toml"],
483            lockfiles: &["poetry.lock", "uv.lock"],
484        });
485
486        registry.register(ecosystem);
487
488        let retrieved1 = registry.get_for_lockfile("poetry.lock").unwrap();
489        assert_eq!(retrieved1.id(), "pypi");
490
491        let retrieved2 = registry.get_for_lockfile("uv.lock").unwrap();
492        assert_eq!(retrieved2.id(), "pypi");
493    }
494
495    #[test]
496    fn test_all_lockfile_patterns_empty() {
497        let registry = EcosystemRegistry::new();
498        assert!(registry.all_lockfile_patterns().is_empty());
499    }
500
501    #[test]
502    fn test_all_lockfile_patterns_single_ecosystem() {
503        let registry = EcosystemRegistry::new();
504        let ecosystem = Arc::new(MockEcosystem {
505            id: "cargo",
506            display_name: "Cargo",
507            filenames: &["Cargo.toml"],
508            lockfiles: &["Cargo.lock"],
509        });
510
511        registry.register(ecosystem);
512
513        let patterns = registry.all_lockfile_patterns();
514        assert_eq!(patterns.len(), 1);
515        assert_eq!(patterns[0], "**/Cargo.lock");
516    }
517
518    #[test]
519    fn test_all_lockfile_patterns_multiple_ecosystems() {
520        let registry = EcosystemRegistry::new();
521
522        let eco1 = Arc::new(MockEcosystem {
523            id: "cargo",
524            display_name: "Cargo",
525            filenames: &["Cargo.toml"],
526            lockfiles: &["Cargo.lock"],
527        });
528
529        let eco2 = Arc::new(MockEcosystem {
530            id: "npm",
531            display_name: "npm",
532            filenames: &["package.json"],
533            lockfiles: &["package-lock.json"],
534        });
535
536        let eco3 = Arc::new(MockEcosystem {
537            id: "pypi",
538            display_name: "PyPI",
539            filenames: &["pyproject.toml"],
540            lockfiles: &["poetry.lock", "uv.lock"],
541        });
542
543        registry.register(eco1);
544        registry.register(eco2);
545        registry.register(eco3);
546
547        let patterns = registry.all_lockfile_patterns();
548        assert_eq!(patterns.len(), 4);
549        assert!(patterns.contains(&"**/Cargo.lock".to_string()));
550        assert!(patterns.contains(&"**/package-lock.json".to_string()));
551        assert!(patterns.contains(&"**/poetry.lock".to_string()));
552        assert!(patterns.contains(&"**/uv.lock".to_string()));
553    }
554
555    #[test]
556    fn test_all_lockfile_patterns_no_lockfiles() {
557        let registry = EcosystemRegistry::new();
558        let ecosystem = Arc::new(MockEcosystem {
559            id: "test",
560            display_name: "Test",
561            filenames: &["test.toml"],
562            lockfiles: &[],
563        });
564
565        registry.register(ecosystem);
566
567        let patterns = registry.all_lockfile_patterns();
568        assert!(patterns.is_empty());
569    }
570}