1pub trait StringCheck {
11 fn is_whitespace_or_empty(&self) -> bool;
13
14 fn is_ldh_string(&self) -> bool;
16
17 fn is_ldh_domain_name(&self) -> bool;
20
21 fn is_unicode_domain_name(&self) -> bool;
23
24 fn is_tld(&self) -> bool;
26
27 fn is_ldh_hostname(&self) -> bool;
29}
30
31impl<T: ToString> StringCheck for T {
32 fn is_whitespace_or_empty(&self) -> bool {
33 let s = self.to_string();
34 s.is_empty() || s.chars().all(char::is_whitespace)
35 }
36
37 fn is_ldh_string(&self) -> bool {
38 let s = self.to_string();
39 !s.is_empty() && s.chars().all(char::is_ldh)
40 }
41
42 fn is_ldh_domain_name(&self) -> bool {
43 let s = self.to_string();
44 s == "." || (!s.is_empty() && s.split_terminator('.').all(|s| s.is_ldh_string()))
45 }
46
47 fn is_unicode_domain_name(&self) -> bool {
48 let s = self.to_string();
49 s == "."
50 || (!s.is_empty()
51 && s.split_terminator('.').all(|s| {
52 s.chars()
53 .all(|c| c == '-' || (!c.is_ascii_punctuation() && !c.is_whitespace()))
54 }))
55 }
56
57 fn is_tld(&self) -> bool {
58 let s = self.to_string();
59 s.starts_with('.')
60 && s.len() > 2
61 && s.matches('.').count() == 1
62 && s.split_terminator('.').all(|s| {
63 s.chars()
64 .all(|c| !c.is_ascii_punctuation() && !c.is_whitespace())
65 })
66 }
67
68 fn is_ldh_hostname(&self) -> bool {
69 let s = self.to_string();
70 let count = s.split_terminator('.').try_fold(0, |acc, s| {
71 if s.is_ldh_string() {
72 Ok(acc + 1)
73 } else {
74 Err(acc)
75 }
76 });
77 match count {
78 Ok(count) => count > 1,
79 Err(_) => false,
80 }
81 }
82}
83
84pub trait StringListCheck {
94 fn is_empty_or_any_empty_or_whitespace(&self) -> bool;
97
98 fn is_ldh_string_list(&self) -> bool;
100}
101
102impl<T: ToString> StringListCheck for &[T] {
103 fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
104 self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
105 }
106
107 fn is_ldh_string_list(&self) -> bool {
108 !self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
109 }
110}
111
112impl<T: ToString> StringListCheck for Vec<T> {
113 fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
114 self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
115 }
116
117 fn is_ldh_string_list(&self) -> bool {
118 !self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
119 }
120}
121
122pub trait CharCheck {
132 #[allow(clippy::wrong_self_convention)]
134 fn is_ldh(self) -> bool;
135}
136
137impl CharCheck for char {
138 fn is_ldh(self) -> bool {
139 matches!(self, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-')
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use rstest::rstest;
146
147 use crate::check::string::{CharCheck, StringListCheck};
148
149 use super::StringCheck;
150
151 #[rstest]
152 #[case("foo", false)]
153 #[case("", true)]
154 #[case(" ", true)]
155 #[case("foo bar", false)]
156 fn test_is_whitespace_or_empty(#[case] test_string: &str, #[case] expected: bool) {
157 let actual = test_string.is_whitespace_or_empty();
161
162 assert_eq!(actual, expected);
164 }
165
166 #[rstest]
167 #[case(&[], true)]
168 #[case(&["foo"], false)]
169 #[case(&["foo",""], true)]
170 #[case(&["foo","bar"], false)]
171 #[case(&["foo","bar baz"], false)]
172 #[case(&[""], true)]
173 #[case(&[" "], true)]
174 fn test_is_whitespace_or_any_empty_or_whitespace(
175 #[case] test_list: &[&str],
176 #[case] expected: bool,
177 ) {
178 let actual = test_list.is_empty_or_any_empty_or_whitespace();
182
183 assert_eq!(actual, expected);
185 }
186
187 #[rstest]
188 #[case('a', true)]
189 #[case('l', true)]
190 #[case('z', true)]
191 #[case('A', true)]
192 #[case('L', true)]
193 #[case('Z', true)]
194 #[case('0', true)]
195 #[case('3', true)]
196 #[case('9', true)]
197 #[case('-', true)]
198 #[case('_', false)]
199 #[case('.', false)]
200 fn test_is_ldh(#[case] test_char: char, #[case] expected: bool) {
201 let actual = test_char.is_ldh();
205
206 assert_eq!(actual, expected);
208 }
209
210 #[rstest]
211 #[case("foo", true)]
212 #[case("", false)]
213 #[case("foo-bar", true)]
214 #[case("foo bar", false)]
215 fn test_is_ldh_string(#[case] test_string: &str, #[case] expected: bool) {
216 let actual = test_string.is_ldh_string();
220
221 assert_eq!(actual, expected);
223 }
224
225 #[rstest]
226 #[case("foo", false)]
227 #[case("", false)]
228 #[case("foo-bar", false)]
229 #[case("foo bar", false)]
230 #[case(".", false)]
231 #[case(".foo.bar", false)]
232 #[case(".foo", true)]
233 fn test_is_tld(#[case] test_string: &str, #[case] expected: bool) {
234 let actual = test_string.is_tld();
238
239 assert_eq!(actual, expected);
241 }
242
243 #[rstest]
244 #[case(&[], false)]
245 #[case(&["foo"], true)]
246 #[case(&["foo",""], false)]
247 #[case(&["foo","bar"], true)]
248 #[case(&["foo","bar baz"], false)]
249 #[case(&[""], false)]
250 #[case(&[" "], false)]
251 fn test_is_ldh_string_list(#[case] test_list: &[&str], #[case] expected: bool) {
252 let actual = test_list.is_ldh_string_list();
256
257 assert_eq!(actual, expected);
259 }
260
261 #[rstest]
262 #[case("foo", true)]
263 #[case("", false)]
264 #[case(".", true)]
265 #[case("foo.bar", true)]
266 #[case("foo.bar.", true)]
267 fn test_is_ldh_domain_name(#[case] test_string: &str, #[case] expected: bool) {
268 let actual = test_string.is_ldh_domain_name();
272
273 assert_eq!(actual, expected);
275 }
276
277 #[rstest]
278 #[case("foo", true)]
279 #[case("", false)]
280 #[case(".", true)]
281 #[case("foo.bar", true)]
282 #[case("foè.bar", true)]
283 #[case("foo.bar.", true)]
284 #[case("fo_o.bar.", false)]
285 #[case("fo o.bar.", false)]
286 fn test_is_unicode_domain_name(#[case] test_string: &str, #[case] expected: bool) {
287 let actual = test_string.is_unicode_domain_name();
291
292 assert_eq!(actual, expected);
294 }
295
296 #[rstest]
297 #[case("foo", false)]
298 #[case("", false)]
299 #[case(".", false)]
300 #[case("foo.bar", true)]
301 #[case("foè.bar", false)]
302 #[case("foo.bar.", true)]
303 #[case("fo_o.bar.", false)]
304 #[case("fo o.bar.", false)]
305 #[case("bar.foo.bar", true)]
306 #[case("https://foo.bar", false)]
307 #[case("http://foo.bar", false)]
308 fn test_is_ldh_hostname(#[case] test_string: &str, #[case] expected: bool) {
309 let actual = test_string.is_ldh_hostname();
313
314 assert_eq!(actual, expected);
316 }
317}