1use dashmap::DashMap;
2use std::sync::Arc;
3use tower_lsp_server::ls_types::Uri;
4
5use crate::Ecosystem;
6
7pub struct EcosystemRegistry {
36 ecosystems: DashMap<&'static str, Arc<dyn Ecosystem>>,
38 filename_map: DashMap<&'static str, &'static str>,
40}
41
42impl EcosystemRegistry {
43 pub fn new() -> Self {
54 Self {
55 ecosystems: DashMap::new(),
56 filename_map: DashMap::new(),
57 }
58 }
59
60 pub fn register(&self, ecosystem: Arc<dyn Ecosystem>) {
79 let id = ecosystem.id();
80
81 for filename in ecosystem.manifest_filenames() {
83 self.filename_map.insert(*filename, id);
84 }
85
86 self.ecosystems.insert(id, ecosystem);
88 }
89
90 pub fn get(&self, id: &str) -> Option<Arc<dyn Ecosystem>> {
112 self.ecosystems.get(id).map(|e| Arc::clone(&e))
113 }
114
115 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 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 pub fn ecosystem_ids(&self) -> Vec<&'static str> {
196 self.ecosystems.iter().map(|e| *e.key()).collect()
197 }
198
199 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 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 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 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}