Skip to main content

normalize_package_index/index/
racket.rs

1//! Racket package index fetcher.
2//!
3//! Fetches package metadata from pkgs.racket-lang.org.
4//! Uses the pkgs-all.json.gz endpoint which contains all package data.
5//!
6//! ## API Strategy
7//! - **fetch**: Searches cached `pkgs.racket-lang.org/pkgs-all.json.gz`
8//! - **fetch_versions**: Same, single version per package
9//! - **search**: Filters cached pkgs-all.json
10//! - **fetch_all**: `pkgs.racket-lang.org/pkgs-all.json.gz` (cached 1 hour)
11
12use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
13use crate::cache;
14use std::collections::HashMap;
15use std::time::Duration;
16
17/// Cache TTL for Racket package index (1 hour).
18const CACHE_TTL: Duration = Duration::from_secs(60 * 60);
19
20/// Racket package index fetcher.
21pub struct Racket;
22
23impl Racket {
24    /// Racket packages JSON endpoint.
25    const PACKAGES_URL: &'static str = "https://pkgs.racket-lang.org/pkgs-all.json.gz";
26
27    /// Parse a package from Racket JSON format.
28    fn parse_package(name: &str, pkg: &serde_json::Value) -> Option<PackageMeta> {
29        let mut extra = HashMap::new();
30
31        // Extract dependencies
32        if let Some(deps) = pkg["dependencies"].as_array() {
33            let dep_names: Vec<serde_json::Value> = deps
34                .iter()
35                .filter_map(|d| {
36                    // Dependencies can be strings or arrays like ["base", {"kw": "version"}, "7.6"]
37                    if let Some(s) = d.as_str() {
38                        Some(serde_json::Value::String(s.to_string()))
39                    } else if let Some(arr) = d.as_array() {
40                        arr.first()
41                            .and_then(|f| f.as_str())
42                            .map(|s| serde_json::Value::String(s.to_string()))
43                    } else {
44                        None
45                    }
46                })
47                .collect();
48            if !dep_names.is_empty() {
49                extra.insert("depends".to_string(), serde_json::Value::Array(dep_names));
50            }
51        }
52
53        // Extract tags as keywords
54        if let Some(tags) = pkg["tags"].as_array() {
55            let tag_list: Vec<serde_json::Value> = tags
56                .iter()
57                .filter_map(|t| t.as_str().map(|s| serde_json::Value::String(s.to_string())))
58                .collect();
59            if !tag_list.is_empty() {
60                extra.insert("keywords".to_string(), serde_json::Value::Array(tag_list));
61            }
62        }
63
64        // Extract ring (quality tier)
65        if let Some(ring) = pkg["ring"].as_u64() {
66            extra.insert("ring".to_string(), serde_json::Value::Number(ring.into()));
67        }
68
69        // Get source URL
70        let source_url = pkg["source"].as_str().map(String::from);
71
72        // Get checksum
73        let checksum = pkg["checksum"].as_str().map(|c| format!("sha1:{}", c));
74
75        // Get authors/maintainers
76        let maintainers: Vec<String> = pkg["authors"]
77            .as_array()
78            .map(|authors| {
79                authors
80                    .iter()
81                    .filter_map(|a| a.as_str().map(String::from))
82                    .collect()
83            })
84            .unwrap_or_default();
85
86        // Get version (from default version or use checksum as pseudo-version)
87        let version = pkg["versions"]["default"]["checksum"]
88            .as_str()
89            .map(|c| c[..8].to_string()) // Use first 8 chars of checksum as version
90            .unwrap_or_else(|| "latest".to_string());
91
92        Some(PackageMeta {
93            name: name.to_string(),
94            version,
95            description: pkg["description"].as_str().map(String::from),
96            homepage: Some(format!("https://pkgs.racket-lang.org/package/{}", name)),
97            repository: source_url.clone(),
98            license: pkg["license"].as_str().map(String::from),
99            binaries: Vec::new(),
100            archive_url: source_url,
101            keywords: Vec::new(),
102            maintainers,
103            published: None,
104            downloads: None,
105            checksum,
106            extra,
107        })
108    }
109
110    /// Load all packages from the index.
111    fn load_all_packages() -> Result<serde_json::Value, IndexError> {
112        let (data, _was_cached) =
113            cache::fetch_with_cache("racket", "pkgs-all", Self::PACKAGES_URL, CACHE_TTL)
114                .map_err(IndexError::Network)?;
115
116        serde_json::from_slice(&data).map_err(|e| IndexError::Parse(e.to_string()))
117    }
118}
119
120impl PackageIndex for Racket {
121    fn ecosystem(&self) -> &'static str {
122        "racket"
123    }
124
125    fn display_name(&self) -> &'static str {
126        "Racket"
127    }
128
129    fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
130        let packages = Self::load_all_packages()?;
131
132        packages
133            .get(name)
134            .and_then(|pkg| Self::parse_package(name, pkg))
135            .ok_or_else(|| IndexError::NotFound(name.to_string()))
136    }
137
138    fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
139        let packages = Self::load_all_packages()?;
140
141        let pkg = packages
142            .get(name)
143            .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
144
145        // Racket packages typically only have one "version" (the current state)
146        // But the versions field can contain multiple
147        let mut versions = Vec::new();
148
149        if let Some(vers) = pkg["versions"].as_object() {
150            for (ver_name, ver_data) in vers {
151                if let Some(checksum) = ver_data["checksum"].as_str() {
152                    versions.push(VersionMeta {
153                        version: if ver_name == "default" {
154                            checksum[..8].to_string()
155                        } else {
156                            ver_name.clone()
157                        },
158                        released: None,
159                        yanked: false,
160                    });
161                }
162            }
163        }
164
165        if versions.is_empty() {
166            let pkg_meta = Self::parse_package(name, pkg)
167                .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
168            versions.push(VersionMeta {
169                version: pkg_meta.version,
170                released: None,
171                yanked: false,
172            });
173        }
174
175        Ok(versions)
176    }
177
178    fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
179        let packages = Self::load_all_packages()?;
180        let query_lower = query.to_lowercase();
181
182        let results: Vec<PackageMeta> = packages
183            .as_object()
184            .ok_or_else(|| IndexError::Parse("expected object".into()))?
185            .iter()
186            .filter(|(name, pkg)| {
187                // Match on name
188                name.to_lowercase().contains(&query_lower)
189                    // Or description
190                    || pkg["description"]
191                        .as_str()
192                        .map(|d| d.to_lowercase().contains(&query_lower))
193                        .unwrap_or(false)
194                    // Or tags
195                    || pkg["tags"]
196                        .as_array()
197                        .map(|tags| {
198                            tags.iter()
199                                .any(|t| t.as_str().map(|s| s.contains(&query_lower)).unwrap_or(false))
200                        })
201                        .unwrap_or(false)
202            })
203            .take(50)
204            .filter_map(|(name, pkg)| Self::parse_package(name, pkg))
205            .collect();
206
207        Ok(results)
208    }
209
210    fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
211        let packages = Self::load_all_packages()?;
212
213        let results: Vec<PackageMeta> = packages
214            .as_object()
215            .ok_or_else(|| IndexError::Parse("expected object".into()))?
216            .iter()
217            .filter_map(|(name, pkg)| Self::parse_package(name, pkg))
218            .collect();
219
220        Ok(results)
221    }
222}