diff --git a/src/error.rs b/src/error.rs index 620bbb9..2a30b64 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ -use std::path::PathBuf; use thiserror::Error; +use std::path::PathBuf; #[derive(Error, Debug)] pub enum FileTagsError { @@ -27,7 +27,10 @@ pub enum FileTagsError { }, #[error("Failed to parse tags in {file}: {source}")] - Parse { file: PathBuf, source: ParseError }, + Parse { + file: PathBuf, + source: ParseError, + }, #[error("Tag error: {0}")] Tag(#[from] TagError), @@ -68,7 +71,7 @@ mod tests { 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 { .. })); } @@ -85,7 +88,7 @@ mod tests { path: PathBuf::from("/test"), source: io_err, }; - + assert!(err.source().is_some()); } } diff --git a/src/symlink.rs b/src/symlink.rs index c6ec56f..1ab47fa 100644 --- a/src/symlink.rs +++ b/src/symlink.rs @@ -1,17 +1,18 @@ -use crate::error::FileTagsError; -use fs_err as fs; use std::path::{Path, PathBuf}; +use fs_err as fs; +use crate::error::FileTagsError; 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(|_| FileTagsError::InvalidPath(path.clone()))?; - + let abs_path = fs::canonicalize(&path).map_err(|_| { + FileTagsError::InvalidPath(path.clone()) + })?; + // Get the file name for the symlink - let file_name = abs_path - .file_name() - .ok_or_else(|| FileTagsError::InvalidPath(abs_path.clone()))?; + let file_name = abs_path.file_name().ok_or_else(|| { + FileTagsError::InvalidPath(abs_path.clone()) + })?; // Create target directory if it doesn't exist fs::create_dir_all(target_dir).map_err(|e| FileTagsError::CreateDir { @@ -22,21 +23,17 @@ pub fn create_symlink_tree(paths: Vec, target_dir: &Path) -> Result<(), // 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| { - FileTagsError::CreateLink { - from: abs_path, - to: link_path, - source: e, - } + 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| { - FileTagsError::CreateLink { - from: abs_path, - to: link_path, - source: e, - } + std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink { + from: abs_path, + to: link_path, + source: e, })?; } @@ -52,19 +49,22 @@ mod tests { fn test_create_symlink_tree_basic() -> Result<(), Box> { let source_dir = TempDir::new()?; let target_dir = TempDir::new()?; - + // Create a test file let test_file = source_dir.path().join("test.txt"); fs::write(&test_file, "test content")?; // Create symlink tree - create_symlink_tree(vec![test_file.clone()], target_dir.path())?; + create_symlink_tree( + vec![test_file.clone()], + target_dir.path() + )?; // Verify symlink exists and points to correct file let symlink = target_dir.path().join("test.txt"); assert!(symlink.exists()); assert!(symlink.is_symlink()); - + #[cfg(unix)] { use std::os::unix::fs::MetadataExt; @@ -81,7 +81,7 @@ mod tests { fn test_create_symlink_tree_nested() -> Result<(), Box> { let source_dir = TempDir::new()?; let target_dir = TempDir::new()?; - + // Create nested test file let nested_dir = source_dir.path().join("nested"); fs::create_dir_all(&nested_dir)?; @@ -89,7 +89,10 @@ mod tests { fs::write(&test_file, "test content")?; // Create symlink tree - create_symlink_tree(vec![test_file], &target_dir.path().join("nested"))?; + create_symlink_tree( + vec![test_file], + &target_dir.path().join("nested") + )?; // Verify directory and symlink were created let symlink = target_dir.path().join("nested/test.txt"); @@ -102,10 +105,12 @@ mod tests { #[test] fn test_create_symlink_tree_invalid_path() { let target_dir = TempDir::new().unwrap(); - + // Try to create symlink with non-existent source - let result = - create_symlink_tree(vec![PathBuf::from("/nonexistent/path")], target_dir.path()); + let result = create_symlink_tree( + vec![PathBuf::from("/nonexistent/path")], + target_dir.path() + ); assert!(matches!(result, Err(FileTagsError::InvalidPath(_)))); } diff --git a/src/tag_engine.rs b/src/tag_engine.rs index 25cf985..89503ff 100644 --- a/src/tag_engine.rs +++ b/src/tag_engine.rs @@ -25,7 +25,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse .unwrap_or_else(|| filename.to_string()); let parts: Vec<&str> = file_name.split(TAG_DELIMITER).collect(); - + if parts.len() > 2 { return Err(ParseError::MultipleDelimiters); } @@ -37,10 +37,10 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse _ => String::new(), }; let base_name = base_parts.last().unwrap_or(&parts[0]).to_string(); - + let tags = if parts.len() == 2 { let mut tag_part = parts[1].to_string(); - + // Check if the last tag contains an extension if let Some(last_part) = tag_part.split_whitespace().last() { if let Some(dot_pos) = last_part.rfind('.') { @@ -48,7 +48,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse tag_part.truncate(tag_part.len() - extension.len()); } } - + // First parse all tags let parsed_tags: Vec = tag_part .split_whitespace() @@ -78,7 +78,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec, String), Parse pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec> { let mut result = Vec::new(); - + // Handle empty tags or depth 0 if tags.is_empty() || depth == 0 { return result; @@ -92,15 +92,17 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec // Generate combinations up to specified depth for len in 2..=depth.min(tags.len()) { let mut temp = Vec::new(); - + // Start with existing combinations of length-1 for combo in result.iter().filter(|c| c.len() == len - 1) { - // Try to add each remaining tag - for tag in tags { - if !combo.contains(tag) { - let mut new_combo = combo.clone(); - new_combo.push(tag.clone()); - temp.push(new_combo); + // Try to add each remaining tag that comes after the last tag in combo + if let Some(last) = combo.last() { + for tag in tags { + if tag > last && !combo.contains(tag) { + let mut new_combo = combo.clone(); + new_combo.push(tag.clone()); + temp.push(new_combo); + } } } } @@ -111,21 +113,20 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec } pub fn filter_tags(current: Vec, remove: &[String]) -> Vec { - current - .into_iter() + current.into_iter() .filter(|tag| !remove.contains(tag)) .collect() } pub fn add_tags(current: Vec, new: Vec) -> Vec { let mut result = current; - + for tag in new { if !result.contains(&tag) { result.push(tag); } } - + result } @@ -136,13 +137,7 @@ pub fn serialize_tags(base: &str, tags: &[String], extension: &str) -> String { if sorted_tags.is_empty() { format!("{}{}", base, extension) } else { - format!( - "{}{}{}{}", - base, - TAG_DELIMITER, - sorted_tags.join(" "), - extension - ) + format!("{}{}{}{}", base, TAG_DELIMITER, sorted_tags.join(" "), extension) } } @@ -324,19 +319,32 @@ mod tests { fn test_create_tag_combinations_depth_limit() { let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()]; let result = create_tag_combinations(&tags, 2); - + // Should contain individual tags and pairs, but no triples assert!(result.iter().all(|combo| combo.len() <= 2)); - assert_eq!(result.len(), 9); // 3 individual + 6 pairs + assert_eq!(result.len(), 6); // 3 individual + 3 pairs + } + + #[test] + fn test_create_tag_combinations_order() { + let tags = vec!["tag2".to_string(), "tag1".to_string(), "tag3".to_string()]; + let result = create_tag_combinations(&tags, 2); + + // Check that all combinations maintain alphabetical order + for combo in result { + if combo.len() > 1 { + assert!(combo.windows(2).all(|w| w[0] < w[1])); + } + } } #[test] fn test_create_tag_combinations_uniqueness() { let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()]; let result = create_tag_combinations(&tags, 3); - + // Convert to set to check for duplicates let result_set: std::collections::HashSet<_> = result.into_iter().collect(); - assert_eq!(result_set.len(), 15); // 3 individual + 6 pairs + 9 triple + assert_eq!(result_set.len(), 7); // 3 individual + 3 pairs + 1 triple } }