normalize_package_index/index/
cran.rs1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
13
14pub struct Cran;
16
17impl Cran {
18 const API_BASE: &'static str = "https://crandb.r-pkg.org";
20
21 const CRAN_MIRROR: &'static str = "https://cran.r-project.org";
23}
24
25impl PackageIndex for Cran {
26 fn ecosystem(&self) -> &'static str {
27 "cran"
28 }
29
30 fn display_name(&self) -> &'static str {
31 "CRAN (R)"
32 }
33
34 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
35 let url = format!("{}/{}", Self::API_BASE, name);
36 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
37
38 if response["error"].is_string() {
40 return Err(IndexError::NotFound(name.to_string()));
41 }
42
43 let version = response["Version"]
44 .as_str()
45 .unwrap_or("unknown")
46 .to_string();
47
48 Ok(PackageMeta {
49 name: response["Package"].as_str().unwrap_or(name).to_string(),
50 version: version.clone(),
51 description: response["Title"]
52 .as_str()
53 .or_else(|| response["Description"].as_str())
54 .map(String::from),
55 homepage: response["URL"]
56 .as_str()
57 .and_then(|urls| urls.split(',').next())
58 .map(|s| s.trim().to_string()),
59 repository: response["BugReports"]
60 .as_str()
61 .filter(|s| s.contains("github.com") || s.contains("gitlab.com"))
62 .map(String::from),
63 license: response["License"].as_str().map(String::from),
64 binaries: Vec::new(),
65 maintainers: response["Maintainer"]
66 .as_str()
67 .map(|m| vec![m.to_string()])
68 .unwrap_or_default(),
69 keywords: Vec::new(),
70 published: None,
71 downloads: None,
72 archive_url: Some(format!(
73 "{}/src/contrib/{}_{}.tar.gz",
74 Self::CRAN_MIRROR,
75 name,
76 version
77 )),
78 checksum: None,
79 extra: Default::default(),
80 })
81 }
82
83 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
84 let url = format!("{}/{}/all", Self::API_BASE, name);
86 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
87
88 if response["error"].is_string() {
90 return Err(IndexError::NotFound(name.to_string()));
91 }
92
93 let versions = response["versions"]
94 .as_object()
95 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
96
97 let mut result: Vec<VersionMeta> = versions
98 .iter()
99 .map(|(version, data)| VersionMeta {
100 version: version.clone(),
101 released: data["crandb_file_date"].as_str().map(String::from),
102 yanked: false, })
104 .collect();
105
106 result.sort_by(|a, b| version_compare(&b.version, &a.version));
108
109 Ok(result)
110 }
111
112 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
113 let url = format!(
115 "{}/-/search?q={}&size=50",
116 Self::API_BASE,
117 urlencoding::encode(query)
118 );
119 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
120
121 let packages = response
122 .as_array()
123 .ok_or_else(|| IndexError::Parse("expected array".into()))?;
124
125 Ok(packages
126 .iter()
127 .filter_map(|pkg| {
128 Some(PackageMeta {
129 name: pkg["Package"].as_str()?.to_string(),
130 version: pkg["Version"].as_str().unwrap_or("unknown").to_string(),
131 description: pkg["Title"].as_str().map(String::from),
132 homepage: None,
133 repository: None,
134 license: pkg["License"].as_str().map(String::from),
135 binaries: Vec::new(),
136 keywords: Vec::new(),
137 maintainers: Vec::new(),
138 published: None,
139 downloads: None,
140 archive_url: None,
141 checksum: None,
142 extra: Default::default(),
143 })
144 })
145 .collect())
146 }
147}
148
149fn version_compare(a: &str, b: &str) -> std::cmp::Ordering {
151 let parse = |s: &str| -> Vec<u32> {
152 s.split(|c: char| !c.is_ascii_digit())
153 .filter_map(|p| p.parse().ok())
154 .collect()
155 };
156 parse(a).cmp(&parse(b))
157}