Skip to main content

icann_rdap_common/check/
string.rs

1/// Functions for types that can be turned into strings.
2///
3/// Example:
4/// ```rust
5/// use icann_rdap_common::check::*;
6///
7/// let s = "  ";
8/// assert!(s.is_whitespace_or_empty());
9/// ```
10pub trait StringCheck {
11    /// Tests if the string is empty, including for if the string only has whitespace.
12    fn is_whitespace_or_empty(&self) -> bool;
13
14    /// Tests if the string contains only letters, digits, or hyphens and is not empty.
15    fn is_ldh_string(&self) -> bool;
16
17    /// Tests if a string is an LDH domain name. This is not to be confused with [StringCheck::is_ldh_string],
18    /// which checks individual domain labels.
19    fn is_ldh_domain_name(&self) -> bool;
20
21    /// Tests if a string is a Unicode domain name.
22    fn is_unicode_domain_name(&self) -> bool;
23
24    /// Tests if a string begins with a period and only has one label.
25    fn is_tld(&self) -> bool;
26
27    /// Tests if a string is an ldh host name (i.e., at least to labels)
28    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
84/// Functions for types that can be turned into arrays of strings.
85///
86/// Example:
87/// ```rust
88/// use icann_rdap_common::check::*;
89///
90/// let a: &[&str] = &["foo",""];
91/// assert!(a.is_empty_or_any_empty_or_whitespace());
92/// ```
93pub trait StringListCheck {
94    /// Tests if a list of strings is empty, or if any of the
95    /// elements of the list are empty or whitespace.
96    fn is_empty_or_any_empty_or_whitespace(&self) -> bool;
97
98    /// Tests if a list of strings are LDH strings. See [CharCheck::is_ldh].
99    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
122/// Functions for chars.
123///
124/// Example:
125/// ```rust
126/// use icann_rdap_common::check::*;
127///
128/// let c = 'a';
129/// assert!(c.is_ldh());
130/// ```
131pub trait CharCheck {
132    /// Checks if the character is a letter, digit or a hyphen
133    #[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        // GIVEN in parameters
158
159        // WHEN
160        let actual = test_string.is_whitespace_or_empty();
161
162        // THEN
163        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        // GIVEN in parameters
179
180        // WHEN
181        let actual = test_list.is_empty_or_any_empty_or_whitespace();
182
183        // THEN
184        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        // GIVEN in parameters
202
203        // WHEN
204        let actual = test_char.is_ldh();
205
206        // THEN
207        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        // GIVEN in parameters
217
218        // WHEN
219        let actual = test_string.is_ldh_string();
220
221        // THEN
222        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        // GIVEN in parameters
235
236        // WHEN
237        let actual = test_string.is_tld();
238
239        // THEN
240        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        // GIVEN in parameters
253
254        // WHEN
255        let actual = test_list.is_ldh_string_list();
256
257        // THEN
258        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        // GIVEN in parameters
269
270        // WHEN
271        let actual = test_string.is_ldh_domain_name();
272
273        // THEN
274        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        // GIVEN in parameters
288
289        // WHEN
290        let actual = test_string.is_unicode_domain_name();
291
292        // THEN
293        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        // GIVEN in parameters
310
311        // WHEN
312        let actual = test_string.is_ldh_hostname();
313
314        // THEN
315        assert_eq!(actual, expected);
316    }
317}