1use crate::types::{BundlerVersion, GemInfo};
6use crate::version::{compare_versions, version_matches_requirement};
7use deps_core::{HttpCache, Result};
8use serde::Deserialize;
9use std::any::Any;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13const RUBYGEMS_API_BASE: &str = "https://rubygems.org/api/v1";
14
15pub const REGISTRY: &str = "RubyGems";
17
18pub const RUBYGEMS_URL: &str = "https://rubygems.org/gems";
20
21pub fn gem_url(name: &str) -> String {
27 format!("{RUBYGEMS_URL}/{}", urlencoding::encode(name))
28}
29
30fn versions_url(name: &str) -> String {
32 format!(
33 "{RUBYGEMS_API_BASE}/versions/{}.json",
34 urlencoding::encode(name)
35 )
36}
37
38fn gem_info_url(name: &str) -> String {
40 format!(
41 "{RUBYGEMS_API_BASE}/gems/{}.json",
42 urlencoding::encode(name)
43 )
44}
45
46#[derive(Clone)]
48pub struct RubyGemsRegistry {
49 cache: Arc<HttpCache>,
50}
51
52impl RubyGemsRegistry {
53 pub const fn new(cache: Arc<HttpCache>) -> Self {
55 Self { cache }
56 }
57
58 pub async fn get_versions(&self, name: &str) -> Result<Vec<BundlerVersion>> {
60 let url = versions_url(name);
61 let data = self.cache.get_cached(&url).await?;
62 parse_versions_response(&data, name)
63 }
64
65 pub async fn get_latest_matching(
67 &self,
68 name: &str,
69 req_str: &str,
70 ) -> Result<Option<BundlerVersion>> {
71 let versions = self.get_versions(name).await?;
72 Ok(versions
73 .into_iter()
74 .find(|v| version_matches_requirement(v.number.as_str(), req_str) && !v.yanked))
75 }
76
77 pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<GemInfo>> {
79 let url = format!(
80 "{}/search.json?query={}",
81 RUBYGEMS_API_BASE,
82 urlencoding::encode(query)
83 );
84 let data = self.cache.get_cached(&url).await?;
85 let gems = parse_search_response(&data)?;
86 Ok(gems.into_iter().take(limit).collect())
87 }
88
89 pub async fn get_gem_info(&self, name: &str) -> Result<GemInfo> {
91 let url = gem_info_url(name);
92 let data = self.cache.get_cached(&url).await?;
93 parse_gem_info(&data)
94 }
95}
96
97#[derive(Deserialize)]
98struct VersionEntry {
99 number: String,
100 #[serde(default)]
101 prerelease: bool,
102 #[serde(default)]
109 yanked: bool,
110 created_at: Option<String>,
111 #[serde(default = "default_platform")]
112 platform: String,
113}
114
115fn default_platform() -> String {
116 "ruby".to_string()
117}
118
119fn parse_versions_response(data: &[u8], _gem_name: &str) -> Result<Vec<BundlerVersion>> {
120 let entries: Vec<VersionEntry> = deps_core::parse_json_checked(data)?;
121
122 let mut versions: Vec<BundlerVersion> = Vec::with_capacity(entries.len());
132 let mut index_by_number: HashMap<String, usize> = HashMap::with_capacity(entries.len());
133 for e in entries {
134 let version = BundlerVersion {
135 number: e.number.into(),
136 prerelease: e.prerelease,
137 yanked: e.yanked,
138 published_at: e
139 .created_at
140 .as_deref()
141 .and_then(deps_core::PublishTime::parse_rfc3339),
142 platform: e.platform,
143 };
144 if let Some(&idx) = index_by_number.get(version.number.as_str()) {
145 if version.platform == "ruby" && versions[idx].platform != "ruby" {
146 versions[idx] = version;
147 }
148 } else {
149 index_by_number.insert(version.number.to_string(), versions.len());
150 versions.push(version);
151 }
152 }
153
154 versions.sort_by(|a, b| compare_versions(b.number.as_str(), a.number.as_str()));
156
157 Ok(versions)
158}
159
160#[derive(Deserialize)]
161struct SearchEntry {
162 name: String,
163 info: Option<String>,
164 version: String,
165 #[serde(default)]
166 downloads: u64,
167}
168
169fn parse_search_response(data: &[u8]) -> Result<Vec<GemInfo>> {
170 let entries: Vec<SearchEntry> = deps_core::parse_json_checked(data)?;
171
172 Ok(entries
173 .into_iter()
174 .map(|e| GemInfo {
175 name: e.name.into(),
176 info: e.info,
177 homepage_uri: None,
178 source_code_uri: None,
179 documentation_uri: None,
180 version: e.version.into(),
181 licenses: vec![],
182 authors: None,
183 downloads: e.downloads,
184 })
185 .collect())
186}
187
188#[derive(Deserialize)]
189struct GemInfoResponse {
190 name: String,
191 info: Option<String>,
192 version: String,
193 homepage_uri: Option<String>,
194 source_code_uri: Option<String>,
195 documentation_uri: Option<String>,
196 #[serde(default)]
197 licenses: Vec<String>,
198 authors: Option<String>,
199 #[serde(default)]
200 downloads: u64,
201}
202
203fn parse_gem_info(data: &[u8]) -> Result<GemInfo> {
204 let response: GemInfoResponse = deps_core::parse_json_checked(data)?;
205
206 Ok(GemInfo {
207 name: response.name.into(),
208 info: response.info,
209 homepage_uri: response.homepage_uri,
210 source_code_uri: response.source_code_uri,
211 documentation_uri: response.documentation_uri,
212 version: response.version.into(),
213 licenses: response.licenses,
214 authors: response.authors,
215 downloads: response.downloads,
216 })
217}
218
219impl deps_core::Version for BundlerVersion {
220 fn version_string(&self) -> &deps_core::ConcreteVersion {
221 &self.number
222 }
223
224 fn removal_status(&self) -> deps_core::RemovalStatus {
225 deps_core::RemovalStatus::from_yanked(self.yanked)
226 }
227
228 fn is_prerelease(&self) -> bool {
233 self.prerelease
234 }
235
236 fn published_at(&self) -> Option<deps_core::PublishTime> {
237 self.published_at
238 }
239
240 fn as_any(&self) -> &dyn std::any::Any {
241 self
242 }
243}
244
245impl deps_core::Metadata for GemInfo {
246 fn name(&self) -> &deps_core::PackageName {
247 &self.name
248 }
249
250 fn description(&self) -> Option<&str> {
251 self.info.as_deref()
252 }
253
254 fn repository(&self) -> Option<&str> {
255 self.source_code_uri.as_deref()
256 }
257
258 fn documentation(&self) -> Option<&str> {
259 self.documentation_uri.as_deref()
260 }
261
262 fn latest_version(&self) -> &deps_core::ConcreteVersion {
263 &self.version
264 }
265
266 fn as_any(&self) -> &dyn std::any::Any {
267 self
268 }
269}
270
271impl deps_core::Registry for RubyGemsRegistry {
273 fn get_versions<'a>(
274 &'a self,
275 name: &'a deps_core::PackageName,
276 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
277 Box::pin(async move {
278 let versions = self.get_versions(name.as_str()).await?;
279 Ok(versions
280 .into_iter()
281 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
282 .collect())
283 })
284 }
285
286 fn get_latest_matching<'a>(
287 &'a self,
288 name: &'a deps_core::PackageName,
289 req: &'a deps_core::VersionReq,
290 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
291 Box::pin(async move {
292 let version = self
293 .get_latest_matching(name.as_str(), req.as_str())
294 .await?;
295 Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
296 })
297 }
298
299 fn search<'a>(
300 &'a self,
301 query: &'a str,
302 limit: usize,
303 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
304 Box::pin(async move {
305 let results = self.search(query, limit).await?;
306 Ok(results
307 .into_iter()
308 .map(|m| Box::new(m) as Box<dyn deps_core::Metadata>)
309 .collect())
310 })
311 }
312
313 fn select_latest_matching(
314 &self,
315 versions: &[Box<dyn deps_core::Version>],
316 req: &deps_core::VersionReq,
317 ) -> Option<usize> {
318 versions.iter().position(|v| {
319 version_matches_requirement(v.version_string().as_str(), req.as_str())
320 && !v.removal_status().blocks_resolution()
321 })
322 }
323
324 fn reports_yanked(&self) -> bool {
333 false
334 }
335
336 fn as_any(&self) -> &dyn Any {
337 self
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_gem_url() {
347 assert_eq!(gem_url("rails"), "https://rubygems.org/gems/rails");
348 assert_eq!(gem_url("nokogiri"), "https://rubygems.org/gems/nokogiri");
349 }
350
351 #[test]
352 fn test_gem_url_special_chars() {
353 assert_eq!(
354 gem_url("rspec-rails"),
355 "https://rubygems.org/gems/rspec-rails"
356 );
357 assert_eq!(
358 gem_url("activerecord-import"),
359 "https://rubygems.org/gems/activerecord-import"
360 );
361 }
362
363 #[test]
364 fn test_gem_url_encodes_malicious_name() {
365 let url = gem_url("evil](https://evil.example)[pkg");
366 assert!(!url.contains('('));
367 assert!(!url.contains(')'));
368 assert!(!url.contains('['));
369 assert!(!url.contains(']'));
370 }
371
372 #[test]
373 fn test_gem_url_encodes_newline_autolink_and_percent() {
374 let url = gem_url("evil\n<https://evil%zz.example>");
375 assert!(!url.contains('\n'));
376 assert!(!url.contains('<'));
377 assert!(!url.contains('>'));
378 assert!(url.contains("%25"));
379 }
380
381 #[test]
382 fn test_gem_url_empty_name() {
383 assert_eq!(gem_url(""), "https://rubygems.org/gems/");
384 }
385
386 #[test]
387 fn test_parse_versions_response() {
388 let json = r#"[
389 {"number": "7.0.8", "prerelease": false, "yanked": false, "platform": "ruby"},
390 {"number": "7.0.7", "prerelease": false, "yanked": false, "platform": "ruby"},
391 {"number": "7.1.0.beta1", "prerelease": true, "yanked": false, "platform": "ruby"}
392 ]"#;
393
394 let versions = parse_versions_response(json.as_bytes(), "rails").unwrap();
395 assert_eq!(versions.len(), 3);
396 assert!(versions[0].prerelease); }
398
399 #[test]
400 fn test_parse_versions_response_with_yanked() {
401 let json = r#"[
402 {"number": "1.0.0", "prerelease": false, "yanked": true, "platform": "ruby"},
403 {"number": "0.9.0", "prerelease": false, "yanked": false, "platform": "ruby"}
404 ]"#;
405
406 let versions = parse_versions_response(json.as_bytes(), "test").unwrap();
407 assert_eq!(versions.len(), 2);
408 assert!(versions[0].yanked);
409 assert!(!versions[1].yanked);
410 }
411
412 #[test]
413 fn test_parse_versions_response_with_created_at() {
414 let json = r#"[
415 {"number": "1.0.0", "prerelease": false, "yanked": false, "created_at": "2024-01-15T10:30:00Z", "platform": "ruby"}
416 ]"#;
417
418 let versions = parse_versions_response(json.as_bytes(), "test").unwrap();
419 assert_eq!(versions.len(), 1);
420 assert_eq!(
421 versions[0].published_at,
422 deps_core::PublishTime::parse_rfc3339("2024-01-15T10:30:00Z")
423 );
424 }
425
426 #[test]
427 fn test_parse_versions_response_without_created_at() {
428 let json = r#"[
429 {"number": "1.0.0", "prerelease": false, "yanked": false, "platform": "ruby"}
430 ]"#;
431
432 let versions = parse_versions_response(json.as_bytes(), "test").unwrap();
433 assert_eq!(versions.len(), 1);
434 assert!(versions[0].published_at.is_none());
435 }
436
437 #[test]
438 fn test_parse_versions_response_with_malformed_created_at() {
439 let json = r#"[
440 {"number": "1.0.0", "prerelease": false, "yanked": false, "created_at": "not-a-timestamp", "platform": "ruby"}
441 ]"#;
442
443 let versions = parse_versions_response(json.as_bytes(), "test").unwrap();
444 assert_eq!(versions.len(), 1);
445 assert!(
446 versions[0].published_at.is_none(),
447 "malformed created_at degrades to None, not an error"
448 );
449 }
450
451 #[test]
452 fn test_parse_versions_response_default_platform() {
453 let json = r#"[
454 {"number": "1.0.0", "prerelease": false, "yanked": false}
455 ]"#;
456
457 let versions = parse_versions_response(json.as_bytes(), "test").unwrap();
458 assert_eq!(versions.len(), 1);
459 assert_eq!(versions[0].platform, "ruby");
460 }
461
462 #[test]
463 fn test_parse_versions_response_sorting() {
464 let json = r#"[
465 {"number": "1.0.0", "prerelease": false, "yanked": false},
466 {"number": "2.0.0", "prerelease": false, "yanked": false},
467 {"number": "1.5.0", "prerelease": false, "yanked": false}
468 ]"#;
469
470 let versions = parse_versions_response(json.as_bytes(), "test").unwrap();
471 assert_eq!(versions[0].number, "2.0.0");
472 assert_eq!(versions[1].number, "1.5.0");
473 assert_eq!(versions[2].number, "1.0.0");
474 }
475
476 #[test]
477 fn test_parse_versions_response_dedups_platform_duplicates() {
478 let json = r#"[
479 {"number": "1.19.4", "prerelease": false, "yanked": false, "platform": "x86_64-linux"},
480 {"number": "1.19.4", "prerelease": false, "yanked": false, "platform": "ruby"},
481 {"number": "1.19.4", "prerelease": false, "yanked": false, "platform": "x64-mingw32"},
482 {"number": "1.19.3", "prerelease": false, "yanked": false, "platform": "java"}
483 ]"#;
484
485 let versions = parse_versions_response(json.as_bytes(), "nokogiri").unwrap();
486 assert_eq!(versions.len(), 2);
487 assert_eq!(versions[0].number, "1.19.4");
488 assert_eq!(
489 versions[0].platform, "ruby",
490 "dedup should prefer the ruby platform entry"
491 );
492 assert_eq!(versions[1].number, "1.19.3");
493 assert_eq!(versions[1].platform, "java");
494 }
495
496 #[test]
497 fn test_parse_versions_response_dedup_without_ruby_platform() {
498 let json = r#"[
499 {"number": "2.0.0", "prerelease": false, "yanked": false, "platform": "x86-mswin32"},
500 {"number": "2.0.0", "prerelease": false, "yanked": false, "platform": "java"}
501 ]"#;
502
503 let versions = parse_versions_response(json.as_bytes(), "grpc").unwrap();
504 assert_eq!(versions.len(), 1);
505 assert_eq!(versions[0].number, "2.0.0");
506 }
507
508 #[test]
509 fn test_parse_versions_response_single_version_across_all_platforms() {
510 let json = r#"[
514 {"number": "1.0.0", "prerelease": false, "yanked": false, "platform": "x86_64-linux"},
515 {"number": "1.0.0", "prerelease": false, "yanked": false, "platform": "x86_64-darwin"},
516 {"number": "1.0.0", "prerelease": false, "yanked": false, "platform": "x64-mingw32"},
517 {"number": "1.0.0", "prerelease": false, "yanked": false, "platform": "java"},
518 {"number": "1.0.0", "prerelease": false, "yanked": false, "platform": "ruby"}
519 ]"#;
520
521 let versions = parse_versions_response(json.as_bytes(), "grpc").unwrap();
522 assert_eq!(
523 versions.len(),
524 1,
525 "five platform entries for one version number must dedup to one distinct version"
526 );
527 assert_eq!(versions[0].platform, "ruby");
528 }
529
530 #[test]
531 fn test_parse_versions_response_dedup_is_order_deterministic_across_runs() {
532 let json = r#"[
541 {"number": "1.0", "prerelease": false, "yanked": false, "platform": "ruby"},
542 {"number": "2.0.0", "prerelease": false, "yanked": false, "platform": "ruby"},
543 {"number": "1.0.0", "prerelease": false, "yanked": false, "platform": "ruby"}
544 ]"#;
545
546 for _ in 0..20 {
547 let versions = parse_versions_response(json.as_bytes(), "mime-types").unwrap();
548 assert_eq!(versions.len(), 3);
549 assert_eq!(versions[0].number, "2.0.0");
550 assert_eq!(versions[1].number, "1.0");
551 assert_eq!(versions[2].number, "1.0.0");
552 }
553 }
554
555 #[test]
556 fn test_parse_versions_response_orders_dot_notation_prerelease_below_stable() {
557 let json = r#"[
563 {"number": "3.7.0.pre2", "prerelease": true, "yanked": false, "platform": "ruby"},
564 {"number": "3.7.0.pre1", "prerelease": true, "yanked": false, "platform": "ruby"},
565 {"number": "3.7.0", "prerelease": false, "yanked": false, "platform": "ruby"}
566 ]"#;
567
568 let versions = parse_versions_response(json.as_bytes(), "mime-types").unwrap();
569 assert_eq!(versions.len(), 3);
570 assert_eq!(versions[0].number, "3.7.0");
571 assert_eq!(versions[1].number, "3.7.0.pre2");
572 assert_eq!(versions[2].number, "3.7.0.pre1");
573 }
574
575 #[test]
576 fn test_parse_versions_response_empty() {
577 let json = r"[]";
578 let versions = parse_versions_response(json.as_bytes(), "test").unwrap();
579 assert!(versions.is_empty());
580 }
581
582 #[test]
583 fn test_parse_search_response() {
584 let json = r#"[
585 {"name": "rails", "info": "Ruby on Rails", "version": "7.0.8", "downloads": 500000000},
586 {"name": "railties", "info": "Core", "version": "7.0.8", "downloads": 100000000}
587 ]"#;
588
589 let results = parse_search_response(json.as_bytes()).unwrap();
590 assert_eq!(results.len(), 2);
591 assert_eq!(results[0].name, "rails");
592 assert_eq!(results[0].info, Some("Ruby on Rails".to_string()));
593 assert_eq!(results[0].version, "7.0.8");
594 assert_eq!(results[0].downloads, 500_000_000);
595 }
596
597 #[test]
598 fn test_parse_search_response_minimal() {
599 let json = r#"[
600 {"name": "test", "version": "1.0.0"}
601 ]"#;
602
603 let results = parse_search_response(json.as_bytes()).unwrap();
604 assert_eq!(results.len(), 1);
605 assert_eq!(results[0].name, "test");
606 assert!(results[0].info.is_none());
607 assert_eq!(results[0].downloads, 0);
608 }
609
610 #[test]
611 fn test_parse_search_response_empty() {
612 let json = r"[]";
613 let results = parse_search_response(json.as_bytes()).unwrap();
614 assert!(results.is_empty());
615 }
616
617 #[test]
618 fn test_parse_gem_info_full() {
619 let json = r#"{
620 "name": "rails",
621 "info": "Full-stack web application framework",
622 "version": "7.0.8",
623 "homepage_uri": "https://rubyonrails.org",
624 "source_code_uri": "https://github.com/rails/rails",
625 "documentation_uri": "https://api.rubyonrails.org",
626 "licenses": ["MIT"],
627 "authors": "David Heinemeier Hansson",
628 "downloads": 500000000
629 }"#;
630
631 let info = parse_gem_info(json.as_bytes()).unwrap();
632 assert_eq!(info.name, "rails");
633 assert_eq!(
634 info.info,
635 Some("Full-stack web application framework".to_string())
636 );
637 assert_eq!(info.version, "7.0.8");
638 assert_eq!(
639 info.homepage_uri,
640 Some("https://rubyonrails.org".to_string())
641 );
642 assert_eq!(
643 info.source_code_uri,
644 Some("https://github.com/rails/rails".to_string())
645 );
646 assert_eq!(
647 info.documentation_uri,
648 Some("https://api.rubyonrails.org".to_string())
649 );
650 assert_eq!(info.licenses, vec!["MIT"]);
651 assert_eq!(info.authors, Some("David Heinemeier Hansson".to_string()));
652 assert_eq!(info.downloads, 500_000_000);
653 }
654
655 #[test]
656 fn test_parse_gem_info_minimal() {
657 let json = r#"{
658 "name": "minimal",
659 "version": "0.1.0"
660 }"#;
661
662 let info = parse_gem_info(json.as_bytes()).unwrap();
663 assert_eq!(info.name, "minimal");
664 assert_eq!(info.version, "0.1.0");
665 assert!(info.info.is_none());
666 assert!(info.homepage_uri.is_none());
667 assert!(info.source_code_uri.is_none());
668 assert!(info.documentation_uri.is_none());
669 assert!(info.licenses.is_empty());
670 assert!(info.authors.is_none());
671 assert_eq!(info.downloads, 0);
672 }
673
674 #[test]
675 fn test_parse_gem_info_with_multiple_licenses() {
676 let json = r#"{
677 "name": "test",
678 "version": "1.0.0",
679 "licenses": ["MIT", "Apache-2.0", "BSD-3-Clause"]
680 }"#;
681
682 let info = parse_gem_info(json.as_bytes()).unwrap();
683 assert_eq!(info.licenses.len(), 3);
684 assert!(info.licenses.contains(&"MIT".to_string()));
685 assert!(info.licenses.contains(&"Apache-2.0".to_string()));
686 }
687
688 #[tokio::test]
689 async fn test_registry_creation() {
690 let cache = Arc::new(HttpCache::new());
691 let _registry = RubyGemsRegistry::new(cache);
692 }
693
694 #[test]
701 fn test_get_versions_url_encodes_query_injection() {
702 let name = "foo?callback=evil";
703 let url = versions_url(name);
704 let parsed = url::Url::parse(&url).unwrap();
705 assert!(parsed.query().is_none());
706 assert_eq!(parsed.path_segments().unwrap().count(), 4);
707 let last = parsed.path_segments().unwrap().next_back().unwrap();
708 assert_eq!(
709 urlencoding::decode(last.strip_suffix(".json").unwrap()).unwrap(),
710 name
711 );
712 }
713
714 #[test]
716 fn test_get_gem_info_url_encodes_query_injection() {
717 let name = "foo?callback=evil";
718 let url = gem_info_url(name);
719 let parsed = url::Url::parse(&url).unwrap();
720 assert!(parsed.query().is_none());
721 assert_eq!(parsed.path_segments().unwrap().count(), 4);
722 let last = parsed.path_segments().unwrap().next_back().unwrap();
723 assert_eq!(
724 urlencoding::decode(last.strip_suffix(".json").unwrap()).unwrap(),
725 name
726 );
727 }
728
729 #[test]
736 fn test_versions_and_gem_info_url_dot_segment_sweep() {
737 deps_core::test_util::assert_dot_segment_gated_or_contained(
738 |seg| Some(versions_url(seg)),
739 "rubygems.org",
740 "/api/v1/versions/",
741 );
742 deps_core::test_util::assert_dot_segment_gated_or_contained(
743 |seg| Some(gem_info_url(seg)),
744 "rubygems.org",
745 "/api/v1/gems/",
746 );
747 }
748
749 #[test]
750 fn test_version_trait() {
751 use deps_core::Version;
752
753 let version = BundlerVersion {
754 number: "1.0.0".into(),
755 prerelease: false,
756 yanked: true,
757 published_at: None,
758 platform: "ruby".into(),
759 };
760
761 assert_eq!(version.version_string(), "1.0.0");
762 assert!(version.removal_status().blocks_resolution());
763 assert!(version.features().is_empty());
764 }
765
766 #[test]
767 fn test_version_trait_is_prerelease_uses_registry_flag_not_hyphen_heuristic() {
768 use deps_core::Version;
769
770 let prerelease = BundlerVersion {
773 number: "7.1.0.beta1".into(),
774 prerelease: true,
775 yanked: false,
776 published_at: None,
777 platform: "ruby".into(),
778 };
779 assert!(
780 prerelease.is_prerelease(),
781 "the registry's own prerelease flag must be trusted, not the hyphen heuristic"
782 );
783
784 let stable = BundlerVersion {
785 number: "7.1.0".into(),
786 prerelease: false,
787 yanked: false,
788 published_at: None,
789 platform: "ruby".into(),
790 };
791 assert!(!stable.is_prerelease());
792 }
793
794 #[test]
795 fn test_find_latest_stable_skips_dot_notation_bundler_prerelease() {
796 use deps_core::{Version, find_latest_stable};
797
798 let versions: Vec<Box<dyn Version>> = vec![
799 Box::new(BundlerVersion {
800 number: "7.1.0.beta1".into(),
801 prerelease: true,
802 yanked: false,
803 published_at: None,
804 platform: "ruby".into(),
805 }),
806 Box::new(BundlerVersion {
807 number: "7.0.8".into(),
808 prerelease: false,
809 yanked: false,
810 published_at: None,
811 platform: "ruby".into(),
812 }),
813 ];
814
815 let latest = find_latest_stable(&versions);
816 assert_eq!(
817 latest.map(|v| v.version_string().as_str()),
818 Some("7.0.8"),
819 "find_latest_stable (which #313's hover fix depends on) must skip the dot-notation prerelease"
820 );
821 }
822
823 #[test]
824 fn test_metadata_trait() {
825 use deps_core::Metadata;
826
827 let gem = GemInfo {
828 name: "test".into(),
829 info: Some("A test gem".into()),
830 homepage_uri: None,
831 source_code_uri: Some("https://github.com/test/test".into()),
832 documentation_uri: Some("https://docs.test.com".into()),
833 version: "1.0.0".into(),
834 licenses: vec![],
835 authors: None,
836 downloads: 0,
837 };
838
839 assert_eq!(gem.name(), "test");
840 assert_eq!(gem.description(), Some("A test gem"));
841 assert_eq!(gem.repository(), Some("https://github.com/test/test"));
842 assert_eq!(gem.documentation(), Some("https://docs.test.com"));
843 assert_eq!(gem.latest_version(), "1.0.0");
844 }
845
846 #[test]
847 fn test_metadata_trait_empty_optionals() {
848 use deps_core::Metadata;
849
850 let gem = GemInfo {
851 name: "empty".into(),
852 info: None,
853 homepage_uri: None,
854 source_code_uri: None,
855 documentation_uri: None,
856 version: "0.1.0".into(),
857 licenses: vec![],
858 authors: None,
859 downloads: 0,
860 };
861
862 assert!(gem.description().is_none());
863 assert!(gem.repository().is_none());
864 assert!(gem.documentation().is_none());
865 }
866
867 #[test]
868 fn test_registry_reports_yanked_false() {
869 use deps_core::Registry;
870
871 let cache = Arc::new(HttpCache::new());
872 let registry = RubyGemsRegistry::new(cache);
873
874 assert!(!registry.reports_yanked());
875 }
876
877 #[test]
878 fn test_registry_as_any() {
879 use deps_core::Registry;
880
881 let cache = Arc::new(HttpCache::new());
882 let registry = RubyGemsRegistry::new(cache);
883
884 let any = registry.as_any();
885 assert!(any.is::<RubyGemsRegistry>());
886 assert!(any.downcast_ref::<RubyGemsRegistry>().is_some());
887 }
888
889 #[test]
890 fn test_default_platform_function() {
891 assert_eq!(default_platform(), "ruby");
892 }
893
894 #[test]
895 fn test_select_latest_matching_not_default_none() {
896 use deps_core::{Registry, VersionReq};
897
898 let cache = Arc::new(HttpCache::new());
899 let registry = RubyGemsRegistry::new(cache);
900 let versions: Vec<Box<dyn deps_core::Version>> = vec![
901 Box::new(BundlerVersion {
902 number: "2.0.0".into(),
903 prerelease: false,
904 yanked: true,
905 published_at: None,
906 platform: "ruby".into(),
907 }),
908 Box::new(BundlerVersion {
909 number: "1.0.0".into(),
910 prerelease: false,
911 yanked: false,
912 published_at: None,
913 platform: "ruby".into(),
914 }),
915 ];
916 let req = VersionReq::new("*");
917 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
918 }
919
920 #[tokio::test]
921 #[ignore] async fn test_fetch_real_rails_versions() {
923 let cache = Arc::new(HttpCache::new());
924 let registry = RubyGemsRegistry::new(cache);
925 let versions = registry.get_versions("rails").await.unwrap();
926
927 assert!(!versions.is_empty());
928 assert!(versions.iter().any(|v| v.number.as_str().starts_with("7.")));
929 }
930}