use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("storage error: {0}")]
Storage(#[from] StorageError),
#[error("chunking error: {0}")]
Chunking(#[from] ChunkingError),
#[error("I/O error: {0}")]
Io(#[from] IoError),
#[error("command error: {0}")]
Command(#[from] CommandError),
#[error("search error: {0}")]
Search(#[from] SearchError),
#[error("invalid state: {message}")]
InvalidState {
message: String,
},
#[error("configuration error: {message}")]
Config {
message: String,
},
}
#[derive(Error, Debug)]
pub enum StorageError {
#[error("database error: {0}")]
Database(String),
#[error("RLM not initialized. Run: rlm-cli init")]
NotInitialized,
#[error("context not found")]
ContextNotFound,
#[error("buffer not found: {identifier}")]
BufferNotFound {
identifier: String,
},
#[error("chunk not found: {id}")]
ChunkNotFound {
id: i64,
},
#[error("migration error: {0}")]
Migration(String),
#[error("transaction error: {0}")]
Transaction(String),
#[error("serialization error: {0}")]
Serialization(String),
#[cfg(feature = "usearch-hnsw")]
#[error("vector search error: {0}")]
VectorSearch(String),
#[cfg(feature = "fastembed-embeddings")]
#[error("embedding error: {0}")]
Embedding(String),
}
#[derive(Error, Debug)]
pub enum ChunkingError {
#[error("invalid UTF-8 at byte offset {offset}")]
InvalidUtf8 {
offset: usize,
},
#[error("chunk size {size} exceeds maximum {max}")]
ChunkTooLarge {
size: usize,
max: usize,
},
#[error("invalid chunk configuration: {reason}")]
InvalidConfig {
reason: String,
},
#[error("overlap {overlap} must be less than chunk size {size}")]
OverlapTooLarge {
overlap: usize,
size: usize,
},
#[error("parallel processing failed: {reason}")]
ParallelFailed {
reason: String,
},
#[error("semantic analysis failed: {0}")]
SemanticFailed(String),
#[error("regex error: {0}")]
Regex(String),
#[error("unknown chunking strategy: {name}")]
UnknownStrategy {
name: String,
},
}
#[derive(Error, Debug)]
pub enum IoError {
#[error("file not found: {path}")]
FileNotFound {
path: String,
},
#[error("failed to read file: {path}: {reason}")]
ReadFailed {
path: String,
reason: String,
},
#[error("failed to write file: {path}: {reason}")]
WriteFailed {
path: String,
reason: String,
},
#[error("memory mapping failed: {path}: {reason}")]
MmapFailed {
path: String,
reason: String,
},
#[error("failed to create directory: {path}: {reason}")]
DirectoryFailed {
path: String,
reason: String,
},
#[error("path traversal denied: {path}")]
PathTraversal {
path: String,
},
#[error("I/O error: {0}")]
Generic(String),
}
#[derive(Error, Debug)]
pub enum SearchError {
#[error("index error: {message}")]
IndexError {
message: String,
},
#[error("dimension mismatch: expected {expected}, got {got}")]
DimensionMismatch {
expected: usize,
got: usize,
},
#[error("feature not enabled: {feature}")]
FeatureNotEnabled {
feature: String,
},
#[error("query error: {message}")]
QueryError {
message: String,
},
}
#[derive(Error, Debug)]
pub enum CommandError {
#[error("unknown command: {0}")]
UnknownCommand(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("missing required argument: {0}")]
MissingArgument(String),
#[error("command execution failed: {0}")]
ExecutionFailed(String),
#[error("operation cancelled by user")]
Cancelled,
#[error("output format error: {0}")]
OutputFormat(String),
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::Io(IoError::Generic(err.to_string()))
}
}
impl From<rusqlite::Error> for Error {
fn from(err: rusqlite::Error) -> Self {
Self::Storage(StorageError::Database(err.to_string()))
}
}
impl From<rusqlite::Error> for StorageError {
fn from(err: rusqlite::Error) -> Self {
Self::Database(err.to_string())
}
}
impl From<regex::Error> for ChunkingError {
fn from(err: regex::Error) -> Self {
Self::Regex(err.to_string())
}
}
impl From<serde_json::Error> for StorageError {
fn from(err: serde_json::Error) -> Self {
Self::Serialization(err.to_string())
}
}
impl From<std::string::FromUtf8Error> for ChunkingError {
fn from(err: std::string::FromUtf8Error) -> Self {
Self::InvalidUtf8 {
offset: err.utf8_error().valid_up_to(),
}
}
}
impl From<std::str::Utf8Error> for ChunkingError {
fn from(err: std::str::Utf8Error) -> Self {
Self::InvalidUtf8 {
offset: err.valid_up_to(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = Error::InvalidState {
message: "test error".to_string(),
};
assert_eq!(err.to_string(), "invalid state: test error");
}
#[test]
fn test_storage_error_display() {
let err = StorageError::NotInitialized;
assert_eq!(err.to_string(), "RLM not initialized. Run: rlm-cli init");
let err = StorageError::BufferNotFound {
identifier: "test-buffer".to_string(),
};
assert_eq!(err.to_string(), "buffer not found: test-buffer");
}
#[test]
fn test_chunking_error_display() {
let err = ChunkingError::InvalidUtf8 { offset: 42 };
assert_eq!(err.to_string(), "invalid UTF-8 at byte offset 42");
let err = ChunkingError::OverlapTooLarge {
overlap: 100,
size: 50,
};
assert_eq!(
err.to_string(),
"overlap 100 must be less than chunk size 50"
);
}
#[test]
fn test_io_error_display() {
let err = IoError::FileNotFound {
path: "/tmp/test.txt".to_string(),
};
assert_eq!(err.to_string(), "file not found: /tmp/test.txt");
}
#[test]
fn test_command_error_display() {
let err = CommandError::MissingArgument("--file".to_string());
assert_eq!(err.to_string(), "missing required argument: --file");
}
#[test]
fn test_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn test_error_from_storage() {
let storage_err = StorageError::NotInitialized;
let err: Error = storage_err.into();
assert!(matches!(err, Error::Storage(_)));
}
#[test]
fn test_error_from_chunking() {
let chunk_err = ChunkingError::InvalidUtf8 { offset: 0 };
let err: Error = chunk_err.into();
assert!(matches!(err, Error::Chunking(_)));
}
#[test]
fn test_error_from_command() {
let cmd_err = CommandError::Cancelled;
let err: Error = cmd_err.into();
assert!(matches!(err, Error::Command(_)));
}
#[test]
fn test_error_config() {
let err = Error::Config {
message: "bad config".to_string(),
};
assert_eq!(err.to_string(), "configuration error: bad config");
}
#[test]
fn test_storage_error_variants() {
let err = StorageError::Database("connection failed".to_string());
assert!(err.to_string().contains("connection failed"));
let err = StorageError::ContextNotFound;
assert_eq!(err.to_string(), "context not found");
let err = StorageError::ChunkNotFound { id: 42 };
assert_eq!(err.to_string(), "chunk not found: 42");
let err = StorageError::Migration("schema error".to_string());
assert!(err.to_string().contains("schema error"));
let err = StorageError::Transaction("rollback".to_string());
assert!(err.to_string().contains("rollback"));
let err = StorageError::Serialization("invalid json".to_string());
assert!(err.to_string().contains("invalid json"));
}
#[test]
fn test_chunking_error_variants() {
let err = ChunkingError::ChunkTooLarge {
size: 1000,
max: 500,
};
assert!(err.to_string().contains("1000"));
assert!(err.to_string().contains("500"));
let err = ChunkingError::InvalidConfig {
reason: "bad overlap".to_string(),
};
assert!(err.to_string().contains("bad overlap"));
let err = ChunkingError::ParallelFailed {
reason: "thread panic".to_string(),
};
assert!(err.to_string().contains("thread panic"));
let err = ChunkingError::SemanticFailed("model error".to_string());
assert!(err.to_string().contains("model error"));
let err = ChunkingError::Regex("invalid pattern".to_string());
assert!(err.to_string().contains("invalid pattern"));
let err = ChunkingError::UnknownStrategy {
name: "foobar".to_string(),
};
assert!(err.to_string().contains("foobar"));
}
#[test]
fn test_io_error_variants() {
let err = IoError::ReadFailed {
path: "/tmp/test".to_string(),
reason: "permission denied".to_string(),
};
assert!(err.to_string().contains("/tmp/test"));
assert!(err.to_string().contains("permission denied"));
let err = IoError::WriteFailed {
path: "/tmp/out".to_string(),
reason: "disk full".to_string(),
};
assert!(err.to_string().contains("disk full"));
let err = IoError::MmapFailed {
path: "/tmp/big".to_string(),
reason: "out of memory".to_string(),
};
assert!(err.to_string().contains("memory mapping"));
let err = IoError::DirectoryFailed {
path: "/tmp/dir".to_string(),
reason: "exists".to_string(),
};
assert!(err.to_string().contains("directory"));
let err = IoError::PathTraversal {
path: "../etc/passwd".to_string(),
};
assert!(err.to_string().contains("traversal"));
let err = IoError::Generic("unknown error".to_string());
assert!(err.to_string().contains("unknown error"));
}
#[test]
fn test_command_error_variants() {
let err = CommandError::UnknownCommand("foo".to_string());
assert!(err.to_string().contains("unknown command"));
let err = CommandError::InvalidArgument("--bad".to_string());
assert!(err.to_string().contains("invalid argument"));
let err = CommandError::ExecutionFailed("timeout".to_string());
assert!(err.to_string().contains("execution failed"));
let err = CommandError::Cancelled;
assert!(err.to_string().contains("cancelled"));
let err = CommandError::OutputFormat("json error".to_string());
assert!(err.to_string().contains("output format"));
}
#[test]
fn test_from_rusqlite_error_to_error() {
let rusqlite_err = rusqlite::Error::InvalidQuery;
let err: Error = rusqlite_err.into();
assert!(matches!(err, Error::Storage(StorageError::Database(_))));
}
#[test]
fn test_from_rusqlite_error_to_storage_error() {
let rusqlite_err = rusqlite::Error::InvalidQuery;
let err: StorageError = rusqlite_err.into();
assert!(matches!(err, StorageError::Database(_)));
}
#[test]
#[allow(clippy::invalid_regex)]
fn test_from_regex_error_to_chunking_error() {
let regex_err = regex::Regex::new("[invalid").unwrap_err();
let err: ChunkingError = regex_err.into();
assert!(matches!(err, ChunkingError::Regex(_)));
}
#[test]
fn test_from_serde_json_error_to_storage_error() {
let json_err: serde_json::Error = serde_json::from_str::<i32>("invalid").unwrap_err();
let err: StorageError = json_err.into();
assert!(matches!(err, StorageError::Serialization(_)));
}
#[test]
fn test_from_string_utf8_error_to_chunking_error() {
let invalid_bytes = vec![0xff, 0xfe];
let utf8_err = String::from_utf8(invalid_bytes).unwrap_err();
let err: ChunkingError = utf8_err.into();
assert!(matches!(err, ChunkingError::InvalidUtf8 { .. }));
}
#[test]
fn test_from_str_utf8_error_to_chunking_error() {
let invalid_bytes: Vec<u8> = vec![0xff, 0xfe];
let utf8_err = std::str::from_utf8(&invalid_bytes).unwrap_err();
let err: ChunkingError = utf8_err.into();
assert!(matches!(err, ChunkingError::InvalidUtf8 { .. }));
}
}