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 && !author.is_empty()
71 {
72 m.push(author.to_string());
73 }
74 if let Some(maintainer) = response["maintainer"].as_str()
75 && !maintainer.is_empty()
76 && !m.contains(&maintainer.to_string())
77 {
78 m.push(maintainer.to_string());
79 }
80 m
81 },
82 published: None, downloads: response["downloads"].as_u64(),
84 archive_url: Some(format!(
85 "{}/package/{}-{}/{}-{}.tar.gz",
86 Self::API_BASE,
87 name,
88 latest_version,
89 name,
90 latest_version
91 )),
92 checksum: None, extra: Default::default(),
94 })
95 }
96
97 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
98 let url = format!("{}/package/{}/preferred", Self::API_BASE, name);
99 let response: serde_json::Value = ureq::get(&url)
100 .set("Accept", "application/json")
101 .call()?
102 .into_json()?;
103
104 let normal = response["normal-version"]
105 .as_array()
106 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
107
108 let deprecated = response["deprecated-version"]
109 .as_array()
110 .map(|arr| {
111 arr.iter()
112 .filter_map(|v| v.as_str())
113 .map(String::from)
114 .collect::<Vec<_>>()
115 })
116 .unwrap_or_default();
117
118 Ok(normal
119 .iter()
120 .filter_map(|v| {
121 let version = v.as_str()?.to_string();
122 Some(VersionMeta {
123 yanked: deprecated.contains(&version),
124 version,
125 released: None,
126 })
127 })
128 .collect())
129 }
130
131 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
132 let url = format!(
134 "{}/packages/search?terms={}",
135 Self::API_BASE,
136 urlencoding::encode(query)
137 );
138 let response: serde_json::Value = ureq::get(&url)
139 .set("Accept", "application/json")
140 .call()?
141 .into_json()?;
142
143 let packages = response
144 .as_array()
145 .ok_or_else(|| IndexError::Parse("expected array".into()))?;
146
147 Ok(packages
148 .iter()
149 .take(50)
150 .filter_map(|pkg| {
151 Some(PackageMeta {
152 name: pkg["name"].as_str()?.to_string(),
153 version: "unknown".to_string(), description: pkg["synopsis"].as_str().map(String::from),
155 homepage: None,
156 repository: None,
157 license: None,
158 binaries: Vec::new(),
159 keywords: Vec::new(),
160 maintainers: Vec::new(),
161 published: None,
162 downloads: pkg["downloads"].as_u64(),
163 archive_url: None,
164 checksum: None,
165 extra: Default::default(),
166 })
167 })
168 .collect())
169 }
170}