| 62 |
62 |
|
|
| 63 |
63 |
|
/// Error returned when a tag string is invalid.
|
| 64 |
64 |
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
| 65 |
|
- |
pub struct TagError(pub String);
|
|
65 |
+ |
pub enum TagError {
|
|
66 |
+ |
/// The tag was empty.
|
|
67 |
+ |
Empty,
|
|
68 |
+ |
/// The tag exceeded the configured maximum length (in characters).
|
|
69 |
+ |
TooLong { max: usize },
|
|
70 |
+ |
/// The tag started or ended with a separator dot.
|
|
71 |
+ |
LeadingOrTrailingDot,
|
|
72 |
+ |
/// The tag contained consecutive separator dots.
|
|
73 |
+ |
ConsecutiveDots,
|
|
74 |
+ |
/// The tag contained a character outside the allowed set.
|
|
75 |
+ |
InvalidChar(char),
|
|
76 |
+ |
/// The tag had more segments than the configured maximum depth.
|
|
77 |
+ |
TooDeep { levels: usize, max: usize },
|
|
78 |
+ |
/// The tag had fewer segments than the configured semantic prefix requires.
|
|
79 |
+ |
InsufficientDepth { min_segments: usize, semantic_depth: usize },
|
|
80 |
+ |
}
|
| 66 |
81 |
|
|
| 67 |
82 |
|
impl fmt::Display for TagError {
|
| 68 |
83 |
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
| 69 |
|
- |
write!(f, "invalid tag: {}", self.0)
|
|
84 |
+ |
match self {
|
|
85 |
+ |
Self::Empty => write!(f, "tag cannot be empty"),
|
|
86 |
+ |
Self::TooLong { max } => write!(f, "tag exceeds {max} characters"),
|
|
87 |
+ |
Self::LeadingOrTrailingDot => write!(f, "tag cannot start or end with a dot"),
|
|
88 |
+ |
Self::ConsecutiveDots => write!(f, "tag cannot contain consecutive dots"),
|
|
89 |
+ |
Self::InvalidChar(ch) => write!(
|
|
90 |
+ |
f,
|
|
91 |
+ |
"invalid character '{ch}': only lowercase alphanumeric, hyphens, and dots allowed"
|
|
92 |
+ |
),
|
|
93 |
+ |
Self::TooDeep { levels, max } => {
|
|
94 |
+ |
write!(f, "tag has {levels} levels, maximum is {max}")
|
|
95 |
+ |
}
|
|
96 |
+ |
Self::InsufficientDepth { min_segments, semantic_depth } => write!(
|
|
97 |
+ |
f,
|
|
98 |
+ |
"tag must have at least {min_segments} segments (semantic prefix requires {semantic_depth} + at least 1 value)"
|
|
99 |
+ |
),
|
|
100 |
+ |
}
|
| 70 |
101 |
|
}
|
| 71 |
102 |
|
}
|
| 72 |
103 |
|
|
| 93 |
124 |
|
/// ```
|
| 94 |
125 |
|
pub fn validate_with(tag: &str, config: &TagConfig) -> Result<(), TagError> {
|
| 95 |
126 |
|
if tag.is_empty() {
|
| 96 |
|
- |
return Err(TagError("tag cannot be empty".into()));
|
|
127 |
+ |
return Err(TagError::Empty);
|
| 97 |
128 |
|
}
|
| 98 |
129 |
|
if tag.len() > config.max_length {
|
| 99 |
|
- |
return Err(TagError(format!(
|
| 100 |
|
- |
"tag exceeds {} characters",
|
| 101 |
|
- |
config.max_length
|
| 102 |
|
- |
)));
|
|
130 |
+ |
return Err(TagError::TooLong { max: config.max_length });
|
| 103 |
131 |
|
}
|
| 104 |
132 |
|
if tag.starts_with(SEPARATOR) || tag.ends_with(SEPARATOR) {
|
| 105 |
|
- |
return Err(TagError("tag cannot start or end with a dot".into()));
|
|
133 |
+ |
return Err(TagError::LeadingOrTrailingDot);
|
| 106 |
134 |
|
}
|
| 107 |
135 |
|
if tag.contains("..") {
|
| 108 |
|
- |
return Err(TagError("tag cannot contain consecutive dots".into()));
|
|
136 |
+ |
return Err(TagError::ConsecutiveDots);
|
| 109 |
137 |
|
}
|
| 110 |
138 |
|
for ch in tag.chars() {
|
| 111 |
139 |
|
if !(ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == SEPARATOR) {
|
| 112 |
|
- |
return Err(TagError(format!(
|
| 113 |
|
- |
"invalid character '{ch}': only lowercase alphanumeric, hyphens, and dots allowed"
|
| 114 |
|
- |
)));
|
|
140 |
+ |
return Err(TagError::InvalidChar(ch));
|
| 115 |
141 |
|
}
|
| 116 |
142 |
|
}
|
| 117 |
143 |
|
let d = depth(tag);
|
| 118 |
144 |
|
if d > config.max_depth {
|
| 119 |
|
- |
return Err(TagError(format!(
|
| 120 |
|
- |
"tag has {} levels, maximum is {}",
|
| 121 |
|
- |
d, config.max_depth
|
| 122 |
|
- |
)));
|
|
145 |
+ |
return Err(TagError::TooDeep { levels: d, max: config.max_depth });
|
| 123 |
146 |
|
}
|
| 124 |
147 |
|
if config.semantic_depth > 0 && d < config.semantic_depth + 1 {
|
| 125 |
|
- |
return Err(TagError(format!(
|
| 126 |
|
- |
"tag must have at least {} segments (semantic prefix requires {} + at least 1 value)",
|
| 127 |
|
- |
config.semantic_depth + 1,
|
| 128 |
|
- |
config.semantic_depth
|
| 129 |
|
- |
)));
|
|
148 |
+ |
return Err(TagError::InsufficientDepth {
|
|
149 |
+ |
min_segments: config.semantic_depth + 1,
|
|
150 |
+ |
semantic_depth: config.semantic_depth,
|
|
151 |
+ |
});
|
| 130 |
152 |
|
}
|
| 131 |
153 |
|
Ok(())
|
| 132 |
154 |
|
}
|
| 1880 |
1902 |
|
fn tag_error_display_includes_message() {
|
| 1881 |
1903 |
|
// Catches `<impl fmt::Display for TagError>::fmt -> Ok(Default::default())`.
|
| 1882 |
1904 |
|
// Without this test, the Display impl was never exercised.
|
| 1883 |
|
- |
let err = TagError("bad thing".into());
|
|
1905 |
+ |
let err = TagError::TooLong { max: 50 };
|
| 1884 |
1906 |
|
let s = format!("{err}");
|
| 1885 |
|
- |
assert!(s.contains("bad thing"), "display output: {s:?}");
|
| 1886 |
|
- |
assert!(s.starts_with("invalid tag:"), "display output: {s:?}");
|
|
1907 |
+ |
assert!(s.contains("50 characters"), "display output: {s:?}");
|
|
1908 |
+ |
assert!(!s.is_empty(), "display output: {s:?}");
|
| 1887 |
1909 |
|
}
|
| 1888 |
1910 |
|
|
| 1889 |
1911 |
|
#[test]
|