From 4517fa34f82d8624922cc691c0594188ba34d751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 16:35:16 +0100 Subject: [PATCH] feat: Create unified error handling with FileTagsError --- src/error.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 39 +++++++------------- src/symlink.rs | 43 +++++++--------------- src/tag_engine.rs | 18 +-------- 4 files changed, 121 insertions(+), 73 deletions(-) create mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..2a30b64 --- /dev/null +++ b/src/error.rs @@ -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 for FileTagsError { + fn from(err: ParseError) -> Self { + FileTagsError::Parse { + file: PathBuf::from(""), + 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()); + } +} diff --git a/src/main.rs b/src/main.rs index 229d353..fcb5adf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, CommandError> { +fn list_tags(files: &[String]) -> Result, 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, 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> { +fn main() -> Result<(), FileTagsError> { let cli = Cli::parse(); match cli.command { diff --git a/src/symlink.rs b/src/symlink.rs index 6816c15..1ab47fa 100644 --- a/src/symlink.rs +++ b/src/symlink.rs @@ -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, target_dir: &Path) -> Result<(), SymlinkError> { +pub fn create_symlink_tree(paths: Vec, 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(_)))); } } diff --git a/src/tag_engine.rs b/src/tag_engine.rs index b3aee70..89503ff 100644 --- a/src/tag_engine.rs +++ b/src/tag_engine.rs @@ -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() {