normalize_package_index/index/
hackage.rs1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
12
13pub struct Hackage;
15
16impl Hackage {
17 const API_BASE: &'static str = "https://hackage.haskell.org";
19}
20
21impl PackageIndex for Hackage {
22 fn ecosystem(&self) -> &'static str {
23 "hackage"
24 }
25
26 fn display_name(&self) -> &'static str {
27 "Hackage (Haskell)"
28 }
29
30 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
31 let url = format!("{}/package/{}", Self::API_BASE, name);
33 let response: serde_json::Value = ureq::get(&url)
34 .set("Accept", "application/json")
35 .call()?
36 .into_json()?;
37
38 let versions_url = format!("{}/package/{}/preferred", Self::API_BASE, name);
40 let versions: serde_json::Value = ureq::get(&versions_url)
41 .set("Accept", "application/json")
42 .call()
43 .ok()
44 .and_then(|r| r.into_json().ok())
45 .unwrap_or_default();
46
47 let latest_version = versions["normal-version"]
48 .as_array()
49 .and_then(|v| v.first())
50 .and_then(|v| v.as_str())
51 .unwrap_or("unknown");
52
53 Ok(PackageMeta {
54 name: response["packageName"].as_str().unwrap_or(name).to_string(),
55 version: latest_version.to_string(),
56 description: response["packageDescription"].as_str().map(String::from),
57 homepage: response["packageHomepage"].as_str().map(String::from),
58 repository: response["packageSourceRepository"]
59 .as_str()
60 .map(String::from),
61 license: response["license"].as_str().map(String::from),
62 binaries: Vec::new(),
63 keywords: response["category"]
64 .as_str()
65 .map(|c| c.split(',').map(|s| s.trim().to_string()).collect())
66 .unwrap_or_default(),
67 maintainers: {
68 let mut m = Vec::new();
69 if let Some(author) = response["author"].as_str() {
70 if !author.is_empty() {
71 m.push(author.to_string());
72 }
73 }
74 if let Some(maintainer) = response["maintainer"].as_str() {
75 if !maintainer.is_empty() && !m.contains(&maintainer.to_string()) {
76 m.push(maintainer.to_string());
77 }
78 }
79 m
80 },
81 published: None, downloads: response["downloads"].as_u64(),
83 archive_url: Some(format!(
84 "{}/package/{}-{}/{}-{}.tar.gz",
85 Self::API_BASE,
86 name,
87 latest_version,
88 name,
89 latest_version
90 )),
91 checksum: None, extra: Default::default(),
93 })
94 }
95
96 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
97 let url = format!("{}/package/{}/preferred", Self::API_BASE, name);
98 let response: serde_json::Value = ureq::get(&url)
99 .set("Accept", "application/json")
100 .call()?
101 .into_json()?;
102
103 let normal = response["normal-version"]
104 .as_array()
105 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
106
107 let deprecated = response["deprecated-version"]
108 .as_array()
109 .map(|arr| {
110 arr.iter()
111 .filter_map(|v| v.as_str())
112 .map(String::from)
113 .collect::<Vec<_>>()
114 })
115 .unwrap_or_default();
116
117 Ok(normal
118 .iter()
119 .filter_map(|v| {
120 let version = v.as_str()?.to_string();
121 Some(VersionMeta {
122 yanked: deprecated.contains(&version),
123 version,
124 released: None,
125 })
126 })
127 .collect())
128 }
129
130 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
131 let url = format!(
133 "{}/packages/search?terms={}",
134 Self::API_BASE,
135 urlencoding::encode(query)
136 );
137 let response: serde_json::Value = ureq::get(&url)
138 .set("Accept", "application/json")
139 .call()?
140 .into_json()?;
141
142 let packages = response
143 .as_array()
144 .ok_or_else(|| IndexError::Parse("expected array".into()))?;
145
146 Ok(packages
147 .iter()
148 .take(50)
149 .filter_map(|pkg| {
150 Some(PackageMeta {
151 name: pkg["name"].as_str()?.to_string(),
152 version: "unknown".to_string(), description: pkg["synopsis"].as_str().map(String::from),
154 homepage: None,
155 repository: None,
156 license: None,
157 binaries: Vec::new(),
158 keywords: Vec::new(),
159 maintainers: Vec::new(),
160 published: None,
161 downloads: pkg["downloads"].as_u64(),
162 archive_url: None,
163 checksum: None,
164 extra: Default::default(),
165 })
166 })
167 .collect())
168 }
169}