normalize_package_index/index/
flatpak.rs1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
23use std::collections::HashMap;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum FlatpakRemote {
28 Flathub,
30 FlathubBeta,
32}
33
34impl FlatpakRemote {
35 fn api_url(&self) -> &'static str {
37 match self {
38 Self::Flathub => "https://flathub.org/api/v2",
39 Self::FlathubBeta => "https://beta.flathub.org/api/v2",
40 }
41 }
42
43 pub fn name(&self) -> &'static str {
45 match self {
46 Self::Flathub => "flathub",
47 Self::FlathubBeta => "flathub-beta",
48 }
49 }
50
51 pub fn all() -> &'static [FlatpakRemote] {
53 &[Self::Flathub, Self::FlathubBeta]
54 }
55
56 pub fn flathub() -> &'static [FlatpakRemote] {
58 &[Self::Flathub]
59 }
60}
61
62pub struct Flatpak {
64 remotes: Vec<FlatpakRemote>,
65}
66
67impl Flatpak {
68 pub fn all() -> Self {
70 Self {
71 remotes: FlatpakRemote::all().to_vec(),
72 }
73 }
74
75 pub fn flathub() -> Self {
77 Self {
78 remotes: FlatpakRemote::flathub().to_vec(),
79 }
80 }
81
82 pub fn with_remotes(remotes: &[FlatpakRemote]) -> Self {
84 Self {
85 remotes: remotes.to_vec(),
86 }
87 }
88
89 fn fetch_from_remote(name: &str, remote: FlatpakRemote) -> Result<PackageMeta, IndexError> {
91 let url = format!("{}/appstream/{}", remote.api_url(), name);
92 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
93
94 Ok(app_to_meta(&response, name, remote))
95 }
96
97 fn fetch_versions_from_remote(
99 name: &str,
100 remote: FlatpakRemote,
101 ) -> Result<Vec<VersionMeta>, IndexError> {
102 let url = format!("{}/appstream/{}", remote.api_url(), name);
103 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
104
105 let version = response["releases"]
107 .as_array()
108 .and_then(|r| r.first())
109 .and_then(|r| r["version"].as_str())
110 .or_else(|| response["bundle"]["runtime"].as_str())
111 .unwrap_or("unknown");
112
113 Ok(vec![VersionMeta {
114 version: format!("{} ({})", version, remote.name()),
115 released: response["releases"]
116 .as_array()
117 .and_then(|r| r.first())
118 .and_then(|r| r["timestamp"].as_str())
119 .map(String::from),
120 yanked: false,
121 }])
122 }
123
124 fn search_remote(query: &str, remote: FlatpakRemote) -> Result<Vec<PackageMeta>, IndexError> {
126 let url = format!(
127 "{}/search?q={}",
128 remote.api_url(),
129 urlencoding::encode(query)
130 );
131 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
132
133 let hits = response["hits"]
134 .as_array()
135 .ok_or_else(|| IndexError::Parse("missing hits".into()))?;
136
137 Ok(hits
138 .iter()
139 .take(50)
140 .filter_map(|hit| {
141 let mut extra = HashMap::new();
142 extra.insert(
143 "source_repo".to_string(),
144 serde_json::Value::String(remote.name().to_string()),
145 );
146
147 Some(PackageMeta {
148 name: hit["id"].as_str()?.to_string(),
149 version: "unknown".to_string(),
150 description: hit["summary"].as_str().map(String::from),
151 homepage: hit["project_url"].as_str().map(String::from),
152 repository: None,
153 license: None,
154 binaries: Vec::new(),
155 keywords: Vec::new(),
156 maintainers: Vec::new(),
157 published: None,
158 downloads: None,
159 archive_url: None,
160 checksum: None,
161 extra,
162 })
163 })
164 .collect())
165 }
166
167 fn fetch_all_from_remote(remote: FlatpakRemote) -> Result<Vec<PackageMeta>, IndexError> {
169 let url = format!("{}/appstream", remote.api_url());
170 let app_ids: Vec<String> = ureq::get(&url).call()?.into_json()?;
171
172 let mut extra = HashMap::new();
173 extra.insert(
174 "source_repo".to_string(),
175 serde_json::Value::String(remote.name().to_string()),
176 );
177
178 Ok(app_ids
179 .into_iter()
180 .map(|id| PackageMeta {
181 name: id,
182 version: "unknown".to_string(),
183 description: None,
184 homepage: None,
185 repository: None,
186 license: None,
187 binaries: Vec::new(),
188 keywords: Vec::new(),
189 maintainers: Vec::new(),
190 published: None,
191 downloads: None,
192 archive_url: None,
193 checksum: None,
194 extra: extra.clone(),
195 })
196 .collect())
197 }
198}
199
200impl PackageIndex for Flatpak {
201 fn ecosystem(&self) -> &'static str {
202 "flatpak"
203 }
204
205 fn display_name(&self) -> &'static str {
206 "Flathub (Flatpak)"
207 }
208
209 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
210 for &remote in &self.remotes {
212 match Self::fetch_from_remote(name, remote) {
213 Ok(pkg) => return Ok(pkg),
214 Err(IndexError::Network(_)) => continue,
215 Err(e) => return Err(e),
216 }
217 }
218
219 Err(IndexError::NotFound(name.to_string()))
220 }
221
222 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
223 let mut all_versions = Vec::new();
224
225 for &remote in &self.remotes {
226 if let Ok(versions) = Self::fetch_versions_from_remote(name, remote) {
227 all_versions.extend(versions);
228 }
229 }
230
231 if all_versions.is_empty() {
232 return Err(IndexError::NotFound(name.to_string()));
233 }
234
235 Ok(all_versions)
236 }
237
238 fn supports_fetch_all(&self) -> bool {
239 true
240 }
241
242 fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
243 let mut all_apps = Vec::new();
244
245 for &remote in &self.remotes {
246 if let Ok(apps) = Self::fetch_all_from_remote(remote) {
247 all_apps.extend(apps);
248 }
249 }
250
251 Ok(all_apps)
252 }
253
254 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
255 let mut results = Vec::new();
256
257 for &remote in &self.remotes {
258 if let Ok(packages) = Self::search_remote(query, remote) {
259 results.extend(packages);
260 }
261 }
262
263 Ok(results)
264 }
265}
266
267fn app_to_meta(app: &serde_json::Value, fallback_name: &str, remote: FlatpakRemote) -> PackageMeta {
268 let version = app["releases"]
269 .as_array()
270 .and_then(|r| r.first())
271 .and_then(|r| r["version"].as_str())
272 .unwrap_or("unknown");
273
274 let mut extra = HashMap::new();
275 extra.insert(
276 "source_repo".to_string(),
277 serde_json::Value::String(remote.name().to_string()),
278 );
279
280 let published = app["releases"]
281 .as_array()
282 .and_then(|r| r.first())
283 .and_then(|r| r["timestamp"].as_str())
284 .map(String::from);
285
286 PackageMeta {
287 name: app["id"].as_str().unwrap_or(fallback_name).to_string(),
288 version: version.to_string(),
289 description: app["summary"].as_str().map(String::from),
290 homepage: app["project_url"].as_str().map(String::from),
291 repository: app["vcs_url"].as_str().map(String::from),
292 license: app["project_license"].as_str().map(String::from),
293 binaries: Vec::new(),
294 keywords: app["categories"]
295 .as_array()
296 .map(|c| {
297 c.iter()
298 .filter_map(|v| v.as_str().map(String::from))
299 .collect()
300 })
301 .unwrap_or_default(),
302 maintainers: app["developer_name"]
303 .as_str()
304 .map(|d| vec![d.to_string()])
305 .unwrap_or_default(),
306 published,
307 downloads: app["installs_last_month"].as_u64(),
308 archive_url: None,
309 checksum: None,
310 extra,
311 }
312}