Skip to main content

normalize_package_index/index/
winget.rs

1//! Winget package index fetcher (Windows Package Manager).
2//!
3//! Fetches package metadata from the winget-pkgs repository.
4//!
5//! ## API Strategy
6//! - **fetch**: `api.winget.run/v2/packages/{id}` - Community winget.run JSON API
7//! - **fetch_versions**: Same API, extracts versions array
8//! - **search**: `api.winget.run/v2/packages?query=`
9//! - **fetch_all**: `api.winget.run/v2/packages` (all packages)
10//!
11//! ## Multi-source Support
12//! ```rust,ignore
13//! use normalize_packages::index::winget::{Winget, WingetSource};
14//!
15//! // All sources (default)
16//! let all = Winget::all();
17//!
18//! // Winget community only
19//! let winget = Winget::winget_only();
20//! ```
21
22use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
23use std::collections::HashMap;
24
25/// Available WinGet sources.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum WingetSource {
28    /// Winget community repository (via winget.run API)
29    Winget,
30    /// Microsoft Store (not yet supported via API)
31    MsStore,
32}
33
34impl WingetSource {
35    /// Get the API base URL for this source.
36    fn api_url(&self) -> Option<&'static str> {
37        match self {
38            Self::Winget => Some("https://api.winget.run/v2/packages"),
39            Self::MsStore => None, // Not supported via public API
40        }
41    }
42
43    /// Get the source name for tagging.
44    pub fn name(&self) -> &'static str {
45        match self {
46            Self::Winget => "winget",
47            Self::MsStore => "msstore",
48        }
49    }
50
51    /// All available sources.
52    pub fn all() -> &'static [WingetSource] {
53        &[Self::Winget, Self::MsStore]
54    }
55
56    /// Winget only.
57    pub fn winget() -> &'static [WingetSource] {
58        &[Self::Winget]
59    }
60}
61
62/// Winget package index fetcher with configurable sources.
63pub struct Winget {
64    sources: Vec<WingetSource>,
65}
66
67impl Winget {
68    /// Create a fetcher with all sources.
69    pub fn all() -> Self {
70        Self {
71            sources: WingetSource::all().to_vec(),
72        }
73    }
74
75    /// Create a fetcher with winget source only.
76    pub fn winget_only() -> Self {
77        Self {
78            sources: WingetSource::winget().to_vec(),
79        }
80    }
81
82    /// Create a fetcher with custom source selection.
83    pub fn with_sources(sources: &[WingetSource]) -> Self {
84        Self {
85            sources: sources.to_vec(),
86        }
87    }
88
89    /// Fetch a package from a specific source.
90    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    /// Fetch versions from a specific source.
135    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    /// Search a specific source.
170    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        // Try each configured source until we find the package
227        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}