deps_core/registry.rs
1use crate::error::Result;
2use async_trait::async_trait;
3use std::any::Any;
4
5/// Generic package registry interface.
6///
7/// Implementors provide access to a package registry (crates.io, npm, PyPI, etc.)
8/// with version lookup, search, and metadata retrieval capabilities.
9///
10/// All methods return `Result<T>` to allow graceful error handling.
11/// LSP handlers must never panic on registry errors.
12///
13/// # Type Erasure
14///
15/// This trait uses `Box<dyn Trait>` return types instead of associated types
16/// to allow runtime polymorphism and dynamic ecosystem registration.
17///
18/// # Examples
19///
20/// ```no_run
21/// use deps_core::{Registry, Version, Metadata};
22/// use async_trait::async_trait;
23/// use std::any::Any;
24///
25/// struct MyRegistry;
26///
27/// #[derive(Clone)]
28/// struct MyVersion {
29/// version: String,
30/// }
31///
32/// impl Version for MyVersion {
33/// fn version_string(&self) -> &str {
34/// &self.version
35/// }
36///
37/// fn is_yanked(&self) -> bool {
38/// false
39/// }
40///
41/// fn as_any(&self) -> &dyn Any {
42/// self
43/// }
44/// }
45///
46/// #[derive(Clone)]
47/// struct MyMetadata {
48/// name: String,
49/// }
50///
51/// impl Metadata for MyMetadata {
52/// fn name(&self) -> &str {
53/// &self.name
54/// }
55///
56/// fn description(&self) -> Option<&str> {
57/// None
58/// }
59///
60/// fn repository(&self) -> Option<&str> {
61/// None
62/// }
63///
64/// fn documentation(&self) -> Option<&str> {
65/// None
66/// }
67///
68/// fn latest_version(&self) -> &str {
69/// "1.0.0"
70/// }
71///
72/// fn as_any(&self) -> &dyn Any {
73/// self
74/// }
75/// }
76///
77/// #[async_trait]
78/// impl Registry for MyRegistry {
79/// async fn get_versions(&self, name: &str) -> deps_core::error::Result<Vec<Box<dyn Version>>> {
80/// Ok(vec![Box::new(MyVersion { version: "1.0.0".into() })])
81/// }
82///
83/// async fn get_latest_matching(
84/// &self,
85/// _name: &str,
86/// _req: &str,
87/// ) -> deps_core::error::Result<Option<Box<dyn Version>>> {
88/// Ok(None)
89/// }
90///
91/// async fn search(&self, _query: &str, _limit: usize) -> deps_core::error::Result<Vec<Box<dyn Metadata>>> {
92/// Ok(vec![])
93/// }
94///
95/// fn package_url(&self, name: &str) -> String {
96/// format!("https://example.com/packages/{}", name)
97/// }
98///
99/// fn as_any(&self) -> &dyn Any {
100/// self
101/// }
102/// }
103/// ```
104#[async_trait]
105pub trait Registry: Send + Sync {
106 /// Fetches all available versions for a package.
107 ///
108 /// Returns versions sorted newest-first. May include yanked/deprecated versions.
109 ///
110 /// # Errors
111 ///
112 /// Returns error if:
113 /// - Package does not exist
114 /// - Network request fails
115 /// - Response parsing fails
116 async fn get_versions(&self, name: &str) -> Result<Vec<Box<dyn Version>>>;
117
118 /// Finds the latest version matching a version requirement.
119 ///
120 /// Only returns stable (non-yanked, non-deprecated) versions unless
121 /// explicitly requested in the version requirement.
122 ///
123 /// # Arguments
124 ///
125 /// * `name` - Package name
126 /// * `req` - Version requirement string (e.g., "^1.0", ">=2.0")
127 ///
128 /// # Returns
129 ///
130 /// - `Ok(Some(version))` - Latest matching version found
131 /// - `Ok(None)` - No matching version found
132 /// - `Err(_)` - Network or parsing error
133 async fn get_latest_matching(&self, name: &str, req: &str) -> Result<Option<Box<dyn Version>>>;
134
135 /// Searches for packages by name or keywords.
136 ///
137 /// Returns up to `limit` results sorted by relevance/popularity.
138 ///
139 /// # Errors
140 ///
141 /// Returns error if network request or parsing fails.
142 async fn search(&self, query: &str, limit: usize) -> Result<Vec<Box<dyn Metadata>>>;
143
144 /// Package URL for ecosystem (e.g., <https://crates.io/crates/serde>)
145 ///
146 /// Returns a URL that links to the package page on the registry website.
147 fn package_url(&self, name: &str) -> String;
148
149 /// Downcast to concrete registry type for ecosystem-specific operations
150 fn as_any(&self) -> &dyn Any;
151}
152
153/// Version information trait.
154///
155/// All version types must implement this to work with generic handlers.
156pub trait Version: Send + Sync {
157 /// Version string (e.g., "1.0.214", "14.21.3").
158 fn version_string(&self) -> &str;
159
160 /// Whether this version is yanked/deprecated.
161 fn is_yanked(&self) -> bool;
162
163 /// Whether this version is a pre-release (alpha, beta, rc, etc.).
164 ///
165 /// Default implementation checks for common pre-release patterns.
166 fn is_prerelease(&self) -> bool {
167 let v = self.version_string().to_lowercase();
168 v.contains("-alpha")
169 || v.contains("-beta")
170 || v.contains("-rc")
171 || v.contains("-dev")
172 || v.contains("-pre")
173 || v.contains("-snapshot")
174 || v.contains("-canary")
175 || v.contains("-nightly")
176 }
177
178 /// Available feature flags (empty if not supported by ecosystem).
179 fn features(&self) -> Vec<String> {
180 vec![]
181 }
182
183 /// Downcast to concrete version type
184 fn as_any(&self) -> &dyn Any;
185
186 /// Whether this version is stable (not yanked and not pre-release).
187 fn is_stable(&self) -> bool {
188 !self.is_yanked() && !self.is_prerelease()
189 }
190}
191
192/// Finds the latest stable version from a list of versions.
193///
194/// Returns the first version that is:
195/// - Not yanked/deprecated
196/// - Not a pre-release (alpha, beta, rc, etc.)
197///
198/// Assumes versions are sorted newest-first (as returned by registries).
199///
200/// # Examples
201///
202/// ```
203/// use deps_core::registry::{Version, find_latest_stable};
204/// use std::any::Any;
205///
206/// struct MyVersion { version: String, yanked: bool }
207///
208/// impl Version for MyVersion {
209/// fn version_string(&self) -> &str { &self.version }
210/// fn is_yanked(&self) -> bool { self.yanked }
211/// fn as_any(&self) -> &dyn Any { self }
212/// }
213///
214/// let versions: Vec<Box<dyn Version>> = vec![
215/// Box::new(MyVersion { version: "2.0.0-alpha.1".into(), yanked: false }),
216/// Box::new(MyVersion { version: "1.5.0".into(), yanked: true }),
217/// Box::new(MyVersion { version: "1.4.0".into(), yanked: false }),
218/// ];
219///
220/// let latest = find_latest_stable(&versions);
221/// assert_eq!(latest.map(|v| v.version_string()), Some("1.4.0"));
222/// ```
223pub fn find_latest_stable(versions: &[Box<dyn Version>]) -> Option<&dyn Version> {
224 versions.iter().find(|v| v.is_stable()).map(|v| v.as_ref())
225}
226
227/// Package metadata trait.
228///
229/// Used for completion items and hover documentation.
230pub trait Metadata: Send + Sync {
231 /// Package name.
232 fn name(&self) -> &str;
233
234 /// Short description (optional).
235 fn description(&self) -> Option<&str>;
236
237 /// Repository URL (optional).
238 fn repository(&self) -> Option<&str>;
239
240 /// Documentation URL (optional).
241 fn documentation(&self) -> Option<&str>;
242
243 /// Latest stable version.
244 fn latest_version(&self) -> &str;
245
246 /// Downcast to concrete metadata type
247 fn as_any(&self) -> &dyn Any;
248}
249
250// Legacy traits for backward compatibility during migration
251// DEPRECATED: Use Registry, Version, Metadata instead
252//
253// These traits will be removed in Phase 3 after all ecosystem implementations
254// are migrated to the new trait object-based system.
255
256/// Legacy package registry trait with associated types.
257///
258/// # Deprecation Notice
259///
260/// This trait is deprecated. Use `Registry` trait instead which uses
261/// trait objects (`Box<dyn Version>`) for better extensibility.
262#[async_trait]
263pub trait PackageRegistry: Send + Sync {
264 /// Version information type for this registry.
265 type Version: VersionInfo + Clone + Send + Sync;
266
267 /// Metadata type for search results.
268 type Metadata: PackageMetadata + Clone + Send + Sync;
269
270 /// Version requirement type (e.g., semver::VersionReq for Cargo, npm semver for npm).
271 type VersionReq: Clone + Send + Sync;
272
273 /// Fetches all available versions for a package.
274 async fn get_versions(&self, name: &str) -> Result<Vec<Self::Version>>;
275
276 /// Finds the latest version matching a version requirement.
277 async fn get_latest_matching(
278 &self,
279 name: &str,
280 req: &Self::VersionReq,
281 ) -> Result<Option<Self::Version>>;
282
283 /// Searches for packages by name or keywords.
284 async fn search(&self, query: &str, limit: usize) -> Result<Vec<Self::Metadata>>;
285}
286
287/// Legacy version information trait.
288///
289/// # Deprecation Notice
290///
291/// This trait is deprecated. Use `Version` trait instead.
292pub trait VersionInfo {
293 /// Version string (e.g., "1.0.214", "14.21.3").
294 fn version_string(&self) -> &str;
295
296 /// Whether this version is yanked/deprecated.
297 fn is_yanked(&self) -> bool;
298
299 /// Whether this version is a pre-release (alpha, beta, rc, etc.).
300 ///
301 /// Default implementation checks for common pre-release patterns.
302 fn is_prerelease(&self) -> bool {
303 let v = self.version_string().to_lowercase();
304 v.contains("-alpha")
305 || v.contains("-beta")
306 || v.contains("-rc")
307 || v.contains("-dev")
308 || v.contains("-pre")
309 || v.contains("-snapshot")
310 || v.contains("-canary")
311 || v.contains("-nightly")
312 }
313
314 /// Available feature flags (empty if not supported by ecosystem).
315 fn features(&self) -> Vec<String> {
316 vec![]
317 }
318}
319
320/// Legacy package metadata trait.
321///
322/// # Deprecation Notice
323///
324/// This trait is deprecated. Use `Metadata` trait instead.
325pub trait PackageMetadata {
326 /// Package name.
327 fn name(&self) -> &str;
328
329 /// Short description (optional).
330 fn description(&self) -> Option<&str>;
331
332 /// Repository URL (optional).
333 fn repository(&self) -> Option<&str>;
334
335 /// Documentation URL (optional).
336 fn documentation(&self) -> Option<&str>;
337
338 /// Latest stable version.
339 fn latest_version(&self) -> &str;
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 struct MockVersion {
347 version: String,
348 yanked: bool,
349 }
350
351 impl Version for MockVersion {
352 fn version_string(&self) -> &str {
353 &self.version
354 }
355
356 fn is_yanked(&self) -> bool {
357 self.yanked
358 }
359
360 fn as_any(&self) -> &dyn Any {
361 self
362 }
363 }
364
365 #[test]
366 fn test_version_default_features() {
367 let version = MockVersion {
368 version: "1.0.0".into(),
369 yanked: false,
370 };
371
372 assert_eq!(version.features(), Vec::<String>::new());
373 }
374
375 #[test]
376 fn test_version_trait_object() {
377 let version = MockVersion {
378 version: "1.2.3".into(),
379 yanked: false,
380 };
381
382 let boxed: Box<dyn Version> = Box::new(version);
383 assert_eq!(boxed.version_string(), "1.2.3");
384 assert!(!boxed.is_yanked());
385 }
386
387 #[test]
388 fn test_version_downcast() {
389 let version = MockVersion {
390 version: "1.0.0".into(),
391 yanked: true,
392 };
393
394 let boxed: Box<dyn Version> = Box::new(version);
395 let any = boxed.as_any();
396
397 assert!(any.is::<MockVersion>());
398 }
399
400 struct MockMetadata {
401 name: String,
402 latest: String,
403 }
404
405 impl Metadata for MockMetadata {
406 fn name(&self) -> &str {
407 &self.name
408 }
409
410 fn description(&self) -> Option<&str> {
411 None
412 }
413
414 fn repository(&self) -> Option<&str> {
415 None
416 }
417
418 fn documentation(&self) -> Option<&str> {
419 None
420 }
421
422 fn latest_version(&self) -> &str {
423 &self.latest
424 }
425
426 fn as_any(&self) -> &dyn Any {
427 self
428 }
429 }
430
431 #[test]
432 fn test_metadata_trait_object() {
433 let metadata = MockMetadata {
434 name: "test-package".into(),
435 latest: "2.0.0".into(),
436 };
437
438 let boxed: Box<dyn Metadata> = Box::new(metadata);
439 assert_eq!(boxed.name(), "test-package");
440 assert_eq!(boxed.latest_version(), "2.0.0");
441 assert!(boxed.description().is_none());
442 assert!(boxed.repository().is_none());
443 assert!(boxed.documentation().is_none());
444 }
445
446 #[test]
447 fn test_metadata_with_full_info() {
448 struct FullMetadata {
449 name: String,
450 desc: String,
451 repo: String,
452 docs: String,
453 latest: String,
454 }
455
456 impl Metadata for FullMetadata {
457 fn name(&self) -> &str {
458 &self.name
459 }
460 fn description(&self) -> Option<&str> {
461 Some(&self.desc)
462 }
463 fn repository(&self) -> Option<&str> {
464 Some(&self.repo)
465 }
466 fn documentation(&self) -> Option<&str> {
467 Some(&self.docs)
468 }
469 fn latest_version(&self) -> &str {
470 &self.latest
471 }
472 fn as_any(&self) -> &dyn Any {
473 self
474 }
475 }
476
477 let meta = FullMetadata {
478 name: "serde".into(),
479 desc: "Serialization framework".into(),
480 repo: "https://github.com/serde-rs/serde".into(),
481 docs: "https://docs.rs/serde".into(),
482 latest: "1.0.214".into(),
483 };
484
485 assert_eq!(meta.description(), Some("Serialization framework"));
486 assert_eq!(meta.repository(), Some("https://github.com/serde-rs/serde"));
487 assert_eq!(meta.documentation(), Some("https://docs.rs/serde"));
488 }
489
490 struct MockVersionInfo {
491 version: String,
492 }
493
494 impl VersionInfo for MockVersionInfo {
495 fn version_string(&self) -> &str {
496 &self.version
497 }
498
499 fn is_yanked(&self) -> bool {
500 false
501 }
502 }
503
504 #[test]
505 fn test_is_prerelease_alpha() {
506 let version = MockVersionInfo {
507 version: "4.0.0-alpha.13".into(),
508 };
509 assert!(version.is_prerelease());
510 }
511
512 #[test]
513 fn test_is_prerelease_beta() {
514 let version = MockVersionInfo {
515 version: "2.0.0-beta.1".into(),
516 };
517 assert!(version.is_prerelease());
518 }
519
520 #[test]
521 fn test_is_prerelease_rc() {
522 let version = MockVersionInfo {
523 version: "1.5.0-rc.2".into(),
524 };
525 assert!(version.is_prerelease());
526 }
527
528 #[test]
529 fn test_is_prerelease_dev() {
530 let version = MockVersionInfo {
531 version: "3.0.0-dev".into(),
532 };
533 assert!(version.is_prerelease());
534 }
535
536 #[test]
537 fn test_is_prerelease_canary() {
538 let version = MockVersionInfo {
539 version: "5.0.0-canary".into(),
540 };
541 assert!(version.is_prerelease());
542 }
543
544 #[test]
545 fn test_is_prerelease_nightly() {
546 let version = MockVersionInfo {
547 version: "6.0.0-nightly".into(),
548 };
549 assert!(version.is_prerelease());
550 }
551
552 #[test]
553 fn test_is_not_prerelease_stable() {
554 let version = MockVersionInfo {
555 version: "1.2.3".into(),
556 };
557 assert!(!version.is_prerelease());
558 }
559
560 #[test]
561 fn test_is_not_prerelease_patch() {
562 let version = MockVersionInfo {
563 version: "1.0.214".into(),
564 };
565 assert!(!version.is_prerelease());
566 }
567
568 #[test]
569 fn test_is_stable_true() {
570 let version = MockVersion {
571 version: "1.0.0".into(),
572 yanked: false,
573 };
574 assert!(version.is_stable());
575 }
576
577 #[test]
578 fn test_is_stable_false_yanked() {
579 let version = MockVersion {
580 version: "1.0.0".into(),
581 yanked: true,
582 };
583 assert!(!version.is_stable());
584 }
585
586 #[test]
587 fn test_is_stable_false_prerelease() {
588 let version = MockVersion {
589 version: "1.0.0-alpha.1".into(),
590 yanked: false,
591 };
592 assert!(!version.is_stable());
593 }
594
595 #[test]
596 fn test_find_latest_stable_skips_prerelease() {
597 let versions: Vec<Box<dyn Version>> = vec![
598 Box::new(MockVersion {
599 version: "2.0.0-alpha.1".into(),
600 yanked: false,
601 }),
602 Box::new(MockVersion {
603 version: "1.5.0".into(),
604 yanked: false,
605 }),
606 ];
607 let latest = super::find_latest_stable(&versions);
608 assert_eq!(latest.map(|v| v.version_string()), Some("1.5.0"));
609 }
610
611 #[test]
612 fn test_find_latest_stable_skips_yanked() {
613 let versions: Vec<Box<dyn Version>> = vec![
614 Box::new(MockVersion {
615 version: "2.0.0".into(),
616 yanked: true,
617 }),
618 Box::new(MockVersion {
619 version: "1.5.0".into(),
620 yanked: false,
621 }),
622 ];
623 let latest = super::find_latest_stable(&versions);
624 assert_eq!(latest.map(|v| v.version_string()), Some("1.5.0"));
625 }
626
627 #[test]
628 fn test_find_latest_stable_returns_first_stable() {
629 let versions: Vec<Box<dyn Version>> = vec![
630 Box::new(MockVersion {
631 version: "3.0.0-beta.1".into(),
632 yanked: false,
633 }),
634 Box::new(MockVersion {
635 version: "2.0.0".into(),
636 yanked: true,
637 }),
638 Box::new(MockVersion {
639 version: "1.5.0".into(),
640 yanked: false,
641 }),
642 Box::new(MockVersion {
643 version: "1.4.0".into(),
644 yanked: false,
645 }),
646 ];
647 let latest = super::find_latest_stable(&versions);
648 assert_eq!(latest.map(|v| v.version_string()), Some("1.5.0"));
649 }
650
651 #[test]
652 fn test_find_latest_stable_empty_list() {
653 let versions: Vec<Box<dyn Version>> = vec![];
654 let latest = super::find_latest_stable(&versions);
655 assert!(latest.is_none());
656 }
657
658 #[test]
659 fn test_find_latest_stable_no_stable_versions() {
660 let versions: Vec<Box<dyn Version>> = vec![
661 Box::new(MockVersion {
662 version: "2.0.0-alpha.1".into(),
663 yanked: false,
664 }),
665 Box::new(MockVersion {
666 version: "1.0.0".into(),
667 yanked: true,
668 }),
669 ];
670 let latest = super::find_latest_stable(&versions);
671 assert!(latest.is_none());
672 }
673}