use crate::search::SearchError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid input: {message}")]
InvalidInput {
message: String,
},
#[error("not found: {resource}")]
NotFound {
resource: String,
},
#[error("payload too large: {size} bytes (limit {limit})")]
PayloadTooLarge {
size: u64,
limit: u64,
},
#[error("bucket name too long: {name} ({len} chars, max 63)")]
BucketNameTooLong {
name: String,
len: usize,
},
#[error("configuration error: {message}")]
Config {
message: String,
},
#[error(transparent)]
Storage(StorageError),
#[error("malformed Range header: {0}")]
MalformedRange(String),
#[error("not modified")]
NotModified,
#[error("precondition failed")]
PreconditionFailed,
#[error("range not satisfiable (object size: {complete_length})")]
RangeNotSatisfiable {
complete_length: u64,
},
}
#[derive(Debug)]
pub enum StorageError {
NotFound {
key: String,
},
BucketNotFound {
bucket: String,
},
BackendUnavailable {
message: String,
},
Other {
source: Box<dyn std::error::Error + Send + Sync>,
},
NotModified,
PreconditionFailed,
RangeNotSatisfiable {
complete_length: u64,
},
}
impl std::fmt::Display for StorageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound { key } => write!(f, "object not found: {key}"),
Self::BucketNotFound { bucket } => write!(f, "bucket not found: {bucket}"),
Self::BackendUnavailable { message } => write!(f, "backend unavailable: {message}"),
Self::Other { source } => write!(f, "storage error: {source}"),
Self::NotModified => write!(f, "not modified"),
Self::PreconditionFailed => write!(f, "precondition failed"),
Self::RangeNotSatisfiable { complete_length } => {
write!(f, "range not satisfiable (object size: {complete_length})")
}
}
}
}
impl std::error::Error for StorageError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if let Self::Other { source } = self {
Some(source.as_ref())
} else {
None
}
}
}
impl StorageError {
pub fn is_not_found(&self) -> bool {
matches!(self, Self::NotFound { .. } | Self::BucketNotFound { .. })
}
}
impl From<StorageError> for Error {
fn from(err: StorageError) -> Self {
match err {
StorageError::NotModified => Error::NotModified,
StorageError::PreconditionFailed => Error::PreconditionFailed,
StorageError::RangeNotSatisfiable { complete_length } => {
Error::RangeNotSatisfiable { complete_length }
}
other => Error::Storage(other),
}
}
}
impl From<SearchError> for Error {
fn from(e: SearchError) -> Self {
match e {
SearchError::InvalidInput { message } => Error::InvalidInput { message },
SearchError::UnknownKb { slug } => Error::NotFound {
resource: format!("knowledgebase '{slug}'"),
},
SearchError::BackendUnavailable { message } => {
Error::Storage(StorageError::BackendUnavailable { message })
}
SearchError::Internal { message } => Error::Config { message },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_invalid_input_display() {
let e = Error::InvalidInput {
message: "bad".into(),
};
let s = e.to_string();
assert!(s.contains("bad"), "Display should contain the message: {s}");
}
#[test]
fn test_error_payload_too_large_display() {
let e = Error::PayloadTooLarge {
size: 17_000_000_u64,
limit: 16_777_216_u64,
};
let s = e.to_string();
assert!(
s.contains("17000000") || s.contains("17_000_000"),
"Display should contain size: {s}"
);
assert!(
s.contains("16777216") || s.contains("16_777_216"),
"Display should contain limit: {s}"
);
}
#[test]
fn test_storage_error_not_found_display() {
let e = StorageError::NotFound { key: "foo".into() };
let s = e.to_string();
assert!(s.contains("foo"), "Display should contain the key: {s}");
}
#[test]
fn test_storage_error_is_not_found_true_for_not_found() {
let e = StorageError::NotFound { key: "bar".into() };
assert!(e.is_not_found());
}
#[test]
fn test_storage_error_is_not_found_true_for_bucket_not_found() {
let e = StorageError::BucketNotFound {
bucket: "my-bucket".into(),
};
assert!(e.is_not_found());
}
#[test]
fn test_storage_error_is_not_found_false_for_backend_unavailable() {
let e = StorageError::BackendUnavailable {
message: "connection refused".into(),
};
assert!(!e.is_not_found());
}
#[test]
fn test_from_storage_error_for_error() {
let se = StorageError::NotFound { key: "obj".into() };
let e: Error = Error::from(se);
assert!(matches!(e, Error::Storage(_)));
}
#[test]
fn test_error_implements_std_error() {
fn assert_std_error<T: std::error::Error>(_: &T) {}
let e = Error::InvalidInput {
message: "test".into(),
};
assert_std_error(&e);
}
#[test]
fn test_storage_error_implements_std_error() {
fn assert_std_error<T: std::error::Error>(_: &T) {}
let e = StorageError::BucketNotFound { bucket: "b".into() };
assert_std_error(&e);
}
#[test]
fn test_error_not_found_display() {
let e = Error::NotFound {
resource: "kb:my-notes".into(),
};
let s = e.to_string();
assert!(
s.contains("my-notes"),
"Display should contain resource: {s}"
);
}
#[test]
fn test_error_bucket_name_too_long_fields() {
let e = Error::BucketNameTooLong {
name: "nt-toolong-name".into(),
len: 15_usize,
};
if let Error::BucketNameTooLong { name, len } = &e {
assert_eq!(name, "nt-toolong-name");
assert_eq!(*len, 15);
} else {
panic!("Wrong variant");
}
}
#[test]
fn storage_error_range_not_satisfiable_converts_to_error() {
let storage_err = StorageError::RangeNotSatisfiable {
complete_length: 100,
};
let err: Error = Error::from(storage_err);
assert!(matches!(
err,
Error::RangeNotSatisfiable {
complete_length: 100
}
));
}
#[test]
fn storage_error_not_modified_converts_to_error() {
let storage_err = StorageError::NotModified;
let err: Error = Error::from(storage_err);
assert!(matches!(err, Error::NotModified));
}
#[test]
fn storage_error_precondition_failed_converts_to_error() {
let storage_err = StorageError::PreconditionFailed;
let err: Error = Error::from(storage_err);
assert!(matches!(err, Error::PreconditionFailed));
}
}