1#![cfg_attr(docsrs, feature(doc_cfg))]
58#![cfg_attr(feature = "unsafe", allow(unsafe_code))]
59#![cfg_attr(not(feature = "unsafe"), forbid(unsafe_code))]
60
61mod tag_union;
62
63#[cfg(feature = "serde")]
64use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
65use std::error::Error;
66use std::fmt::{Display, Formatter};
67use std::ops::Deref;
68use std::str::FromStr;
69
70pub use tag_union::{MatchesAnyTagUnion, TagUnion, TagUnionFromStringError};
71
72#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
82pub struct Tag(String);
83
84impl Tag {
85 pub const EMPTY: Tag = Tag(String::new());
87
88 pub const MAX_LEN: usize = 63;
90
91 pub fn new<V: AsRef<str>>(value: V) -> Self {
105 value.as_ref().parse().expect("invalid input")
106 }
107
108 #[cfg_attr(docsrs, doc(cfg(feature = "unsafe")))]
119 #[cfg(feature = "unsafe")]
120 pub unsafe fn new_unchecked<V: Into<String>>(value: V) -> Self {
121 Self(value.into())
122 }
123
124 pub fn from_str<S: AsRef<str>>(value: S) -> Result<Self, TagFromStringError> {
132 let value = value.as_ref();
133 if value.is_empty() {
134 return Ok(Tag::EMPTY.clone());
135 }
136
137 if value.len() > Tag::MAX_LEN {
138 return Err(TagFromStringError::LimitExceeded(value.len()));
139 }
140
141 let mut chars = value.chars();
142 let first = chars.next().expect("tag is not empty");
143 if !first.is_ascii_lowercase() {
144 return Err(TagFromStringError::MustStartAlphabetic(first));
145 }
146
147 let mut previous = first;
148 while let Some(c) = chars.next() {
149 if !c.is_ascii_digit() && !c.is_ascii_lowercase() && c != '-' {
150 return Err(TagFromStringError::InvalidCharacter(c));
151 }
152
153 previous = c;
154 }
155
156 if !previous.is_ascii_lowercase() {
157 return Err(TagFromStringError::MustEndAlphanumeric(previous));
158 }
159
160 Ok(Self(value.into()))
161 }
162}
163
164impl Display for Tag {
165 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
166 write!(f, "{}", self.0)
167 }
168}
169
170impl Deref for Tag {
171 type Target = str;
172
173 #[inline(always)]
174 fn deref(&self) -> &Self::Target {
175 self.0.deref()
176 }
177}
178
179impl PartialEq<str> for Tag {
180 #[inline(always)]
181 fn eq(&self, other: &str) -> bool {
182 self.0.eq(other)
183 }
184}
185
186impl PartialEq<&str> for Tag {
187 #[inline(always)]
188 fn eq(&self, other: &&str) -> bool {
189 self.0.eq(other)
190 }
191}
192
193impl FromStr for Tag {
194 type Err = TagFromStringError;
195
196 #[inline(always)]
197 fn from_str(value: &str) -> Result<Self, Self::Err> {
198 Tag::from_str(value)
199 }
200}
201
202impl TryFrom<&str> for Tag {
203 type Error = TagFromStringError;
204
205 fn try_from(value: &str) -> Result<Self, Self::Error> {
206 value.parse()
207 }
208}
209
210impl TryFrom<String> for Tag {
211 type Error = TagFromStringError;
212
213 fn try_from(value: String) -> Result<Self, Self::Error> {
214 value.parse()
215 }
216}
217
218impl TryFrom<&String> for Tag {
219 type Error = TagFromStringError;
220
221 fn try_from(value: &String) -> Result<Self, Self::Error> {
222 value.parse()
223 }
224}
225
226#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
227#[cfg(feature = "serde")]
228impl<'de> Deserialize<'de> for Tag {
229 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230 where
231 D: Deserializer<'de>,
232 {
233 let tag = String::deserialize(deserializer)?;
234 match Tag::from_str(&tag) {
235 Ok(tag) => Ok(tag),
236 Err(e) => Err(de::Error::custom(e)),
237 }
238 }
239}
240
241#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
242#[cfg(feature = "serde")]
243impl Serialize for Tag {
244 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
245 where
246 S: Serializer,
247 {
248 serializer.serialize_str(&self.0)
249 }
250}
251
252#[derive(Debug, Eq, PartialEq)]
253pub enum TagFromStringError {
254 MustStartAlphabetic(char),
255 MustEndAlphanumeric(char),
256 InvalidCharacter(char),
257 LimitExceeded(usize),
258}
259
260impl Display for TagFromStringError {
261 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
262 match self {
263 TagFromStringError::MustStartAlphabetic(c) => write!(
264 f,
265 "Tag name must begin with a lowercase alphabetic character, got '{c}'"
266 ),
267 TagFromStringError::MustEndAlphanumeric(c) => write!(
268 f,
269 "Tag name must end with a lowercase alphanumeric character, got '{c}'"
270 ),
271 TagFromStringError::InvalidCharacter(c) => write!(
272 f,
273 "Tag name must only contain lowercase alphanumeric characters or '-', got '{c}'"
274 ),
275 TagFromStringError::LimitExceeded(len) => write!(
276 f,
277 "Tag name must be not longer than 63 characters, got '{len}'"
278 ),
279 }
280 }
281}
282
283impl Error for TagFromStringError {}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_trivial() {
291 assert_eq!(Tag::from_str("test").unwrap(), "test");
292 assert_eq!(Tag::from_str("test-case").unwrap(), "test-case");
293 assert_eq!(Tag::from_str("test---12e").unwrap(), "test---12e");
294 assert!(
295 Tag::from_str("a123456789a123456789a123456789a123456789a123456789a12345678901a")
296 .is_ok()
297 );
298 }
299
300 #[test]
301 fn test_invalid() {
302 assert!(Tag::from_str("1").is_err());
303 assert!(Tag::from_str("-").is_err());
304 assert!(Tag::from_str("a-").is_err());
305 assert!(Tag::from_str("a1").is_err());
306 assert!(Tag::from_str("a_b_c").is_err());
307 assert!(
308 Tag::from_str("a123456789a123456789a123456789a123456789a123456789a123456789012a")
309 .is_err()
310 );
311 }
312
313 #[test]
314 #[cfg(feature = "serde")]
315 fn test_serde_de_trivial() {
316 assert_eq!(serde_json::from_str::<Tag>(r#""test""#).unwrap(), "test");
317 assert_eq!(
318 serde_json::from_str::<Tag>(r#""test-case""#).unwrap(),
319 "test-case"
320 );
321 assert_eq!(
322 serde_json::from_str::<Tag>(r#""test---12e""#).unwrap(),
323 "test---12e"
324 );
325 assert!(serde_json::from_str::<Tag>(
326 r#""a123456789a123456789a123456789a123456789a123456789a12345678901a""#
327 )
328 .is_ok());
329 }
330
331 #[test]
332 #[cfg(feature = "serde")]
333 fn test_serde_de_invalid() {
334 assert!(serde_json::from_str::<Tag>(r#""1""#).is_err());
335 assert!(serde_json::from_str::<Tag>(r#""-""#).is_err());
336 assert!(serde_json::from_str::<Tag>(r#""a-""#).is_err());
337 assert!(serde_json::from_str::<Tag>(r#""a1""#).is_err());
338 assert!(serde_json::from_str::<Tag>(r#""a_b_c""#).is_err());
339 assert!(serde_json::from_str::<Tag>(
340 r#""a123456789a123456789a123456789a123456789a123456789a123456789012a""#
341 )
342 .is_err());
343 }
344
345 #[test]
346 #[cfg(feature = "serde")]
347 fn test_serde_ser_invalid() {
348 let json = serde_json::to_string(&Tag::new("foo")).unwrap();
349 assert_eq!(json, r#""foo""#);
350 }
351}