kubizone_common/
segment.rs1use std::{fmt::Display, ops::Add};
2
3use thiserror::Error;
4
5use crate::{DomainName, FullyQualifiedDomainName, PartiallyQualifiedDomainName};
6
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct DomainSegment(String);
12
13impl DomainSegment {
14 pub fn new_unchecked(segment: &str) -> Self {
16 DomainSegment(segment.to_string())
17 }
18
19 pub fn len(&self) -> usize {
21 self.0.len()
22 }
23
24 pub fn is_empty(&self) -> bool {
26 self.0.is_empty()
27 }
28
29 pub fn is_wildcard(&self) -> bool {
31 self.0 == "*"
32 }
33}
34
35#[derive(Error, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
38pub enum DomainSegmentError {
39 #[error("illegal hyphen at position {0}")]
45 IllegalHyphen(usize),
46 #[error("invalid character {0}")]
48 InvalidCharacter(char),
49 #[error("segment too long {0} > 63")]
51 TooLong(usize),
52 #[error("segment is an empty string")]
54 EmptyString,
55 #[error("wildcard segments must have length 1")]
57 NonStandaloneWildcard,
58}
59
60const VALID_CHARACTERS: &str = "_-0123456789abcdefghijklmnopqrstuvwxyz*";
61
62impl TryFrom<&str> for DomainSegment {
63 type Error = DomainSegmentError;
64
65 fn try_from(value: &str) -> Result<Self, Self::Error> {
66 let value = value.to_ascii_lowercase();
67
68 if value.is_empty() {
69 return Err(DomainSegmentError::EmptyString);
70 }
71
72 if value.len() > 63 {
73 return Err(DomainSegmentError::TooLong(value.len()));
74 }
75
76 if value.contains('*') && value.len() != 1 {
77 return Err(DomainSegmentError::NonStandaloneWildcard);
78 }
79
80 if let Some(character) = value.chars().find(|c| !VALID_CHARACTERS.contains(*c)) {
81 return Err(DomainSegmentError::InvalidCharacter(character));
82 }
83
84 if value.starts_with('-') {
85 return Err(DomainSegmentError::IllegalHyphen(1));
86 }
87
88 if value.ends_with('-') {
89 return Err(DomainSegmentError::IllegalHyphen(value.len()));
90 }
91
92 if value.get(2..4) == Some("--") {
93 return Err(DomainSegmentError::IllegalHyphen(3));
94 }
95
96 Ok(DomainSegment(value))
97 }
98}
99
100impl TryFrom<String> for DomainSegment {
101 type Error = DomainSegmentError;
102
103 fn try_from(value: String) -> Result<Self, Self::Error> {
104 Self::try_from(value.as_str())
105 }
106}
107
108impl Display for DomainSegment {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.write_str(&self.0)
111 }
112}
113
114impl AsRef<str> for DomainSegment {
115 fn as_ref(&self) -> &str {
116 self.0.as_str()
117 }
118}
119
120impl Add for DomainSegment {
121 type Output = PartiallyQualifiedDomainName;
122
123 fn add(self, rhs: Self) -> Self::Output {
124 PartiallyQualifiedDomainName::from_iter([self, rhs])
125 }
126}
127
128impl Add<PartiallyQualifiedDomainName> for DomainSegment {
129 type Output = PartiallyQualifiedDomainName;
130
131 fn add(self, mut rhs: PartiallyQualifiedDomainName) -> Self::Output {
132 rhs.0.insert(0, self);
133 rhs
134 }
135}
136
137impl Add<&PartiallyQualifiedDomainName> for DomainSegment {
138 type Output = PartiallyQualifiedDomainName;
139
140 fn add(self, rhs: &PartiallyQualifiedDomainName) -> Self::Output {
141 let mut out = rhs.clone();
142 out.0.insert(0, self);
143 out
144 }
145}
146
147impl Add<FullyQualifiedDomainName> for DomainSegment {
148 type Output = FullyQualifiedDomainName;
149
150 fn add(self, mut rhs: FullyQualifiedDomainName) -> Self::Output {
151 rhs.0.insert(0, self);
152 rhs
153 }
154}
155
156impl Add<&FullyQualifiedDomainName> for DomainSegment {
157 type Output = FullyQualifiedDomainName;
158
159 fn add(self, rhs: &FullyQualifiedDomainName) -> Self::Output {
160 let mut out = rhs.clone();
161 out.0.insert(0, self);
162 out
163 }
164}
165
166impl Add<DomainName> for DomainSegment {
167 type Output = DomainName;
168
169 fn add(self, rhs: DomainName) -> Self::Output {
170 match rhs {
171 DomainName::Full(full) => DomainName::Full(self + full),
172 DomainName::Partial(partial) => DomainName::Partial(self + partial),
173 }
174 }
175}
176
177impl Add<&DomainName> for DomainSegment {
178 type Output = DomainName;
179
180 fn add(self, rhs: &DomainName) -> Self::Output {
181 match rhs {
182 DomainName::Full(full) => DomainName::Full(self + full),
183 DomainName::Partial(partial) => DomainName::Partial(self + partial),
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use crate::segment::{DomainSegment, DomainSegmentError};
191
192 #[test]
193 fn segment_construction() {
194 assert_eq!(DomainSegment::try_from("abcd").unwrap().as_ref(), "abcd");
195
196 assert_eq!(
197 DomainSegment::try_from(""),
198 Err(DomainSegmentError::EmptyString)
199 );
200 }
201
202 #[test]
203 fn invalid_character() {
204 assert_eq!(
205 DomainSegment::try_from("ab.cd"),
206 Err(DomainSegmentError::InvalidCharacter('.'))
207 );
208 }
209
210 #[test]
211 fn invalid_hyphens() {
212 assert_eq!(DomainSegment::try_from("ab-cd").unwrap().as_ref(), "ab-cd");
213
214 assert_eq!(DomainSegment::try_from("abc-d").unwrap().as_ref(), "abc-d");
215
216 assert_eq!(
217 DomainSegment::try_from("ab--cd"),
218 Err(DomainSegmentError::IllegalHyphen(3))
219 );
220
221 assert_eq!(
222 DomainSegment::try_from("-abcd"),
223 Err(DomainSegmentError::IllegalHyphen(1))
224 );
225
226 assert_eq!(
227 DomainSegment::try_from("abcd-"),
228 Err(DomainSegmentError::IllegalHyphen(5))
229 );
230 }
231
232 #[test]
233 fn wildcards() {
234 assert_eq!(DomainSegment::try_from("*").unwrap().as_ref(), "*");
235
236 assert!(DomainSegment::try_from("*").unwrap().is_wildcard())
237 }
238}