use crate::format::Format;
use crate::path::{Path, PathError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CodecErrorKind {
UnsupportedProfile,
UnsupportedVersion,
Syntax,
InvalidUnicode,
InvalidNode,
InvalidBase64,
DuplicateKey,
OutOfRange,
UnsupportedValue,
TypeMismatch,
AmbiguousOption,
Noncanonical,
ResourceLimit,
Io,
}
impl CodecErrorKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::UnsupportedProfile => "unsupported_profile",
Self::UnsupportedVersion => "unsupported_version",
Self::Syntax => "syntax",
Self::InvalidUnicode => "invalid_unicode",
Self::InvalidNode => "invalid_node",
Self::InvalidBase64 => "invalid_base64",
Self::DuplicateKey => "duplicate_key",
Self::OutOfRange => "out_of_range",
Self::UnsupportedValue => "unsupported_value",
Self::TypeMismatch => "type_mismatch",
Self::AmbiguousOption => "ambiguous_option",
Self::Noncanonical => "noncanonical",
Self::ResourceLimit => "resource_limit",
Self::Io => "io",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodecOperation {
Encode,
Decode,
}
impl std::fmt::Display for CodecOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CodecOperation::Encode => write!(f, "encode"),
CodecOperation::Decode => write!(f, "decode"),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Path(PathError),
NoRoute { path: Path },
Codec {
kind: CodecErrorKind,
operation: CodecOperation,
format: Format,
message: String,
},
UnsupportedFormat(Format),
Ll(structfs_ll_store::LLError),
Io(std::io::Error),
Store {
store: &'static str,
operation: &'static str,
message: String,
},
NotFound { path: Path },
PermissionDenied { message: String },
Conflict { message: String },
Overloaded { message: String },
DeadlineExceeded { message: String },
ResourceLimit { message: String },
Cancelled { message: String },
}
impl Error {
pub fn store(store: &'static str, operation: &'static str, message: impl Into<String>) -> Self {
Error::Store {
store,
operation,
message: message.into(),
}
}
pub fn decode(format: Format, message: impl Into<String>) -> Self {
Error::Codec {
kind: CodecErrorKind::Syntax,
operation: CodecOperation::Decode,
format,
message: message.into(),
}
}
pub fn encode(format: Format, message: impl Into<String>) -> Self {
Error::Codec {
kind: CodecErrorKind::UnsupportedValue,
operation: CodecOperation::Encode,
format,
message: message.into(),
}
}
pub fn not_found(path: Path) -> Self {
Error::NotFound { path }
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Error::PermissionDenied {
message: message.into(),
}
}
pub fn conflict(message: impl Into<String>) -> Self {
Error::Conflict {
message: message.into(),
}
}
pub fn overloaded(message: impl Into<String>) -> Self {
Error::Overloaded {
message: message.into(),
}
}
pub fn deadline_exceeded(message: impl Into<String>) -> Self {
Error::DeadlineExceeded {
message: message.into(),
}
}
pub fn resource_limit(message: impl Into<String>) -> Self {
Error::ResourceLimit {
message: message.into(),
}
}
pub fn cancelled(message: impl Into<String>) -> Self {
Error::Cancelled {
message: message.into(),
}
}
pub fn is_cancelled(&self) -> bool {
matches!(self, Error::Cancelled { .. })
}
pub fn is_not_found(&self) -> bool {
matches!(self, Error::NotFound { .. })
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Path(e) => write!(f, "path error: {}", e),
Error::NoRoute { path } => write!(f, "no route to {}", path),
Error::Codec {
operation,
format,
message,
..
} => {
write!(f, "{} failed for format {}: {}", operation, format, message)
}
Error::UnsupportedFormat(format) => {
write!(f, "unsupported format: {}", format)
}
Error::Ll(e) => write!(f, "low-level error: {}", e),
Error::Io(e) => write!(f, "I/O error: {}", e),
Error::Store {
store,
operation,
message,
} => write!(f, "{}::{}: {}", store, operation, message),
Error::NotFound { path } => write!(f, "not found: {}", path),
Error::PermissionDenied { message } => write!(f, "permission denied: {}", message),
Error::Conflict { message } => write!(f, "conflict: {}", message),
Error::Overloaded { message } => write!(f, "overloaded: {}", message),
Error::DeadlineExceeded { message } => write!(f, "deadline exceeded: {}", message),
Error::ResourceLimit { message } => write!(f, "resource limit: {}", message),
Error::Cancelled { message } => write!(f, "cancelled: {}", message),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Path(e) => Some(e),
Error::Ll(e) => Some(e),
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<PathError> for Error {
fn from(e: PathError) -> Self {
Error::Path(e)
}
}
impl From<structfs_ll_store::LLError> for Error {
fn from(e: structfs_ll_store::LLError) -> Self {
Error::Ll(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[test]
fn error_display() {
let e = Error::NoRoute {
path: Path::parse("foo/bar").unwrap(),
};
assert_eq!(e.to_string(), "no route to foo/bar");
let e = Error::UnsupportedFormat(Format::PROTOBUF);
assert!(format!("{}", e).contains("protobuf"));
}
#[test]
fn path_error_display() {
let e = Error::Path(PathError::InvalidComponent {
component: "bad".to_string(),
position: 1,
message: "invalid".to_string(),
});
assert!(format!("{}", e).contains("path error"));
}
#[test]
fn codec_decode_error_display() {
let e = Error::decode(Format::JSON, "unexpected token");
let display = format!("{}", e);
assert!(display.contains("decode"));
assert!(display.contains("json"));
assert!(display.contains("unexpected token"));
}
#[test]
fn codec_encode_error_display() {
let e = Error::encode(Format::CBOR, "serialization failed");
let display = format!("{}", e);
assert!(display.contains("encode"));
assert!(display.contains("cbor"));
assert!(display.contains("serialization failed"));
}
#[test]
fn ll_error_display() {
let ll_err = structfs_ll_store::LLError::NotSupported;
let e = Error::Ll(ll_err);
let display = format!("{}", e);
assert!(display.contains("low-level error"));
}
#[test]
fn store_error_display() {
let e = Error::store("http_broker", "read", "Request 42 not found");
assert_eq!(e.to_string(), "http_broker::read: Request 42 not found");
}
#[test]
fn io_error_display() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let e = Error::Io(io_err);
let display = format!("{}", e);
assert!(display.contains("I/O error"));
assert!(display.contains("file not found"));
}
#[test]
fn path_error_source() {
let e = Error::Path(PathError::InvalidPath {
message: "test".to_string(),
});
assert!(StdError::source(&e).is_some());
}
#[test]
fn ll_error_source() {
let ll_err = structfs_ll_store::LLError::NotSupported;
let e = Error::Ll(ll_err);
assert!(StdError::source(&e).is_some());
}
#[test]
fn io_error_source() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let e = Error::Io(io_err);
assert!(StdError::source(&e).is_some());
}
#[test]
fn store_error_source_is_none() {
let e = Error::store("test", "op", "message");
assert!(StdError::source(&e).is_none());
}
#[test]
fn path_error_conversion() {
let path_err = PathError::InvalidPath {
message: "test".to_string(),
};
let e: Error = path_err.into();
assert!(matches!(e, Error::Path(_)));
}
#[test]
fn ll_error_conversion() {
let ll_err = structfs_ll_store::LLError::ResourceExhausted;
let e: Error = ll_err.into();
assert!(matches!(e, Error::Ll(_)));
}
#[test]
fn io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let e: Error = io_err.into();
assert!(matches!(e, Error::Io(_)));
}
#[test]
fn typed_variant_display() {
let e = Error::not_found(Path::parse("users/123").unwrap());
assert_eq!(e.to_string(), "not found: users/123");
assert!(e.is_not_found());
assert_eq!(
Error::permission_denied("read-only mount").to_string(),
"permission denied: read-only mount"
);
assert_eq!(
Error::conflict("stale version").to_string(),
"conflict: stale version"
);
assert_eq!(
Error::overloaded("queue full").to_string(),
"overloaded: queue full"
);
assert_eq!(
Error::deadline_exceeded("10s elapsed").to_string(),
"deadline exceeded: 10s elapsed"
);
assert_eq!(
Error::resource_limit("frame too large").to_string(),
"resource limit: frame too large"
);
}
#[test]
fn typed_variants_have_no_source() {
assert!(StdError::source(&Error::conflict("x")).is_none());
assert!(!Error::conflict("x").is_not_found());
}
#[test]
fn codec_operation_display() {
assert_eq!(CodecOperation::Encode.to_string(), "encode");
assert_eq!(CodecOperation::Decode.to_string(), "decode");
}
#[test]
fn codec_error_with_operation() {
let e = Error::Codec {
kind: CodecErrorKind::Syntax,
operation: CodecOperation::Decode,
format: Format::JSON,
message: "test".to_string(),
};
assert!(matches!(
e,
Error::Codec {
operation: CodecOperation::Decode,
..
}
));
}
}