feat: Create unified error handling with FileTagsError

This commit is contained in:
Moritz Böhme 2025-02-23 16:35:16 +01:00
parent aa18a35f8d
commit 4517fa34f8
4 changed files with 121 additions and 73 deletions

94
src/error.rs Normal file
View file

@ -0,0 +1,94 @@
use thiserror::Error;
use std::path::PathBuf;
#[derive(Error, Debug)]
pub enum FileTagsError {
#[error("Failed to create directory {path}: {source}")]
CreateDir {
path: PathBuf,
source: std::io::Error,
},
#[error("Failed to create symlink from {from} to {to}: {source}")]
CreateLink {
from: PathBuf,
to: PathBuf,
source: std::io::Error,
},
#[error("Invalid path: {0}")]
InvalidPath(PathBuf),
#[error("Failed to rename {from} to {to}: {source}")]
Rename {
from: PathBuf,
to: PathBuf,
source: std::io::Error,
},
#[error("Failed to parse tags in {file}: {source}")]
Parse {
file: PathBuf,
source: ParseError,
},
#[error("Tag error: {0}")]
Tag(#[from] TagError),
}
#[derive(Error, Debug, PartialEq)]
pub enum TagError {
#[error("tag cannot be empty")]
Empty,
#[error("tag contains invalid character: {0}")]
InvalidChar(char),
}
#[derive(Error, Debug, PartialEq)]
pub enum ParseError {
#[error("multiple tag delimiters found")]
MultipleDelimiters,
#[error("invalid tag: {0}")]
InvalidTag(#[from] TagError),
}
impl From<ParseError> for FileTagsError {
fn from(err: ParseError) -> Self {
FileTagsError::Parse {
file: PathBuf::from("<unknown>"),
source: err,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn test_error_conversion() {
let tag_err = TagError::Empty;
let parse_err = ParseError::InvalidTag(tag_err);
let file_err = FileTagsError::from(parse_err);
assert!(matches!(file_err, FileTagsError::Parse { .. }));
}
#[test]
fn test_error_display() {
let err = FileTagsError::InvalidPath(PathBuf::from("/bad/path"));
assert_eq!(err.to_string(), "Invalid path: /bad/path");
}
#[test]
fn test_error_source() {
let io_err = std::io::Error::from(std::io::ErrorKind::NotFound);
let err = FileTagsError::CreateDir {
path: PathBuf::from("/test"),
source: io_err,
};
assert!(err.source().is_some());
}
}

View file

@ -1,26 +1,13 @@
mod tag_engine;
mod symlink;
mod error;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::error::Error;
use clap::{Parser, Subcommand};
use fs_err as fs;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CommandError {
#[error("Failed to rename {from} to {to}: {source}")]
Rename {
from: String,
to: String,
source: std::io::Error,
},
#[error("Failed to parse tags in {file}: {source}")]
Parse {
file: String,
source: tag_engine::ParseError,
},
}
use crate::error::{FileTagsError, ParseError};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
@ -46,14 +33,14 @@ enum Commands {
},
}
fn list_tags(files: &[String]) -> Result<Vec<String>, CommandError> {
fn list_tags(files: &[String]) -> Result<Vec<String>, FileTagsError> {
let mut unique_tags = BTreeSet::new();
for file in files {
match tag_engine::parse_tags(file) {
Ok((_, tags, _)) => unique_tags.extend(tags),
Err(e) => return Err(CommandError::Parse {
file: file.to_string(),
Err(e) => return Err(FileTagsError::Parse {
file: PathBuf::from(file),
source: e,
}),
}
@ -62,9 +49,9 @@ fn list_tags(files: &[String]) -> Result<Vec<String>, CommandError> {
Ok(unique_tags.into_iter().collect())
}
fn add_tags_to_file(file: &str, new_tags: &[String]) -> Result<(), CommandError> {
let (base, current_tags, ext) = tag_engine::parse_tags(file).map_err(|e| CommandError::Parse {
file: file.to_string(),
fn add_tags_to_file(file: &str, new_tags: &[String]) -> Result<(), FileTagsError> {
let (base, current_tags, ext) = tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse {
file: PathBuf::from(file),
source: e,
})?;
@ -83,9 +70,9 @@ fn add_tags_to_file(file: &str, new_tags: &[String]) -> Result<(), CommandError>
// Only rename if the name would actually change
if file != new_path {
fs::rename(file, &new_path).map_err(|e| CommandError::Rename {
from: file.to_string(),
to: new_path,
fs::rename(file, &new_path).map_err(|e| FileTagsError::Rename {
from: PathBuf::from(file),
to: PathBuf::from(&new_path),
source: e,
})?;
}
@ -93,7 +80,7 @@ fn add_tags_to_file(file: &str, new_tags: &[String]) -> Result<(), CommandError>
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
fn main() -> Result<(), FileTagsError> {
let cli = Cli::parse();
match cli.command {

View file

@ -1,55 +1,38 @@
use std::path::{Path, PathBuf};
use thiserror::Error;
use fs_err as fs;
use crate::error::FileTagsError;
#[derive(Error, Debug)]
pub enum SymlinkError {
#[error("Failed to create directory {path}: {source}")]
CreateDir {
path: String,
source: std::io::Error,
},
#[error("Failed to create symlink from {from} to {to}: {source}")]
CreateLink {
from: String,
to: String,
source: std::io::Error,
},
#[error("Invalid path: {0}")]
InvalidPath(String),
}
pub fn create_symlink_tree(paths: Vec<PathBuf>, target_dir: &Path) -> Result<(), SymlinkError> {
pub fn create_symlink_tree(paths: Vec<PathBuf>, target_dir: &Path) -> Result<(), FileTagsError> {
for path in paths {
// Ensure path is absolute and clean
let abs_path = fs::canonicalize(&path).map_err(|_| {
SymlinkError::InvalidPath(path.to_string_lossy().into_owned())
FileTagsError::InvalidPath(path.clone())
})?;
// Get the file name for the symlink
let file_name = abs_path.file_name().ok_or_else(|| {
SymlinkError::InvalidPath(abs_path.to_string_lossy().into_owned())
FileTagsError::InvalidPath(abs_path.clone())
})?;
// Create target directory if it doesn't exist
fs::create_dir_all(target_dir).map_err(|e| SymlinkError::CreateDir {
path: target_dir.to_string_lossy().into_owned(),
fs::create_dir_all(target_dir).map_err(|e| FileTagsError::CreateDir {
path: target_dir.to_path_buf(),
source: e,
})?;
// Create the symlink
let link_path = target_dir.join(file_name);
#[cfg(unix)]
std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| SymlinkError::CreateLink {
from: abs_path.to_string_lossy().into_owned(),
to: link_path.to_string_lossy().into_owned(),
std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink {
from: abs_path,
to: link_path,
source: e,
})?;
#[cfg(windows)]
std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| SymlinkError::CreateLink {
from: abs_path.to_string_lossy().into_owned(),
to: link_path.to_string_lossy().into_owned(),
std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink {
from: abs_path,
to: link_path,
source: e,
})?;
}
@ -129,6 +112,6 @@ mod tests {
target_dir.path()
);
assert!(matches!(result, Err(SymlinkError::InvalidPath(_))));
assert!(matches!(result, Err(FileTagsError::InvalidPath(_))));
}
}

View file

@ -1,20 +1,4 @@
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum TagError {
#[error("tag cannot be empty")]
Empty,
#[error("tag contains invalid character: {0}")]
InvalidChar(char),
}
#[derive(Error, Debug, PartialEq)]
pub enum ParseError {
#[error("multiple tag delimiters found")]
MultipleDelimiters,
#[error("invalid tag: {0}")]
InvalidTag(#[from] TagError),
}
use crate::error::{ParseError, TagError};
pub fn validate_tag(tag: &str) -> Result<(), TagError> {
if tag.is_empty() {