Skip to main content

normalize_package_index/index/
bioconductor.rs

1//! Bioconductor package index fetcher.
2//!
3//! Fetches package metadata from Bioconductor via r-universe.dev API.
4//! Bioconductor provides bioinformatics packages for R.
5//!
6//! ## API Strategy
7//! - **fetch**: `bioconductor.r-universe.dev/api/packages/{name}` - r-universe JSON API
8//! - **fetch_versions**: Same API, extracts version history
9//! - **search**: `bioconductor.r-universe.dev/api/search?q=` - r-universe search
10//! - **fetch_all**: `bioconductor.r-universe.dev/api/packages` (cached 1 hour)
11
12use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
13use std::collections::HashMap;
14
15/// Bioconductor package index fetcher.
16pub struct Bioconductor;
17
18impl Bioconductor {
19    /// Bioconductor r-universe API base.
20    const API_BASE: &'static str = "https://bioconductor.r-universe.dev/api";
21
22    /// Parse a package from r-universe JSON format.
23    fn parse_package(pkg: &serde_json::Value) -> Option<PackageMeta> {
24        let name = pkg["Package"].as_str()?;
25        let version = pkg["Version"].as_str().unwrap_or("unknown");
26
27        let mut extra = HashMap::new();
28
29        // Extract dependencies from Imports/Depends
30        let mut deps = Vec::new();
31        for dep_field in ["Imports", "Depends", "LinkingTo"] {
32            if let Some(dep_str) = pkg[dep_field].as_str() {
33                for dep in dep_str.split(',') {
34                    let dep_name = dep
35                        .trim()
36                        .split(|c| c == ' ' || c == '(' || c == '\n')
37                        .next()
38                        .unwrap_or("")
39                        .trim();
40                    if !dep_name.is_empty() && dep_name != "R" {
41                        deps.push(serde_json::Value::String(dep_name.to_string()));
42                    }
43                }
44            }
45        }
46        if !deps.is_empty() {
47            extra.insert("depends".to_string(), serde_json::Value::Array(deps));
48        }
49
50        // Extract file info
51        if let Some(size) = pkg["_filesize"].as_u64() {
52            extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
53        }
54
55        // Build archive URL
56        let archive_url = pkg["_file"]
57            .as_str()
58            .map(|file| format!("https://bioconductor.r-universe.dev/src/contrib/{}", file));
59
60        // Get checksum
61        let checksum = pkg["_sha256"].as_str().map(|s| format!("sha256:{}", s));
62
63        // Get maintainer
64        let maintainers: Vec<String> = pkg["Maintainer"]
65            .as_str()
66            .map(|m| vec![m.to_string()])
67            .unwrap_or_default();
68
69        Some(PackageMeta {
70            name: name.to_string(),
71            version: version.to_string(),
72            description: pkg["Title"].as_str().map(String::from),
73            homepage: pkg["URL"].as_str().map(String::from),
74            repository: pkg["RemoteUrl"].as_str().map(String::from),
75            license: pkg["License"].as_str().map(String::from),
76            binaries: Vec::new(),
77            archive_url,
78            keywords: Vec::new(),
79            maintainers,
80            published: None,
81            downloads: None,
82            checksum,
83            extra,
84        })
85    }
86}
87
88impl PackageIndex for Bioconductor {
89    fn ecosystem(&self) -> &'static str {
90        "bioconductor"
91    }
92
93    fn display_name(&self) -> &'static str {
94        "Bioconductor"
95    }
96
97    fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
98        let url = format!("{}/packages/{}", Self::API_BASE, name);
99        let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
100
101        Self::parse_package(&response).ok_or_else(|| IndexError::NotFound(name.to_string()))
102    }
103
104    fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
105        // r-universe provides version history
106        let url = format!("{}/packages/{}/versions", Self::API_BASE, name);
107
108        match ureq::get(&url).call() {
109            Ok(resp) => {
110                let versions: Vec<serde_json::Value> = resp.into_json()?;
111                Ok(versions
112                    .iter()
113                    .filter_map(|v| {
114                        Some(VersionMeta {
115                            version: v["Version"].as_str()?.to_string(),
116                            released: v["_published"].as_str().map(String::from),
117                            yanked: false,
118                        })
119                    })
120                    .collect())
121            }
122            Err(_) => {
123                // Fall back to just current version
124                let pkg = self.fetch(name)?;
125                Ok(vec![VersionMeta {
126                    version: pkg.version,
127                    released: None,
128                    yanked: false,
129                }])
130            }
131        }
132    }
133
134    fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
135        // r-universe has a search endpoint
136        let url = format!(
137            "{}/packages?q={}&limit=50",
138            Self::API_BASE,
139            urlencoding::encode(query)
140        );
141        let response: Vec<serde_json::Value> = ureq::get(&url).call()?.into_json()?;
142
143        Ok(response.iter().filter_map(Self::parse_package).collect())
144    }
145
146    fn fetch_all(&self) -> Result<Vec<PackageMeta>, IndexError> {
147        let url = format!("{}/packages", Self::API_BASE);
148        let response: Vec<serde_json::Value> = ureq::get(&url).call()?.into_json()?;
149
150        Ok(response.iter().filter_map(Self::parse_package).collect())
151    }
152}