1const LONGEST: usize = 24;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum TagError {
16 Empty,
18 TooLong {
20 length: usize,
22 },
23 BadStart {
25 character: char,
27 },
28 Illegal {
30 position: usize,
32 character: char,
34 },
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
39pub struct Tag(String);
40
41impl Tag {
42 pub fn parse(name: &str) -> Result<Self, TagError> {
48 let characters: Vec<char> = name.chars().collect();
49 let Some(first) = characters.first().copied() else { return Err(TagError::Empty) };
50 if characters.len() > LONGEST {
51 return Err(TagError::TooLong { length: characters.len() });
52 }
53 if !first.is_ascii_alphabetic() {
57 return Err(TagError::BadStart { character: first });
58 }
59 for (index, character) in characters.iter().copied().enumerate() {
60 if !(character.is_ascii_alphanumeric() || character == '-' || character == '_') {
61 return Err(TagError::Illegal { position: index + 1, character });
62 }
63 }
64 Ok(Self(name.to_owned()))
65 }
66
67 #[must_use]
69 pub fn as_str(&self) -> &str {
70 &self.0
71 }
72}
73
74impl std::fmt::Display for Tag {
75 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 formatter.write_str(&self.0)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn a_plain_name_is_a_tag_and_reads_back_as_itself() {
86 for name in ["ev", "ev-makinesi", "openrouter_2", "a"] {
87 let tag = Tag::parse(name).expect(name);
88 assert_eq!(tag.as_str(), name);
89 assert_eq!(tag.to_string(), name);
90 }
91 }
92
93 #[test]
94 fn a_character_that_would_read_as_something_else_is_refused_where_it_stands() {
95 assert_eq!(Tag::parse("ev/makine"), Err(TagError::Illegal { position: 3, character: '/' }));
97 assert_eq!(Tag::parse("ev makine"), Err(TagError::Illegal { position: 3, character: ' ' }));
98 assert_eq!(Tag::parse("ev.makine"), Err(TagError::Illegal { position: 3, character: '.' }));
99 assert_eq!(Tag::parse("evÇ"), Err(TagError::Illegal { position: 3, character: 'Ç' }));
100 }
101
102 #[test]
103 fn a_tag_starts_with_a_letter_and_is_never_empty() {
104 assert_eq!(Tag::parse(""), Err(TagError::Empty));
105 assert_eq!(Tag::parse("2ev"), Err(TagError::BadStart { character: '2' }));
106 assert_eq!(Tag::parse("-ev"), Err(TagError::BadStart { character: '-' }));
107 assert_eq!(Tag::parse("_ev"), Err(TagError::BadStart { character: '_' }));
108 }
109
110 #[test]
111 fn a_tag_long_enough_to_take_the_whole_row_is_refused_with_its_length() {
112 let long = "e".repeat(LONGEST + 1);
113 assert_eq!(Tag::parse(&long), Err(TagError::TooLong { length: LONGEST + 1 }));
114 assert!(Tag::parse(&"e".repeat(LONGEST)).is_ok(), "the longest allowed tag still passes");
115 }
116}