use std::{fmt, io};
use error_forge::ForgeError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
Io {
context: &'static str,
source: io::Error,
},
Corruption {
reason: &'static str,
},
}
impl Error {
pub(crate) fn io(context: &'static str, source: io::Error) -> Self {
Error::Io { context, source }
}
pub(crate) fn corruption(reason: &'static str) -> Self {
Error::Corruption { reason }
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io { context, source } => {
write!(f, "i/o error while {context}: {source}")
}
Error::Corruption { reason } => {
write!(f, "sorted-run corruption: {reason}")
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { source, .. } => Some(source),
Error::Corruption { .. } => None,
}
}
}
impl From<io::Error> for Error {
fn from(source: io::Error) -> Self {
Error::Io {
context: "performing a storage i/o operation",
source,
}
}
}
impl ForgeError for Error {
fn kind(&self) -> &'static str {
match self {
Error::Io { .. } => "Io",
Error::Corruption { .. } => "Corruption",
}
}
fn caption(&self) -> &'static str {
"lsm storage engine error"
}
fn is_fatal(&self) -> bool {
matches!(self, Error::Corruption { .. })
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_io_error_exposes_source() {
let inner = io::Error::new(io::ErrorKind::PermissionDenied, "denied");
let err = Error::io("open database directory", inner);
let source = std::error::Error::source(&err).expect("io error has a source");
let io_source = source
.downcast_ref::<io::Error>()
.expect("source downcasts to io::Error");
assert_eq!(io_source.kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn test_corruption_has_no_source() {
let err = Error::corruption("length prefix exceeds file size");
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn test_corruption_is_fatal_io_is_not() {
assert!(Error::corruption("truncated record").is_fatal());
let io = Error::io("read run", io::Error::from(io::ErrorKind::UnexpectedEof));
assert!(!io.is_fatal());
}
#[test]
fn test_kind_matches_variant() {
assert_eq!(Error::corruption("x").kind(), "Corruption");
let io = Error::io("x", io::Error::from(io::ErrorKind::Other));
assert_eq!(io.kind(), "Io");
}
#[test]
fn test_display_is_actionable() {
let err = Error::corruption("truncated value");
assert_eq!(err.to_string(), "sorted-run corruption: truncated value");
}
}