Skip to main content

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}