use std::fmt::Write as _;
use std::ops::Range;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("error reading {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to write {path} ({what})")]
Write { path: String, what: String },
#[error("error fetching {url}{}: {message}", .status.map(|s| format!(" (HTTP {s})")).unwrap_or_default())]
Http {
url: String,
status: Option<u16>,
message: String,
},
#[error("{path} is closed")]
Closed { path: String },
#[error("{}", unknown_chr_message(.id, *.key_size, .truncated_match, .available))]
UnknownChromosome {
id: String,
key_size: Option<usize>,
truncated_match: Option<String>,
available: Vec<String>,
},
#[error("{0}")]
InvalidArgument(String),
#[error("{path}: {what}")]
Format { path: String, what: String },
#[error("{path}: {what} (at offset {offset})")]
Corrupt {
path: String,
offset: u64,
what: String,
},
#[error("{0}")]
Unsupported(String),
}
fn unknown_chr_message(
id: &str,
key_size: Option<usize>,
truncated_match: &Option<String>,
available: &[String],
) -> String {
let mut message = format!("Chromosome {id} not found");
if let Some(key_size) = key_size {
let _ = write!(
message,
", and cannot be: it is {} characters long and this file stores \
chromosome names in a {key_size}-character field",
id.chars().count()
);
if let Some(name) = truncated_match {
let _ = write!(
message,
". The file does hold {name}, which is what the first {key_size} \
characters spell: if it was written from names too long for its \
field, that is this chromosome stored truncated. Ids are matched \
whole, so ask for it by the name the file carries"
);
}
}
let _ = write!(message, " (available: {})", available.join(", "));
message
}
impl Error {
pub fn invalid(msg: impl Into<String>) -> Self {
Error::InvalidArgument(msg.into())
}
pub fn format(path: impl Into<String>, what: impl Into<String>) -> Self {
Error::Format {
path: path.into(),
what: what.into(),
}
}
pub fn corrupt(path: impl Into<String>, offset: u64, what: impl Into<String>) -> Self {
Error::Corrupt {
path: path.into(),
offset,
what: what.into(),
}
}
pub fn write(path: impl Into<String>, what: impl Into<String>) -> Self {
Error::Write {
path: path.into(),
what: what.into(),
}
}
pub fn io(path: impl Into<String>, source: std::io::Error) -> Self {
Error::Io {
path: path.into(),
source,
}
}
}
pub fn check_range(path: &str, buf_len: usize, range: Range<usize>, what: &str) -> Result<()> {
if range.end > buf_len || range.start > range.end {
return Err(Error::corrupt(
path,
range.start as u64,
format!(
"{what}: bytes {}..{} of a {buf_len}-byte buffer",
range.start, range.end
),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn names(ids: &[&str]) -> Vec<String> {
ids.iter().map(|s| s.to_string()).collect()
}
#[test]
fn an_unknown_chromosome_names_itself_and_what_was_available() {
let err = Error::UnknownChromosome {
id: "chrZ".into(),
key_size: None,
truncated_match: None,
available: names(&["chr1", "chr2"]),
};
assert_eq!(
err.to_string(),
"Chromosome chrZ not found (available: chr1, chr2)"
);
}
#[test]
fn an_over_long_id_says_the_field_is_too_narrow() {
let err = Error::UnknownChromosome {
id: "chrXVII".into(),
key_size: Some(4),
truncated_match: None,
available: names(&["chrI"]),
};
let message = err.to_string();
assert!(message.contains("7 characters long"), "{message}");
assert!(message.contains("4-character field"), "{message}");
}
#[test]
fn a_prefix_that_spells_a_real_chromosome_is_named() {
let err = Error::UnknownChromosome {
id: "chrXVII".into(),
key_size: Some(6),
truncated_match: Some("chrXVI".into()),
available: names(&["chrXVI"]),
};
let message = err.to_string();
assert!(message.contains("The file does hold chrXVI"), "{message}");
}
#[test]
fn the_parts_are_reachable_without_parsing_the_message() {
let err = Error::UnknownChromosome {
id: "12".into(),
key_size: Some(1),
truncated_match: Some("chr1".into()),
available: names(&["chr1"]),
};
let Error::UnknownChromosome {
id,
key_size,
truncated_match,
available,
} = &err
else {
panic!("wrong variant");
};
assert_eq!(id, "12");
assert_eq!(*key_size, Some(1));
assert_eq!(truncated_match.as_deref(), Some("chr1"));
assert_eq!(available.len(), 1);
}
}