Skip to main content

deps_core/
registry.rs

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