use std::collections::HashMap;
use std::sync::OnceLock;
use parking_lot::Mutex;
use crate::error::Result;
use crate::genomic::ChrMap;
mod bundled;
fn fetch_cache() -> &'static Mutex<HashMap<(String, bool), ChrMap>> {
static CACHE: OnceLock<Mutex<HashMap<(String, bool), ChrMap>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn get_chr_sizes(genome: &str, full: bool) -> Result<ChrMap> {
let bundled = bundled::get(genome);
if !full {
if let Some((_, sizes)) = bundled {
return Ok(ChrMap::from_entries(
sizes.iter().map(|(id, size)| ((*id).to_string(), *size)),
));
}
}
let resolved = bundled.map(|(name, _)| name).unwrap_or(genome);
fetch_cached(resolved, full).map_err(|error| annotate(genome, error))
}
pub fn bundled_genomes() -> &'static [&'static str] {
bundled::GENOME_NAMES
}
fn annotate(genome: &str, error: crate::error::Error) -> crate::error::Error {
let client_error = matches!(
&error,
crate::error::Error::Http {
status: Some(400..=499),
..
}
);
if !client_error {
return error;
}
crate::error::Error::invalid(format!(
"no genome {genome:?} at UCSC ({error}). Bundled, and needing no network: {}",
bundled::GENOME_NAMES.join(", ")
))
}
fn fetch_cached(genome: &str, full: bool) -> Result<ChrMap> {
let key = (genome.to_string(), full);
if let Some(hit) = fetch_cache().lock().get(&key) {
return Ok(hit.clone());
}
let fetched = fetch_ucsc(genome, full)?;
fetch_cache().lock().insert(key, fetched.clone());
Ok(fetched)
}
#[cfg(feature = "url")]
fn fetch_ucsc(genome: &str, full: bool) -> Result<ChrMap> {
use crate::error::Error;
let url = format!("https://api.genome.ucsc.edu/list/chromosomes?genome={genome}");
let body = crate::source::http_get_text(&url)?;
let parsed: serde_json::Value = serde_json::from_str(&body).map_err(|e| Error::Http {
url: url.clone(),
status: None,
message: format!("response was not JSON: {e}"),
})?;
let chromosomes = parsed
.get("chromosomes")
.and_then(|c| c.as_object())
.ok_or_else(|| Error::Http {
url: url.clone(),
status: None,
message: format!("no chromosomes for genome {genome:?}"),
})?;
let mut entries: Vec<(String, i64)> = chromosomes
.iter()
.filter(|(id, _)| full || !id.contains('_'))
.filter_map(|(id, size)| size.as_i64().map(|size| (id.clone(), size)))
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
if entries.is_empty() {
return Err(Error::Http {
url,
status: None,
message: format!("genome {genome:?} has no chromosomes"),
});
}
Ok(ChrMap::from_entries(entries))
}
#[cfg(not(feature = "url"))]
fn fetch_ucsc(genome: &str, _full: bool) -> Result<ChrMap> {
Err(crate::error::Error::Unsupported(format!(
"genome {genome:?} is not bundled and the `url` feature is off"
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bundled_genomes_need_no_network() {
let mm10 = get_chr_sizes("mm10", false).unwrap();
assert_eq!(mm10.len(), 22);
assert_eq!(mm10.resolve("chr19").unwrap().size, 61_431_566);
assert_eq!(mm10.resolve("19").unwrap().id, "chr19");
}
#[test]
fn genome_names_are_case_insensitive() {
let a = get_chr_sizes("sacCer3", false).unwrap();
let b = get_chr_sizes("SACCER3", false).unwrap();
assert_eq!(a.names(), b.names());
}
#[test]
fn the_order_is_string_order() {
let hg38 = get_chr_sizes("hg38", false).unwrap();
let names = hg38.names();
assert_eq!(&names[..3], ["chr1", "chr10", "chr11"]);
assert_eq!(hg38.by_index(0).unwrap().id, "chr1");
}
#[test]
fn an_unknown_genome_is_a_bad_argument_and_a_dead_network_is_not() {
use crate::error::Error;
let refused = Error::Http {
url: "https://api.genome.ucsc.edu/list/chromosomes?genome=nosuchgenome_zz".into(),
status: Some(400),
message: "fetching failed with HTTP 400".into(),
};
let message = match annotate("nosuchgenome_zz", refused) {
Error::InvalidArgument(message) => message,
other => panic!("a 400 should be the caller's argument: {other:?}"),
};
assert!(message.contains("nosuchgenome_zz"), "{message}");
assert!(message.contains("Bundled"), "{message}");
assert!(
message.contains("mm10") && message.contains("sacCer3"),
"{message}"
);
for error in [
Error::Http {
url: "https://api.genome.ucsc.edu/x".into(),
status: Some(503),
message: "fetching failed with HTTP 503".into(),
},
Error::Http {
url: "https://api.genome.ucsc.edu/x".into(),
status: None,
message: "fetching failed: dns error".into(),
},
Error::io(
"https://api.genome.ucsc.edu/x",
std::io::Error::other("down"),
),
] {
let kind = format!("{error:?}");
assert!(
!matches!(annotate("hg38", error), Error::InvalidArgument(_)),
"{kind} was flattened into a bad-argument error"
);
}
}
#[test]
fn bundled_names_are_listed() {
assert!(bundled_genomes().contains(&"hg38"));
assert_eq!(bundled_genomes().len(), 7);
}
}