feat: Create unified error handling with FileTagsError
This commit is contained in:
parent
aa18a35f8d
commit
4517fa34f8
4 changed files with 121 additions and 73 deletions
94
src/error.rs
Normal file
94
src/error.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
39
src/main.rs
39
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<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 {
|
||||
|
|
|
|||
|
|
@ -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(_))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue