use thiserror::Error;
use crate::nfs::VaultPath;
#[derive(Error, Debug)]
pub enum VaultError {
#[error("Path {path} doesn't exist")]
VaultPathNotFound {
path: String,
},
#[error("Path {path} is not a directory")]
PathIsNotDirectory {
path: VaultPath,
},
#[error("DB Error: {0}")]
DBError(#[from] DBError),
#[error("File System Error: {0}")]
FSError(#[from] FSError),
#[error("Note already exists at: {path}")]
NoteExists {
path: VaultPath,
},
#[error("Directory already exists at: {path}")]
DirectoryExists {
path: VaultPath,
},
#[error("Text to replace not found in note: {path}")]
ReplaceTextNotFound {
path: VaultPath,
},
#[error("Text to replace is not unique in note: {path}; replace every occurrence to proceed")]
ReplaceTextNotUnique {
path: VaultPath,
},
#[error("Invalid regular expression '{pattern}': {message}")]
InvalidRegex {
pattern: String,
message: String,
},
#[error("Case-sensitivity conflicts detected in vault:\n{}", conflicts.join("\n"))]
CaseConflict {
conflicts: Vec<String>,
},
#[error("Background task failed: {0}")]
TaskJoin(String),
}
impl From<sqlx::Error> for VaultError {
fn from(e: sqlx::Error) -> Self {
VaultError::DBError(DBError::from(e))
}
}
impl VaultError {
pub fn is_not_found(&self) -> bool {
match self {
VaultError::VaultPathNotFound { .. } => true,
VaultError::FSError(e) => e.is_not_found(),
_ => false,
}
}
pub fn is_user_error(&self) -> bool {
self.user_message().is_some()
}
pub fn user_message(&self) -> Option<String> {
match self {
VaultError::VaultPathNotFound { path } => Some(format!("Note not found: {path}")),
VaultError::FSError(FSError::VaultPathNotFound { path }) => {
Some(format!("Note not found: {path}"))
}
VaultError::FSError(FSError::NoFileOrDirectoryFound { path }) => {
Some(format!("Note not found: {path}"))
}
VaultError::NoteExists { path } => Some(format!("Note already exists: {path}")),
VaultError::DirectoryExists { path } => {
Some(format!("Directory already exists: {path}"))
}
VaultError::FSError(FSError::AlreadyExists { path }) => {
Some(format!("Already exists: {path}"))
}
VaultError::FSError(FSError::InvalidPath { path, message }) => {
Some(format!("Invalid path '{path}': {message}"))
}
VaultError::PathIsNotDirectory { path } => Some(format!("Not a directory: {path}")),
VaultError::ReplaceTextNotFound { .. }
| VaultError::ReplaceTextNotUnique { .. }
| VaultError::InvalidRegex { .. } => Some(self.to_string()),
VaultError::DBError(_)
| VaultError::CaseConflict { .. }
| VaultError::TaskJoin(_)
| VaultError::FSError(FSError::ReadFileError(_))
| VaultError::FSError(FSError::EncodingError(_))
| VaultError::FSError(FSError::SerializationError(_)) => None,
}
}
}
#[derive(Error, Debug)]
pub enum FSError {
#[error("IO Error: {0}")]
ReadFileError(#[from] std::io::Error),
#[error("Decoding Error: {0}")]
EncodingError(#[from] std::string::FromUtf8Error),
#[error("No File or Directory found at {path}")]
NoFileOrDirectoryFound {
path: String,
},
#[error("Invalid path {path}, {message}")]
InvalidPath {
path: String,
message: String,
},
#[error("Path doesn't exists at: {path}")]
VaultPathNotFound {
path: VaultPath,
},
#[error("Path already exists at: {path}")]
AlreadyExists {
path: VaultPath,
},
#[error("Serialization error: {0}")]
SerializationError(String),
}
impl FSError {
pub fn is_not_found(&self) -> bool {
matches!(
self,
FSError::VaultPathNotFound { .. } | FSError::NoFileOrDirectoryFound { .. }
)
}
}
#[derive(Error, Debug)]
pub enum DBError {
#[error("Database Error: {0}")]
DBError(#[from] sqlx::Error),
#[error("Error DB Connection Closed")]
DBConnectionClosed,
#[error("Error Querying Data: {0}")]
QueryError(String),
#[error("Error reading cached notes in the DB: {0}")]
NonCritical(String),
#[error("DB related error: {0}")]
Other(String),
#[error("Pool error: {0}")]
PoolError(String),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nfs::VaultPath;
#[test]
fn user_messages_are_clean_and_llm_facing() {
assert_eq!(
VaultError::FSError(FSError::VaultPathNotFound {
path: VaultPath::note_path_from("a")
})
.user_message()
.as_deref(),
Some("Note not found: a.md")
);
assert_eq!(
VaultError::NoteExists {
path: VaultPath::note_path_from("a")
}
.user_message()
.as_deref(),
Some("Note already exists: a.md")
);
assert!(VaultError::ReplaceTextNotUnique {
path: VaultPath::note_path_from("a")
}
.user_message()
.unwrap()
.contains("not unique"));
}
#[test]
fn internal_failures_have_no_user_message() {
assert!(VaultError::DBError(DBError::DBConnectionClosed)
.user_message()
.is_none());
assert!(VaultError::TaskJoin("boom".into()).user_message().is_none());
assert!(VaultError::FSError(FSError::EncodingError(
String::from_utf8(vec![0xff]).unwrap_err()
))
.user_message()
.is_none());
assert!(VaultError::NoteExists {
path: VaultPath::note_path_from("a")
}
.is_user_error());
assert!(!VaultError::DBError(DBError::DBConnectionClosed).is_user_error());
}
#[test]
fn not_found_recognized_through_the_fs_layer() {
assert!(VaultError::FSError(FSError::VaultPathNotFound {
path: VaultPath::note_path_from("a")
})
.is_not_found());
assert!(
VaultError::FSError(FSError::NoFileOrDirectoryFound { path: "a".into() })
.is_not_found()
);
assert!(!VaultError::NoteExists {
path: VaultPath::note_path_from("a")
}
.is_not_found());
}
}