Skip to main content

deps_bundler/
types.rs

1//! Domain types for Bundler dependencies.
2
3use std::any::Any;
4use tower_lsp_server::ls_types::Range;
5
6/// Parsed dependency from Gemfile with position tracking.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct BundlerDependency {
9    pub name: deps_core::PackageName,
10    pub name_range: Range,
11    pub version_req: Option<deps_core::VersionReq>,
12    pub version_range: Option<Range>,
13    pub group: DependencyGroup,
14    pub source: DependencySource,
15    pub platforms: Vec<String>,
16    pub require: Option<String>,
17}
18
19pub use deps_core::parser::DependencySource;
20
21/// Gem group classification.
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
23pub enum DependencyGroup {
24    /// No explicit group (runtime dependency)
25    #[default]
26    Default,
27    /// :development group
28    Development,
29    /// :test group
30    Test,
31    /// :production group
32    Production,
33    /// Custom group name
34    Custom(String),
35}
36
37/// Version information for a gem from rubygems.org.
38#[derive(Debug, Clone)]
39pub struct BundlerVersion {
40    pub number: deps_core::ConcreteVersion,
41    pub prerelease: bool,
42    pub yanked: bool,
43    /// Publish timestamp, parsed eagerly from the API's `created_at` field.
44    ///
45    /// `None` when the response omits it or the value fails to parse as
46    /// RFC 3339 — degrades gracefully, per
47    /// [US-003](https://github.com/bug-ops/deps-lsp/issues/145).
48    pub published_at: Option<deps_core::PublishTime>,
49    pub platform: String,
50}
51
52impl BundlerVersion {
53    /// Returns true if this is a stable (non-prerelease) version.
54    pub fn is_stable(&self) -> bool {
55        !self.prerelease && !self.yanked
56    }
57}
58
59/// Gem metadata from rubygems.org.
60#[derive(Debug, Clone)]
61pub struct GemInfo {
62    pub name: deps_core::PackageName,
63    pub info: Option<String>,
64    pub homepage_uri: Option<String>,
65    pub source_code_uri: Option<String>,
66    pub documentation_uri: Option<String>,
67    pub version: deps_core::ConcreteVersion,
68    pub licenses: Vec<String>,
69    pub authors: Option<String>,
70    pub downloads: u64,
71}
72
73// Trait implementations for deps-core integration
74
75impl deps_core::Dependency for BundlerDependency {
76    fn name(&self) -> &deps_core::PackageName {
77        &self.name
78    }
79
80    fn name_range(&self) -> Range {
81        self.name_range
82    }
83
84    fn version_requirement(&self) -> Option<&deps_core::VersionReq> {
85        self.version_req.as_ref()
86    }
87
88    fn version_range(&self) -> Option<Range> {
89        self.version_range
90    }
91
92    fn source(&self) -> deps_core::parser::DependencySource {
93        self.source.clone()
94    }
95
96    fn features(&self) -> &[String] {
97        &[]
98    }
99
100    fn as_any(&self) -> &dyn Any {
101        self
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use std::assert_matches;
109    use tower_lsp_server::ls_types::Position;
110
111    fn create_test_dependency(source: DependencySource) -> BundlerDependency {
112        BundlerDependency {
113            name: "test_gem".into(),
114            name_range: Range::new(Position::new(1, 5), Position::new(1, 13)),
115            version_req: Some("~> 1.0".into()),
116            version_range: Some(Range::new(Position::new(1, 17), Position::new(1, 23))),
117            group: DependencyGroup::Default,
118            source,
119            platforms: vec![],
120            require: None,
121        }
122    }
123
124    #[test]
125    fn test_dependency_source_variants() {
126        let registry = DependencySource::Registry;
127        let git = DependencySource::Git {
128            url: "https://github.com/rails/rails".into(),
129            rev: Some("main".into()),
130        };
131        let path = DependencySource::Path {
132            path: "../local".into(),
133        };
134        let custom = DependencySource::CustomRegistry {
135            url: "https://custom.gem.source".into(),
136        };
137
138        assert!(registry.is_registry());
139        assert!(!git.is_registry());
140        assert!(!path.is_registry());
141        assert!(custom.is_registry());
142    }
143
144    #[test]
145    fn test_dependency_group_variants() {
146        let default = DependencyGroup::Default;
147        let dev = DependencyGroup::Development;
148        let test = DependencyGroup::Test;
149        let prod = DependencyGroup::Production;
150        let custom = DependencyGroup::Custom("staging".into());
151
152        assert_matches!(default, DependencyGroup::Default);
153        assert_matches!(dev, DependencyGroup::Development);
154        assert_matches!(test, DependencyGroup::Test);
155        assert_matches!(prod, DependencyGroup::Production);
156        assert_matches!(custom, DependencyGroup::Custom(_));
157    }
158
159    #[test]
160    fn test_dependency_group_default() {
161        let group = DependencyGroup::default();
162        assert_matches!(group, DependencyGroup::Default);
163    }
164
165    #[test]
166    fn test_bundler_version_creation() {
167        let version = BundlerVersion {
168            number: "7.0.8".into(),
169            prerelease: false,
170            yanked: false,
171            published_at: Some(deps_core::PublishTime::from_unix_secs(1_694_208_000)),
172            platform: "ruby".into(),
173        };
174
175        assert_eq!(version.number, "7.0.8");
176        assert!(!version.yanked);
177        assert!(version.is_stable());
178    }
179
180    #[test]
181    fn test_bundler_version_prerelease() {
182        let version = BundlerVersion {
183            number: "7.1.0.beta1".into(),
184            prerelease: true,
185            yanked: false,
186            published_at: None,
187            platform: "ruby".into(),
188        };
189
190        assert!(version.prerelease);
191        assert!(!version.is_stable());
192    }
193
194    #[test]
195    fn test_bundler_version_yanked() {
196        let version = BundlerVersion {
197            number: "1.0.0".into(),
198            prerelease: false,
199            yanked: true,
200            published_at: None,
201            platform: "ruby".into(),
202        };
203
204        assert!(version.yanked);
205        assert!(!version.is_stable());
206    }
207
208    #[test]
209    fn test_bundler_dependency_trait() {
210        use deps_core::Dependency;
211
212        let dep = BundlerDependency {
213            name: "rails".into(),
214            name_range: Range::new(Position::new(1, 5), Position::new(1, 10)),
215            version_req: Some("~> 7.0".into()),
216            version_range: Some(Range::new(Position::new(1, 14), Position::new(1, 20))),
217            group: DependencyGroup::Default,
218            source: DependencySource::Registry,
219            platforms: vec![],
220            require: None,
221        };
222
223        assert_eq!(dep.name(), "rails");
224        assert_eq!(
225            dep.version_requirement().map(deps_core::VersionReq::as_str),
226            Some("~> 7.0")
227        );
228    }
229
230    #[test]
231    fn test_dependency_info_trait_registry() {
232        use deps_core::Dependency;
233
234        let dep = create_test_dependency(DependencySource::Registry);
235        assert_eq!(dep.name(), "test_gem");
236        assert_eq!(dep.name_range().start.line, 1);
237        assert_eq!(
238            dep.version_requirement().map(deps_core::VersionReq::as_str),
239            Some("~> 1.0")
240        );
241        assert!(dep.version_range().is_some());
242        assert!(dep.features().is_empty());
243        assert!(dep.source().is_registry());
244    }
245
246    #[test]
247    fn test_dependency_info_trait_git() {
248        use deps_core::Dependency;
249
250        let dep = create_test_dependency(DependencySource::Git {
251            url: "https://github.com/rails/rails".into(),
252            rev: Some("abc123".into()),
253        });
254
255        match dep.source() {
256            deps_core::parser::DependencySource::Git { url, rev } => {
257                assert_eq!(url, "https://github.com/rails/rails");
258                assert_eq!(rev, Some("abc123".to_string()));
259            }
260            _ => panic!("Expected Git source"),
261        }
262    }
263
264    #[test]
265    fn test_dependency_info_trait_path() {
266        use deps_core::Dependency;
267
268        let dep = create_test_dependency(DependencySource::Path {
269            path: "../local_gem".into(),
270        });
271
272        match dep.source() {
273            deps_core::parser::DependencySource::Path { path } => {
274                assert_eq!(path, "../local_gem");
275            }
276            _ => panic!("Expected Path source"),
277        }
278    }
279
280    #[test]
281    fn test_dependency_info_trait_custom_source() {
282        use deps_core::Dependency;
283
284        let dep = create_test_dependency(DependencySource::CustomRegistry {
285            url: "https://gems.example.com".into(),
286        });
287
288        assert!(dep.source().is_registry());
289    }
290
291    #[test]
292    fn test_dependency_trait_as_any() {
293        use deps_core::Dependency;
294
295        let dep = create_test_dependency(DependencySource::Registry);
296        let any = dep.as_any();
297        assert!(any.is::<BundlerDependency>());
298        assert!(any.downcast_ref::<BundlerDependency>().is_some());
299    }
300
301    #[test]
302    fn test_dependency_trait_source_conversions() {
303        use deps_core::Dependency;
304
305        let sources = vec![
306            DependencySource::Registry,
307            DependencySource::Git {
308                url: "https://github.com/test/repo".into(),
309                rev: Some("v1.0".into()),
310            },
311            DependencySource::Path {
312                path: "./local".into(),
313            },
314            DependencySource::CustomRegistry {
315                url: "https://custom.example.com".into(),
316            },
317        ];
318
319        for source in sources {
320            let dep = create_test_dependency(source);
321            let _ = dep.source();
322        }
323    }
324
325    #[test]
326    fn test_dependency_without_version() {
327        use deps_core::Dependency;
328
329        let dep = BundlerDependency {
330            name: "test".into(),
331            name_range: Range::default(),
332            version_req: None,
333            version_range: None,
334            group: DependencyGroup::Default,
335            source: DependencySource::Registry,
336            platforms: vec![],
337            require: None,
338        };
339
340        assert_eq!(dep.name(), "test");
341        assert!(dep.version_requirement().is_none());
342        assert!(dep.version_range().is_none());
343    }
344
345    #[test]
346    fn test_version_trait_implementation() {
347        use deps_core::Version;
348
349        let version = BundlerVersion {
350            number: "1.2.3".into(),
351            prerelease: false,
352            yanked: false,
353            published_at: Some(deps_core::PublishTime::from_unix_secs(1_704_067_200)),
354            platform: "ruby".into(),
355        };
356
357        assert_eq!(version.version_string(), "1.2.3");
358        assert!(!version.removal_status().blocks_resolution());
359        assert!(version.features().is_empty());
360        assert!(version.as_any().is::<BundlerVersion>());
361    }
362
363    #[test]
364    fn test_version_trait_yanked() {
365        use deps_core::Version;
366
367        let version = BundlerVersion {
368            number: "1.0.0".into(),
369            prerelease: false,
370            yanked: true,
371            published_at: None,
372            platform: "ruby".into(),
373        };
374
375        assert!(version.removal_status().blocks_resolution());
376    }
377
378    #[test]
379    fn test_metadata_trait_full() {
380        use deps_core::Metadata;
381
382        let gem = GemInfo {
383            name: "rails".into(),
384            info: Some("Full-stack web application framework".into()),
385            homepage_uri: Some("https://rubyonrails.org".into()),
386            source_code_uri: Some("https://github.com/rails/rails".into()),
387            documentation_uri: Some("https://api.rubyonrails.org".into()),
388            version: "7.0.8".into(),
389            licenses: vec!["MIT".into()],
390            authors: Some("DHH".into()),
391            downloads: 500_000_000,
392        };
393
394        assert_eq!(gem.name(), "rails");
395        assert_eq!(
396            gem.description(),
397            Some("Full-stack web application framework")
398        );
399        assert_eq!(gem.repository(), Some("https://github.com/rails/rails"));
400        assert_eq!(gem.documentation(), Some("https://api.rubyonrails.org"));
401        assert_eq!(gem.latest_version(), "7.0.8");
402        assert!(gem.as_any().is::<GemInfo>());
403    }
404
405    #[test]
406    fn test_metadata_trait_minimal() {
407        use deps_core::Metadata;
408
409        let gem = GemInfo {
410            name: "minimal".into(),
411            info: None,
412            homepage_uri: None,
413            source_code_uri: None,
414            documentation_uri: None,
415            version: "0.1.0".into(),
416            licenses: vec![],
417            authors: None,
418            downloads: 0,
419        };
420
421        assert_eq!(gem.name(), "minimal");
422        assert!(gem.description().is_none());
423        assert!(gem.repository().is_none());
424        assert!(gem.documentation().is_none());
425        assert_eq!(gem.latest_version(), "0.1.0");
426    }
427
428    #[test]
429    fn test_gem_info_fields() {
430        let gem = GemInfo {
431            name: "test".into(),
432            info: Some("Test gem".into()),
433            homepage_uri: Some("https://example.com".into()),
434            source_code_uri: Some("https://github.com/test/test".into()),
435            documentation_uri: Some("https://docs.example.com".into()),
436            version: "1.0.0".into(),
437            licenses: vec!["MIT".into(), "Apache-2.0".into()],
438            authors: Some("Test Author".into()),
439            downloads: 1000,
440        };
441
442        assert_eq!(gem.licenses.len(), 2);
443        assert_eq!(gem.authors, Some("Test Author".into()));
444        assert_eq!(gem.downloads, 1000);
445    }
446
447    #[test]
448    fn test_bundler_dependency_clone() {
449        let dep = create_test_dependency(DependencySource::Registry);
450        let cloned = dep.clone();
451        assert_eq!(dep, cloned);
452    }
453
454    #[test]
455    fn test_bundler_dependency_debug() {
456        let dep = create_test_dependency(DependencySource::Registry);
457        let debug_str = format!("{:?}", dep);
458        assert!(debug_str.contains("test_gem"));
459    }
460}