feat: Integrate all components with new commands and comprehensive tests

This commit is contained in:
Moritz Böhme 2025-02-23 16:37:10 +01:00
parent 4517fa34f8
commit f23beef347

View file

@ -4,10 +4,9 @@ mod error;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::error::Error;
use clap::{Parser, Subcommand};
use fs_err as fs;
use crate::error::{FileTagsError, ParseError};
use crate::error::FileTagsError;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
@ -18,17 +17,40 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
/// List all unique tags
/// List all unique tags found in files
List {
/// Files to process
#[arg(required = true, help = "One or more files to process")]
files: Vec<String>,
},
/// Add tags to files
Add {
/// Tags to add
#[arg(long = "tag", required = true)]
#[arg(long = "tag", required = true, help = "Tags to add to files")]
tags: Vec<String>,
/// Files to process
#[arg(required = true, help = "One or more files to add tags to")]
files: Vec<String>,
},
/// Remove tags from files
Remove {
/// Tags to remove
#[arg(long = "tag", required = true, help = "Tags to remove from files")]
tags: Vec<String>,
/// Files to process
#[arg(required = true, help = "One or more files to remove tags from")]
files: Vec<String>,
},
/// Create a tag-based directory tree with symlinks
Tree {
/// Target directory for the tree
#[arg(long, required = true, help = "Target directory for creating the tree")]
dir: String,
/// Maximum depth of the tree
#[arg(long, default_value = "3", help = "Maximum depth of the directory tree")]
depth: usize,
/// Files to process
#[arg(required = true, help = "One or more files to create tree from")]
files: Vec<String>,
},
}
@ -95,6 +117,75 @@ fn main() -> Result<(), FileTagsError> {
add_tags_to_file(&file, &tags)?;
}
}
Commands::Remove { tags, files } => {
for file in files {
remove_tags_from_file(&file, &tags)?;
}
}
Commands::Tree { dir, depth, files } => {
create_tag_tree(&files, &dir, depth)?;
}
}
Ok(())
}
fn remove_tags_from_file(file: &str, remove_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,
})?;
let filtered_tags = tag_engine::filter_tags(current_tags, remove_tags);
let new_filename = tag_engine::serialize_tags(&base, &filtered_tags, &ext);
// Preserve the original directory
let parent = std::path::Path::new(file).parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let new_path = if parent.is_empty() {
new_filename
} else {
format!("{}/{}", parent, new_filename)
};
// Only rename if tags actually changed
if file != new_path {
fs::rename(file, &new_path).map_err(|e| FileTagsError::Rename {
from: PathBuf::from(file),
to: PathBuf::from(&new_path),
source: e,
})?;
}
Ok(())
}
fn create_tag_tree(files: &[String], target_dir: &str, depth: usize) -> Result<(), FileTagsError> {
let target = PathBuf::from(target_dir);
for file in files {
let (_base, tags, _ext) = tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse {
file: PathBuf::from(file),
source: e,
})?;
// Create root symlink
let paths = vec![PathBuf::from(file)];
symlink::create_symlink_tree(paths, &target)?;
// Generate all tag combinations and create directory structure
let combinations = tag_engine::create_tag_combinations(&tags, depth);
for combo in combinations {
let mut dir_path = target.clone();
for tag in &combo {
dir_path.push(tag);
}
let paths = vec![PathBuf::from(file)];
symlink::create_symlink_tree(paths, &dir_path)?;
}
}
Ok(())
@ -209,4 +300,69 @@ mod tests {
assert!(new_path.exists(), "Tagged file was not created in original directory");
Ok(())
}
#[test]
fn test_remove_tags() -> Result<(), Box<dyn Error>> {
let tmp_dir = TempDir::new()?;
let file = create_test_file(&tmp_dir, "test -- tag1 tag2 tag3.txt")?;
remove_tags_from_file(&file, &vec!["tag2".to_string()])?;
let new_path = tmp_dir.path().join("test -- tag1 tag3.txt");
assert!(new_path.exists(), "File with removed tag not found");
Ok(())
}
#[test]
fn test_create_tag_tree() -> Result<(), Box<dyn Error>> {
let tmp_dir = TempDir::new()?;
let source = create_test_file(&tmp_dir, "test -- tag1 tag2.txt")?;
let tree_dir = tmp_dir.path().join("tree");
create_tag_tree(&[source], tree_dir.to_str().unwrap(), 2)?;
// Check root symlink
assert!(tree_dir.join("test -- tag1 tag2.txt").exists());
// Check tag directories
assert!(tree_dir.join("tag1").join("test -- tag1 tag2.txt").exists());
assert!(tree_dir.join("tag2").join("test -- tag1 tag2.txt").exists());
assert!(tree_dir.join("tag1").join("tag2").join("test -- tag1 tag2.txt").exists());
Ok(())
}
#[test]
fn test_integration_all_commands() -> Result<(), Box<dyn Error>> {
let tmp_dir = TempDir::new()?;
let file1 = create_test_file(&tmp_dir, "doc1.txt")?;
let file2 = create_test_file(&tmp_dir, "doc2.txt")?;
// Add tags
add_tags_to_file(&file1, &vec!["work".to_string(), "draft".to_string()])?;
add_tags_to_file(&file2, &vec!["work".to_string(), "final".to_string()])?;
// List tags
let tags = list_tags(&[
tmp_dir.path().join("doc1 -- draft work.txt").to_str().unwrap().to_string(),
tmp_dir.path().join("doc2 -- final work.txt").to_str().unwrap().to_string(),
])?;
assert_eq!(tags, vec!["draft", "final", "work"]);
// Remove a tag
let file_to_remove = tmp_dir.path().join("doc1 -- draft work.txt").to_str().unwrap().to_string();
remove_tags_from_file(&file_to_remove, &vec!["draft".to_string()])?;
// Create tree
let tree_dir = tmp_dir.path().join("tree");
create_tag_tree(
&[tmp_dir.path().join("doc1 -- work.txt").to_str().unwrap().to_string()],
tree_dir.to_str().unwrap(),
2
)?;
assert!(tree_dir.join("work").join("doc1 -- work.txt").exists());
Ok(())
}
}