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 async_trait::async_trait;
272    use std::any::Any;
273    use tower_lsp_server::ls_types::{
274        CodeAction, CompletionItem, Diagnostic, Hover, InlayHint, Position,
275    };
276
277    use crate::{EcosystemConfig, ParseResult, Registry};
278
279    // Mock ecosystem for testing
280    struct MockEcosystem {
281        id: &'static str,
282        display_name: &'static str,
283        filenames: &'static [&'static str],
284        lockfiles: &'static [&'static str],
285    }
286
287    #[async_trait]
288    impl Ecosystem for MockEcosystem {
289        fn id(&self) -> &'static str {
290            self.id
291        }
292
293        fn display_name(&self) -> &'static str {
294            self.display_name
295        }
296
297        fn manifest_filenames(&self) -> &[&'static str] {
298            self.filenames
299        }
300
301        fn lockfile_filenames(&self) -> &[&'static str] {
302            self.lockfiles
303        }
304
305        async fn parse_manifest(
306            &self,
307            _content: &str,
308            _uri: &Uri,
309        ) -> crate::error::Result<Box<dyn ParseResult>> {
310            unimplemented!()
311        }
312
313        fn registry(&self) -> Arc<dyn Registry> {
314            unimplemented!()
315        }
316
317        async fn generate_inlay_hints(
318            &self,
319            _parse_result: &dyn ParseResult,
320            _cached_versions: &std::collections::HashMap<String, String>,
321            _resolved_versions: &std::collections::HashMap<String, String>,
322            _loading_state: crate::LoadingState,
323            _config: &EcosystemConfig,
324        ) -> Vec<InlayHint> {
325            vec![]
326        }
327
328        async fn generate_hover(
329            &self,
330            _parse_result: &dyn ParseResult,
331            _position: Position,
332            _cached_versions: &std::collections::HashMap<String, String>,
333            _resolved_versions: &std::collections::HashMap<String, String>,
334        ) -> Option<Hover> {
335            None
336        }
337
338        async fn generate_code_actions(
339            &self,
340            _parse_result: &dyn ParseResult,
341            _position: Position,
342            _cached_versions: &std::collections::HashMap<String, String>,
343            _uri: &Uri,
344        ) -> Vec<CodeAction> {
345            vec![]
346        }
347
348        async fn generate_diagnostics(
349            &self,
350            _parse_result: &dyn ParseResult,
351            _cached_versions: &std::collections::HashMap<String, String>,
352            _resolved_versions: &std::collections::HashMap<String, String>,
353            _uri: &Uri,
354        ) -> Vec<Diagnostic> {
355            vec![]
356        }
357
358        async fn generate_completions(
359            &self,
360            _parse_result: &dyn ParseResult,
361            _position: Position,
362            _content: &str,
363        ) -> Vec<CompletionItem> {
364            vec![]
365        }
366
367        fn as_any(&self) -> &dyn Any {
368            self
369        }
370    }
371
372    #[test]
373    fn test_new_registry_is_empty() {
374        let registry = EcosystemRegistry::new();
375        assert_eq!(registry.ecosystem_ids().len(), 0);
376    }
377
378    #[test]
379    fn test_register_ecosystem() {
380        let registry = EcosystemRegistry::new();
381        let ecosystem = Arc::new(MockEcosystem {
382            id: "test",
383            display_name: "Test Ecosystem",
384            filenames: &["test.toml"],
385            lockfiles: &[],
386        });
387
388        registry.register(ecosystem);
389
390        assert_eq!(registry.ecosystem_ids().len(), 1);
391        assert!(registry.get("test").is_some());
392    }
393
394    #[test]
395    fn test_get_by_id() {
396        let registry = EcosystemRegistry::new();
397        let ecosystem = Arc::new(MockEcosystem {
398            id: "test",
399            display_name: "Test Ecosystem",
400            filenames: &["test.toml"],
401            lockfiles: &[],
402        });
403
404        registry.register(ecosystem);
405
406        let retrieved = registry.get("test").unwrap();
407        assert_eq!(retrieved.id(), "test");
408        assert_eq!(retrieved.display_name(), "Test Ecosystem");
409    }
410
411    #[test]
412    fn test_get_by_filename() {
413        let registry = EcosystemRegistry::new();
414        let ecosystem = Arc::new(MockEcosystem {
415            id: "test",
416            display_name: "Test Ecosystem",
417            filenames: &["test.toml", "test.json"],
418            lockfiles: &[],
419        });
420
421        registry.register(ecosystem);
422
423        let retrieved1 = registry.get_for_filename("test.toml").unwrap();
424        assert_eq!(retrieved1.id(), "test");
425
426        let retrieved2 = registry.get_for_filename("test.json").unwrap();
427        assert_eq!(retrieved2.id(), "test");
428
429        assert!(registry.get_for_filename("unknown.toml").is_none());
430    }
431
432    #[test]
433    fn test_get_by_uri() {
434        let registry = EcosystemRegistry::new();
435        let ecosystem = Arc::new(MockEcosystem {
436            id: "test",
437            display_name: "Test Ecosystem",
438            filenames: &["test.toml"],
439            lockfiles: &[],
440        });
441
442        registry.register(ecosystem);
443
444        let uri = Uri::from_file_path("/home/user/project/test.toml").unwrap();
445        let retrieved = registry.get_for_uri(&uri).unwrap();
446        assert_eq!(retrieved.id(), "test");
447
448        let unknown_uri = Uri::from_file_path("/home/user/project/unknown.toml").unwrap();
449        assert!(registry.get_for_uri(&unknown_uri).is_none());
450    }
451
452    #[test]
453    fn test_multiple_ecosystems() {
454        let registry = EcosystemRegistry::new();
455
456        let eco1 = Arc::new(MockEcosystem {
457            id: "cargo",
458            display_name: "Cargo",
459            filenames: &["Cargo.toml"],
460            lockfiles: &["Cargo.lock"],
461        });
462
463        let eco2 = Arc::new(MockEcosystem {
464            id: "npm",
465            display_name: "npm",
466            filenames: &["package.json"],
467            lockfiles: &["package-lock.json"],
468        });
469
470        registry.register(eco1);
471        registry.register(eco2);
472
473        assert_eq!(registry.ecosystem_ids().len(), 2);
474
475        assert_eq!(
476            registry.get_for_filename("Cargo.toml").unwrap().id(),
477            "cargo"
478        );
479        assert_eq!(
480            registry.get_for_filename("package.json").unwrap().id(),
481            "npm"
482        );
483    }
484
485    #[test]
486    fn test_get_for_lockfile() {
487        let registry = EcosystemRegistry::new();
488        let ecosystem = Arc::new(MockEcosystem {
489            id: "cargo",
490            display_name: "Cargo",
491            filenames: &["Cargo.toml"],
492            lockfiles: &["Cargo.lock"],
493        });
494
495        registry.register(ecosystem);
496
497        let retrieved = registry.get_for_lockfile("Cargo.lock").unwrap();
498        assert_eq!(retrieved.id(), "cargo");
499        assert_eq!(retrieved.display_name(), "Cargo");
500
501        // Unknown lockfile should return None
502        assert!(registry.get_for_lockfile("unknown.lock").is_none());
503    }
504
505    #[test]
506    fn test_get_for_lockfile_multiple_lockfiles() {
507        let registry = EcosystemRegistry::new();
508        let ecosystem = Arc::new(MockEcosystem {
509            id: "pypi",
510            display_name: "PyPI",
511            filenames: &["pyproject.toml"],
512            lockfiles: &["poetry.lock", "uv.lock"],
513        });
514
515        registry.register(ecosystem);
516
517        let retrieved1 = registry.get_for_lockfile("poetry.lock").unwrap();
518        assert_eq!(retrieved1.id(), "pypi");
519
520        let retrieved2 = registry.get_for_lockfile("uv.lock").unwrap();
521        assert_eq!(retrieved2.id(), "pypi");
522    }
523
524    #[test]
525    fn test_all_lockfile_patterns_empty() {
526        let registry = EcosystemRegistry::new();
527        assert!(registry.all_lockfile_patterns().is_empty());
528    }
529
530    #[test]
531    fn test_all_lockfile_patterns_single_ecosystem() {
532        let registry = EcosystemRegistry::new();
533        let ecosystem = Arc::new(MockEcosystem {
534            id: "cargo",
535            display_name: "Cargo",
536            filenames: &["Cargo.toml"],
537            lockfiles: &["Cargo.lock"],
538        });
539
540        registry.register(ecosystem);
541
542        let patterns = registry.all_lockfile_patterns();
543        assert_eq!(patterns.len(), 1);
544        assert_eq!(patterns[0], "**/Cargo.lock");
545    }
546
547    #[test]
548    fn test_all_lockfile_patterns_multiple_ecosystems() {
549        let registry = EcosystemRegistry::new();
550
551        let eco1 = Arc::new(MockEcosystem {
552            id: "cargo",
553            display_name: "Cargo",
554            filenames: &["Cargo.toml"],
555            lockfiles: &["Cargo.lock"],
556        });
557
558        let eco2 = Arc::new(MockEcosystem {
559            id: "npm",
560            display_name: "npm",
561            filenames: &["package.json"],
562            lockfiles: &["package-lock.json"],
563        });
564
565        let eco3 = Arc::new(MockEcosystem {
566            id: "pypi",
567            display_name: "PyPI",
568            filenames: &["pyproject.toml"],
569            lockfiles: &["poetry.lock", "uv.lock"],
570        });
571
572        registry.register(eco1);
573        registry.register(eco2);
574        registry.register(eco3);
575
576        let patterns = registry.all_lockfile_patterns();
577        assert_eq!(patterns.len(), 4);
578        assert!(patterns.contains(&"**/Cargo.lock".to_string()));
579        assert!(patterns.contains(&"**/package-lock.json".to_string()));
580        assert!(patterns.contains(&"**/poetry.lock".to_string()));
581        assert!(patterns.contains(&"**/uv.lock".to_string()));
582    }
583
584    #[test]
585    fn test_all_lockfile_patterns_no_lockfiles() {
586        let registry = EcosystemRegistry::new();
587        let ecosystem = Arc::new(MockEcosystem {
588            id: "test",
589            display_name: "Test",
590            filenames: &["test.toml"],
591            lockfiles: &[],
592        });
593
594        registry.register(ecosystem);
595
596        let patterns = registry.all_lockfile_patterns();
597        assert!(patterns.is_empty());
598    }
599}