Skip to main content

normalize_package_index/index/
pub_dev.rs

1//! pub.dev package index fetcher (Dart/Flutter).
2//!
3//! Fetches package metadata from pub.dev API.
4//!
5//! ## API Strategy
6//! - **fetch**: `pub.dev/api/packages/{name}` - Official pub.dev JSON API
7//! - **fetch_versions**: Same API, extracts versions array
8//! - **search**: `pub.dev/api/search?q=`
9//! - **fetch_all**: Not supported (too large)
10
11use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
12
13/// pub.dev package index fetcher.
14pub struct Pub;
15
16impl Pub {
17    /// pub.dev API base.
18    const API_BASE: &'static str = "https://pub.dev/api";
19}
20
21impl PackageIndex for Pub {
22    fn ecosystem(&self) -> &'static str {
23        "pub"
24    }
25
26    fn display_name(&self) -> &'static str {
27        "pub.dev (Dart/Flutter)"
28    }
29
30    fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
31        let url = format!("{}/packages/{}", Self::API_BASE, name);
32        let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
33
34        let latest = &response["latest"];
35        let pubspec = &latest["pubspec"];
36
37        Ok(PackageMeta {
38            name: response["name"].as_str().unwrap_or(name).to_string(),
39            version: latest["version"].as_str().unwrap_or("unknown").to_string(),
40            description: pubspec["description"].as_str().map(String::from),
41            homepage: pubspec["homepage"].as_str().map(String::from),
42            repository: pubspec["repository"].as_str().map(String::from),
43            license: None, // pub.dev doesn't expose license in API
44            binaries: Vec::new(),
45            keywords: Vec::new(), // pub.dev doesn't have keywords
46            maintainers: pubspec["authors"]
47                .as_array()
48                .map(|a| {
49                    a.iter()
50                        .filter_map(|author| author.as_str().map(String::from))
51                        .collect()
52                })
53                .unwrap_or_default(),
54            published: latest["published"].as_str().map(String::from),
55            downloads: None, // pub.dev doesn't expose download counts
56            archive_url: latest["archive_url"].as_str().map(String::from),
57            checksum: latest["archive_sha256"]
58                .as_str()
59                .map(|h| format!("sha256:{}", h)),
60            extra: Default::default(),
61        })
62    }
63
64    fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
65        let url = format!("{}/packages/{}", Self::API_BASE, name);
66        let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
67
68        let versions = response["versions"]
69            .as_array()
70            .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
71
72        Ok(versions
73            .iter()
74            .filter_map(|v| {
75                Some(VersionMeta {
76                    version: v["version"].as_str()?.to_string(),
77                    released: v["published"].as_str().map(String::from),
78                    yanked: v["retracted"].as_bool().unwrap_or(false),
79                })
80            })
81            .collect())
82    }
83
84    fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
85        let url = format!("{}/search?q={}", Self::API_BASE, urlencoding::encode(query));
86        let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
87
88        let packages = response["packages"]
89            .as_array()
90            .ok_or_else(|| IndexError::Parse("missing packages".into()))?;
91
92        // Search only returns package names, need to fetch details for each
93        // For efficiency, just return names with unknown version
94        Ok(packages
95            .iter()
96            .take(50)
97            .filter_map(|pkg| {
98                Some(PackageMeta {
99                    name: pkg["package"].as_str()?.to_string(),
100                    version: "unknown".to_string(), // Search only returns names
101                    description: None,
102                    homepage: None,
103                    repository: None,
104                    license: None,
105                    binaries: Vec::new(),
106                    keywords: Vec::new(),
107                    maintainers: Vec::new(),
108                    published: None,
109                    downloads: None,
110                    archive_url: None,
111                    checksum: None,
112                    extra: Default::default(),
113                })
114            })
115            .collect())
116    }
117}