normalize_package_index/index/
winget.rs1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
23use std::collections::HashMap;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum WingetSource {
28 Winget,
30 MsStore,
32}
33
34impl WingetSource {
35 fn api_url(&self) -> Option<&'static str> {
37 match self {
38 Self::Winget => Some("https://api.winget.run/v2/packages"),
39 Self::MsStore => None, }
41 }
42
43 pub fn name(&self) -> &'static str {
45 match self {
46 Self::Winget => "winget",
47 Self::MsStore => "msstore",
48 }
49 }
50
51 pub fn all() -> &'static [WingetSource] {
53 &[Self::Winget, Self::MsStore]
54 }
55
56 pub fn winget() -> &'static [WingetSource] {
58 &[Self::Winget]
59 }
60}
61
62pub struct Winget {
64 sources: Vec<WingetSource>,
65}
66
67impl Winget {
68 pub fn all() -> Self {
70 Self {
71 sources: WingetSource::all().to_vec(),
72 }
73 }
74
75 pub fn winget_only() -> Self {
77 Self {
78 sources: WingetSource::winget().to_vec(),
79 }
80 }
81
82 pub fn with_sources(sources: &[WingetSource]) -> Self {
84 Self {
85 sources: sources.to_vec(),
86 }
87 }
88
89 fn fetch_from_source(name: &str, source: WingetSource) -> Result<PackageMeta, IndexError> {
91 let api_url = source.api_url().ok_or_else(|| {
92 IndexError::NotImplemented(format!("{} API not available", source.name()))
93 })?;
94
95 let url = format!("{}/{}", api_url, name);
96 let response: serde_json::Value = ureq::get(&url)
97 .set("Accept", "application/json")
98 .call()?
99 .into_json()?;
100
101 if response.get("error").is_some() {
102 return Err(IndexError::NotFound(name.to_string()));
103 }
104
105 let latest = response["versions"]
106 .as_array()
107 .and_then(|v| v.first())
108 .unwrap_or(&response);
109
110 let mut extra = HashMap::new();
111 extra.insert(
112 "source_repo".to_string(),
113 serde_json::Value::String(source.name().to_string()),
114 );
115
116 Ok(PackageMeta {
117 name: response["id"].as_str().unwrap_or(name).to_string(),
118 version: latest["version"].as_str().unwrap_or("unknown").to_string(),
119 description: response["description"].as_str().map(String::from),
120 homepage: response["homepage"].as_str().map(String::from),
121 repository: response["repository"].as_str().map(String::from),
122 license: response["license"].as_str().map(String::from),
123 binaries: Vec::new(),
124 keywords: Vec::new(),
125 maintainers: Vec::new(),
126 published: None,
127 downloads: None,
128 archive_url: None,
129 checksum: None,
130 extra,
131 })
132 }
133
134 fn fetch_versions_from_source(
136 name: &str,
137 source: WingetSource,
138 ) -> Result<Vec<VersionMeta>, IndexError> {
139 let api_url = source.api_url().ok_or_else(|| {
140 IndexError::NotImplemented(format!("{} API not available", source.name()))
141 })?;
142
143 let url = format!("{}/{}", api_url, name);
144 let response: serde_json::Value = ureq::get(&url)
145 .set("Accept", "application/json")
146 .call()?
147 .into_json()?;
148
149 if response.get("error").is_some() {
150 return Err(IndexError::NotFound(name.to_string()));
151 }
152
153 let versions = response["versions"]
154 .as_array()
155 .ok_or_else(|| IndexError::Parse("missing versions".into()))?;
156
157 Ok(versions
158 .iter()
159 .filter_map(|v| {
160 Some(VersionMeta {
161 version: v["version"].as_str()?.to_string(),
162 released: v["date"].as_str().map(String::from),
163 yanked: false,
164 })
165 })
166 .collect())
167 }
168
169 fn search_source(query: &str, source: WingetSource) -> Result<Vec<PackageMeta>, IndexError> {
171 let api_url = source.api_url().ok_or_else(|| {
172 IndexError::NotImplemented(format!("{} API not available", source.name()))
173 })?;
174
175 let url = format!("{}?q={}", api_url, query);
176 let response: serde_json::Value = ureq::get(&url)
177 .set("Accept", "application/json")
178 .call()?
179 .into_json()?;
180
181 let packages = response["packages"]
182 .as_array()
183 .or_else(|| response.as_array())
184 .ok_or_else(|| IndexError::Parse("missing packages".into()))?;
185
186 Ok(packages
187 .iter()
188 .filter_map(|pkg| {
189 let mut extra = HashMap::new();
190 extra.insert(
191 "source_repo".to_string(),
192 serde_json::Value::String(source.name().to_string()),
193 );
194
195 Some(PackageMeta {
196 name: pkg["id"].as_str()?.to_string(),
197 version: pkg["version"].as_str().unwrap_or("unknown").to_string(),
198 description: pkg["description"].as_str().map(String::from),
199 homepage: pkg["homepage"].as_str().map(String::from),
200 repository: pkg["repository"].as_str().map(String::from),
201 license: pkg["license"].as_str().map(String::from),
202 binaries: Vec::new(),
203 keywords: Vec::new(),
204 maintainers: Vec::new(),
205 published: None,
206 downloads: None,
207 archive_url: None,
208 checksum: None,
209 extra,
210 })
211 })
212 .collect())
213 }
214}
215
216impl PackageIndex for Winget {
217 fn ecosystem(&self) -> &'static str {
218 "winget"
219 }
220
221 fn display_name(&self) -> &'static str {
222 "Winget (Windows)"
223 }
224
225 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
226 for &source in &self.sources {
228 match Self::fetch_from_source(name, source) {
229 Ok(pkg) => return Ok(pkg),
230 Err(IndexError::NotFound(_)) | Err(IndexError::NotImplemented(_)) => continue,
231 Err(e) => return Err(e),
232 }
233 }
234
235 Err(IndexError::NotFound(name.to_string()))
236 }
237
238 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
239 let mut all_versions = Vec::new();
240
241 for &source in &self.sources {
242 if let Ok(versions) = Self::fetch_versions_from_source(name, source) {
243 all_versions.extend(versions);
244 }
245 }
246
247 if all_versions.is_empty() {
248 return Err(IndexError::NotFound(name.to_string()));
249 }
250
251 Ok(all_versions)
252 }
253
254 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
255 let mut results = Vec::new();
256
257 for &source in &self.sources {
258 if let Ok(packages) = Self::search_source(query, source) {
259 results.extend(packages);
260 }
261 }
262
263 Ok(results)
264 }
265}