From 06a25589b9aceef42ea6e88659c0ddea6db191d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 15:41:32 +0100 Subject: [PATCH] feat: Add tag engine module with tag validation and parsing --- Cargo.toml | 1 + src/main.rs | 2 + src/tag_engine.rs | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 src/tag_engine.rs diff --git a/Cargo.toml b/Cargo.toml index 0e88bad..cd1b656 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +thiserror = "1.0" diff --git a/src/main.rs b/src/main.rs index e7a11a9..2ee2541 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +mod tag_engine; + fn main() { println!("Hello, world!"); } diff --git a/src/tag_engine.rs b/src/tag_engine.rs new file mode 100644 index 0000000..36fb955 --- /dev/null +++ b/src/tag_engine.rs @@ -0,0 +1,115 @@ +use std::path::Path; +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), +} + +pub fn validate_tag(tag: &str) -> Result<(), TagError> { + if tag.is_empty() { + return Err(TagError::Empty); + } + + // Check for prohibited characters + for c in tag.chars() { + if c == '\0' || c == ':' || c == '/' || c == ' ' { + return Err(TagError::InvalidChar(c)); + } + } + + Ok(()) +} + +pub fn parse_tags(filename: &str) -> Result<(String, Vec), ParseError> { + const TAG_DELIMITER: &str = " -- "; + + let parts: Vec<&str> = filename.split(TAG_DELIMITER).collect(); + + if parts.len() > 2 { + return Err(ParseError::MultipleDelimiters); + } + + let base_name = parts[0].to_string(); + + let tags = if parts.len() == 2 { + let tag_part = parts[1]; + let tags: Vec = tag_part + .split_whitespace() + .map(str::to_string) + .collect(); + + // Validate each tag + for tag in &tags { + validate_tag(tag)?; + } + + tags + } else { + Vec::new() + }; + + Ok((base_name, tags)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_tag_valid() { + assert!(validate_tag("valid-tag").is_ok()); + assert!(validate_tag("tag123").is_ok()); + assert!(validate_tag("_tag_").is_ok()); + } + + #[test] + fn test_validate_tag_invalid() { + assert_eq!(validate_tag(""), Err(TagError::Empty)); + assert_eq!(validate_tag("bad tag"), Err(TagError::InvalidChar(' '))); + assert_eq!(validate_tag("bad:tag"), Err(TagError::InvalidChar(':'))); + assert_eq!(validate_tag("bad/tag"), Err(TagError::InvalidChar('/'))); + assert_eq!(validate_tag("bad\0tag"), Err(TagError::InvalidChar('\0'))); + } + + #[test] + fn test_parse_tags_no_tags() { + let (base, tags) = parse_tags("file.txt").unwrap(); + assert_eq!(base, "file.txt"); + assert!(tags.is_empty()); + } + + #[test] + fn test_parse_tags_with_tags() { + let (base, tags) = parse_tags("file.txt -- tag1 tag2").unwrap(); + assert_eq!(base, "file.txt"); + assert_eq!(tags, vec!["tag1", "tag2"]); + } + + #[test] + fn test_parse_tags_multiple_delimiters() { + assert_eq!( + parse_tags("file.txt -- tag1 -- tag2"), + Err(ParseError::MultipleDelimiters) + ); + } + + #[test] + fn test_parse_tags_invalid_tag() { + assert_eq!( + parse_tags("file.txt -- invalid:tag"), + Err(ParseError::InvalidTag(TagError::InvalidChar(':'))) + ); + } +}