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