Skip to main content

normalize_package_index/index/
nimble.rs

1//! Nimble package index fetcher (Nim).
2//!
3//! Fetches package metadata from the Nim packages repository on GitHub.
4//!
5//! ## API Strategy
6//! - **fetch**: Searches cached `github.com/nim-lang/packages/packages.json`
7//! - **fetch_versions**: Same, parses package URL for tags
8//! - **search**: Filters cached packages.json
9//! - **fetch_all**: `github.com/nim-lang/packages/packages.json` (cached 1 hour)
10
11use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
12use crate::cache;
13use std::time::Duration;
14
15/// Cache TTL for packages list (1 hour).
16const INDEX_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
17
18/// Nimble package index fetcher.
19pub struct Nimble;
20
21impl Nimble {
22    /// Nimble packages JSON URL.
23    const PACKAGES_URL: &'static str =
24        "https://raw.githubusercontent.com/nim-lang/packages/master/packages.json";
25
26    /// Fetch and cache the packages list.
27    fn fetch_packages_list() -> Result<Vec<serde_json::Value>, IndexError> {
28        let (data, _was_cached) = cache::fetch_with_cache(
29            "nimble",
30            "packages-all",
31            Self::PACKAGES_URL,
32            INDEX_CACHE_TTL,
33        )
34        .map_err(IndexError::Network)?;
35
36        let packages: Vec<serde_json::Value> = serde_json::from_slice(&data)?;
37        Ok(packages)
38    }
39}
40
41impl PackageIndex for Nimble {
42    fn ecosystem(&self) -> &'static str {
43        "nimble"
44    }
45
46    fn display_name(&self) -> &'static str {
47        "Nimble (Nim)"
48    }
49
50    fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
51        let packages = Self::fetch_packages_list()?;
52
53        let pkg = packages
54            .iter()
55            .find(|p| p["name"].as_str().map(|n| n.to_lowercase()) == Some(name.to_lowercase()))
56            .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
57
58        Ok(pkg_to_meta(pkg))
59    }
60
61    fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
62        // Nimble packages.json doesn't include version info
63        // Just return "unknown" as the version
64        let packages = Self::fetch_packages_list()?;
65
66        let _pkg = packages
67            .iter()
68            .find(|p| p["name"].as_str().map(|n| n.to_lowercase()) == Some(name.to_lowercase()))
69            .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
70
71        // Version info would require fetching from git tags
72        Ok(vec![VersionMeta {
73            version: "latest".to_string(),
74            released: None,
75            yanked: false,
76        }])
77    }
78
79    fn supports_fetch_all(&self) -> bool {
80        true
81    }
82
83    fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
84        let packages = Self::fetch_packages_list()?;
85        Ok(packages.iter().map(pkg_to_meta).collect())
86    }
87
88    fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
89        let packages = Self::fetch_packages_list()?;
90        let query_lower = query.to_lowercase();
91
92        Ok(packages
93            .iter()
94            .filter(|pkg| {
95                let name = pkg["name"].as_str().unwrap_or("");
96                let desc = pkg["description"].as_str().unwrap_or("");
97                let tags = pkg["tags"]
98                    .as_array()
99                    .map(|t| {
100                        t.iter()
101                            .filter_map(|v| v.as_str())
102                            .collect::<Vec<_>>()
103                            .join(" ")
104                    })
105                    .unwrap_or_default();
106
107                name.to_lowercase().contains(&query_lower)
108                    || desc.to_lowercase().contains(&query_lower)
109                    || tags.to_lowercase().contains(&query_lower)
110            })
111            .take(50)
112            .map(pkg_to_meta)
113            .collect())
114    }
115}
116
117fn pkg_to_meta(pkg: &serde_json::Value) -> PackageMeta {
118    let url = pkg["url"].as_str().unwrap_or("");
119
120    PackageMeta {
121        name: pkg["name"].as_str().unwrap_or("").to_string(),
122        version: "latest".to_string(), // packages.json doesn't include versions
123        description: pkg["description"].as_str().map(String::from),
124        homepage: pkg["web"].as_str().map(String::from),
125        repository: if url.contains("github.com") || url.contains("gitlab.com") {
126            Some(url.to_string())
127        } else {
128            None
129        },
130        license: pkg["license"].as_str().map(String::from),
131        binaries: Vec::new(),
132        keywords: pkg["tags"]
133            .as_array()
134            .map(|t| {
135                t.iter()
136                    .filter_map(|v| v.as_str().map(String::from))
137                    .collect()
138            })
139            .unwrap_or_default(),
140        maintainers: Vec::new(),
141        published: None,
142        downloads: None,
143        archive_url: Some(url.to_string()),
144        checksum: None,
145        extra: Default::default(),
146    }
147}