use std::{fmt, io};
use error_forge::ForgeError;
pub type Result<T, E = WalError> = std::result::Result<T, E>;
#[non_exhaustive]
#[derive(Debug)]
pub enum WalError {
Io {
context: &'static str,
source: io::Error,
},
RecordTooLarge {
len: usize,
max: u32,
},
Corruption {
offset: u64,
reason: &'static str,
},
Encoding {
detail: String,
},
}
impl WalError {
pub(crate) fn io(context: &'static str, source: io::Error) -> Self {
WalError::Io { context, source }
}
pub(crate) fn corruption(offset: u64, reason: &'static str) -> Self {
WalError::Corruption { offset, reason }
}
#[cfg(feature = "pack-io")]
pub(crate) fn encoding(detail: impl core::fmt::Display) -> Self {
WalError::Encoding {
detail: detail.to_string(),
}
}
}
impl fmt::Display for WalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WalError::Io { context, source } => {
write!(f, "i/o error while {context}: {source}")
}
WalError::RecordTooLarge { len, max } => {
write!(
f,
"record of {len} bytes exceeds the maximum of {max} bytes"
)
}
WalError::Corruption { offset, reason } => {
write!(f, "log corruption at byte offset {offset}: {reason}")
}
WalError::Encoding { detail } => {
write!(f, "typed record codec error: {detail}")
}
}
}
}
impl std::error::Error for WalError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
WalError::Io { source, .. } => Some(source),
WalError::RecordTooLarge { .. }
| WalError::Corruption { .. }
| WalError::Encoding { .. } => None,
}
}
}
impl From<io::Error> for WalError {
fn from(source: io::Error) -> Self {
WalError::Io {
context: "performing a log i/o operation",
source,
}
}
}
impl ForgeError for WalError {
fn kind(&self) -> &'static str {
match self {
WalError::Io { .. } => "Io",
WalError::RecordTooLarge { .. } => "RecordTooLarge",
WalError::Corruption { .. } => "Corruption",
WalError::Encoding { .. } => "Encoding",
}
}
fn caption(&self) -> &'static str {
"write-ahead log error"
}
fn is_fatal(&self) -> bool {
matches!(self, WalError::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 = WalError::io("open log file", 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_record_too_large_has_no_source() {
let err = WalError::RecordTooLarge {
len: 4096,
max: 1024,
};
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn test_corruption_is_fatal_others_are_not() {
assert!(WalError::corruption(128, "checksum mismatch").is_fatal());
assert!(!WalError::RecordTooLarge { len: 1, max: 0 }.is_fatal());
let io = WalError::io("x", io::Error::from(io::ErrorKind::Other));
assert!(!io.is_fatal());
}
#[test]
fn test_kind_matches_variant() {
assert_eq!(WalError::corruption(0, "x").kind(), "Corruption");
assert_eq!(
WalError::RecordTooLarge { len: 1, max: 0 }.kind(),
"RecordTooLarge"
);
}
#[test]
fn test_display_is_actionable() {
let err = WalError::RecordTooLarge {
len: 4096,
max: 1024,
};
assert_eq!(
err.to_string(),
"record of 4096 bytes exceeds the maximum of 1024 bytes"
);
}
}