Compare commits

...

2 commits

3 changed files with 59 additions and 75 deletions

View file

@ -1,5 +1,5 @@
use thiserror::Error;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FileTagsError {
@ -27,10 +27,7 @@ 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),
@ -71,7 +68,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 { .. }));
}
@ -88,7 +85,7 @@ mod tests {
path: PathBuf::from("/test"),
source: io_err,
};
assert!(err.source().is_some());
}
}

View file

@ -1,18 +1,17 @@
use std::path::{Path, PathBuf};
use fs_err as fs;
use crate::error::FileTagsError;
use fs_err as fs;
use std::path::{Path, PathBuf};
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(|_| {
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 {
@ -23,17 +22,21 @@ pub fn create_symlink_tree(paths: Vec<PathBuf>, 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,
}
})?;
}
@ -49,22 +52,19 @@ mod tests {
fn test_create_symlink_tree_basic() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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,10 +89,7 @@ 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");
@ -105,12 +102,10 @@ 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(_))));
}

View file

@ -25,7 +25,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec<String>, 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>, 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>, String), Parse
tag_part.truncate(tag_part.len() - extension.len());
}
}
// First parse all tags
let parsed_tags: Vec<String> = tag_part
.split_whitespace()
@ -78,7 +78,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec<String>, String), Parse
pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec<Vec<String>> {
let mut result = Vec::new();
// Handle empty tags or depth 0
if tags.is_empty() || depth == 0 {
return result;
@ -92,17 +92,15 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec<Vec<String>
// 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 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);
}
// 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);
}
}
}
@ -113,20 +111,21 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec<Vec<String>
}
pub fn filter_tags(current: Vec<String>, remove: &[String]) -> Vec<String> {
current.into_iter()
current
.into_iter()
.filter(|tag| !remove.contains(tag))
.collect()
}
pub fn add_tags(current: Vec<String>, new: Vec<String>) -> Vec<String> {
let mut result = current;
for tag in new {
if !result.contains(&tag) {
result.push(tag);
}
}
result
}
@ -137,7 +136,13 @@ 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
)
}
}
@ -319,32 +324,19 @@ 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(), 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]));
}
}
assert_eq!(result.len(), 9); // 3 individual + 6 pairs
}
#[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(), 7); // 3 individual + 3 pairs + 1 triple
assert_eq!(result_set.len(), 15); // 3 individual + 6 pairs + 9 triple
}
}