1use serde::{Deserialize, Serialize};
2
3#[derive(Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
5#[serde(into = "String", try_from = "String")]
6pub struct Label(String);
7
8impl AsRef<str> for Label {
9 fn as_ref(&self) -> &str {
10 &self.0
11 }
12}
13
14impl std::fmt::Display for Label {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 write!(f, "{}", self.0)
17 }
18}
19
20impl std::fmt::Debug for Label {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "{:?}", self.0)
23 }
24}
25
26impl From<Label> for String {
27 fn from(value: Label) -> Self {
28 value.0
29 }
30}
31
32impl TryFrom<String> for Label {
33 type Error = InvalidLabel;
34
35 fn try_from(label: String) -> Result<Self, Self::Error> {
36 if label.is_empty() {
37 return Err(InvalidLabel::Empty);
38 }
39 for word in label.split('-') {
40 let mut chars = word.chars();
41 match chars.next() {
42 None => return Err(InvalidLabel::EmptyWord),
43 Some(ch) if !ch.is_ascii_lowercase() => {
44 return Err(InvalidLabel::InvalidWordFirstChar)
45 }
46 _ => (),
47 }
48 if !chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) {
49 return Err(InvalidLabel::InvalidChar);
50 }
51 }
52 Ok(Self(label))
53 }
54}
55
56impl std::str::FromStr for Label {
57 type Err = InvalidLabel;
58
59 fn from_str(s: &str) -> Result<Self, Self::Err> {
60 s.to_owned().try_into()
61 }
62}
63
64#[derive(Debug, thiserror::Error)]
65pub enum InvalidLabel {
66 #[error("labels may not be empty")]
67 Empty,
68 #[error("dash-separated words may not be empty")]
69 EmptyWord,
70 #[error("dash-separated words may contain only lowercase alphanumeric ASCII characters")]
71 InvalidChar,
72 #[error("dash-separated words must begin with an ASCII lowercase letter")]
73 InvalidWordFirstChar,
74}